@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +6 -13
  4. package/SECURITY.md +1 -1
  5. package/dist/cli/commands/extras.d.ts.map +1 -1
  6. package/dist/cli/index.js +1135 -853
  7. package/dist/db/cloud.d.ts +16 -8
  8. package/dist/db/cloud.d.ts.map +1 -1
  9. package/dist/db/database.d.ts +19 -0
  10. package/dist/db/database.d.ts.map +1 -1
  11. package/dist/index.js +198 -19
  12. package/dist/ingest/claude.d.ts.map +1 -1
  13. package/dist/ingest/loops.d.ts +9 -0
  14. package/dist/ingest/loops.d.ts.map +1 -1
  15. package/dist/lib/accounts-store.d.ts +83 -0
  16. package/dist/lib/accounts-store.d.ts.map +1 -0
  17. package/dist/lib/accounts.d.ts +2 -1
  18. package/dist/lib/accounts.d.ts.map +1 -1
  19. package/dist/lib/api-display-url.d.ts +19 -0
  20. package/dist/lib/api-display-url.d.ts.map +1 -0
  21. package/dist/lib/autosync-gate.d.ts +15 -0
  22. package/dist/lib/autosync-gate.d.ts.map +1 -0
  23. package/dist/lib/cloud-ingest.d.ts +15 -1
  24. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  25. package/dist/lib/cloud-storage.d.ts +166 -10
  26. package/dist/lib/cloud-storage.d.ts.map +1 -1
  27. package/dist/lib/gatherer.d.ts.map +1 -1
  28. package/dist/lib/serve-auth.d.ts.map +1 -1
  29. package/dist/lib/store/index.d.ts +6 -3
  30. package/dist/lib/store/index.d.ts.map +1 -1
  31. package/dist/lib/test-hermetic-accounts.d.ts +3 -2
  32. package/dist/lib/test-hermetic-accounts.d.ts.map +1 -1
  33. package/dist/lib/test-hermetic-fleet-env.d.ts +4 -0
  34. package/dist/lib/test-hermetic-fleet-env.d.ts.map +1 -0
  35. package/dist/mcp/agent-registry.d.ts +85 -0
  36. package/dist/mcp/agent-registry.d.ts.map +1 -0
  37. package/dist/mcp/harness.d.ts +26 -0
  38. package/dist/mcp/harness.d.ts.map +1 -0
  39. package/dist/mcp/index.js +1340 -302
  40. package/dist/mcp/server.d.ts.map +1 -1
  41. package/dist/otel/index.js +430 -16
  42. package/dist/server/index.js +548 -155
  43. package/dist/server/serve.d.ts +0 -2
  44. package/dist/server/serve.d.ts.map +1 -1
  45. package/docs/cli.md +2 -3
  46. package/docs/configuration.md +23 -10
  47. package/docs/ingestion.md +1 -1
  48. package/docs/mcp.md +4 -2
  49. package/docs/otel.md +8 -2
  50. package/docs/rest-api.md +2 -2
  51. package/package.json +5 -11
  52. package/postinstall.js +52 -1
  53. package/dashboard/README.md +0 -26
  54. package/dist/cli/brains.d.ts +0 -3
  55. 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 { dataDir } from "@hasna/paths";
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["ECONOMY_MACHINE_ID"]);
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.3.28",
2411
- description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
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: "cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && 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",
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/accounts": "0.2.23",
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": "latest",
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
- const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
3523
- return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
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
- var ECONOMY_APP = "economy", cache;
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
- init_storage();
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["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
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 chalk3 from "chalk";
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(chalk3.bold.cyan(" economy"));
4067
- console.log(chalk3.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"));
4068
- console.log(` Today ${chalk3.green(fmt(data.today.total_usd))} ${data.today.sessions} sessions`);
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(chalk3.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"));
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(chalk3.dim(" API auth: ECONOMY_API_TOKEN set"));
4079
- console.log(chalk3.dim(`
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 join4 } from "path";
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: join4(home, ".claude", "projects"),
4133
- claudeCredentials: join4(home, ".claude", ".credentials.json"),
4134
- takumiProjects: join4(home, ".takumi", "projects"),
4135
- codexDir: join4(home, ".codex"),
4136
- codexDb: join4(home, ".codex", "state_5.sqlite"),
4137
- codexAuth: join4(home, ".codex", "auth.json"),
4138
- codexConfig: join4(home, ".codex", "config.toml"),
4139
- geminiTmp: join4(home, ".gemini", "tmp"),
4140
- geminiHistory: join4(home, ".gemini", "history"),
4141
- opencodeMessages: join4(home, ".local", "share", "opencode", "storage", "message"),
4142
- piSessions: join4(home, ".pi", "agent", "sessions"),
4143
- hermesDir: join4(home, ".hermes"),
4144
- hermesDb: join4(home, ".hermes", "state.db")
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 homedir3 } from "os";
4266
- import { join as join6, basename } from "path";
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(join6(dir, entry.name));
4817
+ walk(join5(dir, entry.name));
4280
4818
  else if (entry.name.endsWith(".jsonl"))
4281
- files.push(join6(dir, entry.name));
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 = join6(projectsDir, projectDirEntry.name);
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 = getIngestState(db, agentName, stateKey);
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 = join6(homedir3(), ".claude", "projects");
4446
- TAKUMI_PROJECTS_DIR = join6(homedir3(), ".takumi", "projects");
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 homedir4 } from "os";
4452
- import { join as join7, basename as basename2 } from "path";
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 = join7(homedir4(), ".codex", "state_5.sqlite");
4699
- DEFAULT_CODEX_CONFIG_PATH = join7(homedir4(), ".codex", "config.toml");
4700
- DEFAULT_CODEWITH_DB_PATH = join7(homedir4(), ".codewith", "state_5.sqlite");
4701
- DEFAULT_CODEWITH_CONFIG_PATH = join7(homedir4(), ".codewith", "config.toml");
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 homedir5 } from "os";
4707
- import { join as join8, basename as basename3 } from "path";
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(join8(root, entry.name));
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 = join8(projectDir, ".project_root");
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 = join8(projectDir, "chats");
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) => join8(chatsDir, 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(homedir5(), "~");
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 = join8(homedir5(), ".gemini", "tmp");
4864
- DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
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 homedir6 } from "os";
4870
- import { join as join9 } from "path";
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 = join9(dir, entry.name);
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 = join9(OPENCODE_STORAGE, "message");
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 = join9(homedir6(), ".local", "share", "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 homedir7 } from "os";
5108
- import { join as join10 } from "path";
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 = join10(dir, entry.name);
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"] ?? join10(homedir7(), ".pi", "agent", "sessions");
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 homedir8 } from "os";
5206
- import { join as join11 } from "path";
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 = join11(homedir8(), ".hermes", "state.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 homedir9 } from "os";
5301
- import { join as join12 } from "path";
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 { dataDir as dataDir2 } from "@hasna/paths";
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 = join12(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
5310
- return existsSync10(join12(resolved, "loops.db")) ? join12(resolved, "loops.db") : legacy;
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: () => saveConfig2,
6134
- loadConfig: () => loadConfig2,
6731
+ saveConfig: () => saveConfig,
6732
+ loadConfig: () => loadConfig,
6135
6733
  getConfigValue: () => getConfigValue
6136
6734
  });
6137
- import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
6138
- import { dirname as dirname2, join as join14 } from "path";
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"] ?? join14(getDataDir(), "config.json");
6738
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
6141
6739
  }
6142
- function loadConfig2() {
6740
+ function loadConfig() {
6143
6741
  try {
6144
6742
  const configPath = getConfigPath();
6145
6743
  if (existsSync13(configPath)) {
6146
- const raw = readFileSync10(configPath, "utf-8");
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 saveConfig2(config) {
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 = loadConfig2();
6759
+ const config = loadConfig();
6162
6760
  return config[key] ?? null;
6163
6761
  }
6164
6762
  function setConfigValue(key, value) {
6165
- const config = loadConfig2();
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
- saveConfig2(config);
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 = loadConfig2();
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 chalk5 from "chalk";
6938
+ import chalk4 from "chalk";
6341
6939
  import { watch } from "fs";
6342
6940
  function fmt3(usd) {
6343
- return chalk5.green(`$${usd.toFixed(4)}`);
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(chalk5.bold.cyan(" economy watch") + chalk5.dim(` \u2014 live cost stream (${mode})`));
6348
- console.log(chalk5.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"));
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(chalk5.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"));
6351
- console.log(chalk5.dim(" [agent] cost model tokens project"));
6352
- console.log(chalk5.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"));
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 chalk5.blue("[claude]");
6954
+ return chalk4.blue("[claude]");
6357
6955
  if (agent === "codex")
6358
- return chalk5.yellow("[codex] ");
6956
+ return chalk4.yellow("[codex] ");
6359
6957
  if (agent === "gemini")
6360
- return chalk5.green("[gemini]");
6958
+ return chalk4.green("[gemini]");
6361
6959
  if (agent === "takumi")
6362
- return chalk5.magenta("[takumi]");
6960
+ return chalk4.magenta("[takumi]");
6363
6961
  if (agent === "opencode")
6364
- return chalk5.cyan("[opncde]");
6962
+ return chalk4.cyan("[opncde]");
6365
6963
  if (agent === "cursor")
6366
- return chalk5.white("[cursor]");
6964
+ return chalk4.white("[cursor]");
6367
6965
  if (agent === "pi")
6368
- return chalk5.white("[pi ]");
6966
+ return chalk4.white("[pi ]");
6369
6967
  if (agent === "hermes")
6370
- return chalk5.white("[hermes]");
6371
- return chalk5.gray(`[${agent.slice(0, 6).padEnd(6)}]`);
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(chalk5.dim(`
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(chalk5.dim(`
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(chalk5.dim(`
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(chalk5.dim(" Waiting for new requests..."));
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(chalk5.dim(`
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(chalk5.dim(`
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(chalk5.dim(`
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 join15 } from "path";
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
- join15(here, "pg-sync-worker.ts"),
6556
- join15(here, "pg-sync-worker.js"),
6557
- join15(here, "..", "db", "pg-sync-worker.js")
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: createServerFetch(apiHandler, options.dashboardDir)
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, DEFAULT_DASHBOARD_DIR, FOUNDATION_PATHS;
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 chalk6 from "chalk";
9589
+ import chalk5 from "chalk";
9028
9590
  import { execFileSync as execFileSync3 } from "child_process";
9029
- import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
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 join16 } from "path";
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 existsSync17(APP_PATH);
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(chalk6.yellow("Economy Bar is already installed. Use --force to reinstall."));
9049
- console.log(chalk6.dim(` Location: ${APP_PATH}`));
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(chalk6.cyan(`\u2192 Detecting architecture: ${cpuArch}`));
9054
- console.log(chalk6.cyan("\u2192 Fetching latest release info..."));
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(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
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 = join16(tmpdir(), `economy-bar-${cpuArch}.zip`);
9074
- const extractDir = join16(tmpdir(), "economy-bar-extracted");
9075
- console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
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(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
9644
+ console.log(chalk5.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
9083
9645
  } catch (e) {
9084
- console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
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(chalk6.cyan("\u2192 Installing to /Applications..."));
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(join16(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
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(chalk6.green(`\u2713 Installed to ${APP_PATH}`));
9662
+ console.log(chalk5.green(`\u2713 Installed to ${APP_PATH}`));
9101
9663
  } catch (e) {
9102
- console.error(chalk6.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
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(chalk6.cyan("\u2192 Launching Economy Bar..."));
9667
+ console.log(chalk5.cyan("\u2192 Launching Economy Bar..."));
9106
9668
  try {
9107
9669
  execFileSync3("open", [APP_PATH], { stdio: "ignore" });
9108
- console.log(chalk6.bold.green(`
9670
+ console.log(chalk5.bold.green(`
9109
9671
  \u2713 Economy Bar is running in your menu bar!`));
9110
- console.log(chalk6.dim(" Make sure economy serve is running: economy serve"));
9672
+ console.log(chalk5.dim(" Make sure economy serve is running: economy serve"));
9111
9673
  } catch (e) {
9112
- console.log(chalk6.yellow("\u26A0 Installed but could not auto-launch. Open from /Applications manually."));
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(chalk6.yellow("Economy Bar is not installed."));
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(chalk6.green("\u2713 Economy Bar uninstalled"));
9689
+ console.log(chalk5.green("\u2713 Economy Bar uninstalled"));
9128
9690
  }
9129
9691
  function menubarStart() {
9130
9692
  if (!isInstalled()) {
9131
- console.error(chalk6.red("Economy Bar is not installed. Run: economy menubar install"));
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(chalk6.green("\u2713 Economy Bar launched"));
9697
+ console.log(chalk5.green("\u2713 Economy Bar launched"));
9136
9698
  }
9137
9699
  function menubarStop() {
9138
9700
  if (!isRunning()) {
9139
- console.log(chalk6.yellow("Economy Bar is not running."));
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(chalk6.green("\u2713 Economy Bar stopped"));
9706
+ console.log(chalk5.green("\u2713 Economy Bar stopped"));
9145
9707
  } catch {
9146
- console.log(chalk6.yellow("Could not quit Economy Bar gracefully"));
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 chalk7 from "chalk";
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 chalk2 from "chalk";
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 ECONOMY_MACHINE_ID, cloud URL, MCP install, cloud schedule, cron hint",
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 (ECONOMY_API_TOKEN) \u2014 localhost-only default, optional LAN bind",
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 chalk2.green("done");
10405
+ return chalk.green("done");
10203
10406
  if (status === "in_progress")
10204
- return chalk2.yellow("in progress");
10205
- return chalk2.dim("pending");
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(chalk2.red("--limit must be a positive integer"));
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(chalk2.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
10442
+ console.log(chalk.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
10240
10443
  console.log();
10241
- console.log(chalk2.dim(` Version: ${ROADMAP_VERSION}`));
10444
+ console.log(chalk.dim(` Version: ${ROADMAP_VERSION}`));
10242
10445
  console.log(` ${ROADMAP_GOAL}`);
10243
10446
  console.log();
10244
- console.log(chalk2.bold(" Commands"));
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(chalk2.bold(" Multi-machine storage"));
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(chalk2.bold(" Agents covered"));
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(chalk2.bold(" Key metrics (target)"));
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(chalk2.bold.cyan(" Economy Roadmap"));
10277
- console.log(chalk2.dim(` ${ROADMAP_VERSION} \u2014 ${ROADMAP_GOAL}`));
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(` ${chalk2.green(String(totals.done))} done \xB7 ${chalk2.yellow(String(totals.in_progress))} in progress \xB7 ${chalk2.dim(String(totals.pending))} pending \xB7 ${allTasks.length} total`);
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(` ${chalk2.bold(phase.id)} ${phase.title} ${chalk2.dim(`(${progress}% \xB7 ${phase.tasks.length} tasks)`)}`);
10287
- console.log(chalk2.dim(` ${phase.summary}`));
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(chalk2.dim(" Run: economy todos list --limit 20"));
10291
- console.log(chalk2.dim(" Run: economy todos list --verbose"));
10292
- console.log(chalk2.dim(" Run: economy todos --help"));
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(chalk2.red(`Unknown phase: ${opts.phase}`));
10299
- console.log(chalk2.dim(`Valid phases: ${ROADMAP_PHASES.map((phase) => phase.id).join(", ")}`));
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(chalk2.bold.cyan(" Economy Roadmap Tasks"));
10320
- console.log(chalk2.dim(` ${allMatches.length} matching tasks \xB7 showing ${visible.length}${visible.length < allMatches.length ? ` of ${allMatches.length}` : ""}`));
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(chalk2.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
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(chalk2.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
10538
+ console.log(chalk.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
10336
10539
  if (opts.verbose)
10337
- console.log(chalk2.dim(` ${phase.summary}`));
10540
+ console.log(chalk.dim(` ${phase.summary}`));
10338
10541
  console.log();
10339
10542
  for (const task of tasks) {
10340
- const deps = task.deps?.length ? chalk2.dim(` deps: ${task.deps.join(", ")}`) : "";
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(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
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(chalk2.dim(` ... ${allMatches.length - visible.length} more tasks hidden. Use --limit <n>, --verbose, --phase <id>, or economy todos show <task-id>.`));
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(chalk2.bold.cyan(` ${task.id} \u2014 ${task.title}`));
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(chalk2.dim(`
10567
+ console.log(chalk.dim(`
10365
10568
  ${phase.summary}`));
10366
10569
  console.log();
10367
10570
  return;
10368
10571
  }
10369
- console.log(chalk2.red(`Task not found: ${taskId}`));
10370
- console.log(chalk2.dim("Run: economy todos list"));
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(chalk2.red("--status must be one of: pending, in_progress, done"));
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 chalk4 from "chalk";
10410
- import { existsSync as existsSync3 } from "fs";
10411
- import { join as join5 } from "path";
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(chalk4.red("--period must be today|yesterday|week|month|year|all"));
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(chalk4.red("--limit must be a positive integer"));
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(chalk4.bold.cyan(` Usage \u2014 ${period}`));
10830
+ console.log(chalk3.bold.cyan(` Usage \u2014 ${period}`));
10598
10831
  console.log();
10599
10832
  if (snaps.length === 0) {
10600
- console.log(chalk4.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
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(chalk4.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
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(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
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(chalk4.dim(`
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
- ${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
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(chalk4.bold.cyan(` Savings \u2014 ${period}${agent ? ` (${agent})` : ""}`));
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(` ${chalk4.bold("Saved:")} ${chalk4.green(fmt2(savings.saved_usd))}`);
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(chalk4.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
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(chalk4.yellow("No subscriptions configured."));
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(chalk4.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
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(` ${chalk4.white(r.provider)} / ${r.plan} ${fmt2(r.monthly_fee_usd)}/mo included ${fmt2(r.included_usage_usd)} ${r.agent ?? "all agents"}`);
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(chalk4.dim(`
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(chalk4.green("\u2713 Subscription removed"));
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", join5(agentPaths().geminiTmp, "..")],
10696
- ["opencode", join5(agentPaths().opencodeMessages, "..", "..")],
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: existsSync3(path), msg: `${agent}: ${existsSync3(path) ? path : "not found"}` });
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 ? "self_hosted/cloud (HASNA_ECONOMY_API_URL + key)" : "local"}` });
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(chalk4.bold.cyan(" Economy Doctor"));
10970
+ console.log(chalk3.bold.cyan(" Economy Doctor"));
10729
10971
  console.log();
10730
10972
  for (const c of checks) {
10731
- console.log(` ${c.ok ? chalk4.green("\u2713") : chalk4.yellow("!")} ${c.msg}`);
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(chalk4.bold.cyan(`
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 ECONOMY_MACHINE_ID=spark01");
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(chalk4.bold.cyan(` Billing diff \u2014 ${period}`));
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(chalk4.yellow(`
11065
+ console.log(chalk3.yellow(`
10794
11066
  ${billingDriftCheck(diff).msg}`));
10795
11067
  if (diff.is_alert)
10796
- console.log(chalk4.yellow(`
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(chalk4.bold.cyan(` Fleet \u2014 ${period}`));
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(chalk4.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
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(` ${chalk4.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
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(chalk4.dim(`
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 { join as join13 } from "path";
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
- return join13(getDataDir(), "ingest-cache.db");
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 copyIngestState(from, to) {
11021
- const rows = from.prepare(`SELECT source, key, value FROM ingest_state`).all();
11022
- to.prepare(`DELETE FROM ingest_state`).run();
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 cache2 = openDatabase(opts.cachePath ?? getIngestCachePath(), true);
11350
+ const cachePath = opts.cachePath ?? getIngestCachePath();
11055
11351
  try {
11056
- if (opts.force)
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
- copyIngestState(scratch, cache2);
11360
+ writeIngestCache(cachePath, dumpIngestState(scratch));
11067
11361
  return { ...result, posted: false };
11068
11362
  }
11069
11363
  const response = await postIngest(cloud, body);
11070
- copyIngestState(scratch, cache2);
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
- const cloud = economyCloudStorage();
11131
- if (cloud.active)
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 chalk7.green(formatted);
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 chalk7.blue("claude");
11503
+ return chalk6.blue("claude");
11177
11504
  if (agent === "codex")
11178
- return chalk7.yellow("codex");
11505
+ return chalk6.yellow("codex");
11179
11506
  if (agent === "gemini")
11180
- return chalk7.green("gemini");
11507
+ return chalk6.green("gemini");
11181
11508
  if (agent === "takumi")
11182
- return chalk7.magenta("takumi");
11509
+ return chalk6.magenta("takumi");
11183
11510
  if (agent === "opencode")
11184
- return chalk7.cyan("opencode");
11511
+ return chalk6.cyan("opencode");
11185
11512
  if (agent === "cursor")
11186
- return chalk7.white("cursor");
11513
+ return chalk6.white("cursor");
11187
11514
  if (agent === "pi")
11188
- return chalk7.hex("#FF6B6B")("pi");
11515
+ return chalk6.hex("#FF6B6B")("pi");
11189
11516
  if (agent === "hermes")
11190
- return chalk7.hex("#9B59B6")("hermes");
11191
- return chalk7.gray(agent);
11517
+ return chalk6.hex("#9B59B6")("hermes");
11518
+ return chalk6.gray(agent);
11192
11519
  }
11193
11520
  function fail(message) {
11194
- console.error(chalk7.red(message));
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(chalk7.dim(` ... ${total - shown} more rows hidden. ${detail}`));
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 sep2 = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
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${sep2.replace(/\u253C/g, "\u252C")}\u2510`);
11585
+ console.log(`\u250C${sep.replace(/\u253C/g, "\u252C")}\u2510`);
11259
11586
  console.log(`\u2502${header}\u2502`);
11260
- console.log(`\u251C${sep2}\u2524`);
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${sep2.replace(/\u253C/g, "\u2534")}\u2518`);
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
- chalk7.white(accountDisplayName(r)),
11602
+ chalk6.white(accountDisplayName(r)),
11276
11603
  fmtAgent(r.account_tool),
11277
- chalk7.dim(r.account_source || "unknown"),
11604
+ chalk6.dim(r.account_source || "unknown"),
11278
11605
  String(r.sessions),
11279
11606
  String(r.requests),
11280
- chalk7.cyan(fmtTokens2(r.total_tokens)),
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(chalk7.bold.cyan(` ${label}`));
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", chalk7.yellow(fmtCount(s.sessions))],
11304
- ["Requests", chalk7.yellow(fmtCount(s.requests))],
11305
- ["Tokens", chalk7.yellow(fmtTokens2(s.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(chalk7.bold.cyan(" Economy"));
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
- ${chalk7.dim("14-day trend:")} ${sparkline(dailyValues)}`);
11663
+ ${chalk6.dim("14-day trend:")} ${sparkline(dailyValues)}`);
11337
11664
  }
11338
11665
  if (projects.length > 0) {
11339
11666
  console.log(`
11340
- ${chalk7.dim("Top projects:")}`);
11667
+ ${chalk6.dim("Top projects:")}`);
11341
11668
  for (const p of projects) {
11342
- console.log(` ${chalk7.white(p.project_name.padEnd(25))} ${fmt4(p.cost_usd)}`);
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(chalk7.yellow("cloud mode: sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
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(chalk7.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
11698
+ console.log(chalk6.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
11372
11699
  } else {
11373
- console.log(chalk7.dim("\u2713 no new provider data to push (mtime cache current)"));
11700
+ console.log(chalk6.dim("\u2713 no new provider data to push (mtime cache current)"));
11374
11701
  }
11375
- console.log(chalk7.bold.green(`
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(chalk7.dim(`Synced open-projects registry: ${p.imported} imported, ${p.skipped} skipped`));
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(chalk7.dim(`open-projects registry sync skipped: ${e instanceof Error ? e.message : String(e)}`));
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(chalk7.dim("Cleared ingest cache"));
11724
+ console.log(chalk6.dim("Cleared ingest cache"));
11398
11725
  }
11399
- process.stdout.write(chalk7.cyan("\u2192 Ingesting agent data... "));
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(chalk7.green(`\u2713 deduped ${result.deduped}`));
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(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${r.requests} requests, ${r.sessions} sessions`));
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(chalk7.cyan(`\u2192 Recalculated: ${r.fixed}/${r.total} zero-cost requests now have pricing`));
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(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
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(chalk7.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
11755
+ console.log(chalk6.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
11429
11756
  }
11430
- console.log(chalk7.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
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(chalk7.bold.green(`
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(chalk7.yellow("No sessions found."));
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
- chalk7.dim(s.id.substring(0, 12)),
11816
+ chalk6.dim(s.id.substring(0, 12)),
11490
11817
  fmtAgent(s.agent),
11491
- chalk7.white(s.project_name || chalk7.dim("unknown")),
11818
+ chalk6.white(s.project_name || chalk6.dim("unknown")),
11492
11819
  fmt4(s.total_cost_usd),
11493
- chalk7.cyan(fmtTokens2(s.total_tokens)),
11820
+ chalk6.cyan(fmtTokens2(s.total_tokens)),
11494
11821
  fmtCount(s.request_count),
11495
- chalk7.dim(s.started_at.substring(0, 16))
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(chalk7.yellow("No sessions found. Run `economy sync` first."));
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
- chalk7.dim(String(i + 1)),
11511
- chalk7.white(s.project_name || chalk7.dim("unknown")),
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
- chalk7.cyan(fmtTokens2(s.total_tokens)),
11515
- chalk7.dim(s.started_at.substring(0, 16))
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
- chalk7.white(r.project_name || chalk7.dim("unknown")),
11861
+ chalk6.white(r.project_name || chalk6.dim("unknown")),
11535
11862
  String(r.sessions),
11536
11863
  String(r.requests),
11537
- chalk7.cyan(fmtTokens2(r.total_tokens)),
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
- chalk7.cyan(fmtTokens2(r.total_tokens)),
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(chalk7.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
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
- chalk7.white(r.kind),
11580
- chalk7.white(r.name),
11906
+ chalk6.white(r.kind),
11907
+ chalk6.white(r.name),
11581
11908
  String(r.sessions),
11582
11909
  String(r.requests),
11583
- chalk7.cyan(fmtTokens2(r.total_tokens)),
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
- chalk7.white(r.model),
11923
+ chalk6.white(r.model),
11597
11924
  fmtAgent(r.agent),
11598
11925
  String(r.requests),
11599
- chalk7.cyan(fmtTokens2(r.total_tokens)),
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(chalk7.yellow("No account-attributed sessions yet. Run `economy sync` first."));
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(chalk7.bold.cyan(` Accounts \u2014 ${period}`));
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(chalk7.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
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(chalk7.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
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(chalk7.yellow("No budgets set."));
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(chalk7.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
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 ? chalk7.red("OVER") : b.is_over_alert ? chalk7.yellow("ALERT") : chalk7.green("OK");
11673
- const pctColor = b.is_over_limit ? chalk7.red(pct + "%") : b.is_over_alert ? chalk7.yellow(pct + "%") : chalk7.green(pct + "%");
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
- chalk7.white(b.cost_center_id ?? b.project_path ?? "global"),
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(chalk7.green(`\u2713 Budget removed`));
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(chalk7.green(`\u2713 Project added: ${path}`));
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(chalk7.yellow("No projects tracked yet."));
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(chalk7.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
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
- chalk7.white(p.project_name || chalk7.dim("unknown")),
11712
- chalk7.dim(p.project_path.substring(0, 40)),
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
- chalk7.dim(p.last_active?.substring(0, 16) ?? "\u2014")
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(chalk7.green(`\u2713 Project removed`));
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(chalk7.red(e instanceof Error ? e.message : String(e)));
12055
+ console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
11729
12056
  process.exit(1);
11730
12057
  }
11731
- console.log(chalk7.green(`\u2713 Renamed to: ${name}`));
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(chalk7.yellow(`No sessions found for: ${nameOrPath}`));
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(chalk7.bold.cyan(` ${detail.project_name}`));
11743
- console.log(chalk7.dim(` ${detail.project_path}`));
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
- ${chalk7.dim("14-day trend:")} ${sparkline(dailyValues)}`);
12079
+ ${chalk6.dim("14-day trend:")} ${sparkline(dailyValues)}`);
11753
12080
  }
11754
12081
  if (detail.models.length > 0) {
11755
12082
  console.log(`
11756
- ${chalk7.dim("Model breakdown:")}`);
12083
+ ${chalk6.dim("Model breakdown:")}`);
11757
12084
  for (const m of detail.models) {
11758
- console.log(` ${chalk7.white(m.model.padEnd(30))} ${fmt4(m.cost_usd)} (${fmtCount(m.requests)} reqs)`);
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
- ${chalk7.dim("Top sessions:")}`);
12090
+ ${chalk6.dim("Top sessions:")}`);
11764
12091
  for (const s of detail.top_sessions) {
11765
- console.log(` ${chalk7.dim(s.id.substring(0, 12))} ${fmt4(s.total_cost_usd)} ${chalk7.dim(String(s.started_at).substring(0, 16))}`);
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(chalk7.green(`\u2713 ${_key} = ${_value}`));
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) ?? chalk7.dim("(not set)"));
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: loadConfig3 } = await Promise.resolve().then(() => (init_config(), exports_config));
11782
- const config = loadConfig3();
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(chalk7.yellow("No webhook-url configured. Run: economy config set webhook-url <url>"));
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(chalk7.green(`\u2713 Webhook responded: HTTP ${res.status}`));
12129
+ console.log(chalk6.green(`\u2713 Webhook responded: HTTP ${res.status}`));
11803
12130
  if (text)
11804
- console.log(chalk7.dim(text.slice(0, 200)));
12131
+ console.log(chalk6.dim(text.slice(0, 200)));
11805
12132
  } else {
11806
- console.log(chalk7.red(`\u2717 Webhook failed: HTTP ${res.status}`));
12133
+ console.log(chalk6.red(`\u2717 Webhook failed: HTTP ${res.status}`));
11807
12134
  if (text)
11808
- console.log(chalk7.dim(text.slice(0, 200)));
12135
+ console.log(chalk6.dim(text.slice(0, 200)));
11809
12136
  }
11810
12137
  } catch (e) {
11811
- console.log(chalk7.red(`\u2717 Request failed: ${e instanceof Error ? e.message : String(e)}`));
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: loadConfig3 } = await Promise.resolve().then(() => (init_config(), exports_config));
11816
- const config = loadConfig3();
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(chalk7.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
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
- chalk7.white(r.model),
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
- chalk7.dim(fmt4(r.output_per_1m / 1000))
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(chalk7.green(`\u2713 Pricing updated for ${model}`));
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(chalk7.green(`\u2713 Pricing removed for ${model}`));
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(chalk7.bold.cyan(`
12200
+ console.log(chalk6.bold.cyan(`
11918
12201
  Claude Code:`));
11919
- console.log(chalk7.white(" claude mcp add --transport stdio --scope user economy -- economy-mcp"));
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(chalk7.bold.yellow(`
12205
+ console.log(chalk6.bold.yellow(`
11923
12206
  Codex (~/.codex/config.toml):`));
11924
- console.log(chalk7.white(` [mcp_servers.economy]
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(chalk7.bold.green(`
12212
+ console.log(chalk6.bold.green(`
11930
12213
  Gemini (~/.gemini/settings.json):`));
11931
- console.log(chalk7.white(' "mcpServers": { "economy": { "command": "economy-mcp", "args": [] } }'));
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(chalk7.red(`Session not found: ${id}`));
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(chalk7.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
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(chalk7.dim(`
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
- chalk7.dim(String(r["timestamp"]).substring(11, 19)),
11963
- chalk7.white(String(r["model"]).substring(0, 22)),
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(chalk7.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
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(chalk7.yellow(`No machine data yet. Current machine: ${current}`));
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(chalk7.bold.cyan(" Machines"));
12270
+ console.log(chalk6.bold.cyan(" Machines"));
11988
12271
  if (visibleMachines.length < machines.length) {
11989
- console.log(chalk7.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
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 ? chalk7.green(`${m.machine_id} (this)`) : chalk7.white(m.machine_id),
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
- chalk7.dim(m.last_active?.substring(0, 16) ?? "\u2014")
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
- ${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
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(chalk7.green(`\u2713 Exported to ${opts.output}`));
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 ? chalk7.red : d < 0 ? chalk7.green : chalk7.dim;
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(chalk7.bold.cyan(` ${p1} vs ${p2}`));
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(chalk7.bold.cyan(` Forecast (${f.day_of_month} of ${f.days_in_month} days)`));
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
- [chalk7.bold("Projected total"), chalk7.bold(fmt4(f.projected_usd).replace(chalk7.green(""), ""))],
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(chalk7.bold.cyan(" Token Efficiency"));
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 [chalk7.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
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(chalk7.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
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(chalk7.yellow("No goals set."));
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(chalk7.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
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 ? chalk7.red("OVER") : g.is_at_risk ? chalk7.yellow("AT RISK") : chalk7.green("ON TRACK");
12178
- const pctColor = g.is_over ? chalk7.red(pct + "%") : g.is_at_risk ? chalk7.yellow(pct + "%") : chalk7.green(pct + "%");
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
- chalk7.white(scope),
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(chalk7.green(`\u2713 Goal removed`));
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(chalk7.yellow("No goals set."));
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 ? chalk7.red("\u2717 OVER") : g.is_at_risk ? chalk7.yellow("\u26A0 AT RISK") : chalk7.green("\u2713 ON TRACK");
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(chalk7.red(`Unknown type: ${type}. Use: budget | project | goal | pricing`));
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(chalk7.green(`\u2713 ${label[t]} ${id} removed`));
12523
+ console.log(chalk6.green(`\u2713 ${label[t]} ${id} removed`));
12241
12524
  } catch (e) {
12242
- console.error(chalk7.red(`Failed: ${e instanceof Error ? e.message : String(e)}`));
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(chalk7.yellow("cloud mode: billing sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
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(chalk7.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
12543
+ console.log(chalk6.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
12261
12544
  else
12262
- console.log(chalk7.red(`\u2717 ${provider}: ${p.error}`));
12545
+ console.log(chalk6.red(`\u2717 ${provider}: ${p.error}`));
12263
12546
  }
12264
12547
  if (result.posted)
12265
- console.log(chalk7.green(`\u2713 pushed ${result.total} billing rows to the shared API`));
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(chalk7.cyan("\u2192 Syncing Anthropic billing... "));
12554
+ process.stdout.write(chalk6.cyan("\u2192 Syncing Anthropic billing... "));
12272
12555
  try {
12273
12556
  const r = await syncAnthropicBilling(db, { days });
12274
- console.log(chalk7.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
12557
+ console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
12275
12558
  } catch (e) {
12276
- console.log(chalk7.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
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(chalk7.cyan("\u2192 Syncing OpenAI billing... "));
12563
+ process.stdout.write(chalk6.cyan("\u2192 Syncing OpenAI billing... "));
12281
12564
  try {
12282
12565
  const r = await syncOpenAIBilling(db, { days });
12283
- console.log(chalk7.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
12566
+ console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
12284
12567
  } catch (e) {
12285
- console.log(chalk7.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
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(chalk7.cyan("\u2192 Syncing Gemini billing... "));
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(chalk7.yellow(`skipped \u2014 ${r.skipped}`));
12576
+ console.log(chalk6.yellow(`skipped \u2014 ${r.skipped}`));
12294
12577
  } else {
12295
- console.log(chalk7.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
12578
+ console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
12296
12579
  }
12297
12580
  } catch (e) {
12298
- console.log(chalk7.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
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(chalk7.bold.cyan(` Billing ${period} (actual from admin APIs)
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]) => [chalk7.white(p), fmt4(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(` ${chalk7.bold("Actual total:")} ${fmt4(actual.total_usd)}`);
12313
- console.log(` ${chalk7.dim("Our estimate:")} ${fmt4(estimated.total_usd)}`);
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(` ${chalk7.dim("Difference:")} ${fmt4(Math.abs(diff))} (${rendered})`);
12600
+ console.log(` ${chalk6.dim("Difference:")} ${fmt4(Math.abs(diff))} (${rendered})`);
12318
12601
  if (pct === null) {
12319
- console.log(chalk7.yellow(` ${chalk7.dim("Note:")} no provider billing recorded for ${period} \u2014 drift is UNKNOWN, not zero.`));
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);