@hasna/economy 0.3.28 → 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 +95 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +6 -13
- package/SECURITY.md +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +1135 -853
- package/dist/db/cloud.d.ts +16 -8
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +19 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +198 -19
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +9 -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/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 +1340 -302
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +430 -16
- package/dist/server/index.js +548 -155
- 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 +5 -11
- package/postinstall.js +52 -1
- 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
|
@@ -596,7 +596,62 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
|
596
596
|
import { hostname, platform } from "os";
|
|
597
597
|
import { homedir } from "os";
|
|
598
598
|
import { join, resolve } from "path";
|
|
599
|
-
import {
|
|
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
|
+
}
|
|
600
655
|
function normalizeMachineId(value) {
|
|
601
656
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
602
657
|
return id && id.length > 0 ? id : null;
|
|
@@ -619,7 +674,7 @@ function macHostMachineId() {
|
|
|
619
674
|
return null;
|
|
620
675
|
}
|
|
621
676
|
function getMachineId() {
|
|
622
|
-
const envMachine = normalizeMachineId(process.env["
|
|
677
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
623
678
|
if (envMachine)
|
|
624
679
|
return envMachine;
|
|
625
680
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -1947,6 +2002,10 @@ function getIngestState(db, source, key) {
|
|
|
1947
2002
|
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
1948
2003
|
return row?.value ?? null;
|
|
1949
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
|
+
}
|
|
1950
2009
|
function setIngestState(db, source, key, value) {
|
|
1951
2010
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1952
2011
|
}
|
|
@@ -2392,7 +2451,7 @@ function bulkIngest(db, body) {
|
|
|
2392
2451
|
const total = Object.values(ingested).reduce((a, b) => a + b, 0);
|
|
2393
2452
|
return { ingested, total };
|
|
2394
2453
|
}
|
|
2395
|
-
var numOr = (v, d = 0) => {
|
|
2454
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, numOr = (v, d = 0) => {
|
|
2396
2455
|
if (v == null)
|
|
2397
2456
|
return d;
|
|
2398
2457
|
const num = Number(v);
|
|
@@ -2400,6 +2459,13 @@ var numOr = (v, d = 0) => {
|
|
|
2400
2459
|
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
2401
2460
|
var init_database = __esm(() => {
|
|
2402
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]+)*$/;
|
|
2403
2469
|
});
|
|
2404
2470
|
|
|
2405
2471
|
// package.json
|
|
@@ -2407,8 +2473,8 @@ var package_default;
|
|
|
2407
2473
|
var init_package = __esm(() => {
|
|
2408
2474
|
package_default = {
|
|
2409
2475
|
name: "@hasna/economy",
|
|
2410
|
-
version: "0.
|
|
2411
|
-
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",
|
|
2412
2478
|
repository: {
|
|
2413
2479
|
type: "git",
|
|
2414
2480
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -2439,7 +2505,6 @@ var init_package = __esm(() => {
|
|
|
2439
2505
|
files: [
|
|
2440
2506
|
"dist",
|
|
2441
2507
|
"docs",
|
|
2442
|
-
"dashboard/dist",
|
|
2443
2508
|
"postinstall.js",
|
|
2444
2509
|
"LICENSE",
|
|
2445
2510
|
"README.md",
|
|
@@ -2449,12 +2514,11 @@ var init_package = __esm(() => {
|
|
|
2449
2514
|
"CODE_OF_CONDUCT.md"
|
|
2450
2515
|
],
|
|
2451
2516
|
scripts: {
|
|
2452
|
-
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",
|
|
2453
2518
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
2454
2519
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
2455
2520
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
2456
2521
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
2457
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
2458
2522
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
2459
2523
|
prepack: "bun run build && bun run artifact-scan",
|
|
2460
2524
|
typecheck: "tsc --noEmit",
|
|
@@ -2484,12 +2548,8 @@ var init_package = __esm(() => {
|
|
|
2484
2548
|
access: "public"
|
|
2485
2549
|
},
|
|
2486
2550
|
dependencies: {
|
|
2487
|
-
"@hasna/
|
|
2488
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
2489
|
-
"@hasna/contracts": "0.13.4",
|
|
2551
|
+
"@hasna/contracts": "1.0.2",
|
|
2490
2552
|
"@hasna/events": "^0.1.16",
|
|
2491
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
2492
|
-
"@hasna/paths": "0.1.0",
|
|
2493
2553
|
"@hasna/projects": "1.0.0",
|
|
2494
2554
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2495
2555
|
chalk: "^5.4.1",
|
|
@@ -2498,7 +2558,7 @@ var init_package = __esm(() => {
|
|
|
2498
2558
|
zod: "^3.24.2"
|
|
2499
2559
|
},
|
|
2500
2560
|
devDependencies: {
|
|
2501
|
-
"@types/bun": "
|
|
2561
|
+
"@types/bun": "1.3.14",
|
|
2502
2562
|
"@types/pg": "^8.20.0",
|
|
2503
2563
|
"bun-types": "latest",
|
|
2504
2564
|
typescript: "^5.7.2"
|
|
@@ -3513,18 +3573,133 @@ function queryModelEfficiency(db) {
|
|
|
3513
3573
|
FROM requests GROUP BY model ORDER BY cost DESC`).all();
|
|
3514
3574
|
}
|
|
3515
3575
|
|
|
3516
|
-
// src/lib/contracts-client/storage.ts
|
|
3517
|
-
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
3518
|
-
var init_storage = () => {};
|
|
3519
|
-
|
|
3520
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
|
+
}
|
|
3521
3676
|
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
3522
|
-
|
|
3523
|
-
|
|
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 };
|
|
3524
3692
|
}
|
|
3525
3693
|
function economyCloudStorage(env = process.env) {
|
|
3526
|
-
if (cache === undefined)
|
|
3694
|
+
if (cache === undefined) {
|
|
3527
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
|
+
}
|
|
3528
3703
|
return cache;
|
|
3529
3704
|
}
|
|
3530
3705
|
function cleanQuery(query) {
|
|
@@ -3550,9 +3725,59 @@ async function cloudObject(storage, path, query) {
|
|
|
3550
3725
|
}
|
|
3551
3726
|
return raw;
|
|
3552
3727
|
}
|
|
3553
|
-
|
|
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;
|
|
3554
3772
|
var init_cloud_storage = __esm(() => {
|
|
3555
|
-
|
|
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");
|
|
3556
3781
|
});
|
|
3557
3782
|
|
|
3558
3783
|
// src/lib/store/index.ts
|
|
@@ -3953,7 +4178,7 @@ var init_store = __esm(() => {
|
|
|
3953
4178
|
|
|
3954
4179
|
// src/lib/serve-auth.ts
|
|
3955
4180
|
function getServeApiToken() {
|
|
3956
|
-
return process.env["
|
|
4181
|
+
return process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
3957
4182
|
}
|
|
3958
4183
|
function getServeBindHost() {
|
|
3959
4184
|
const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
|
|
@@ -3985,7 +4210,7 @@ __export(exports_tui, {
|
|
|
3985
4210
|
buildWaybarJson: () => buildWaybarJson,
|
|
3986
4211
|
buildStatusLine: () => buildStatusLine
|
|
3987
4212
|
});
|
|
3988
|
-
import
|
|
4213
|
+
import chalk2 from "chalk";
|
|
3989
4214
|
function fmt(usd) {
|
|
3990
4215
|
return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
3991
4216
|
}
|
|
@@ -4063,20 +4288,20 @@ async function runTui(opts) {
|
|
|
4063
4288
|
const render = async () => {
|
|
4064
4289
|
const data = await gatherStatusData();
|
|
4065
4290
|
process.stdout.write("\x1B[H\x1B[2J");
|
|
4066
|
-
console.log(
|
|
4067
|
-
console.log(
|
|
4068
|
-
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`);
|
|
4069
4294
|
console.log(` Week ${fmt(data.week.total_usd)} ${data.week.sessions} sessions`);
|
|
4070
4295
|
console.log(` Month ${fmt(data.month.total_usd)} saved ${fmt(data.savings.saved_usd)}`);
|
|
4071
|
-
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"));
|
|
4072
4297
|
console.log(` Top agent: ${data.topAgent}`);
|
|
4073
4298
|
if (data.quota)
|
|
4074
4299
|
console.log(` Quota: ${data.quota}`);
|
|
4075
4300
|
console.log(` Fleet: ${data.machineCount} machines`);
|
|
4076
4301
|
console.log(` Store: ${data.transport === "cloud-http" ? "self_hosted (cloud API)" : "local"}`);
|
|
4077
4302
|
if (getServeApiToken())
|
|
4078
|
-
console.log(
|
|
4079
|
-
console.log(
|
|
4303
|
+
console.log(chalk2.dim(" API auth: HASNA_ECONOMY_API_TOKEN set"));
|
|
4304
|
+
console.log(chalk2.dim(`
|
|
4080
4305
|
${opts.watch ? `Refreshing every ${interval}s \u2014 Ctrl+C to exit` : "Run with --watch for live refresh"}`));
|
|
4081
4306
|
};
|
|
4082
4307
|
await render();
|
|
@@ -4122,32 +4347,344 @@ var init_agents = __esm(() => {
|
|
|
4122
4347
|
|
|
4123
4348
|
// src/lib/paths.ts
|
|
4124
4349
|
import { homedir as homedir2 } from "os";
|
|
4125
|
-
import { join as
|
|
4350
|
+
import { join as join2 } from "path";
|
|
4126
4351
|
function getHomeDir2() {
|
|
4127
4352
|
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir2();
|
|
4128
4353
|
}
|
|
4129
4354
|
function agentPaths() {
|
|
4130
4355
|
const home = getHomeDir2();
|
|
4131
4356
|
return {
|
|
4132
|
-
claudeProjects:
|
|
4133
|
-
claudeCredentials:
|
|
4134
|
-
takumiProjects:
|
|
4135
|
-
codexDir:
|
|
4136
|
-
codexDb:
|
|
4137
|
-
codexAuth:
|
|
4138
|
-
codexConfig:
|
|
4139
|
-
geminiTmp:
|
|
4140
|
-
geminiHistory:
|
|
4141
|
-
opencodeMessages:
|
|
4142
|
-
piSessions:
|
|
4143
|
-
hermesDir:
|
|
4144
|
-
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")
|
|
4145
4370
|
};
|
|
4146
4371
|
}
|
|
4147
4372
|
var init_paths = () => {};
|
|
4148
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
|
+
|
|
4149
4687
|
// src/lib/accounts.ts
|
|
4150
|
-
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
4151
4688
|
function normalizeEmail(email) {
|
|
4152
4689
|
return (email ?? "").trim().toLowerCase();
|
|
4153
4690
|
}
|
|
@@ -4209,11 +4746,11 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
4209
4746
|
const normalized = normalizeDir(configuredDir);
|
|
4210
4747
|
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
4211
4748
|
}
|
|
4212
|
-
async function resolveAccountForAgent(agent, env = process.env) {
|
|
4749
|
+
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
4213
4750
|
const override = envOverride(agent, env);
|
|
4214
4751
|
if (override)
|
|
4215
4752
|
return override;
|
|
4216
|
-
const store = resolveStore(env);
|
|
4753
|
+
const store = resolveStore(env, options);
|
|
4217
4754
|
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
4218
4755
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
4219
4756
|
const tool = tools.get(toolId);
|
|
@@ -4248,6 +4785,7 @@ function withAccount(record, account) {
|
|
|
4248
4785
|
}
|
|
4249
4786
|
var AGENT_ACCOUNT_TOOLS;
|
|
4250
4787
|
var init_accounts = __esm(() => {
|
|
4788
|
+
init_accounts_store();
|
|
4251
4789
|
AGENT_ACCOUNT_TOOLS = {
|
|
4252
4790
|
claude: ["claude"],
|
|
4253
4791
|
takumi: ["takumi", "claude"],
|
|
@@ -4262,8 +4800,8 @@ var init_accounts = __esm(() => {
|
|
|
4262
4800
|
|
|
4263
4801
|
// src/ingest/claude.ts
|
|
4264
4802
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
4265
|
-
import { homedir as
|
|
4266
|
-
import { join as
|
|
4803
|
+
import { homedir as homedir4 } from "os";
|
|
4804
|
+
import { join as join5, basename } from "path";
|
|
4267
4805
|
function autoDetectProject(cwd, projects) {
|
|
4268
4806
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
4269
4807
|
}
|
|
@@ -4276,9 +4814,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
4276
4814
|
try {
|
|
4277
4815
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
4278
4816
|
if (entry.isDirectory())
|
|
4279
|
-
walk(
|
|
4817
|
+
walk(join5(dir, entry.name));
|
|
4280
4818
|
else if (entry.name.endsWith(".jsonl"))
|
|
4281
|
-
files.push(
|
|
4819
|
+
files.push(join5(dir, entry.name));
|
|
4282
4820
|
}
|
|
4283
4821
|
} catch {}
|
|
4284
4822
|
}
|
|
@@ -4303,9 +4841,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
4303
4841
|
const touchedSessions = new Set;
|
|
4304
4842
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
4305
4843
|
const account = await resolveAccountForAgent(agentName);
|
|
4844
|
+
const ingestState = loadIngestState(db, agentName);
|
|
4306
4845
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
4307
4846
|
for (const projectDirEntry of projectDirs) {
|
|
4308
|
-
const projectDirPath =
|
|
4847
|
+
const projectDirPath = join5(projectsDir, projectDirEntry.name);
|
|
4309
4848
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
4310
4849
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
4311
4850
|
for (const filePath of jsonlFiles) {
|
|
@@ -4316,7 +4855,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
4316
4855
|
} catch {
|
|
4317
4856
|
continue;
|
|
4318
4857
|
}
|
|
4319
|
-
const processed =
|
|
4858
|
+
const processed = ingestState.get(stateKey) ?? null;
|
|
4320
4859
|
if (processed === fileMtime)
|
|
4321
4860
|
continue;
|
|
4322
4861
|
let lines;
|
|
@@ -4442,14 +4981,14 @@ var init_claude = __esm(() => {
|
|
|
4442
4981
|
init_database();
|
|
4443
4982
|
init_pricing();
|
|
4444
4983
|
init_accounts();
|
|
4445
|
-
CLAUDE_PROJECTS_DIR =
|
|
4446
|
-
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");
|
|
4447
4986
|
});
|
|
4448
4987
|
|
|
4449
4988
|
// src/ingest/codex.ts
|
|
4450
4989
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
4451
|
-
import { homedir as
|
|
4452
|
-
import { join as
|
|
4990
|
+
import { homedir as homedir5 } from "os";
|
|
4991
|
+
import { join as join6, basename as basename2 } from "path";
|
|
4453
4992
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
4454
4993
|
function codexDbPath() {
|
|
4455
4994
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -4695,16 +5234,16 @@ var init_codex = __esm(() => {
|
|
|
4695
5234
|
init_database();
|
|
4696
5235
|
init_pricing();
|
|
4697
5236
|
init_accounts();
|
|
4698
|
-
DEFAULT_CODEX_DB_PATH =
|
|
4699
|
-
DEFAULT_CODEX_CONFIG_PATH =
|
|
4700
|
-
DEFAULT_CODEWITH_DB_PATH =
|
|
4701
|
-
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");
|
|
4702
5241
|
});
|
|
4703
5242
|
|
|
4704
5243
|
// src/ingest/gemini.ts
|
|
4705
5244
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
4706
|
-
import { homedir as
|
|
4707
|
-
import { join as
|
|
5245
|
+
import { homedir as homedir6 } from "os";
|
|
5246
|
+
import { join as join7, basename as basename3 } from "path";
|
|
4708
5247
|
function geminiTmpDir() {
|
|
4709
5248
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
4710
5249
|
}
|
|
@@ -4726,7 +5265,7 @@ function listProjectDirs(...roots) {
|
|
|
4726
5265
|
try {
|
|
4727
5266
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
4728
5267
|
if (entry.isDirectory())
|
|
4729
|
-
dirs.add(
|
|
5268
|
+
dirs.add(join7(root, entry.name));
|
|
4730
5269
|
}
|
|
4731
5270
|
} catch {}
|
|
4732
5271
|
}
|
|
@@ -4737,7 +5276,7 @@ function projectRoot(projectDir, chatData) {
|
|
|
4737
5276
|
return chatData.projectPath;
|
|
4738
5277
|
if (chatData.project_path)
|
|
4739
5278
|
return chatData.project_path;
|
|
4740
|
-
const rootFile =
|
|
5279
|
+
const rootFile = join7(projectDir, ".project_root");
|
|
4741
5280
|
try {
|
|
4742
5281
|
if (existsSync6(rootFile))
|
|
4743
5282
|
return readFileSync4(rootFile, "utf-8").trim();
|
|
@@ -4759,17 +5298,17 @@ async function ingestGemini(db, verbose) {
|
|
|
4759
5298
|
const account = await resolveAccountForAgent("gemini");
|
|
4760
5299
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
4761
5300
|
for (const projectDir of projectDirs) {
|
|
4762
|
-
const chatsDir =
|
|
5301
|
+
const chatsDir = join7(projectDir, "chats");
|
|
4763
5302
|
if (!existsSync6(chatsDir))
|
|
4764
5303
|
continue;
|
|
4765
5304
|
let chatFiles = [];
|
|
4766
5305
|
try {
|
|
4767
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
5306
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join7(chatsDir, f));
|
|
4768
5307
|
} catch {
|
|
4769
5308
|
continue;
|
|
4770
5309
|
}
|
|
4771
5310
|
for (const filePath of chatFiles) {
|
|
4772
|
-
const stateKey = filePath.replace(
|
|
5311
|
+
const stateKey = filePath.replace(homedir6(), "~");
|
|
4773
5312
|
let fileMtime = "0";
|
|
4774
5313
|
try {
|
|
4775
5314
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -4860,19 +5399,19 @@ var init_gemini = __esm(() => {
|
|
|
4860
5399
|
init_database();
|
|
4861
5400
|
init_pricing();
|
|
4862
5401
|
init_accounts();
|
|
4863
|
-
DEFAULT_GEMINI_TMP_DIR =
|
|
4864
|
-
DEFAULT_GEMINI_HISTORY_DIR =
|
|
5402
|
+
DEFAULT_GEMINI_TMP_DIR = join7(homedir6(), ".gemini", "tmp");
|
|
5403
|
+
DEFAULT_GEMINI_HISTORY_DIR = join7(homedir6(), ".gemini", "history");
|
|
4865
5404
|
});
|
|
4866
5405
|
|
|
4867
5406
|
// src/ingest/opencode.ts
|
|
4868
5407
|
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
4869
|
-
import { homedir as
|
|
4870
|
-
import { join as
|
|
5408
|
+
import { homedir as homedir7 } from "os";
|
|
5409
|
+
import { join as join8 } from "path";
|
|
4871
5410
|
function walkJsonFiles(dir, acc = []) {
|
|
4872
5411
|
if (!existsSync7(dir))
|
|
4873
5412
|
return acc;
|
|
4874
5413
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
4875
|
-
const p =
|
|
5414
|
+
const p = join8(dir, entry.name);
|
|
4876
5415
|
if (entry.isDirectory())
|
|
4877
5416
|
walkJsonFiles(p, acc);
|
|
4878
5417
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4892,7 +5431,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
4892
5431
|
return null;
|
|
4893
5432
|
}
|
|
4894
5433
|
async function ingestOpenCode(db, verbose = false) {
|
|
4895
|
-
const messageDir =
|
|
5434
|
+
const messageDir = join8(OPENCODE_STORAGE, "message");
|
|
4896
5435
|
const files = walkJsonFiles(messageDir);
|
|
4897
5436
|
let requests = 0;
|
|
4898
5437
|
const touched = new Set;
|
|
@@ -4975,7 +5514,7 @@ var init_opencode = __esm(() => {
|
|
|
4975
5514
|
init_database();
|
|
4976
5515
|
init_pricing();
|
|
4977
5516
|
init_accounts();
|
|
4978
|
-
OPENCODE_STORAGE =
|
|
5517
|
+
OPENCODE_STORAGE = join8(homedir7(), ".local", "share", "opencode", "storage");
|
|
4979
5518
|
});
|
|
4980
5519
|
|
|
4981
5520
|
// src/ingest/cursor.ts
|
|
@@ -5104,13 +5643,13 @@ var init_cursor = __esm(() => {
|
|
|
5104
5643
|
|
|
5105
5644
|
// src/ingest/pi.ts
|
|
5106
5645
|
import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
5107
|
-
import { homedir as
|
|
5108
|
-
import { join as
|
|
5646
|
+
import { homedir as homedir8 } from "os";
|
|
5647
|
+
import { join as join9 } from "path";
|
|
5109
5648
|
function walkSessions(dir, acc = []) {
|
|
5110
5649
|
if (!existsSync8(dir))
|
|
5111
5650
|
return acc;
|
|
5112
5651
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
5113
|
-
const p =
|
|
5652
|
+
const p = join9(dir, entry.name);
|
|
5114
5653
|
if (entry.isDirectory())
|
|
5115
5654
|
walkSessions(p, acc);
|
|
5116
5655
|
else if (entry.name.endsWith(".json"))
|
|
@@ -5197,13 +5736,13 @@ var PI_SESSION_DIR;
|
|
|
5197
5736
|
var init_pi = __esm(() => {
|
|
5198
5737
|
init_database();
|
|
5199
5738
|
init_accounts();
|
|
5200
|
-
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");
|
|
5201
5740
|
});
|
|
5202
5741
|
|
|
5203
5742
|
// src/ingest/hermes.ts
|
|
5204
5743
|
import { existsSync as existsSync9, statSync as statSync6 } from "fs";
|
|
5205
|
-
import { homedir as
|
|
5206
|
-
import { join as
|
|
5744
|
+
import { homedir as homedir9 } from "os";
|
|
5745
|
+
import { join as join10 } from "path";
|
|
5207
5746
|
function mapCostBasis(billingMode) {
|
|
5208
5747
|
if (billingMode === "subscription")
|
|
5209
5748
|
return "subscription_included";
|
|
@@ -5292,22 +5831,74 @@ var HERMES_DB;
|
|
|
5292
5831
|
var init_hermes = __esm(() => {
|
|
5293
5832
|
init_database();
|
|
5294
5833
|
init_accounts();
|
|
5295
|
-
HERMES_DB =
|
|
5834
|
+
HERMES_DB = join10(homedir9(), ".hermes", "state.db");
|
|
5296
5835
|
});
|
|
5297
5836
|
|
|
5298
5837
|
// src/ingest/loops.ts
|
|
5299
5838
|
import { existsSync as existsSync10 } from "fs";
|
|
5300
|
-
import { homedir as
|
|
5301
|
-
import { join as
|
|
5839
|
+
import { homedir as homedir10 } from "os";
|
|
5840
|
+
import { join as join11 } from "path";
|
|
5302
5841
|
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
5303
|
-
import {
|
|
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
|
+
}
|
|
5304
5895
|
function defaultLoopsDbPath() {
|
|
5305
5896
|
const resolved = dataDir2({
|
|
5306
5897
|
app: "loops",
|
|
5307
5898
|
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
5308
5899
|
});
|
|
5309
|
-
const legacy =
|
|
5310
|
-
return existsSync10(
|
|
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;
|
|
5311
5902
|
}
|
|
5312
5903
|
function loopsDbPath() {
|
|
5313
5904
|
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
@@ -5486,10 +6077,17 @@ async function ingestLoops(db, verbose = false) {
|
|
|
5486
6077
|
rollupSession(db, sessionId);
|
|
5487
6078
|
return { sessions: sessions.size, requests };
|
|
5488
6079
|
}
|
|
5489
|
-
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
6080
|
+
var PATHS_RESOLVER_KIND_ENV2, PATHS_RESOLVER_APP_SLUG_RE2, LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
5490
6081
|
var init_loops = __esm(() => {
|
|
5491
6082
|
init_database();
|
|
5492
6083
|
init_pricing();
|
|
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]+)*$/;
|
|
5493
6091
|
});
|
|
5494
6092
|
|
|
5495
6093
|
// src/ingest/claude-quota.ts
|
|
@@ -6130,26 +6728,26 @@ var init_open_projects = __esm(() => {
|
|
|
6130
6728
|
var exports_config = {};
|
|
6131
6729
|
__export(exports_config, {
|
|
6132
6730
|
setConfigValue: () => setConfigValue,
|
|
6133
|
-
saveConfig: () =>
|
|
6134
|
-
loadConfig: () =>
|
|
6731
|
+
saveConfig: () => saveConfig,
|
|
6732
|
+
loadConfig: () => loadConfig,
|
|
6135
6733
|
getConfigValue: () => getConfigValue
|
|
6136
6734
|
});
|
|
6137
|
-
import { existsSync as existsSync13, readFileSync as
|
|
6138
|
-
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";
|
|
6139
6737
|
function getConfigPath() {
|
|
6140
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
6738
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
|
|
6141
6739
|
}
|
|
6142
|
-
function
|
|
6740
|
+
function loadConfig() {
|
|
6143
6741
|
try {
|
|
6144
6742
|
const configPath = getConfigPath();
|
|
6145
6743
|
if (existsSync13(configPath)) {
|
|
6146
|
-
const raw =
|
|
6744
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
6147
6745
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
6148
6746
|
}
|
|
6149
6747
|
} catch {}
|
|
6150
6748
|
return { ...DEFAULTS };
|
|
6151
6749
|
}
|
|
6152
|
-
function
|
|
6750
|
+
function saveConfig(config) {
|
|
6153
6751
|
const configPath = getConfigPath();
|
|
6154
6752
|
const dir = dirname2(configPath);
|
|
6155
6753
|
if (!existsSync13(dir))
|
|
@@ -6158,11 +6756,11 @@ function saveConfig2(config) {
|
|
|
6158
6756
|
`);
|
|
6159
6757
|
}
|
|
6160
6758
|
function getConfigValue(key) {
|
|
6161
|
-
const config =
|
|
6759
|
+
const config = loadConfig();
|
|
6162
6760
|
return config[key] ?? null;
|
|
6163
6761
|
}
|
|
6164
6762
|
function setConfigValue(key, value) {
|
|
6165
|
-
const config =
|
|
6763
|
+
const config = loadConfig();
|
|
6166
6764
|
let parsed = value;
|
|
6167
6765
|
if (value === "true")
|
|
6168
6766
|
parsed = true;
|
|
@@ -6178,7 +6776,7 @@ function setConfigValue(key, value) {
|
|
|
6178
6776
|
} catch {}
|
|
6179
6777
|
}
|
|
6180
6778
|
config[key] = parsed;
|
|
6181
|
-
|
|
6779
|
+
saveConfig(config);
|
|
6182
6780
|
}
|
|
6183
6781
|
var DEFAULTS;
|
|
6184
6782
|
var init_config = __esm(() => {
|
|
@@ -6236,7 +6834,7 @@ __export(exports_webhooks, {
|
|
|
6236
6834
|
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
6237
6835
|
});
|
|
6238
6836
|
async function checkAndFireWebhooks(db) {
|
|
6239
|
-
const config =
|
|
6837
|
+
const config = loadConfig();
|
|
6240
6838
|
const url = config["webhook-url"];
|
|
6241
6839
|
if (!url)
|
|
6242
6840
|
return;
|
|
@@ -6337,38 +6935,38 @@ var exports_watch = {};
|
|
|
6337
6935
|
__export(exports_watch, {
|
|
6338
6936
|
watchCosts: () => watchCosts
|
|
6339
6937
|
});
|
|
6340
|
-
import
|
|
6938
|
+
import chalk4 from "chalk";
|
|
6341
6939
|
import { watch } from "fs";
|
|
6342
6940
|
function fmt3(usd) {
|
|
6343
|
-
return
|
|
6941
|
+
return chalk4.green(`$${usd.toFixed(4)}`);
|
|
6344
6942
|
}
|
|
6345
6943
|
function renderHeader(todayUsd, weekUsd, mode) {
|
|
6346
6944
|
process.stdout.write("\x1B[H\x1B[2J");
|
|
6347
|
-
console.log(
|
|
6348
|
-
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"));
|
|
6349
6947
|
console.log(` Today: ${fmt3(todayUsd)} Week: ${fmt3(weekUsd)}`);
|
|
6350
|
-
console.log(
|
|
6351
|
-
console.log(
|
|
6352
|
-
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"));
|
|
6353
6951
|
}
|
|
6354
6952
|
function agentLabel(agent) {
|
|
6355
6953
|
if (agent === "claude")
|
|
6356
|
-
return
|
|
6954
|
+
return chalk4.blue("[claude]");
|
|
6357
6955
|
if (agent === "codex")
|
|
6358
|
-
return
|
|
6956
|
+
return chalk4.yellow("[codex] ");
|
|
6359
6957
|
if (agent === "gemini")
|
|
6360
|
-
return
|
|
6958
|
+
return chalk4.green("[gemini]");
|
|
6361
6959
|
if (agent === "takumi")
|
|
6362
|
-
return
|
|
6960
|
+
return chalk4.magenta("[takumi]");
|
|
6363
6961
|
if (agent === "opencode")
|
|
6364
|
-
return
|
|
6962
|
+
return chalk4.cyan("[opncde]");
|
|
6365
6963
|
if (agent === "cursor")
|
|
6366
|
-
return
|
|
6964
|
+
return chalk4.white("[cursor]");
|
|
6367
6965
|
if (agent === "pi")
|
|
6368
|
-
return
|
|
6966
|
+
return chalk4.white("[pi ]");
|
|
6369
6967
|
if (agent === "hermes")
|
|
6370
|
-
return
|
|
6371
|
-
return
|
|
6968
|
+
return chalk4.white("[hermes]");
|
|
6969
|
+
return chalk4.gray(`[${agent.slice(0, 6).padEnd(6)}]`);
|
|
6372
6970
|
}
|
|
6373
6971
|
async function watchCosts(opts) {
|
|
6374
6972
|
const store = getStore();
|
|
@@ -6388,7 +6986,7 @@ async function watchCosts(opts) {
|
|
|
6388
6986
|
renderHeader(initialSummaryToday.total_usd, initialSummaryWeek.total_usd, mode);
|
|
6389
6987
|
if (daemon) {
|
|
6390
6988
|
const paths = getWatchPaths();
|
|
6391
|
-
console.log(
|
|
6989
|
+
console.log(chalk4.dim(`
|
|
6392
6990
|
Watching ${paths.length} paths \u2014 sync on change, refresh every ${opts.interval}s
|
|
6393
6991
|
`));
|
|
6394
6992
|
for (const p of paths) {
|
|
@@ -6401,11 +6999,11 @@ async function watchCosts(opts) {
|
|
|
6401
6999
|
}
|
|
6402
7000
|
}
|
|
6403
7001
|
} else if (cloud) {
|
|
6404
|
-
console.log(
|
|
7002
|
+
console.log(chalk4.dim(`
|
|
6405
7003
|
Streaming from cloud API every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
6406
7004
|
`));
|
|
6407
7005
|
} else {
|
|
6408
|
-
console.log(
|
|
7006
|
+
console.log(chalk4.dim(`
|
|
6409
7007
|
Polling every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
6410
7008
|
`));
|
|
6411
7009
|
}
|
|
@@ -6453,9 +7051,9 @@ async function watchCosts(opts) {
|
|
|
6453
7051
|
for (const line of lines)
|
|
6454
7052
|
console.log(line);
|
|
6455
7053
|
if (lines.length === 0)
|
|
6456
|
-
console.log(
|
|
7054
|
+
console.log(chalk4.dim(" Waiting for new requests..."));
|
|
6457
7055
|
const suffix = daemon ? `file watch + ${opts.interval}s refresh` : cloud ? `cloud stream every ${opts.interval}s` : `polling every ${opts.interval}s`;
|
|
6458
|
-
console.log(
|
|
7056
|
+
console.log(chalk4.dim(`
|
|
6459
7057
|
Last updated: ${new Date().toLocaleTimeString()} \u2014 ${suffix} \u2014 Ctrl+C to exit`));
|
|
6460
7058
|
}
|
|
6461
7059
|
if (daemon)
|
|
@@ -6463,7 +7061,7 @@ async function watchCosts(opts) {
|
|
|
6463
7061
|
await poll();
|
|
6464
7062
|
const timer = setInterval(() => {
|
|
6465
7063
|
poll().catch((e) => {
|
|
6466
|
-
console.log(
|
|
7064
|
+
console.log(chalk4.dim(`
|
|
6467
7065
|
Poll error: ${e instanceof Error ? e.message : String(e)} \u2014 retrying next tick`));
|
|
6468
7066
|
});
|
|
6469
7067
|
}, opts.interval * 1000);
|
|
@@ -6471,7 +7069,7 @@ async function watchCosts(opts) {
|
|
|
6471
7069
|
clearInterval(timer);
|
|
6472
7070
|
if (ingestTimer)
|
|
6473
7071
|
clearTimeout(ingestTimer);
|
|
6474
|
-
console.log(
|
|
7072
|
+
console.log(chalk4.dim(`
|
|
6475
7073
|
|
|
6476
7074
|
Stopped watching.`));
|
|
6477
7075
|
process.exit(0);
|
|
@@ -6526,7 +7124,7 @@ function sqliteToPostgres(sql) {
|
|
|
6526
7124
|
// src/db/sync-pg.ts
|
|
6527
7125
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
6528
7126
|
import { fileURLToPath } from "url";
|
|
6529
|
-
import { dirname as dirname3, join as
|
|
7127
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
6530
7128
|
import { existsSync as existsSync15 } from "fs";
|
|
6531
7129
|
function translateSqliteDates(sql) {
|
|
6532
7130
|
let s = sql;
|
|
@@ -6552,9 +7150,9 @@ function translateSqliteDates(sql) {
|
|
|
6552
7150
|
function resolveWorkerPath() {
|
|
6553
7151
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
6554
7152
|
const candidates = [
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
7153
|
+
join14(here, "pg-sync-worker.ts"),
|
|
7154
|
+
join14(here, "pg-sync-worker.js"),
|
|
7155
|
+
join14(here, "..", "db", "pg-sync-worker.js")
|
|
6558
7156
|
];
|
|
6559
7157
|
for (const candidate of candidates)
|
|
6560
7158
|
if (existsSync15(candidate))
|
|
@@ -8365,12 +8963,9 @@ var exports_serve = {};
|
|
|
8365
8963
|
__export(exports_serve, {
|
|
8366
8964
|
startServer: () => startServer,
|
|
8367
8965
|
openApiDocument: () => openApiDocument,
|
|
8368
|
-
createServerFetch: () => createServerFetch,
|
|
8369
8966
|
createHandler: () => createHandler
|
|
8370
8967
|
});
|
|
8371
8968
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8372
|
-
import { existsSync as existsSync16 } from "fs";
|
|
8373
|
-
import { resolve as resolve2, sep } from "path";
|
|
8374
8969
|
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
8375
8970
|
function openApiDocument() {
|
|
8376
8971
|
const spec = openApiSpec;
|
|
@@ -8431,37 +9026,6 @@ function requiredScopesForRequest(method, path) {
|
|
|
8431
9026
|
function stringArray(value) {
|
|
8432
9027
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
8433
9028
|
}
|
|
8434
|
-
function dashboardPath(root, pathname) {
|
|
8435
|
-
let decoded;
|
|
8436
|
-
try {
|
|
8437
|
-
decoded = decodeURIComponent(pathname);
|
|
8438
|
-
} catch {
|
|
8439
|
-
return null;
|
|
8440
|
-
}
|
|
8441
|
-
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
8442
|
-
const rootPath = resolve2(root);
|
|
8443
|
-
const filePath = resolve2(rootPath, relativePath);
|
|
8444
|
-
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
8445
|
-
}
|
|
8446
|
-
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
8447
|
-
return async function fetch2(req) {
|
|
8448
|
-
const url = new URL(req.url);
|
|
8449
|
-
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
8450
|
-
return apiHandler(req);
|
|
8451
|
-
}
|
|
8452
|
-
if (existsSync16(dashboardDir)) {
|
|
8453
|
-
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
8454
|
-
if (filePath && existsSync16(filePath)) {
|
|
8455
|
-
return new Response(Bun.file(filePath));
|
|
8456
|
-
}
|
|
8457
|
-
const indexPath = dashboardPath(dashboardDir, "/");
|
|
8458
|
-
if (indexPath && existsSync16(indexPath)) {
|
|
8459
|
-
return new Response(Bun.file(indexPath));
|
|
8460
|
-
}
|
|
8461
|
-
}
|
|
8462
|
-
return apiHandler(req);
|
|
8463
|
-
};
|
|
8464
|
-
}
|
|
8465
9029
|
function applyFields(obj, fields) {
|
|
8466
9030
|
if (!fields || fields.length === 0)
|
|
8467
9031
|
return obj;
|
|
@@ -8985,13 +9549,13 @@ function startServer(port = 3456, options = {}) {
|
|
|
8985
9549
|
const server = Bun.serve({
|
|
8986
9550
|
port,
|
|
8987
9551
|
hostname: hostname2,
|
|
8988
|
-
fetch:
|
|
9552
|
+
fetch: apiHandler
|
|
8989
9553
|
});
|
|
8990
9554
|
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
8991
9555
|
log(`economy-serve listening on ${address}`);
|
|
8992
9556
|
return server;
|
|
8993
9557
|
}
|
|
8994
|
-
var CORS, APP = "economy", ECONOMY_WRITE_SCOPE, AGENT_ERROR, SYNC_SOURCES
|
|
9558
|
+
var CORS, APP = "economy", ECONOMY_WRITE_SCOPE, AGENT_ERROR, SYNC_SOURCES;
|
|
8995
9559
|
var init_serve = __esm(() => {
|
|
8996
9560
|
init_database();
|
|
8997
9561
|
init_pricing();
|
|
@@ -9012,8 +9576,6 @@ var init_serve = __esm(() => {
|
|
|
9012
9576
|
ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
9013
9577
|
AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
9014
9578
|
SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
9015
|
-
DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
9016
|
-
FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
9017
9579
|
});
|
|
9018
9580
|
|
|
9019
9581
|
// src/cli/commands/menubar.ts
|
|
@@ -9024,16 +9586,16 @@ __export(exports_menubar, {
|
|
|
9024
9586
|
menubarStart: () => menubarStart,
|
|
9025
9587
|
menubarInstall: () => menubarInstall
|
|
9026
9588
|
});
|
|
9027
|
-
import
|
|
9589
|
+
import chalk5 from "chalk";
|
|
9028
9590
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9029
|
-
import { cpSync, existsSync as
|
|
9591
|
+
import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
9030
9592
|
import { tmpdir, arch } from "os";
|
|
9031
|
-
import { join as
|
|
9593
|
+
import { join as join15 } from "path";
|
|
9032
9594
|
function getArch() {
|
|
9033
9595
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
9034
9596
|
}
|
|
9035
9597
|
function isInstalled() {
|
|
9036
|
-
return
|
|
9598
|
+
return existsSync16(APP_PATH);
|
|
9037
9599
|
}
|
|
9038
9600
|
function isRunning() {
|
|
9039
9601
|
try {
|
|
@@ -9045,13 +9607,13 @@ function isRunning() {
|
|
|
9045
9607
|
}
|
|
9046
9608
|
async function menubarInstall(opts) {
|
|
9047
9609
|
if (isInstalled() && !opts.force) {
|
|
9048
|
-
console.log(
|
|
9049
|
-
console.log(
|
|
9610
|
+
console.log(chalk5.yellow("Economy Bar is already installed. Use --force to reinstall."));
|
|
9611
|
+
console.log(chalk5.dim(` Location: ${APP_PATH}`));
|
|
9050
9612
|
return;
|
|
9051
9613
|
}
|
|
9052
9614
|
const cpuArch = getArch();
|
|
9053
|
-
console.log(
|
|
9054
|
-
console.log(
|
|
9615
|
+
console.log(chalk5.cyan(`\u2192 Detecting architecture: ${cpuArch}`));
|
|
9616
|
+
console.log(chalk5.cyan("\u2192 Fetching latest release info..."));
|
|
9055
9617
|
let assetUrl;
|
|
9056
9618
|
try {
|
|
9057
9619
|
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
|
|
@@ -9067,54 +9629,54 @@ async function menubarInstall(opts) {
|
|
|
9067
9629
|
throw new Error(`No asset found for ${assetName}. Check releases at https://github.com/${REPO}/releases`);
|
|
9068
9630
|
assetUrl = asset.browser_download_url;
|
|
9069
9631
|
} catch (e) {
|
|
9070
|
-
console.error(
|
|
9632
|
+
console.error(chalk5.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
9071
9633
|
process.exit(1);
|
|
9072
9634
|
}
|
|
9073
|
-
const zipPath =
|
|
9074
|
-
const extractDir =
|
|
9075
|
-
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}...`));
|
|
9076
9638
|
try {
|
|
9077
9639
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
9078
9640
|
if (!res.ok)
|
|
9079
9641
|
throw new Error(`Download failed: ${res.status}`);
|
|
9080
9642
|
const buffer = await res.arrayBuffer();
|
|
9081
9643
|
writeFileSync3(zipPath, Buffer.from(buffer));
|
|
9082
|
-
console.log(
|
|
9644
|
+
console.log(chalk5.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
9083
9645
|
} catch (e) {
|
|
9084
|
-
console.error(
|
|
9646
|
+
console.error(chalk5.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
9085
9647
|
process.exit(1);
|
|
9086
9648
|
}
|
|
9087
|
-
console.log(
|
|
9649
|
+
console.log(chalk5.cyan("\u2192 Installing to /Applications..."));
|
|
9088
9650
|
try {
|
|
9089
9651
|
rmSync(extractDir, { recursive: true, force: true });
|
|
9090
9652
|
mkdirSync4(extractDir, { recursive: true });
|
|
9091
9653
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
9092
9654
|
if (isInstalled())
|
|
9093
9655
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
9094
|
-
cpSync(
|
|
9656
|
+
cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
9095
9657
|
try {
|
|
9096
9658
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
9097
9659
|
} catch {}
|
|
9098
9660
|
rmSync(zipPath, { force: true });
|
|
9099
9661
|
rmSync(extractDir, { recursive: true, force: true });
|
|
9100
|
-
console.log(
|
|
9662
|
+
console.log(chalk5.green(`\u2713 Installed to ${APP_PATH}`));
|
|
9101
9663
|
} catch (e) {
|
|
9102
|
-
console.error(
|
|
9664
|
+
console.error(chalk5.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
9103
9665
|
process.exit(1);
|
|
9104
9666
|
}
|
|
9105
|
-
console.log(
|
|
9667
|
+
console.log(chalk5.cyan("\u2192 Launching Economy Bar..."));
|
|
9106
9668
|
try {
|
|
9107
9669
|
execFileSync3("open", [APP_PATH], { stdio: "ignore" });
|
|
9108
|
-
console.log(
|
|
9670
|
+
console.log(chalk5.bold.green(`
|
|
9109
9671
|
\u2713 Economy Bar is running in your menu bar!`));
|
|
9110
|
-
console.log(
|
|
9672
|
+
console.log(chalk5.dim(" Make sure economy serve is running: economy serve"));
|
|
9111
9673
|
} catch (e) {
|
|
9112
|
-
console.log(
|
|
9674
|
+
console.log(chalk5.yellow("\u26A0 Installed but could not auto-launch. Open from /Applications manually."));
|
|
9113
9675
|
}
|
|
9114
9676
|
}
|
|
9115
9677
|
function menubarUninstall() {
|
|
9116
9678
|
if (!isInstalled()) {
|
|
9117
|
-
console.log(
|
|
9679
|
+
console.log(chalk5.yellow("Economy Bar is not installed."));
|
|
9118
9680
|
return;
|
|
9119
9681
|
}
|
|
9120
9682
|
if (isRunning()) {
|
|
@@ -9124,26 +9686,26 @@ function menubarUninstall() {
|
|
|
9124
9686
|
} catch {}
|
|
9125
9687
|
}
|
|
9126
9688
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
9127
|
-
console.log(
|
|
9689
|
+
console.log(chalk5.green("\u2713 Economy Bar uninstalled"));
|
|
9128
9690
|
}
|
|
9129
9691
|
function menubarStart() {
|
|
9130
9692
|
if (!isInstalled()) {
|
|
9131
|
-
console.error(
|
|
9693
|
+
console.error(chalk5.red("Economy Bar is not installed. Run: economy menubar install"));
|
|
9132
9694
|
process.exit(1);
|
|
9133
9695
|
}
|
|
9134
9696
|
execFileSync3("open", [APP_PATH], { stdio: "ignore" });
|
|
9135
|
-
console.log(
|
|
9697
|
+
console.log(chalk5.green("\u2713 Economy Bar launched"));
|
|
9136
9698
|
}
|
|
9137
9699
|
function menubarStop() {
|
|
9138
9700
|
if (!isRunning()) {
|
|
9139
|
-
console.log(
|
|
9701
|
+
console.log(chalk5.yellow("Economy Bar is not running."));
|
|
9140
9702
|
return;
|
|
9141
9703
|
}
|
|
9142
9704
|
try {
|
|
9143
9705
|
execFileSync3("osascript", ["-e", 'quit app "Economy Bar"'], { stdio: "ignore" });
|
|
9144
|
-
console.log(
|
|
9706
|
+
console.log(chalk5.green("\u2713 Economy Bar stopped"));
|
|
9145
9707
|
} catch {
|
|
9146
|
-
console.log(
|
|
9708
|
+
console.log(chalk5.yellow("Could not quit Economy Bar gracefully"));
|
|
9147
9709
|
}
|
|
9148
9710
|
}
|
|
9149
9711
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/economy";
|
|
@@ -9152,369 +9714,10 @@ var init_menubar = () => {};
|
|
|
9152
9714
|
// src/cli/index.ts
|
|
9153
9715
|
import { Command } from "commander";
|
|
9154
9716
|
import { registerEventsCommands } from "@hasna/events/commander";
|
|
9155
|
-
import
|
|
9156
|
-
|
|
9157
|
-
// src/cli/brains.ts
|
|
9158
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
9159
|
-
import { join as join3 } from "path";
|
|
9160
|
-
import chalk from "chalk";
|
|
9161
|
-
|
|
9162
|
-
// src/lib/gatherer.ts
|
|
9163
|
-
init_store();
|
|
9164
|
-
var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
|
|
9165
|
-
function hasCostData(summary) {
|
|
9166
|
-
return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
|
|
9167
|
-
}
|
|
9168
|
-
var gatherTrainingData = async (options = {}) => {
|
|
9169
|
-
const limit = options.limit ?? 500;
|
|
9170
|
-
const examples = [];
|
|
9171
|
-
try {
|
|
9172
|
-
const store = getStore();
|
|
9173
|
-
const periods = ["today", "week", "month", "all"];
|
|
9174
|
-
for (const period of periods) {
|
|
9175
|
-
try {
|
|
9176
|
-
const s = await store.summary(period);
|
|
9177
|
-
if (!hasCostData(s))
|
|
9178
|
-
continue;
|
|
9179
|
-
examples.push({
|
|
9180
|
-
messages: [
|
|
9181
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9182
|
-
{ role: "user", content: `What did I spend on AI ${period === "all" ? "in total" : period}?` },
|
|
9183
|
-
{
|
|
9184
|
-
role: "assistant",
|
|
9185
|
-
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.`
|
|
9186
|
-
}
|
|
9187
|
-
]
|
|
9188
|
-
});
|
|
9189
|
-
} catch {}
|
|
9190
|
-
}
|
|
9191
|
-
const sessions = await store.sessions({
|
|
9192
|
-
limit: Math.min(Math.floor(limit / 4), 50),
|
|
9193
|
-
since: options.since?.toISOString().substring(0, 10)
|
|
9194
|
-
});
|
|
9195
|
-
for (const s of sessions) {
|
|
9196
|
-
examples.push({
|
|
9197
|
-
messages: [
|
|
9198
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9199
|
-
{
|
|
9200
|
-
role: "user",
|
|
9201
|
-
content: `How much did the session "${s.id.substring(0, 12)}" cost?`
|
|
9202
|
-
},
|
|
9203
|
-
{
|
|
9204
|
-
role: "assistant",
|
|
9205
|
-
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)}.`
|
|
9206
|
-
}
|
|
9207
|
-
]
|
|
9208
|
-
});
|
|
9209
|
-
examples.push({
|
|
9210
|
-
messages: [
|
|
9211
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9212
|
-
{
|
|
9213
|
-
role: "user",
|
|
9214
|
-
content: `What was the token usage for session ${s.id.substring(0, 12)}?`
|
|
9215
|
-
},
|
|
9216
|
-
{
|
|
9217
|
-
role: "assistant",
|
|
9218
|
-
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}).`
|
|
9219
|
-
}
|
|
9220
|
-
]
|
|
9221
|
-
});
|
|
9222
|
-
if (examples.length >= limit)
|
|
9223
|
-
break;
|
|
9224
|
-
}
|
|
9225
|
-
const modelBreakdown = await store.modelBreakdown();
|
|
9226
|
-
if (modelBreakdown.length > 0) {
|
|
9227
|
-
const topModels = modelBreakdown.slice(0, 5);
|
|
9228
|
-
examples.push({
|
|
9229
|
-
messages: [
|
|
9230
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9231
|
-
{ role: "user", content: "Which AI models have I spent the most on?" },
|
|
9232
|
-
{
|
|
9233
|
-
role: "assistant",
|
|
9234
|
-
content: `Model cost breakdown (top ${topModels.length}):
|
|
9235
|
-
${topModels.map((m) => `- ${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)}, ${m.requests} requests, ${m.total_tokens.toLocaleString()} tokens`).join(`
|
|
9236
|
-
`)}`
|
|
9237
|
-
}
|
|
9238
|
-
]
|
|
9239
|
-
});
|
|
9240
|
-
for (const m of topModels) {
|
|
9241
|
-
examples.push({
|
|
9242
|
-
messages: [
|
|
9243
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9244
|
-
{ role: "user", content: `How much have I spent on ${m.model}?` },
|
|
9245
|
-
{
|
|
9246
|
-
role: "assistant",
|
|
9247
|
-
content: `${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)} total across ${m.requests.toLocaleString()} requests and ${m.total_tokens.toLocaleString()} tokens.`
|
|
9248
|
-
}
|
|
9249
|
-
]
|
|
9250
|
-
});
|
|
9251
|
-
}
|
|
9252
|
-
}
|
|
9253
|
-
const projectBreakdown = await store.projectBreakdown();
|
|
9254
|
-
if (projectBreakdown.length > 0) {
|
|
9255
|
-
const topProjects = projectBreakdown.slice(0, 5);
|
|
9256
|
-
examples.push({
|
|
9257
|
-
messages: [
|
|
9258
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9259
|
-
{ role: "user", content: "Which projects are costing the most?" },
|
|
9260
|
-
{
|
|
9261
|
-
role: "assistant",
|
|
9262
|
-
content: `Project cost breakdown (top ${topProjects.length}):
|
|
9263
|
-
${topProjects.map((p) => `- ${p.project_name || "unknown"}: $${p.cost_usd.toFixed(4)}, ${p.sessions} sessions`).join(`
|
|
9264
|
-
`)}`
|
|
9265
|
-
}
|
|
9266
|
-
]
|
|
9267
|
-
});
|
|
9268
|
-
for (const p of topProjects.slice(0, 3)) {
|
|
9269
|
-
examples.push({
|
|
9270
|
-
messages: [
|
|
9271
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9272
|
-
{ role: "user", content: `What is the AI spend for project "${p.project_name}"?` },
|
|
9273
|
-
{
|
|
9274
|
-
role: "assistant",
|
|
9275
|
-
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"}.`
|
|
9276
|
-
}
|
|
9277
|
-
]
|
|
9278
|
-
});
|
|
9279
|
-
}
|
|
9280
|
-
}
|
|
9281
|
-
try {
|
|
9282
|
-
const budgets = await store.listBudgets();
|
|
9283
|
-
if (budgets.length > 0) {
|
|
9284
|
-
examples.push({
|
|
9285
|
-
messages: [
|
|
9286
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9287
|
-
{ role: "user", content: "How am I tracking against my AI spending budgets?" },
|
|
9288
|
-
{
|
|
9289
|
-
role: "assistant",
|
|
9290
|
-
content: `Budget status:
|
|
9291
|
-
${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(`
|
|
9292
|
-
`)}`
|
|
9293
|
-
}
|
|
9294
|
-
]
|
|
9295
|
-
});
|
|
9296
|
-
}
|
|
9297
|
-
} catch {}
|
|
9298
|
-
try {
|
|
9299
|
-
const goals = await store.listGoals();
|
|
9300
|
-
if (goals.length > 0) {
|
|
9301
|
-
examples.push({
|
|
9302
|
-
messages: [
|
|
9303
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9304
|
-
{ role: "user", content: "Am I on track with my AI cost reduction goals?" },
|
|
9305
|
-
{
|
|
9306
|
-
role: "assistant",
|
|
9307
|
-
content: `Goal progress:
|
|
9308
|
-
${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(`
|
|
9309
|
-
`)}`
|
|
9310
|
-
}
|
|
9311
|
-
]
|
|
9312
|
-
});
|
|
9313
|
-
}
|
|
9314
|
-
} catch {}
|
|
9315
|
-
const weekSummary = await store.summary("week");
|
|
9316
|
-
const monthSummary = await store.summary("month");
|
|
9317
|
-
if (weekSummary.total_usd > 0) {
|
|
9318
|
-
const projectedMonthly = weekSummary.total_usd / 7 * 30;
|
|
9319
|
-
examples.push({
|
|
9320
|
-
messages: [
|
|
9321
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9322
|
-
{ role: "user", content: "Based on my usage, what will I spend this month?" },
|
|
9323
|
-
{
|
|
9324
|
-
role: "assistant",
|
|
9325
|
-
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)}.`
|
|
9326
|
-
}
|
|
9327
|
-
]
|
|
9328
|
-
});
|
|
9329
|
-
}
|
|
9330
|
-
if (modelBreakdown.length > 1) {
|
|
9331
|
-
const expensive = modelBreakdown[0];
|
|
9332
|
-
const cheaper = modelBreakdown[modelBreakdown.length - 1];
|
|
9333
|
-
examples.push({
|
|
9334
|
-
messages: [
|
|
9335
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9336
|
-
{ role: "user", content: "How can I reduce my AI API costs?" },
|
|
9337
|
-
{
|
|
9338
|
-
role: "assistant",
|
|
9339
|
-
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.`
|
|
9340
|
-
}
|
|
9341
|
-
]
|
|
9342
|
-
});
|
|
9343
|
-
}
|
|
9344
|
-
} catch {}
|
|
9345
|
-
const finalExamples = examples.slice(0, limit);
|
|
9346
|
-
return { source: "economy", examples: finalExamples, count: finalExamples.length };
|
|
9347
|
-
};
|
|
9348
|
-
|
|
9349
|
-
// src/lib/model-config.ts
|
|
9350
|
-
init_database();
|
|
9351
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
9352
|
-
import { dirname, join as join2 } from "path";
|
|
9353
|
-
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
9354
|
-
function getModelConfigPath() {
|
|
9355
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
|
|
9356
|
-
}
|
|
9357
|
-
function loadConfig() {
|
|
9358
|
-
try {
|
|
9359
|
-
const configPath = getModelConfigPath();
|
|
9360
|
-
if (existsSync2(configPath)) {
|
|
9361
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
9362
|
-
}
|
|
9363
|
-
} catch {}
|
|
9364
|
-
return {};
|
|
9365
|
-
}
|
|
9366
|
-
function saveConfig(config) {
|
|
9367
|
-
const configPath = getModelConfigPath();
|
|
9368
|
-
const dir = dirname(configPath);
|
|
9369
|
-
if (!existsSync2(dir))
|
|
9370
|
-
mkdirSync2(dir, { recursive: true });
|
|
9371
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
9372
|
-
`);
|
|
9373
|
-
}
|
|
9374
|
-
function getActiveModel() {
|
|
9375
|
-
return loadConfig().activeModel ?? DEFAULT_MODEL;
|
|
9376
|
-
}
|
|
9377
|
-
function setActiveModel(id) {
|
|
9378
|
-
const config = loadConfig();
|
|
9379
|
-
config.activeModel = id;
|
|
9380
|
-
saveConfig(config);
|
|
9381
|
-
}
|
|
9382
|
-
function clearActiveModel() {
|
|
9383
|
-
const config = loadConfig();
|
|
9384
|
-
delete config.activeModel;
|
|
9385
|
-
saveConfig(config);
|
|
9386
|
-
}
|
|
9387
|
-
|
|
9388
|
-
// src/cli/brains.ts
|
|
9389
|
-
init_database();
|
|
9390
|
-
function registerBrainsCommand(program) {
|
|
9391
|
-
const brainsCmd = program.command("brains").description("Fine-tune an AI model on your economy cost data");
|
|
9392
|
-
brainsCmd.command("gather").description("Gather training data from economy cost data and write to the economy data root (training/)").option("--limit <n>", "Maximum number of training examples", "500").option("--output <path>", "Output file path (default: <data-root>/training/training-<timestamp>.jsonl)").action(async (opts) => {
|
|
9393
|
-
const limit = opts.limit ? parseInt(opts.limit, 10) : 500;
|
|
9394
|
-
console.log(chalk.cyan(`Gathering up to ${limit} training examples from economy data...`));
|
|
9395
|
-
try {
|
|
9396
|
-
const result = await gatherTrainingData({ limit });
|
|
9397
|
-
if (result.count === 0) {
|
|
9398
|
-
console.log(chalk.yellow("No training examples found. Make sure you have cost data synced."));
|
|
9399
|
-
console.log(chalk.dim("Run: economy sync"));
|
|
9400
|
-
return;
|
|
9401
|
-
}
|
|
9402
|
-
const defaultDir = join3(getDataDir(), "training");
|
|
9403
|
-
await mkdir(defaultDir, { recursive: true });
|
|
9404
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
9405
|
-
const outputPath = opts.output ?? join3(defaultDir, `training-${timestamp}.jsonl`);
|
|
9406
|
-
const jsonl = result.examples.map((ex) => JSON.stringify(ex)).join(`
|
|
9407
|
-
`);
|
|
9408
|
-
await writeFile(outputPath, jsonl, "utf-8");
|
|
9409
|
-
console.log(chalk.green(`\u2713 Gathered ${result.count} training examples`));
|
|
9410
|
-
console.log(chalk.dim(` Written to: ${outputPath}`));
|
|
9411
|
-
console.log(`
|
|
9412
|
-
${chalk.dim("Next step:")} economy brains train --base-model gpt-4o-mini`);
|
|
9413
|
-
} catch (e) {
|
|
9414
|
-
console.error(chalk.red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
9415
|
-
process.exit(1);
|
|
9416
|
-
}
|
|
9417
|
-
});
|
|
9418
|
-
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 <data-root>/training/)").action(async (opts) => {
|
|
9419
|
-
const baseModel = opts.baseModel ?? "gpt-4o-mini";
|
|
9420
|
-
const name = opts.name ?? "economy-assistant";
|
|
9421
|
-
console.log(chalk.cyan("Starting fine-tuning job..."));
|
|
9422
|
-
console.log(chalk.dim(` Base model: ${baseModel}`));
|
|
9423
|
-
console.log(chalk.dim(` Name: ${name}`));
|
|
9424
|
-
let datasetPath = opts.dataset;
|
|
9425
|
-
if (!datasetPath) {
|
|
9426
|
-
const { readdirSync: readdirSync2 } = await import("fs");
|
|
9427
|
-
const trainingDir = join3(getDataDir(), "training");
|
|
9428
|
-
try {
|
|
9429
|
-
const files = readdirSync2(trainingDir).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
9430
|
-
if (files.length === 0) {
|
|
9431
|
-
console.error(chalk.red("No training data found. Run: economy brains gather"));
|
|
9432
|
-
process.exit(1);
|
|
9433
|
-
}
|
|
9434
|
-
datasetPath = join3(trainingDir, files[0]);
|
|
9435
|
-
console.log(chalk.dim(` Dataset: ${datasetPath}`));
|
|
9436
|
-
} catch {
|
|
9437
|
-
console.error(chalk.red("Training directory not found. Run: economy brains gather"));
|
|
9438
|
-
process.exit(1);
|
|
9439
|
-
}
|
|
9440
|
-
}
|
|
9441
|
-
try {
|
|
9442
|
-
const brains = await import("@hasna/brains");
|
|
9443
|
-
const startFinetune = brains["startFinetune"] ?? brains["start_finetune"];
|
|
9444
|
-
if (typeof startFinetune !== "function") {
|
|
9445
|
-
console.error(chalk.red("@hasna/brains not found or startFinetune not exported."));
|
|
9446
|
-
console.error(chalk.dim("Install with: bun add @hasna/brains"));
|
|
9447
|
-
process.exit(1);
|
|
9448
|
-
}
|
|
9449
|
-
const job = await startFinetune({
|
|
9450
|
-
provider: "openai",
|
|
9451
|
-
baseModel,
|
|
9452
|
-
name,
|
|
9453
|
-
dataset: datasetPath
|
|
9454
|
-
});
|
|
9455
|
-
const jobId = job["id"] ?? job["fine_tune_job_id"] ?? job["jobId"];
|
|
9456
|
-
console.log(chalk.green(`\u2713 Fine-tuning job started: ${String(jobId ?? "unknown")}`));
|
|
9457
|
-
console.log(`
|
|
9458
|
-
${chalk.dim("Check status:")} economy brains status ${String(jobId ?? "")}`);
|
|
9459
|
-
console.log(`${chalk.dim("When complete, set model:")} economy brains model set <model-id>`);
|
|
9460
|
-
} catch (e) {
|
|
9461
|
-
console.error(chalk.red(`Error starting fine-tune: ${e instanceof Error ? e.message : String(e)}`));
|
|
9462
|
-
process.exit(1);
|
|
9463
|
-
}
|
|
9464
|
-
});
|
|
9465
|
-
const modelCmd = brainsCmd.command("model").description("View or set the active fine-tuned model").action(() => {
|
|
9466
|
-
const active = getActiveModel();
|
|
9467
|
-
const isDefault = active === DEFAULT_MODEL;
|
|
9468
|
-
console.log(`Active model: ${chalk.cyan(active)}${isDefault ? chalk.dim(" (default)") : chalk.green(" (fine-tuned)")}`);
|
|
9469
|
-
if (isDefault) {
|
|
9470
|
-
console.log(chalk.dim(`
|
|
9471
|
-
To set a fine-tuned model: economy brains model set <model-id>`));
|
|
9472
|
-
}
|
|
9473
|
-
});
|
|
9474
|
-
modelCmd.command("set <id>").description("Set the active fine-tuned model ID").action((id) => {
|
|
9475
|
-
setActiveModel(id);
|
|
9476
|
-
console.log(chalk.green(`\u2713 Active model set to: ${id}`));
|
|
9477
|
-
console.log(chalk.dim(" Economy AI analysis will now use this model."));
|
|
9478
|
-
});
|
|
9479
|
-
modelCmd.command("clear").description(`Reset to default model (${DEFAULT_MODEL})`).action(() => {
|
|
9480
|
-
clearActiveModel();
|
|
9481
|
-
console.log(chalk.green(`\u2713 Active model cleared, using default: ${DEFAULT_MODEL}`));
|
|
9482
|
-
});
|
|
9483
|
-
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) => {
|
|
9484
|
-
if (!jobId) {
|
|
9485
|
-
console.error(chalk.red("Usage: economy brains status <job-id>"));
|
|
9486
|
-
process.exit(1);
|
|
9487
|
-
}
|
|
9488
|
-
try {
|
|
9489
|
-
const brains = await import("@hasna/brains");
|
|
9490
|
-
const getFinetuneStatus = brains["getFinetuneStatus"] ?? brains["get_finetune_status"];
|
|
9491
|
-
if (typeof getFinetuneStatus !== "function") {
|
|
9492
|
-
console.error(chalk.red("@hasna/brains not installed. Run: bun add @hasna/brains"));
|
|
9493
|
-
process.exit(1);
|
|
9494
|
-
}
|
|
9495
|
-
const status = await getFinetuneStatus({
|
|
9496
|
-
jobId,
|
|
9497
|
-
provider: opts.provider ?? "openai"
|
|
9498
|
-
});
|
|
9499
|
-
console.log(`Job ${chalk.cyan(jobId)}:`);
|
|
9500
|
-
console.log(` Status: ${String(status["status"] ?? "unknown")}`);
|
|
9501
|
-
if (status["fine_tuned_model"]) {
|
|
9502
|
-
console.log(` Fine-tuned model: ${chalk.green(String(status["fine_tuned_model"]))}`);
|
|
9503
|
-
console.log(`
|
|
9504
|
-
${chalk.dim("Set it active:")} economy brains model set ${String(status["fine_tuned_model"])}`);
|
|
9505
|
-
}
|
|
9506
|
-
if (status["error"]) {
|
|
9507
|
-
console.log(chalk.red(` Error: ${String(status["error"])}`));
|
|
9508
|
-
}
|
|
9509
|
-
} catch (e) {
|
|
9510
|
-
console.error(chalk.red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
9511
|
-
process.exit(1);
|
|
9512
|
-
}
|
|
9513
|
-
});
|
|
9514
|
-
}
|
|
9717
|
+
import chalk6 from "chalk";
|
|
9515
9718
|
|
|
9516
9719
|
// src/cli/commands/todos.ts
|
|
9517
|
-
import
|
|
9720
|
+
import chalk from "chalk";
|
|
9518
9721
|
var ROADMAP_VERSION = "2026-05-23-b";
|
|
9519
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.";
|
|
9520
9723
|
var ROADMAP_PHASES = [
|
|
@@ -10110,7 +10313,7 @@ var ROADMAP_PHASES = [
|
|
|
10110
10313
|
},
|
|
10111
10314
|
{
|
|
10112
10315
|
id: "12.4",
|
|
10113
|
-
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",
|
|
10114
10317
|
status: "done",
|
|
10115
10318
|
deps: ["9.7", "12.1"]
|
|
10116
10319
|
},
|
|
@@ -10189,7 +10392,7 @@ var ROADMAP_PHASES = [
|
|
|
10189
10392
|
},
|
|
10190
10393
|
{
|
|
10191
10394
|
id: "14.5",
|
|
10192
|
-
title: "economy serve auth token (
|
|
10395
|
+
title: "economy serve auth token (HASNA_ECONOMY_API_TOKEN) \u2014 localhost-only default, optional LAN bind",
|
|
10193
10396
|
status: "done"
|
|
10194
10397
|
}
|
|
10195
10398
|
]
|
|
@@ -10199,10 +10402,10 @@ var DEFAULT_TODO_LIST_LIMIT = 20;
|
|
|
10199
10402
|
var TODO_TITLE_WIDTH = 140;
|
|
10200
10403
|
function statusLabel(status) {
|
|
10201
10404
|
if (status === "done")
|
|
10202
|
-
return
|
|
10405
|
+
return chalk.green("done");
|
|
10203
10406
|
if (status === "in_progress")
|
|
10204
|
-
return
|
|
10205
|
-
return
|
|
10407
|
+
return chalk.yellow("in progress");
|
|
10408
|
+
return chalk.dim("pending");
|
|
10206
10409
|
}
|
|
10207
10410
|
function countByStatus(tasks) {
|
|
10208
10411
|
return tasks.reduce((acc, task) => {
|
|
@@ -10220,7 +10423,7 @@ function parseListLimit(value) {
|
|
|
10220
10423
|
return;
|
|
10221
10424
|
const parsed = Number(value);
|
|
10222
10425
|
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10223
|
-
console.error(
|
|
10426
|
+
console.error(chalk.red("--limit must be a positive integer"));
|
|
10224
10427
|
process.exit(1);
|
|
10225
10428
|
}
|
|
10226
10429
|
return parsed;
|
|
@@ -10236,12 +10439,12 @@ function matchingTasks(phases, status) {
|
|
|
10236
10439
|
}
|
|
10237
10440
|
function printTodosHelp() {
|
|
10238
10441
|
console.log();
|
|
10239
|
-
console.log(
|
|
10442
|
+
console.log(chalk.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
|
|
10240
10443
|
console.log();
|
|
10241
|
-
console.log(
|
|
10444
|
+
console.log(chalk.dim(` Version: ${ROADMAP_VERSION}`));
|
|
10242
10445
|
console.log(` ${ROADMAP_GOAL}`);
|
|
10243
10446
|
console.log();
|
|
10244
|
-
console.log(
|
|
10447
|
+
console.log(chalk.bold(" Commands"));
|
|
10245
10448
|
console.log(" economy todos Overview of phases and progress");
|
|
10246
10449
|
console.log(" economy todos list Compact task list (first 20 by default)");
|
|
10247
10450
|
console.log(" economy todos list --verbose Full task list with dependencies");
|
|
@@ -10249,12 +10452,12 @@ function printTodosHelp() {
|
|
|
10249
10452
|
console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
|
|
10250
10453
|
console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
|
|
10251
10454
|
console.log();
|
|
10252
|
-
console.log(
|
|
10455
|
+
console.log(chalk.bold(" Multi-machine storage"));
|
|
10253
10456
|
console.log(" local on-box SQLite; economy sync ingests this machine's agent data");
|
|
10254
10457
|
console.log(" cloud CLI/MCP reads and writes the shared /v1 API directly with URL + key");
|
|
10255
10458
|
console.log(" fleet machine_id keeps shared summaries and per-machine filters distinct");
|
|
10256
10459
|
console.log();
|
|
10257
|
-
console.log(
|
|
10460
|
+
console.log(chalk.bold(" Agents covered"));
|
|
10258
10461
|
console.log(" claude Claude Code telemetry + Pro/Max quota (partial)");
|
|
10259
10462
|
console.log(" codex Codex SQLite + ChatGPT subscription mode");
|
|
10260
10463
|
console.log(" opencode ~/.local/share/opencode/storage JSON + OTLP");
|
|
@@ -10263,7 +10466,7 @@ function printTodosHelp() {
|
|
|
10263
10466
|
console.log(" hermes ~/.hermes/state.db sessions + cost routing");
|
|
10264
10467
|
console.log(" gemini/takumi Existing; extend with usage quota where available");
|
|
10265
10468
|
console.log();
|
|
10266
|
-
console.log(
|
|
10469
|
+
console.log(chalk.bold(" Key metrics (target)"));
|
|
10267
10470
|
console.log(" api_equivalent_usd What the same tokens would cost at list API prices");
|
|
10268
10471
|
console.log(" subscription_fee_usd Monthly plan cost (prorated per period)");
|
|
10269
10472
|
console.log(" included_consumed_usd Usage counted against plan inclusion");
|
|
@@ -10273,30 +10476,30 @@ function printTodosHelp() {
|
|
|
10273
10476
|
}
|
|
10274
10477
|
function printTodosOverview() {
|
|
10275
10478
|
console.log();
|
|
10276
|
-
console.log(
|
|
10277
|
-
console.log(
|
|
10479
|
+
console.log(chalk.bold.cyan(" Economy Roadmap"));
|
|
10480
|
+
console.log(chalk.dim(` ${ROADMAP_VERSION} \u2014 ${ROADMAP_GOAL}`));
|
|
10278
10481
|
console.log();
|
|
10279
10482
|
const allTasks = ROADMAP_PHASES.flatMap((phase) => phase.tasks);
|
|
10280
10483
|
const totals = countByStatus(allTasks);
|
|
10281
|
-
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`);
|
|
10282
10485
|
console.log();
|
|
10283
10486
|
for (const phase of ROADMAP_PHASES) {
|
|
10284
10487
|
const counts = countByStatus(phase.tasks);
|
|
10285
10488
|
const progress = phase.tasks.length > 0 ? Math.round(counts.done / phase.tasks.length * 100) : 0;
|
|
10286
|
-
console.log(` ${
|
|
10287
|
-
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}`));
|
|
10288
10491
|
}
|
|
10289
10492
|
console.log();
|
|
10290
|
-
console.log(
|
|
10291
|
-
console.log(
|
|
10292
|
-
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"));
|
|
10293
10496
|
console.log();
|
|
10294
10497
|
}
|
|
10295
10498
|
function printTodosList(opts) {
|
|
10296
10499
|
const phases = opts.phase ? ROADMAP_PHASES.filter((phase) => phase.id === opts.phase) : ROADMAP_PHASES;
|
|
10297
10500
|
if (phases.length === 0) {
|
|
10298
|
-
console.log(
|
|
10299
|
-
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(", ")}`));
|
|
10300
10503
|
return;
|
|
10301
10504
|
}
|
|
10302
10505
|
const allMatches = matchingTasks(phases, opts.status);
|
|
@@ -10316,10 +10519,10 @@ function printTodosList(opts) {
|
|
|
10316
10519
|
return;
|
|
10317
10520
|
}
|
|
10318
10521
|
console.log();
|
|
10319
|
-
console.log(
|
|
10320
|
-
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}` : ""}`));
|
|
10321
10524
|
if (!opts.verbose && visible.length < allMatches.length) {
|
|
10322
|
-
console.log(
|
|
10525
|
+
console.log(chalk.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
|
|
10323
10526
|
}
|
|
10324
10527
|
console.log();
|
|
10325
10528
|
const visibleByPhase = new Map;
|
|
@@ -10332,20 +10535,20 @@ function printTodosList(opts) {
|
|
|
10332
10535
|
const tasks = visibleByPhase.get(phase.id) ?? [];
|
|
10333
10536
|
if (tasks.length === 0)
|
|
10334
10537
|
continue;
|
|
10335
|
-
console.log(
|
|
10538
|
+
console.log(chalk.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
|
|
10336
10539
|
if (opts.verbose)
|
|
10337
|
-
console.log(
|
|
10540
|
+
console.log(chalk.dim(` ${phase.summary}`));
|
|
10338
10541
|
console.log();
|
|
10339
10542
|
for (const task of tasks) {
|
|
10340
|
-
const deps = task.deps?.length ?
|
|
10543
|
+
const deps = task.deps?.length ? chalk.dim(` deps: ${task.deps.join(", ")}`) : "";
|
|
10341
10544
|
const title = opts.verbose ? task.title : truncateText(task.title, TODO_TITLE_WIDTH);
|
|
10342
|
-
console.log(` ${
|
|
10545
|
+
console.log(` ${chalk.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
|
|
10343
10546
|
${" ".repeat(22)}${deps}` : ""}`);
|
|
10344
10547
|
}
|
|
10345
10548
|
console.log();
|
|
10346
10549
|
}
|
|
10347
10550
|
if (visible.length < allMatches.length) {
|
|
10348
|
-
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>.`));
|
|
10349
10552
|
console.log();
|
|
10350
10553
|
}
|
|
10351
10554
|
}
|
|
@@ -10355,19 +10558,19 @@ function printTodoShow(taskId) {
|
|
|
10355
10558
|
if (!task)
|
|
10356
10559
|
continue;
|
|
10357
10560
|
console.log();
|
|
10358
|
-
console.log(
|
|
10561
|
+
console.log(chalk.bold.cyan(` ${task.id} \u2014 ${task.title}`));
|
|
10359
10562
|
console.log(` Phase: ${phase.id} (${phase.title})`);
|
|
10360
10563
|
console.log(` Status: ${statusLabel(task.status)}`);
|
|
10361
10564
|
if (task.deps?.length) {
|
|
10362
10565
|
console.log(` Depends on: ${task.deps.join(", ")}`);
|
|
10363
10566
|
}
|
|
10364
|
-
console.log(
|
|
10567
|
+
console.log(chalk.dim(`
|
|
10365
10568
|
${phase.summary}`));
|
|
10366
10569
|
console.log();
|
|
10367
10570
|
return;
|
|
10368
10571
|
}
|
|
10369
|
-
console.log(
|
|
10370
|
-
console.log(
|
|
10572
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
10573
|
+
console.log(chalk.dim("Run: economy todos list"));
|
|
10371
10574
|
}
|
|
10372
10575
|
function registerTodosCommand(program) {
|
|
10373
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) => {
|
|
@@ -10380,7 +10583,7 @@ function registerTodosCommand(program) {
|
|
|
10380
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) => {
|
|
10381
10584
|
const status = opts.status;
|
|
10382
10585
|
if (status && !["pending", "in_progress", "done"].includes(status)) {
|
|
10383
|
-
console.error(
|
|
10586
|
+
console.error(chalk.red("--status must be one of: pending, in_progress, done"));
|
|
10384
10587
|
process.exit(1);
|
|
10385
10588
|
}
|
|
10386
10589
|
printTodosList({
|
|
@@ -10406,9 +10609,9 @@ init_tui();
|
|
|
10406
10609
|
init_store();
|
|
10407
10610
|
init_pricing();
|
|
10408
10611
|
init_agents();
|
|
10409
|
-
import
|
|
10410
|
-
import { existsSync as
|
|
10411
|
-
import { join as
|
|
10612
|
+
import chalk3 from "chalk";
|
|
10613
|
+
import { existsSync as existsSync2 } from "fs";
|
|
10614
|
+
import { join as join3 } from "path";
|
|
10412
10615
|
|
|
10413
10616
|
// src/cli/commands/completion.ts
|
|
10414
10617
|
var TOP_LEVEL = [
|
|
@@ -10554,6 +10757,36 @@ function printCompletion(shell) {
|
|
|
10554
10757
|
// src/cli/commands/extras.ts
|
|
10555
10758
|
init_paths();
|
|
10556
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
|
|
10557
10790
|
function fmt2(usd) {
|
|
10558
10791
|
return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
10559
10792
|
}
|
|
@@ -10569,7 +10802,7 @@ function fmtTokens(n) {
|
|
|
10569
10802
|
function parsePeriod(value, fallback = "month") {
|
|
10570
10803
|
const p = value ?? fallback;
|
|
10571
10804
|
if (!["today", "yesterday", "week", "month", "year", "all"].includes(p)) {
|
|
10572
|
-
console.error(
|
|
10805
|
+
console.error(chalk3.red("--period must be today|yesterday|week|month|year|all"));
|
|
10573
10806
|
process.exit(1);
|
|
10574
10807
|
}
|
|
10575
10808
|
return p;
|
|
@@ -10579,7 +10812,7 @@ function parseLimit(value, fallback, verbose) {
|
|
|
10579
10812
|
return Number.POSITIVE_INFINITY;
|
|
10580
10813
|
const parsed = Number(value ?? String(fallback));
|
|
10581
10814
|
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10582
|
-
console.error(
|
|
10815
|
+
console.error(chalk3.red("--limit must be a positive integer"));
|
|
10583
10816
|
process.exit(1);
|
|
10584
10817
|
}
|
|
10585
10818
|
return parsed;
|
|
@@ -10594,28 +10827,28 @@ function registerExtendedCommands(program) {
|
|
|
10594
10827
|
return;
|
|
10595
10828
|
}
|
|
10596
10829
|
console.log();
|
|
10597
|
-
console.log(
|
|
10830
|
+
console.log(chalk3.bold.cyan(` Usage \u2014 ${period}`));
|
|
10598
10831
|
console.log();
|
|
10599
10832
|
if (snaps.length === 0) {
|
|
10600
|
-
console.log(
|
|
10833
|
+
console.log(chalk3.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
|
|
10601
10834
|
} else {
|
|
10602
10835
|
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10603
10836
|
const visibleSnaps = snaps.slice(0, limit);
|
|
10604
10837
|
if (visibleSnaps.length < snaps.length) {
|
|
10605
|
-
console.log(
|
|
10838
|
+
console.log(chalk3.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
|
|
10606
10839
|
console.log();
|
|
10607
10840
|
}
|
|
10608
10841
|
for (const s of visibleSnaps) {
|
|
10609
|
-
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)}`);
|
|
10610
10843
|
}
|
|
10611
10844
|
if (visibleSnaps.length < snaps.length) {
|
|
10612
|
-
console.log(
|
|
10845
|
+
console.log(chalk3.dim(`
|
|
10613
10846
|
... ${snaps.length - visibleSnaps.length} more snapshots hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10614
10847
|
}
|
|
10615
10848
|
}
|
|
10616
10849
|
const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
|
|
10617
10850
|
console.log(`
|
|
10618
|
-
${
|
|
10851
|
+
${chalk3.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
|
|
10619
10852
|
console.log();
|
|
10620
10853
|
});
|
|
10621
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) => {
|
|
@@ -10627,13 +10860,13 @@ function registerExtendedCommands(program) {
|
|
|
10627
10860
|
return;
|
|
10628
10861
|
}
|
|
10629
10862
|
console.log();
|
|
10630
|
-
console.log(
|
|
10863
|
+
console.log(chalk3.bold.cyan(` Savings \u2014 ${period}${agent ? ` (${agent})` : ""}`));
|
|
10631
10864
|
console.log();
|
|
10632
10865
|
console.log(` API equivalent: ${fmt2(savings.api_equivalent_usd)}`);
|
|
10633
10866
|
console.log(` Subscription fee: ${fmt2(savings.subscription_fee_usd)}`);
|
|
10634
10867
|
console.log(` Included consumed: ${fmt2(savings.included_consumed_usd)}`);
|
|
10635
10868
|
console.log(` On-demand: ${fmt2(savings.on_demand_usd)}`);
|
|
10636
|
-
console.log(` ${
|
|
10869
|
+
console.log(` ${chalk3.bold("Saved:")} ${chalk3.green(fmt2(savings.saved_usd))}`);
|
|
10637
10870
|
console.log();
|
|
10638
10871
|
});
|
|
10639
10872
|
const subsCmd = program.command("subscriptions").description("Manage subscription plans");
|
|
@@ -10649,7 +10882,7 @@ function registerExtendedCommands(program) {
|
|
|
10649
10882
|
reset_policy: "monthly",
|
|
10650
10883
|
active: 1
|
|
10651
10884
|
});
|
|
10652
|
-
console.log(
|
|
10885
|
+
console.log(chalk3.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
|
|
10653
10886
|
});
|
|
10654
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) => {
|
|
10655
10888
|
const rows = await getStore().listSubscriptions();
|
|
@@ -10658,28 +10891,28 @@ function registerExtendedCommands(program) {
|
|
|
10658
10891
|
return;
|
|
10659
10892
|
}
|
|
10660
10893
|
if (rows.length === 0) {
|
|
10661
|
-
console.log(
|
|
10894
|
+
console.log(chalk3.yellow("No subscriptions configured."));
|
|
10662
10895
|
return;
|
|
10663
10896
|
}
|
|
10664
10897
|
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10665
10898
|
const visibleRows = rows.slice(0, limit);
|
|
10666
10899
|
console.log();
|
|
10667
10900
|
if (visibleRows.length < rows.length) {
|
|
10668
|
-
console.log(
|
|
10901
|
+
console.log(chalk3.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
|
|
10669
10902
|
console.log();
|
|
10670
10903
|
}
|
|
10671
10904
|
for (const r of visibleRows) {
|
|
10672
|
-
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"}`);
|
|
10673
10906
|
}
|
|
10674
10907
|
if (visibleRows.length < rows.length) {
|
|
10675
|
-
console.log(
|
|
10908
|
+
console.log(chalk3.dim(`
|
|
10676
10909
|
... ${rows.length - visibleRows.length} more subscriptions hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10677
10910
|
}
|
|
10678
10911
|
console.log();
|
|
10679
10912
|
});
|
|
10680
10913
|
subsCmd.command("remove <id>").description("Remove subscription by id").action(async (id) => {
|
|
10681
10914
|
await getStore().removeSubscription(id);
|
|
10682
|
-
console.log(
|
|
10915
|
+
console.log(chalk3.green("\u2713 Subscription removed"));
|
|
10683
10916
|
});
|
|
10684
10917
|
program.command("status").description("One-line fleet health and spend summary").action(async () => {
|
|
10685
10918
|
console.log(buildStatusLine(await gatherStatusData()));
|
|
@@ -10692,17 +10925,26 @@ function registerExtendedCommands(program) {
|
|
|
10692
10925
|
const paths = [
|
|
10693
10926
|
["claude", agentPaths().claudeProjects],
|
|
10694
10927
|
["codex", agentPaths().codexDb],
|
|
10695
|
-
["gemini",
|
|
10696
|
-
["opencode",
|
|
10928
|
+
["gemini", join3(agentPaths().geminiTmp, "..")],
|
|
10929
|
+
["opencode", join3(agentPaths().opencodeMessages, "..", "..")],
|
|
10697
10930
|
["pi", agentPaths().piSessions],
|
|
10698
10931
|
["hermes", agentPaths().hermesDb]
|
|
10699
10932
|
];
|
|
10700
10933
|
for (const [agent, path] of paths) {
|
|
10701
|
-
checks.push({ ok:
|
|
10934
|
+
checks.push({ ok: existsSync2(path), msg: `${agent}: ${existsSync2(path) ? path : "not found"}` });
|
|
10702
10935
|
}
|
|
10703
10936
|
checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
|
|
10704
10937
|
}
|
|
10705
|
-
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
|
+
}
|
|
10706
10948
|
if (!cloud) {
|
|
10707
10949
|
const db = openDatabase();
|
|
10708
10950
|
ensurePricingSeeded(db);
|
|
@@ -10725,19 +10967,49 @@ function registerExtendedCommands(program) {
|
|
|
10725
10967
|
}
|
|
10726
10968
|
checks.push(billingDriftCheck(await store.billingDiff("month")));
|
|
10727
10969
|
console.log();
|
|
10728
|
-
console.log(
|
|
10970
|
+
console.log(chalk3.bold.cyan(" Economy Doctor"));
|
|
10729
10971
|
console.log();
|
|
10730
10972
|
for (const c of checks) {
|
|
10731
|
-
console.log(` ${c.ok ?
|
|
10973
|
+
console.log(` ${c.ok ? chalk3.green("\u2713") : chalk3.yellow("!")} ${c.msg}`);
|
|
10732
10974
|
}
|
|
10733
10975
|
console.log();
|
|
10734
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
|
+
});
|
|
10735
11007
|
program.command("init").description("First-run setup wizard hints").action(async () => {
|
|
10736
|
-
console.log(
|
|
11008
|
+
console.log(chalk3.bold.cyan(`
|
|
10737
11009
|
Economy Init
|
|
10738
11010
|
`));
|
|
10739
11011
|
console.log(" Local mode (on-box SQLite):");
|
|
10740
|
-
console.log(" 1. Set machine id: export
|
|
11012
|
+
console.log(" 1. Set machine id: export HASNA_ECONOMY_MACHINE_ID=spark01");
|
|
10741
11013
|
console.log(" 2. Cursor token: export CURSOR_SESSION_TOKEN=...");
|
|
10742
11014
|
console.log(" 3. Run ingest: economy sync --verbose");
|
|
10743
11015
|
console.log(" 4. MCP install: economy mcp --all");
|
|
@@ -10784,16 +11056,16 @@ function registerExtendedCommands(program) {
|
|
|
10784
11056
|
const period = parsePeriod(opts.period, "month");
|
|
10785
11057
|
const diff = await getStore().billingDiff(period);
|
|
10786
11058
|
console.log();
|
|
10787
|
-
console.log(
|
|
11059
|
+
console.log(chalk3.bold.cyan(` Billing diff \u2014 ${period}`));
|
|
10788
11060
|
console.log(` Estimated: ${fmt2(diff.estimated_usd)}`);
|
|
10789
11061
|
console.log(` Actual: ${fmt2(diff.actual_usd)}`);
|
|
10790
11062
|
const deltaPct = diff.comparable ? `${diff.delta_usd >= 0 ? "+" : ""}${diff.delta_pct.toFixed(1)}%` : "n/a";
|
|
10791
11063
|
console.log(` Delta: ${fmt2(Math.abs(diff.delta_usd))} (${deltaPct})`);
|
|
10792
11064
|
if (!diff.comparable)
|
|
10793
|
-
console.log(
|
|
11065
|
+
console.log(chalk3.yellow(`
|
|
10794
11066
|
${billingDriftCheck(diff).msg}`));
|
|
10795
11067
|
if (diff.is_alert)
|
|
10796
|
-
console.log(
|
|
11068
|
+
console.log(chalk3.yellow(`
|
|
10797
11069
|
Suggestion: economy sync --recalculate && economy billing sync`));
|
|
10798
11070
|
console.log();
|
|
10799
11071
|
});
|
|
@@ -10810,17 +11082,17 @@ function registerFleetCommands(program) {
|
|
|
10810
11082
|
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10811
11083
|
const visibleMachines = machines.slice(0, limit);
|
|
10812
11084
|
console.log();
|
|
10813
|
-
console.log(
|
|
11085
|
+
console.log(chalk3.bold.cyan(` Fleet \u2014 ${period}`));
|
|
10814
11086
|
console.log(` Total: ${fmt2(summary.total_usd)} \xB7 ${summary.sessions} sessions \xB7 ${summary.requests} requests`);
|
|
10815
11087
|
if (visibleMachines.length < machines.length) {
|
|
10816
|
-
console.log(
|
|
11088
|
+
console.log(chalk3.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
10817
11089
|
}
|
|
10818
11090
|
console.log();
|
|
10819
11091
|
for (const m of visibleMachines) {
|
|
10820
|
-
console.log(` ${
|
|
11092
|
+
console.log(` ${chalk3.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
|
|
10821
11093
|
}
|
|
10822
11094
|
if (visibleMachines.length < machines.length) {
|
|
10823
|
-
console.log(
|
|
11095
|
+
console.log(chalk3.dim(`
|
|
10824
11096
|
... ${machines.length - visibleMachines.length} more machines hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10825
11097
|
}
|
|
10826
11098
|
console.log();
|
|
@@ -10967,14 +11239,14 @@ init_billing();
|
|
|
10967
11239
|
init_package_metadata();
|
|
10968
11240
|
init_pricing();
|
|
10969
11241
|
init_store();
|
|
10970
|
-
import { execSync } from "child_process";
|
|
10971
11242
|
|
|
10972
11243
|
// src/lib/cloud-ingest.ts
|
|
10973
11244
|
init_database();
|
|
10974
11245
|
init_pricing();
|
|
10975
11246
|
init_sync_all();
|
|
10976
11247
|
init_billing();
|
|
10977
|
-
import {
|
|
11248
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync, writeFileSync } from "fs";
|
|
11249
|
+
import { dirname, join as join12 } from "path";
|
|
10978
11250
|
|
|
10979
11251
|
// src/lib/sync-maintenance.ts
|
|
10980
11252
|
init_database();
|
|
@@ -11015,15 +11287,39 @@ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snaps
|
|
|
11015
11287
|
function getIngestCachePath() {
|
|
11016
11288
|
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
11017
11289
|
return process.env["HASNA_ECONOMY_INGEST_CACHE"];
|
|
11018
|
-
|
|
11290
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || undefined;
|
|
11291
|
+
return join12(cacheDir({ app: "economy", home }), "ingest-cache.json");
|
|
11292
|
+
}
|
|
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);
|
|
11019
11313
|
}
|
|
11020
|
-
function
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
|
|
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 (?, ?, ?)`);
|
|
11024
11317
|
for (const row of rows)
|
|
11025
11318
|
insert.run(row.source, row.key, row.value);
|
|
11026
11319
|
}
|
|
11320
|
+
function dumpIngestState(scratch) {
|
|
11321
|
+
return scratch.prepare(`SELECT source, key, value FROM ingest_state`).all();
|
|
11322
|
+
}
|
|
11027
11323
|
function exportIngestRows(db) {
|
|
11028
11324
|
const body = {};
|
|
11029
11325
|
for (const table2 of CLOUD_INGEST_TABLES) {
|
|
@@ -11051,11 +11347,9 @@ async function postIngest(cloud, body) {
|
|
|
11051
11347
|
async function syncAllToCloud(cloud, opts = {}) {
|
|
11052
11348
|
const scratch = openDatabase(":memory:", true);
|
|
11053
11349
|
ensurePricingSeeded(scratch);
|
|
11054
|
-
const
|
|
11350
|
+
const cachePath = opts.cachePath ?? getIngestCachePath();
|
|
11055
11351
|
try {
|
|
11056
|
-
|
|
11057
|
-
cache2.prepare(`DELETE FROM ingest_state`).run();
|
|
11058
|
-
copyIngestState(cache2, scratch);
|
|
11352
|
+
loadIngestState2(scratch, opts.force ? [] : readIngestCache(cachePath));
|
|
11059
11353
|
const result = await syncAll(scratch, opts);
|
|
11060
11354
|
if (opts.backfillMachine)
|
|
11061
11355
|
backfillMachineId(scratch);
|
|
@@ -11063,19 +11357,16 @@ async function syncAllToCloud(cloud, opts = {}) {
|
|
|
11063
11357
|
await recalculateZeroCostRequests(scratch);
|
|
11064
11358
|
const body = exportIngestRows(scratch);
|
|
11065
11359
|
if (Object.keys(body).length === 0) {
|
|
11066
|
-
|
|
11360
|
+
writeIngestCache(cachePath, dumpIngestState(scratch));
|
|
11067
11361
|
return { ...result, posted: false };
|
|
11068
11362
|
}
|
|
11069
11363
|
const response = await postIngest(cloud, body);
|
|
11070
|
-
|
|
11364
|
+
writeIngestCache(cachePath, dumpIngestState(scratch));
|
|
11071
11365
|
return { ...result, posted: true, ingested: response.ingested, total: response.total };
|
|
11072
11366
|
} finally {
|
|
11073
11367
|
try {
|
|
11074
11368
|
scratch.close();
|
|
11075
11369
|
} catch {}
|
|
11076
|
-
try {
|
|
11077
|
-
cache2.close();
|
|
11078
|
-
} catch {}
|
|
11079
11370
|
}
|
|
11080
11371
|
}
|
|
11081
11372
|
async function billingSyncToCloud(cloud, opts = {}) {
|
|
@@ -11119,6 +11410,43 @@ async function billingSyncToCloud(cloud, opts = {}) {
|
|
|
11119
11410
|
init_cloud_storage();
|
|
11120
11411
|
init_billing_diff();
|
|
11121
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
|
+
|
|
11122
11450
|
// src/lib/contracts-client/transport.ts
|
|
11123
11451
|
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
11124
11452
|
|
|
@@ -11126,15 +11454,14 @@ import { HasnaHttpError } from "@hasna/contracts/client";
|
|
|
11126
11454
|
var program = new Command;
|
|
11127
11455
|
program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
|
|
11128
11456
|
async function autoSync(opts = {}) {
|
|
11129
|
-
if (isCloudStore())
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
await syncAllToCloud(cloud, opts);
|
|
11457
|
+
if (isCloudStore())
|
|
11458
|
+
return;
|
|
11459
|
+
if (!autoSyncDue(undefined, process.env))
|
|
11133
11460
|
return;
|
|
11134
|
-
}
|
|
11135
11461
|
const db = openDatabase();
|
|
11136
11462
|
ensurePricingSeeded(db);
|
|
11137
11463
|
await syncAll(db, opts);
|
|
11464
|
+
markAutoSync(db, process.env);
|
|
11138
11465
|
}
|
|
11139
11466
|
function sparkline(values) {
|
|
11140
11467
|
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
@@ -11157,7 +11484,7 @@ function fmt4(usd) {
|
|
|
11157
11484
|
} else {
|
|
11158
11485
|
formatted = "$0.00";
|
|
11159
11486
|
}
|
|
11160
|
-
return
|
|
11487
|
+
return chalk6.green(formatted);
|
|
11161
11488
|
}
|
|
11162
11489
|
function fmtTokens2(n) {
|
|
11163
11490
|
if (n >= 1e9)
|
|
@@ -11173,25 +11500,25 @@ function fmtCount(n) {
|
|
|
11173
11500
|
}
|
|
11174
11501
|
function fmtAgent(agent) {
|
|
11175
11502
|
if (agent === "claude")
|
|
11176
|
-
return
|
|
11503
|
+
return chalk6.blue("claude");
|
|
11177
11504
|
if (agent === "codex")
|
|
11178
|
-
return
|
|
11505
|
+
return chalk6.yellow("codex");
|
|
11179
11506
|
if (agent === "gemini")
|
|
11180
|
-
return
|
|
11507
|
+
return chalk6.green("gemini");
|
|
11181
11508
|
if (agent === "takumi")
|
|
11182
|
-
return
|
|
11509
|
+
return chalk6.magenta("takumi");
|
|
11183
11510
|
if (agent === "opencode")
|
|
11184
|
-
return
|
|
11511
|
+
return chalk6.cyan("opencode");
|
|
11185
11512
|
if (agent === "cursor")
|
|
11186
|
-
return
|
|
11513
|
+
return chalk6.white("cursor");
|
|
11187
11514
|
if (agent === "pi")
|
|
11188
|
-
return
|
|
11515
|
+
return chalk6.hex("#FF6B6B")("pi");
|
|
11189
11516
|
if (agent === "hermes")
|
|
11190
|
-
return
|
|
11191
|
-
return
|
|
11517
|
+
return chalk6.hex("#9B59B6")("hermes");
|
|
11518
|
+
return chalk6.gray(agent);
|
|
11192
11519
|
}
|
|
11193
11520
|
function fail(message) {
|
|
11194
|
-
console.error(
|
|
11521
|
+
console.error(chalk6.red(message));
|
|
11195
11522
|
process.exit(1);
|
|
11196
11523
|
}
|
|
11197
11524
|
function parseFiniteCliNumber(value, option) {
|
|
@@ -11243,7 +11570,7 @@ function resolveRowLimit(value, verbose, fallback) {
|
|
|
11243
11570
|
function printHiddenRowsHint(total, shown, detail) {
|
|
11244
11571
|
if (total <= shown)
|
|
11245
11572
|
return;
|
|
11246
|
-
console.log(
|
|
11573
|
+
console.log(chalk6.dim(` ... ${total - shown} more rows hidden. ${detail}`));
|
|
11247
11574
|
}
|
|
11248
11575
|
function requireCliChoice(value, option, allowed) {
|
|
11249
11576
|
const selected = value ?? allowed[0];
|
|
@@ -11253,11 +11580,11 @@ function requireCliChoice(value, option, allowed) {
|
|
|
11253
11580
|
}
|
|
11254
11581
|
function printTable(headers, rows) {
|
|
11255
11582
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").replace(/\x1b\[[0-9;]*m/g, "").length)));
|
|
11256
|
-
const
|
|
11583
|
+
const sep = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
11257
11584
|
const header = headers.map((h, i) => ` ${h.padEnd(widths[i] ?? 0)} `).join("\u2502");
|
|
11258
|
-
console.log(`\u250C${
|
|
11585
|
+
console.log(`\u250C${sep.replace(/\u253C/g, "\u252C")}\u2510`);
|
|
11259
11586
|
console.log(`\u2502${header}\u2502`);
|
|
11260
|
-
console.log(`\u251C${
|
|
11587
|
+
console.log(`\u251C${sep}\u2524`);
|
|
11261
11588
|
for (const row of rows) {
|
|
11262
11589
|
const line = row.map((cell, i) => {
|
|
11263
11590
|
const plain = cell.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -11265,19 +11592,19 @@ function printTable(headers, rows) {
|
|
|
11265
11592
|
}).join("\u2502");
|
|
11266
11593
|
console.log(`\u2502${line}\u2502`);
|
|
11267
11594
|
}
|
|
11268
|
-
console.log(`\u2514${
|
|
11595
|
+
console.log(`\u2514${sep.replace(/\u253C/g, "\u2534")}\u2518`);
|
|
11269
11596
|
}
|
|
11270
11597
|
function accountDisplayName(row) {
|
|
11271
11598
|
return row.account_email || row.account_name || row.account_key || "unknown";
|
|
11272
11599
|
}
|
|
11273
11600
|
function printAccountBreakdown(rows) {
|
|
11274
11601
|
printTable(["Account", "Agent", "Source", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
|
|
11275
|
-
|
|
11602
|
+
chalk6.white(accountDisplayName(r)),
|
|
11276
11603
|
fmtAgent(r.account_tool),
|
|
11277
|
-
|
|
11604
|
+
chalk6.dim(r.account_source || "unknown"),
|
|
11278
11605
|
String(r.sessions),
|
|
11279
11606
|
String(r.requests),
|
|
11280
|
-
|
|
11607
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11281
11608
|
fmt4(r.api_equivalent_usd),
|
|
11282
11609
|
fmt4(r.billable_usd),
|
|
11283
11610
|
fmt4(r.subscription_included_usd)
|
|
@@ -11296,13 +11623,13 @@ function parseSinceDate2(since) {
|
|
|
11296
11623
|
async function printSummary(label, period) {
|
|
11297
11624
|
const s = await getStore().summary(period);
|
|
11298
11625
|
console.log();
|
|
11299
|
-
console.log(
|
|
11626
|
+
console.log(chalk6.bold.cyan(` ${label}`));
|
|
11300
11627
|
console.log();
|
|
11301
11628
|
printTable(["Metric", "Value"], [
|
|
11302
11629
|
["Total cost", fmt4(s.total_usd)],
|
|
11303
|
-
["Sessions",
|
|
11304
|
-
["Requests",
|
|
11305
|
-
["Tokens",
|
|
11630
|
+
["Sessions", chalk6.yellow(fmtCount(s.sessions))],
|
|
11631
|
+
["Requests", chalk6.yellow(fmtCount(s.requests))],
|
|
11632
|
+
["Tokens", chalk6.yellow(fmtTokens2(s.tokens))]
|
|
11306
11633
|
]);
|
|
11307
11634
|
console.log();
|
|
11308
11635
|
}
|
|
@@ -11324,7 +11651,7 @@ program.action(async () => {
|
|
|
11324
11651
|
}, {});
|
|
11325
11652
|
dailyValues = Object.values(daily);
|
|
11326
11653
|
console.log();
|
|
11327
|
-
console.log(
|
|
11654
|
+
console.log(chalk6.bold.cyan(" Economy"));
|
|
11328
11655
|
console.log();
|
|
11329
11656
|
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
11330
11657
|
["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens2(t.tokens)],
|
|
@@ -11333,13 +11660,13 @@ program.action(async () => {
|
|
|
11333
11660
|
]);
|
|
11334
11661
|
if (dailyValues.length > 0) {
|
|
11335
11662
|
console.log(`
|
|
11336
|
-
${
|
|
11663
|
+
${chalk6.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
11337
11664
|
}
|
|
11338
11665
|
if (projects.length > 0) {
|
|
11339
11666
|
console.log(`
|
|
11340
|
-
${
|
|
11667
|
+
${chalk6.dim("Top projects:")}`);
|
|
11341
11668
|
for (const p of projects) {
|
|
11342
|
-
console.log(` ${
|
|
11669
|
+
console.log(` ${chalk6.white(p.project_name.padEnd(25))} ${fmt4(p.cost_usd)}`);
|
|
11343
11670
|
}
|
|
11344
11671
|
}
|
|
11345
11672
|
console.log();
|
|
@@ -11348,7 +11675,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11348
11675
|
if (isCloudStore()) {
|
|
11349
11676
|
const cloud = economyCloudStorage();
|
|
11350
11677
|
if (!cloud.active) {
|
|
11351
|
-
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)"));
|
|
11352
11679
|
return;
|
|
11353
11680
|
}
|
|
11354
11681
|
const result2 = await syncAllToCloud(cloud, {
|
|
@@ -11368,11 +11695,11 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11368
11695
|
});
|
|
11369
11696
|
if (result2.posted) {
|
|
11370
11697
|
const parts = Object.entries(result2.ingested ?? {}).filter(([, n]) => n > 0).map(([table2, n]) => `${n} ${table2}`);
|
|
11371
|
-
console.log(
|
|
11698
|
+
console.log(chalk6.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
|
|
11372
11699
|
} else {
|
|
11373
|
-
console.log(
|
|
11700
|
+
console.log(chalk6.dim("\u2713 no new provider data to push (mtime cache current)"));
|
|
11374
11701
|
}
|
|
11375
|
-
console.log(
|
|
11702
|
+
console.log(chalk6.bold.green(`
|
|
11376
11703
|
\u2713 Sync complete`));
|
|
11377
11704
|
return;
|
|
11378
11705
|
}
|
|
@@ -11384,19 +11711,19 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11384
11711
|
const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
|
|
11385
11712
|
const p = await syncOpenProjectsRegistry2(db);
|
|
11386
11713
|
if (opts.verbose)
|
|
11387
|
-
console.log(
|
|
11714
|
+
console.log(chalk6.dim(`Synced open-projects registry: ${p.imported} imported, ${p.skipped} skipped`));
|
|
11388
11715
|
} catch (e) {
|
|
11389
11716
|
if (opts.verbose)
|
|
11390
|
-
console.log(
|
|
11717
|
+
console.log(chalk6.dim(`open-projects registry sync skipped: ${e instanceof Error ? e.message : String(e)}`));
|
|
11391
11718
|
}
|
|
11392
11719
|
}
|
|
11393
11720
|
if (opts.force) {
|
|
11394
11721
|
const ingestSources = [...AGENTS, "codex-codewith", "loops"];
|
|
11395
11722
|
db.exec(`DELETE FROM ingest_state WHERE source IN (${ingestSources.map((a) => `'${a}'`).join(", ")})`);
|
|
11396
11723
|
if (opts.verbose)
|
|
11397
|
-
console.log(
|
|
11724
|
+
console.log(chalk6.dim("Cleared ingest cache"));
|
|
11398
11725
|
}
|
|
11399
|
-
process.stdout.write(
|
|
11726
|
+
process.stdout.write(chalk6.cyan("\u2192 Ingesting agent data... "));
|
|
11400
11727
|
const result = await syncAll(db, {
|
|
11401
11728
|
claude: opts.claude,
|
|
11402
11729
|
takumi: opts.takumi,
|
|
@@ -11409,32 +11736,32 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11409
11736
|
loops: opts.loops,
|
|
11410
11737
|
verbose: opts.verbose
|
|
11411
11738
|
});
|
|
11412
|
-
console.log(
|
|
11739
|
+
console.log(chalk6.green(`\u2713 deduped ${result.deduped}`));
|
|
11413
11740
|
if (opts.backfillMachine) {
|
|
11414
11741
|
const machine = getMachineId();
|
|
11415
11742
|
const r = backfillMachineId(db);
|
|
11416
|
-
console.log(
|
|
11743
|
+
console.log(chalk6.cyan(`\u2192 Backfilled machine_id='${machine}': ${r.requests} requests, ${r.sessions} sessions`));
|
|
11417
11744
|
}
|
|
11418
11745
|
if (opts.recalculate) {
|
|
11419
11746
|
const { getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
11420
11747
|
const r = await recalculateZeroCostRequests(db);
|
|
11421
|
-
console.log(
|
|
11748
|
+
console.log(chalk6.cyan(`\u2192 Recalculated: ${r.fixed}/${r.total} zero-cost requests now have pricing`));
|
|
11422
11749
|
const zeroCostBuckets = r.buckets;
|
|
11423
11750
|
if (zeroCostBuckets.length > 0) {
|
|
11424
|
-
console.log(
|
|
11751
|
+
console.log(chalk6.yellow("\u2192 Zero-cost token buckets remain:"));
|
|
11425
11752
|
for (const row of zeroCostBuckets) {
|
|
11426
11753
|
const pricing = getPricingFromDb2(db, row.model);
|
|
11427
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";
|
|
11428
|
-
console.log(
|
|
11755
|
+
console.log(chalk6.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
|
|
11429
11756
|
}
|
|
11430
|
-
console.log(
|
|
11757
|
+
console.log(chalk6.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
|
|
11431
11758
|
}
|
|
11432
11759
|
}
|
|
11433
11760
|
try {
|
|
11434
11761
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
11435
11762
|
await checkAndFireWebhooks2(db);
|
|
11436
11763
|
} catch {}
|
|
11437
|
-
console.log(
|
|
11764
|
+
console.log(chalk6.bold.green(`
|
|
11438
11765
|
\u2713 Sync complete`));
|
|
11439
11766
|
});
|
|
11440
11767
|
program.command("today").description("Cost summary for today").action(async () => {
|
|
@@ -11464,7 +11791,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
11464
11791
|
search: opts.search
|
|
11465
11792
|
});
|
|
11466
11793
|
if (sessions.length === 0) {
|
|
11467
|
-
console.log(
|
|
11794
|
+
console.log(chalk6.yellow("No sessions found."));
|
|
11468
11795
|
return;
|
|
11469
11796
|
}
|
|
11470
11797
|
const f = opts.format ?? "table";
|
|
@@ -11486,13 +11813,13 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
11486
11813
|
}
|
|
11487
11814
|
console.log();
|
|
11488
11815
|
printTable(["Session ID", "Agent", "Project", "Cost", "Tokens", "Requests", "Started"], sessions.map((s) => [
|
|
11489
|
-
|
|
11816
|
+
chalk6.dim(s.id.substring(0, 12)),
|
|
11490
11817
|
fmtAgent(s.agent),
|
|
11491
|
-
|
|
11818
|
+
chalk6.white(s.project_name || chalk6.dim("unknown")),
|
|
11492
11819
|
fmt4(s.total_cost_usd),
|
|
11493
|
-
|
|
11820
|
+
chalk6.cyan(fmtTokens2(s.total_tokens)),
|
|
11494
11821
|
fmtCount(s.request_count),
|
|
11495
|
-
|
|
11822
|
+
chalk6.dim(s.started_at.substring(0, 16))
|
|
11496
11823
|
]));
|
|
11497
11824
|
console.log();
|
|
11498
11825
|
});
|
|
@@ -11502,17 +11829,17 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
11502
11829
|
const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
11503
11830
|
const sessions = await getStore().topSessions(count, agent, sinceDate);
|
|
11504
11831
|
if (sessions.length === 0) {
|
|
11505
|
-
console.log(
|
|
11832
|
+
console.log(chalk6.yellow("No sessions found. Run `economy sync` first."));
|
|
11506
11833
|
return;
|
|
11507
11834
|
}
|
|
11508
11835
|
console.log();
|
|
11509
11836
|
printTable(["#", "Project", "Agent", "Cost", "Tokens", "Started"], sessions.map((s, i) => [
|
|
11510
|
-
|
|
11511
|
-
|
|
11837
|
+
chalk6.dim(String(i + 1)),
|
|
11838
|
+
chalk6.white(s.project_name || chalk6.dim("unknown")),
|
|
11512
11839
|
fmtAgent(s.agent),
|
|
11513
11840
|
fmt4(s.total_cost_usd),
|
|
11514
|
-
|
|
11515
|
-
|
|
11841
|
+
chalk6.cyan(fmtTokens2(s.total_tokens)),
|
|
11842
|
+
chalk6.dim(s.started_at.substring(0, 16))
|
|
11516
11843
|
]));
|
|
11517
11844
|
console.log();
|
|
11518
11845
|
});
|
|
@@ -11531,10 +11858,10 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11531
11858
|
}
|
|
11532
11859
|
const visibleRows = rows.slice(0, limit);
|
|
11533
11860
|
printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11534
|
-
|
|
11861
|
+
chalk6.white(r.project_name || chalk6.dim("unknown")),
|
|
11535
11862
|
String(r.sessions),
|
|
11536
11863
|
String(r.requests),
|
|
11537
|
-
|
|
11864
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11538
11865
|
fmt4(r.cost_usd)
|
|
11539
11866
|
]));
|
|
11540
11867
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
@@ -11549,7 +11876,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11549
11876
|
fmtAgent(r.agent),
|
|
11550
11877
|
String(r.sessions),
|
|
11551
11878
|
String(r.requests),
|
|
11552
|
-
|
|
11879
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11553
11880
|
fmt4(r.api_equivalent_usd),
|
|
11554
11881
|
fmt4(r.billable_usd),
|
|
11555
11882
|
fmt4(r.subscription_included_usd)
|
|
@@ -11572,15 +11899,15 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11572
11899
|
return;
|
|
11573
11900
|
}
|
|
11574
11901
|
if (rows.length === 0) {
|
|
11575
|
-
console.log(
|
|
11902
|
+
console.log(chalk6.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
|
|
11576
11903
|
} else {
|
|
11577
11904
|
const visibleRows = rows.slice(0, limit);
|
|
11578
11905
|
printTable(["Kind", "Cost Center", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11579
|
-
|
|
11580
|
-
|
|
11906
|
+
chalk6.white(r.kind),
|
|
11907
|
+
chalk6.white(r.name),
|
|
11581
11908
|
String(r.sessions),
|
|
11582
11909
|
String(r.requests),
|
|
11583
|
-
|
|
11910
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11584
11911
|
fmt4(r.cost_usd)
|
|
11585
11912
|
]));
|
|
11586
11913
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
@@ -11593,10 +11920,10 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11593
11920
|
}
|
|
11594
11921
|
const visibleRows = rows.slice(0, limit);
|
|
11595
11922
|
printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11596
|
-
|
|
11923
|
+
chalk6.white(r.model),
|
|
11597
11924
|
fmtAgent(r.agent),
|
|
11598
11925
|
String(r.requests),
|
|
11599
|
-
|
|
11926
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11600
11927
|
fmt4(r.cost_usd)
|
|
11601
11928
|
]));
|
|
11602
11929
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
@@ -11612,15 +11939,15 @@ program.command("accounts [period]").description("List account usage by email ad
|
|
|
11612
11939
|
return;
|
|
11613
11940
|
}
|
|
11614
11941
|
if (rows.length === 0) {
|
|
11615
|
-
console.log(
|
|
11942
|
+
console.log(chalk6.yellow("No account-attributed sessions yet. Run `economy sync` first."));
|
|
11616
11943
|
return;
|
|
11617
11944
|
}
|
|
11618
11945
|
console.log();
|
|
11619
|
-
console.log(
|
|
11946
|
+
console.log(chalk6.bold.cyan(` Accounts \u2014 ${period}`));
|
|
11620
11947
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11621
11948
|
const visibleRows = rows.slice(0, limit);
|
|
11622
11949
|
if (visibleRows.length < rows.length) {
|
|
11623
|
-
console.log(
|
|
11950
|
+
console.log(chalk6.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
|
|
11624
11951
|
}
|
|
11625
11952
|
console.log();
|
|
11626
11953
|
printAccountBreakdown(visibleRows);
|
|
@@ -11652,27 +11979,27 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
11652
11979
|
limit_usd: limitUsd,
|
|
11653
11980
|
alert_at_percent: alertAtPercent
|
|
11654
11981
|
});
|
|
11655
|
-
console.log(
|
|
11982
|
+
console.log(chalk6.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
|
|
11656
11983
|
});
|
|
11657
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) => {
|
|
11658
11985
|
const statuses = await getStore().listBudgets();
|
|
11659
11986
|
if (statuses.length === 0) {
|
|
11660
|
-
console.log(
|
|
11987
|
+
console.log(chalk6.yellow("No budgets set."));
|
|
11661
11988
|
return;
|
|
11662
11989
|
}
|
|
11663
11990
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11664
11991
|
const visibleStatuses = statuses.slice(0, limit);
|
|
11665
11992
|
console.log();
|
|
11666
11993
|
if (visibleStatuses.length < statuses.length) {
|
|
11667
|
-
console.log(
|
|
11994
|
+
console.log(chalk6.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
|
|
11668
11995
|
console.log();
|
|
11669
11996
|
}
|
|
11670
11997
|
printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((b) => {
|
|
11671
11998
|
const pct = b.percent_used.toFixed(1);
|
|
11672
|
-
const status = b.is_over_limit ?
|
|
11673
|
-
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 + "%");
|
|
11674
12001
|
return [
|
|
11675
|
-
|
|
12002
|
+
chalk6.white(b.cost_center_id ?? b.project_path ?? "global"),
|
|
11676
12003
|
b.period,
|
|
11677
12004
|
fmt4(b.limit_usd),
|
|
11678
12005
|
fmt4(b.current_spend_usd),
|
|
@@ -11685,62 +12012,62 @@ budgetCmd.command("list").description("List all budgets").option("--limit <n>",
|
|
|
11685
12012
|
});
|
|
11686
12013
|
budgetCmd.command("remove <id>").description("Remove a budget by ID").action(async (id) => {
|
|
11687
12014
|
await getStore().removeBudget(id);
|
|
11688
|
-
console.log(
|
|
12015
|
+
console.log(chalk6.green(`\u2713 Budget removed`));
|
|
11689
12016
|
});
|
|
11690
12017
|
var projectCmd = program.command("project").description("Manage tracked projects");
|
|
11691
12018
|
projectCmd.command("add <path>").description("Add a project").option("--name <name>", "Human-readable name").action(async (path, opts) => {
|
|
11692
12019
|
const { basename: basename4 } = __require("path");
|
|
11693
12020
|
const name = opts.name ?? basename4(path);
|
|
11694
12021
|
await getStore().addProject(path, name);
|
|
11695
|
-
console.log(
|
|
12022
|
+
console.log(chalk6.green(`\u2713 Project added: ${path}`));
|
|
11696
12023
|
});
|
|
11697
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) => {
|
|
11698
12025
|
const projects = await getStore().listProjects();
|
|
11699
12026
|
if (projects.length === 0) {
|
|
11700
|
-
console.log(
|
|
12027
|
+
console.log(chalk6.yellow("No projects tracked yet."));
|
|
11701
12028
|
return;
|
|
11702
12029
|
}
|
|
11703
12030
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11704
12031
|
const visibleProjects = projects.slice(0, limit);
|
|
11705
12032
|
console.log();
|
|
11706
12033
|
if (visibleProjects.length < projects.length) {
|
|
11707
|
-
console.log(
|
|
12034
|
+
console.log(chalk6.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
|
|
11708
12035
|
console.log();
|
|
11709
12036
|
}
|
|
11710
12037
|
printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], visibleProjects.map((p) => [
|
|
11711
|
-
|
|
11712
|
-
|
|
12038
|
+
chalk6.white(p.project_name || chalk6.dim("unknown")),
|
|
12039
|
+
chalk6.dim(p.project_path.substring(0, 40)),
|
|
11713
12040
|
String(p.sessions),
|
|
11714
12041
|
fmt4(p.cost_usd),
|
|
11715
|
-
|
|
12042
|
+
chalk6.dim(p.last_active?.substring(0, 16) ?? "\u2014")
|
|
11716
12043
|
]));
|
|
11717
12044
|
printHiddenRowsHint(projects.length, visibleProjects.length, "Use --limit <n>, --verbose, or economy project show <path>.");
|
|
11718
12045
|
console.log();
|
|
11719
12046
|
});
|
|
11720
12047
|
projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action(async (path) => {
|
|
11721
12048
|
await getStore().removeProject(path);
|
|
11722
|
-
console.log(
|
|
12049
|
+
console.log(chalk6.green(`\u2713 Project removed`));
|
|
11723
12050
|
});
|
|
11724
12051
|
projectCmd.command("rename <path> <name>").description("Rename a project").action(async (path, name) => {
|
|
11725
12052
|
try {
|
|
11726
12053
|
await getStore().renameProject(path, name);
|
|
11727
12054
|
} catch (e) {
|
|
11728
|
-
console.error(
|
|
12055
|
+
console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
|
|
11729
12056
|
process.exit(1);
|
|
11730
12057
|
}
|
|
11731
|
-
console.log(
|
|
12058
|
+
console.log(chalk6.green(`\u2713 Renamed to: ${name}`));
|
|
11732
12059
|
});
|
|
11733
12060
|
projectCmd.command("show <nameOrPath>").description("Detailed project breakdown with sparkline").action(async (nameOrPath) => {
|
|
11734
12061
|
await autoSync();
|
|
11735
12062
|
const detail = await getStore().projectDetail(nameOrPath);
|
|
11736
12063
|
if (!detail) {
|
|
11737
|
-
console.log(
|
|
12064
|
+
console.log(chalk6.yellow(`No sessions found for: ${nameOrPath}`));
|
|
11738
12065
|
return;
|
|
11739
12066
|
}
|
|
11740
12067
|
const dailyValues = detail.daily.map((d) => d.cost_usd);
|
|
11741
12068
|
console.log();
|
|
11742
|
-
console.log(
|
|
11743
|
-
console.log(
|
|
12069
|
+
console.log(chalk6.bold.cyan(` ${detail.project_name}`));
|
|
12070
|
+
console.log(chalk6.dim(` ${detail.project_path}`));
|
|
11744
12071
|
console.log();
|
|
11745
12072
|
printTable(["Metric", "Value"], [
|
|
11746
12073
|
["Total cost", fmt4(detail.total_cost_usd)],
|
|
@@ -11749,20 +12076,20 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
|
|
|
11749
12076
|
]);
|
|
11750
12077
|
if (dailyValues.length > 0) {
|
|
11751
12078
|
console.log(`
|
|
11752
|
-
${
|
|
12079
|
+
${chalk6.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
11753
12080
|
}
|
|
11754
12081
|
if (detail.models.length > 0) {
|
|
11755
12082
|
console.log(`
|
|
11756
|
-
${
|
|
12083
|
+
${chalk6.dim("Model breakdown:")}`);
|
|
11757
12084
|
for (const m of detail.models) {
|
|
11758
|
-
console.log(` ${
|
|
12085
|
+
console.log(` ${chalk6.white(m.model.padEnd(30))} ${fmt4(m.cost_usd)} (${fmtCount(m.requests)} reqs)`);
|
|
11759
12086
|
}
|
|
11760
12087
|
}
|
|
11761
12088
|
if (detail.top_sessions.length > 0) {
|
|
11762
12089
|
console.log(`
|
|
11763
|
-
${
|
|
12090
|
+
${chalk6.dim("Top sessions:")}`);
|
|
11764
12091
|
for (const s of detail.top_sessions) {
|
|
11765
|
-
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))}`);
|
|
11766
12093
|
}
|
|
11767
12094
|
}
|
|
11768
12095
|
console.log();
|
|
@@ -11771,18 +12098,18 @@ var configCmd = program.command("config").description("Manage economy configurat
|
|
|
11771
12098
|
configCmd.command("set <key> <value>").description("Set a config value").action(async (_key, _value) => {
|
|
11772
12099
|
const { setConfigValue: setConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11773
12100
|
setConfigValue2(_key, _value);
|
|
11774
|
-
console.log(
|
|
12101
|
+
console.log(chalk6.green(`\u2713 ${_key} = ${_value}`));
|
|
11775
12102
|
});
|
|
11776
12103
|
configCmd.command("get <key>").description("Get a config value").action(async (key) => {
|
|
11777
12104
|
const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11778
|
-
console.log(getConfigValue2(key) ??
|
|
12105
|
+
console.log(getConfigValue2(key) ?? chalk6.dim("(not set)"));
|
|
11779
12106
|
});
|
|
11780
12107
|
configCmd.command("webhook-test").description("Send a test payload to the configured webhook URL").action(async () => {
|
|
11781
|
-
const { loadConfig:
|
|
11782
|
-
const config =
|
|
12108
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
12109
|
+
const config = loadConfig2();
|
|
11783
12110
|
const url = config["webhook-url"];
|
|
11784
12111
|
if (!url) {
|
|
11785
|
-
console.log(
|
|
12112
|
+
console.log(chalk6.yellow("No webhook-url configured. Run: economy config set webhook-url <url>"));
|
|
11786
12113
|
return;
|
|
11787
12114
|
}
|
|
11788
12115
|
const payload = {
|
|
@@ -11799,21 +12126,21 @@ configCmd.command("webhook-test").description("Send a test payload to the config
|
|
|
11799
12126
|
});
|
|
11800
12127
|
const text = await res.text().catch(() => "");
|
|
11801
12128
|
if (res.ok) {
|
|
11802
|
-
console.log(
|
|
12129
|
+
console.log(chalk6.green(`\u2713 Webhook responded: HTTP ${res.status}`));
|
|
11803
12130
|
if (text)
|
|
11804
|
-
console.log(
|
|
12131
|
+
console.log(chalk6.dim(text.slice(0, 200)));
|
|
11805
12132
|
} else {
|
|
11806
|
-
console.log(
|
|
12133
|
+
console.log(chalk6.red(`\u2717 Webhook failed: HTTP ${res.status}`));
|
|
11807
12134
|
if (text)
|
|
11808
|
-
console.log(
|
|
12135
|
+
console.log(chalk6.dim(text.slice(0, 200)));
|
|
11809
12136
|
}
|
|
11810
12137
|
} catch (e) {
|
|
11811
|
-
console.log(
|
|
12138
|
+
console.log(chalk6.red(`\u2717 Request failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
11812
12139
|
}
|
|
11813
12140
|
});
|
|
11814
12141
|
configCmd.action(async () => {
|
|
11815
|
-
const { loadConfig:
|
|
11816
|
-
const config =
|
|
12142
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
12143
|
+
const config = loadConfig2();
|
|
11817
12144
|
console.log();
|
|
11818
12145
|
printTable(["Key", "Value"], Object.entries(config).map(([k, v]) => [k, String(v)]));
|
|
11819
12146
|
console.log();
|
|
@@ -11825,17 +12152,17 @@ pricingCmd.command("list").description("List all model prices").option("--limit
|
|
|
11825
12152
|
const visibleRows = rows.slice(0, limit);
|
|
11826
12153
|
console.log();
|
|
11827
12154
|
if (visibleRows.length < rows.length) {
|
|
11828
|
-
console.log(
|
|
12155
|
+
console.log(chalk6.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
|
|
11829
12156
|
console.log();
|
|
11830
12157
|
}
|
|
11831
12158
|
printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], visibleRows.map((r) => [
|
|
11832
|
-
|
|
12159
|
+
chalk6.white(r.model),
|
|
11833
12160
|
fmt4(r.input_per_1m),
|
|
11834
12161
|
fmt4(r.output_per_1m),
|
|
11835
12162
|
fmt4(r.cache_read_per_1m),
|
|
11836
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),
|
|
11837
12164
|
fmt4(r.cache_storage_per_1m_hour ?? 0),
|
|
11838
|
-
|
|
12165
|
+
chalk6.dim(fmt4(r.output_per_1m / 1000))
|
|
11839
12166
|
]));
|
|
11840
12167
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n> or --verbose.");
|
|
11841
12168
|
console.log();
|
|
@@ -11856,79 +12183,35 @@ pricingCmd.command("set <model>").description("Set pricing for a model").option(
|
|
|
11856
12183
|
cache_write_1h_per_1m: cacheWrite1h,
|
|
11857
12184
|
cache_storage_per_1m_hour: cacheStorage
|
|
11858
12185
|
});
|
|
11859
|
-
console.log(
|
|
12186
|
+
console.log(chalk6.green(`\u2713 Pricing updated for ${model}`));
|
|
11860
12187
|
});
|
|
11861
12188
|
pricingCmd.command("remove <model>").description("Remove pricing for a model").action(async (model) => {
|
|
11862
12189
|
await getStore().removePricing(model);
|
|
11863
|
-
console.log(
|
|
12190
|
+
console.log(chalk6.green(`\u2713 Pricing removed for ${model}`));
|
|
11864
12191
|
});
|
|
11865
12192
|
program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
|
|
11866
12193
|
const port = parseCliPort(opts.port ?? "3456", "--port");
|
|
11867
12194
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
11868
12195
|
startServer2(port);
|
|
11869
12196
|
});
|
|
11870
|
-
program.command("dashboard").description("Open the web dashboard (auto-starts server if not running)").option("-p, --port <port>", "Server port", "3456").action(async (opts) => {
|
|
11871
|
-
const port = parseCliPort(opts.port ?? "3456", "--port");
|
|
11872
|
-
const url = `http://localhost:${port}`;
|
|
11873
|
-
let serverRunning = false;
|
|
11874
|
-
try {
|
|
11875
|
-
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(500) });
|
|
11876
|
-
serverRunning = res.ok;
|
|
11877
|
-
} catch {}
|
|
11878
|
-
if (!serverRunning) {
|
|
11879
|
-
console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
11880
|
-
const { spawn } = await import("child_process");
|
|
11881
|
-
const { resolve: resolve3, dirname: dirname4 } = await import("path");
|
|
11882
|
-
const serveScript = resolve3(dirname4(process.argv[1]), "..", "server", "index.js");
|
|
11883
|
-
const child = spawn(process.execPath, [serveScript], {
|
|
11884
|
-
detached: true,
|
|
11885
|
-
stdio: "ignore",
|
|
11886
|
-
env: { ...process.env, ECONOMY_PORT: String(port) }
|
|
11887
|
-
});
|
|
11888
|
-
child.unref();
|
|
11889
|
-
let attempts = 0;
|
|
11890
|
-
while (attempts < 20) {
|
|
11891
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
11892
|
-
try {
|
|
11893
|
-
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(300) });
|
|
11894
|
-
if (res.ok) {
|
|
11895
|
-
serverRunning = true;
|
|
11896
|
-
break;
|
|
11897
|
-
}
|
|
11898
|
-
} catch {}
|
|
11899
|
-
attempts++;
|
|
11900
|
-
}
|
|
11901
|
-
if (serverRunning) {
|
|
11902
|
-
console.log(chalk7.green(`\u2713 Server started`));
|
|
11903
|
-
} else {
|
|
11904
|
-
console.log(chalk7.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
|
|
11905
|
-
}
|
|
11906
|
-
}
|
|
11907
|
-
console.log(chalk7.cyan(`Opening ${url}`));
|
|
11908
|
-
try {
|
|
11909
|
-
execSync(`open ${url}`);
|
|
11910
|
-
} catch {
|
|
11911
|
-
console.log(chalk7.yellow(`Open your browser at ${url}`));
|
|
11912
|
-
}
|
|
11913
|
-
});
|
|
11914
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) => {
|
|
11915
12198
|
const doAll = opts.all || !opts.claude && !opts.codex && !opts.gemini;
|
|
11916
12199
|
if (opts.claude || doAll) {
|
|
11917
|
-
console.log(
|
|
12200
|
+
console.log(chalk6.bold.cyan(`
|
|
11918
12201
|
Claude Code:`));
|
|
11919
|
-
console.log(
|
|
12202
|
+
console.log(chalk6.white(" claude mcp add --transport stdio --scope user economy -- economy-mcp"));
|
|
11920
12203
|
}
|
|
11921
12204
|
if (opts.codex || doAll) {
|
|
11922
|
-
console.log(
|
|
12205
|
+
console.log(chalk6.bold.yellow(`
|
|
11923
12206
|
Codex (~/.codex/config.toml):`));
|
|
11924
|
-
console.log(
|
|
12207
|
+
console.log(chalk6.white(` [mcp_servers.economy]
|
|
11925
12208
|
command = "economy-mcp"
|
|
11926
12209
|
args = []`));
|
|
11927
12210
|
}
|
|
11928
12211
|
if (opts.gemini || doAll) {
|
|
11929
|
-
console.log(
|
|
12212
|
+
console.log(chalk6.bold.green(`
|
|
11930
12213
|
Gemini (~/.gemini/settings.json):`));
|
|
11931
|
-
console.log(
|
|
12214
|
+
console.log(chalk6.white(' "mcpServers": { "economy": { "command": "economy-mcp", "args": [] } }'));
|
|
11932
12215
|
}
|
|
11933
12216
|
console.log();
|
|
11934
12217
|
});
|
|
@@ -11937,12 +12220,12 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11937
12220
|
await autoSync();
|
|
11938
12221
|
const detail = await getStore().sessionDetail(id);
|
|
11939
12222
|
if (!detail) {
|
|
11940
|
-
console.log(
|
|
12223
|
+
console.log(chalk6.red(`Session not found: ${id}`));
|
|
11941
12224
|
process.exit(1);
|
|
11942
12225
|
}
|
|
11943
12226
|
const { session, requests } = detail;
|
|
11944
12227
|
console.log();
|
|
11945
|
-
console.log(
|
|
12228
|
+
console.log(chalk6.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
|
|
11946
12229
|
console.log();
|
|
11947
12230
|
printTable(["Field", "Value"], [
|
|
11948
12231
|
["Agent", String(session["agent"])],
|
|
@@ -11955,12 +12238,12 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11955
12238
|
]);
|
|
11956
12239
|
if (requests.length > 0) {
|
|
11957
12240
|
const visibleRequests = requests.slice(0, requestLimit);
|
|
11958
|
-
console.log(
|
|
12241
|
+
console.log(chalk6.dim(`
|
|
11959
12242
|
Requests (${visibleRequests.length}${visibleRequests.length < requests.length ? ` of ${requests.length}` : ""}):
|
|
11960
12243
|
`));
|
|
11961
12244
|
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], visibleRequests.map((r) => [
|
|
11962
|
-
|
|
11963
|
-
|
|
12245
|
+
chalk6.dim(String(r["timestamp"]).substring(11, 19)),
|
|
12246
|
+
chalk6.white(String(r["model"]).substring(0, 22)),
|
|
11964
12247
|
fmtTokens2(r["input_tokens"]),
|
|
11965
12248
|
fmtTokens2(r["output_tokens"]),
|
|
11966
12249
|
fmtTokens2(r["cache_read_tokens"]),
|
|
@@ -11968,7 +12251,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11968
12251
|
fmt4(r["cost_usd"])
|
|
11969
12252
|
]));
|
|
11970
12253
|
if (requests.length > visibleRequests.length) {
|
|
11971
|
-
console.log(
|
|
12254
|
+
console.log(chalk6.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
|
|
11972
12255
|
}
|
|
11973
12256
|
}
|
|
11974
12257
|
console.log();
|
|
@@ -11978,27 +12261,27 @@ program.command("machines").description("List all machines that have synced data
|
|
|
11978
12261
|
const machines = await getStore().machines();
|
|
11979
12262
|
const current = getMachineId();
|
|
11980
12263
|
if (machines.length === 0) {
|
|
11981
|
-
console.log(
|
|
12264
|
+
console.log(chalk6.yellow(`No machine data yet. Current machine: ${current}`));
|
|
11982
12265
|
return;
|
|
11983
12266
|
}
|
|
11984
12267
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11985
12268
|
const visibleMachines = machines.slice(0, limit);
|
|
11986
12269
|
console.log();
|
|
11987
|
-
console.log(
|
|
12270
|
+
console.log(chalk6.bold.cyan(" Machines"));
|
|
11988
12271
|
if (visibleMachines.length < machines.length) {
|
|
11989
|
-
console.log(
|
|
12272
|
+
console.log(chalk6.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
11990
12273
|
}
|
|
11991
12274
|
console.log();
|
|
11992
12275
|
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], visibleMachines.map((m) => [
|
|
11993
|
-
m.machine_id === current ?
|
|
12276
|
+
m.machine_id === current ? chalk6.green(`${m.machine_id} (this)`) : chalk6.white(m.machine_id),
|
|
11994
12277
|
fmtCount(m.sessions),
|
|
11995
12278
|
fmtCount(m.requests),
|
|
11996
12279
|
fmt4(m.total_cost_usd),
|
|
11997
|
-
|
|
12280
|
+
chalk6.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
11998
12281
|
]));
|
|
11999
12282
|
printHiddenRowsHint(machines.length, visibleMachines.length, "Use --limit <n> or --verbose.");
|
|
12000
12283
|
console.log(`
|
|
12001
|
-
${
|
|
12284
|
+
${chalk6.dim("Current machine:")} ${chalk6.bold(current)}`);
|
|
12002
12285
|
console.log();
|
|
12003
12286
|
});
|
|
12004
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) => {
|
|
@@ -12025,7 +12308,7 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
12025
12308
|
if (opts.output) {
|
|
12026
12309
|
const { writeFileSync: writeFileSync4 } = await import("fs");
|
|
12027
12310
|
writeFileSync4(opts.output, csv);
|
|
12028
|
-
console.log(
|
|
12311
|
+
console.log(chalk6.green(`\u2713 Exported to ${opts.output}`));
|
|
12029
12312
|
} else {
|
|
12030
12313
|
process.stdout.write(csv);
|
|
12031
12314
|
}
|
|
@@ -12081,11 +12364,11 @@ program.command("compare <period1> <period2>").description("Compare two periods
|
|
|
12081
12364
|
const d = v1 - v2;
|
|
12082
12365
|
const pct = v2 > 0 ? (d / v2 * 100).toFixed(1) : "\u2014";
|
|
12083
12366
|
const sign = d >= 0 ? "+" : "";
|
|
12084
|
-
const color = d > 0 ?
|
|
12367
|
+
const color = d > 0 ? chalk6.red : d < 0 ? chalk6.green : chalk6.dim;
|
|
12085
12368
|
return color(`${sign}${pct}%`);
|
|
12086
12369
|
}
|
|
12087
12370
|
console.log();
|
|
12088
|
-
console.log(
|
|
12371
|
+
console.log(chalk6.bold.cyan(` ${p1} vs ${p2}`));
|
|
12089
12372
|
console.log();
|
|
12090
12373
|
printTable(["Metric", p1, p2, "Change"], [
|
|
12091
12374
|
["Cost", fmt4(a.cost), fmt4(b.cost), delta(a.cost, b.cost)],
|
|
@@ -12101,12 +12384,12 @@ program.command("forecast").description("Project end-of-month cost based on curr
|
|
|
12101
12384
|
const cheapest = f.cheapest_day;
|
|
12102
12385
|
const mostExpensive = f.most_expensive_day;
|
|
12103
12386
|
console.log();
|
|
12104
|
-
console.log(
|
|
12387
|
+
console.log(chalk6.bold.cyan(` Forecast (${f.day_of_month} of ${f.days_in_month} days)`));
|
|
12105
12388
|
console.log();
|
|
12106
12389
|
printTable(["Metric", "Value"], [
|
|
12107
12390
|
["Spent so far", fmt4(f.spent_so_far_usd)],
|
|
12108
12391
|
["Daily average", fmt4(f.daily_avg_usd)],
|
|
12109
|
-
[
|
|
12392
|
+
[chalk6.bold("Projected total"), chalk6.bold(fmt4(f.projected_usd).replace(chalk6.green(""), ""))],
|
|
12110
12393
|
["Last 7-day rate", `${fmt4(f.last7_daily_avg_usd)}/day \u2192 ${fmt4(f.last7_projected_usd)}`],
|
|
12111
12394
|
["Cheapest day", cheapest ? `${fmt4(cheapest.cost_usd)} (${cheapest.date})` : "\u2014"],
|
|
12112
12395
|
["Most expensive", mostExpensive ? `${fmt4(mostExpensive.cost_usd)} (${mostExpensive.date})` : "\u2014"]
|
|
@@ -12117,14 +12400,14 @@ program.command("efficiency").description("Show output/input token ratio per mod
|
|
|
12117
12400
|
await autoSync();
|
|
12118
12401
|
const models = await getStore().efficiency();
|
|
12119
12402
|
console.log();
|
|
12120
|
-
console.log(
|
|
12403
|
+
console.log(chalk6.bold.cyan(" Token Efficiency"));
|
|
12121
12404
|
console.log();
|
|
12122
12405
|
printTable(["Model", "Output/Input", "Cache Hit%", "Cost/1k Output", "Requests"], models.map((m) => {
|
|
12123
12406
|
const ratio = m.input > 0 ? (m.output / m.input).toFixed(2) : "\u2014";
|
|
12124
12407
|
const totalInput = m.input + m.cache_read + m.cache_write;
|
|
12125
12408
|
const cacheHit = totalInput > 0 ? (m.cache_read / totalInput * 100).toFixed(1) + "%" : "\u2014";
|
|
12126
12409
|
const costPer1kOutput = m.output > 0 ? fmt4(m.cost / m.output * 1000) : "\u2014";
|
|
12127
|
-
return [
|
|
12410
|
+
return [chalk6.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
|
|
12128
12411
|
}));
|
|
12129
12412
|
console.log();
|
|
12130
12413
|
});
|
|
@@ -12156,29 +12439,29 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
|
|
|
12156
12439
|
agent,
|
|
12157
12440
|
limit_usd: limitUsd
|
|
12158
12441
|
});
|
|
12159
|
-
console.log(
|
|
12442
|
+
console.log(chalk6.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
|
|
12160
12443
|
});
|
|
12161
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) => {
|
|
12162
12445
|
const statuses = await getStore().listGoals();
|
|
12163
12446
|
if (statuses.length === 0) {
|
|
12164
|
-
console.log(
|
|
12447
|
+
console.log(chalk6.yellow("No goals set."));
|
|
12165
12448
|
return;
|
|
12166
12449
|
}
|
|
12167
12450
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12168
12451
|
const visibleStatuses = statuses.slice(0, limit);
|
|
12169
12452
|
console.log();
|
|
12170
12453
|
if (visibleStatuses.length < statuses.length) {
|
|
12171
|
-
console.log(
|
|
12454
|
+
console.log(chalk6.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
|
|
12172
12455
|
console.log();
|
|
12173
12456
|
}
|
|
12174
12457
|
printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((g) => {
|
|
12175
12458
|
const pct = g.percent_used.toFixed(1);
|
|
12176
12459
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
12177
|
-
const status = g.is_over ?
|
|
12178
|
-
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 + "%");
|
|
12179
12462
|
return [
|
|
12180
12463
|
g.period,
|
|
12181
|
-
|
|
12464
|
+
chalk6.white(scope),
|
|
12182
12465
|
fmt4(g.limit_usd),
|
|
12183
12466
|
fmt4(g.current_spend_usd),
|
|
12184
12467
|
pctColor,
|
|
@@ -12190,12 +12473,12 @@ goalCmd.command("list").description("List all goals with progress").option("--li
|
|
|
12190
12473
|
});
|
|
12191
12474
|
goalCmd.command("remove <id>").description("Remove a goal").action(async (id) => {
|
|
12192
12475
|
await getStore().removeGoal(id);
|
|
12193
|
-
console.log(
|
|
12476
|
+
console.log(chalk6.green(`\u2713 Goal removed`));
|
|
12194
12477
|
});
|
|
12195
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) => {
|
|
12196
12479
|
const statuses = await getStore().listGoals();
|
|
12197
12480
|
if (statuses.length === 0) {
|
|
12198
|
-
console.log(
|
|
12481
|
+
console.log(chalk6.yellow("No goals set."));
|
|
12199
12482
|
return;
|
|
12200
12483
|
}
|
|
12201
12484
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
@@ -12207,7 +12490,7 @@ goalCmd.command("status").description("Quick goal progress summary").option("--l
|
|
|
12207
12490
|
const barFilled = Math.round(pct / 10);
|
|
12208
12491
|
const barEmpty = 10 - barFilled;
|
|
12209
12492
|
const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(barEmpty);
|
|
12210
|
-
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");
|
|
12211
12494
|
const label = `${g.period} (${scope})`.padEnd(20);
|
|
12212
12495
|
console.log(` ${label} ${bar} ${fmt4(g.current_spend_usd)} / ${fmt4(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
|
|
12213
12496
|
}
|
|
@@ -12219,7 +12502,7 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
12219
12502
|
try {
|
|
12220
12503
|
const t = type.toLowerCase();
|
|
12221
12504
|
if (!(t in label)) {
|
|
12222
|
-
console.error(
|
|
12505
|
+
console.error(chalk6.red(`Unknown type: ${type}. Use: budget | project | goal | pricing`));
|
|
12223
12506
|
process.exit(1);
|
|
12224
12507
|
}
|
|
12225
12508
|
const store = getStore();
|
|
@@ -12237,9 +12520,9 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
12237
12520
|
await store.removePricing(id);
|
|
12238
12521
|
break;
|
|
12239
12522
|
}
|
|
12240
|
-
console.log(
|
|
12523
|
+
console.log(chalk6.green(`\u2713 ${label[t]} ${id} removed`));
|
|
12241
12524
|
} catch (e) {
|
|
12242
|
-
console.error(
|
|
12525
|
+
console.error(chalk6.red(`Failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
12243
12526
|
process.exit(1);
|
|
12244
12527
|
}
|
|
12245
12528
|
});
|
|
@@ -12251,51 +12534,51 @@ billingCmd.command("sync").description("Sync actual billing from Anthropic, Open
|
|
|
12251
12534
|
if (isCloudStore()) {
|
|
12252
12535
|
const cloud = economyCloudStorage();
|
|
12253
12536
|
if (!cloud.active) {
|
|
12254
|
-
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)"));
|
|
12255
12538
|
return;
|
|
12256
12539
|
}
|
|
12257
12540
|
const result = await billingSyncToCloud(cloud, { days, anthropic: opts.anthropic, openai: opts.openai, gemini: opts.gemini });
|
|
12258
12541
|
for (const [provider, p] of Object.entries(result.providers)) {
|
|
12259
12542
|
if (p.ok)
|
|
12260
|
-
console.log(
|
|
12543
|
+
console.log(chalk6.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
|
|
12261
12544
|
else
|
|
12262
|
-
console.log(
|
|
12545
|
+
console.log(chalk6.red(`\u2717 ${provider}: ${p.error}`));
|
|
12263
12546
|
}
|
|
12264
12547
|
if (result.posted)
|
|
12265
|
-
console.log(
|
|
12548
|
+
console.log(chalk6.green(`\u2713 pushed ${result.total} billing rows to the shared API`));
|
|
12266
12549
|
return;
|
|
12267
12550
|
}
|
|
12268
12551
|
const db = openDatabase();
|
|
12269
12552
|
const doAll = !opts.anthropic && !opts.openai && !opts.gemini;
|
|
12270
12553
|
if (opts.anthropic || doAll) {
|
|
12271
|
-
process.stdout.write(
|
|
12554
|
+
process.stdout.write(chalk6.cyan("\u2192 Syncing Anthropic billing... "));
|
|
12272
12555
|
try {
|
|
12273
12556
|
const r = await syncAnthropicBilling(db, { days });
|
|
12274
|
-
console.log(
|
|
12557
|
+
console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
12275
12558
|
} catch (e) {
|
|
12276
|
-
console.log(
|
|
12559
|
+
console.log(chalk6.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
12277
12560
|
}
|
|
12278
12561
|
}
|
|
12279
12562
|
if (opts.openai || doAll) {
|
|
12280
|
-
process.stdout.write(
|
|
12563
|
+
process.stdout.write(chalk6.cyan("\u2192 Syncing OpenAI billing... "));
|
|
12281
12564
|
try {
|
|
12282
12565
|
const r = await syncOpenAIBilling(db, { days });
|
|
12283
|
-
console.log(
|
|
12566
|
+
console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
12284
12567
|
} catch (e) {
|
|
12285
|
-
console.log(
|
|
12568
|
+
console.log(chalk6.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
12286
12569
|
}
|
|
12287
12570
|
}
|
|
12288
12571
|
if (opts.gemini || doAll) {
|
|
12289
|
-
process.stdout.write(
|
|
12572
|
+
process.stdout.write(chalk6.cyan("\u2192 Syncing Gemini billing... "));
|
|
12290
12573
|
try {
|
|
12291
12574
|
const r = await syncGeminiBilling(db, { days });
|
|
12292
12575
|
if (r.skipped) {
|
|
12293
|
-
console.log(
|
|
12576
|
+
console.log(chalk6.yellow(`skipped \u2014 ${r.skipped}`));
|
|
12294
12577
|
} else {
|
|
12295
|
-
console.log(
|
|
12578
|
+
console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
12296
12579
|
}
|
|
12297
12580
|
} catch (e) {
|
|
12298
|
-
console.log(
|
|
12581
|
+
console.log(chalk6.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
12299
12582
|
}
|
|
12300
12583
|
}
|
|
12301
12584
|
});
|
|
@@ -12305,22 +12588,21 @@ billingCmd.command("show").description("Show actual billing totals vs our estima
|
|
|
12305
12588
|
const actual = await store.billingSummary(period);
|
|
12306
12589
|
const estimated = await store.summary(period);
|
|
12307
12590
|
console.log();
|
|
12308
|
-
console.log(
|
|
12591
|
+
console.log(chalk6.bold.cyan(` Billing ${period} (actual from admin APIs)
|
|
12309
12592
|
`));
|
|
12310
|
-
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)]));
|
|
12311
12594
|
console.log();
|
|
12312
|
-
console.log(` ${
|
|
12313
|
-
console.log(` ${
|
|
12595
|
+
console.log(` ${chalk6.bold("Actual total:")} ${fmt4(actual.total_usd)}`);
|
|
12596
|
+
console.log(` ${chalk6.dim("Our estimate:")} ${fmt4(estimated.total_usd)}`);
|
|
12314
12597
|
const diff = estimated.total_usd - actual.total_usd;
|
|
12315
12598
|
const pct = billingDeltaPct(estimated.total_usd, actual.total_usd);
|
|
12316
12599
|
const rendered = pct === null ? "n/a" : `${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%`;
|
|
12317
|
-
console.log(` ${
|
|
12600
|
+
console.log(` ${chalk6.dim("Difference:")} ${fmt4(Math.abs(diff))} (${rendered})`);
|
|
12318
12601
|
if (pct === null) {
|
|
12319
|
-
console.log(
|
|
12602
|
+
console.log(chalk6.yellow(` ${chalk6.dim("Note:")} no provider billing recorded for ${period} \u2014 drift is UNKNOWN, not zero.`));
|
|
12320
12603
|
}
|
|
12321
12604
|
console.log();
|
|
12322
12605
|
});
|
|
12323
|
-
registerBrainsCommand(program);
|
|
12324
12606
|
registerTodosCommand(program);
|
|
12325
12607
|
registerBriefCommand(program, { beforeRead: () => autoSync({ dedupe: false }) });
|
|
12326
12608
|
registerExtendedCommands(program);
|