@hasna/economy 0.3.27 → 0.4.0

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