@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
@@ -595,7 +595,60 @@ 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
+ }
599
652
  function normalizeMachineId(value) {
600
653
  const id = value?.trim().toLowerCase().split(".")[0];
601
654
  return id && id.length > 0 ? id : null;
@@ -618,7 +671,7 @@ function macHostMachineId() {
618
671
  return null;
619
672
  }
620
673
  function getMachineId() {
621
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
674
+ const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
622
675
  if (envMachine)
623
676
  return envMachine;
624
677
  const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
@@ -626,21 +679,48 @@ function getMachineId() {
626
679
  return macHostMachineId() ?? hostMachine;
627
680
  return hostMachine;
628
681
  }
629
- function getDataDir() {
682
+ function getHomeDir() {
630
683
  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 });
684
+ if (!home)
685
+ throw new Error("Could not resolve the user home directory");
686
+ return home;
687
+ }
688
+ function getResolverDataRoot() {
689
+ return dataDir({
690
+ app: "economy",
691
+ home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
692
+ });
693
+ }
694
+ function getLegacyDataRoot() {
695
+ return join(getHomeDir(), ".hasna", "economy");
696
+ }
697
+ function adoptResolverDataRoot(resolved, env = process.env) {
698
+ const dataOverride = env.HASNA_DATA_HOME;
699
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
700
+ return true;
701
+ return existsSync(join(resolved, "economy.db"));
702
+ }
703
+ function getExactDataRoot() {
704
+ const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
705
+ if (dir && dir.trim())
706
+ return dir.trim();
707
+ return;
708
+ }
709
+ function getDataDir() {
710
+ const exact = getExactDataRoot();
711
+ const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
712
+ const oldDir = join(getHomeDir(), ".economy");
713
+ if (existsSync(oldDir) && !existsSync(effective)) {
714
+ mkdirSync(effective, { recursive: true });
635
715
  for (const file of readdirSync(oldDir)) {
636
716
  const oldPath = join(oldDir, file);
637
717
  if (statSync(oldPath).isFile()) {
638
- copyFileSync(oldPath, join(newDir, file));
718
+ copyFileSync(oldPath, join(effective, file));
639
719
  }
640
720
  }
641
721
  }
642
- mkdirSync(newDir, { recursive: true });
643
- return newDir;
722
+ mkdirSync(effective, { recursive: true });
723
+ return effective;
644
724
  }
645
725
  function getDbPath() {
646
726
  if (process.env["HASNA_ECONOMY_DB_PATH"])
@@ -1898,6 +1978,10 @@ function getIngestState(db, source, key) {
1898
1978
  const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
1899
1979
  return row?.value ?? null;
1900
1980
  }
1981
+ function loadIngestState(db, source) {
1982
+ const rows = db.prepare(`SELECT key, value FROM ingest_state WHERE source = ?`).all(source);
1983
+ return new Map(rows.map((row) => [row.key, row.value]));
1984
+ }
1901
1985
  function setIngestState(db, source, key, value) {
1902
1986
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1903
1987
  }
@@ -2336,7 +2420,7 @@ function bulkIngest(db, body) {
2336
2420
  const total = Object.values(ingested).reduce((a, b) => a + b, 0);
2337
2421
  return { ingested, total };
2338
2422
  }
2339
- var numOr = (v, d = 0) => {
2423
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, numOr = (v, d = 0) => {
2340
2424
  if (v == null)
2341
2425
  return d;
2342
2426
  const num = Number(v);
@@ -2344,6 +2428,13 @@ var numOr = (v, d = 0) => {
2344
2428
  }, strOr = (v, d = "") => v == null ? d : String(v);
2345
2429
  var init_database = __esm(() => {
2346
2430
  init_sqlite_adapter();
2431
+ PATHS_RESOLVER_KIND_ENV = {
2432
+ config: "HASNA_CONFIG_HOME",
2433
+ data: "HASNA_DATA_HOME",
2434
+ state: "HASNA_STATE_HOME",
2435
+ cache: "HASNA_CACHE_HOME"
2436
+ };
2437
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2347
2438
  });
2348
2439
 
2349
2440
  // src/db/dialect.ts
@@ -2386,8 +2477,8 @@ function sqliteToPostgres(sql) {
2386
2477
  // src/db/sync-pg.ts
2387
2478
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
2388
2479
  import { fileURLToPath } from "url";
2389
- import { dirname, join as join10 } from "path";
2390
- import { existsSync as existsSync11 } from "fs";
2480
+ import { dirname, join as join11 } from "path";
2481
+ import { existsSync as existsSync12 } from "fs";
2391
2482
  function translateSqliteDates(sql) {
2392
2483
  let s = sql;
2393
2484
  const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
@@ -2412,12 +2503,12 @@ function translateSqliteDates(sql) {
2412
2503
  function resolveWorkerPath() {
2413
2504
  const here = dirname(fileURLToPath(import.meta.url));
2414
2505
  const candidates = [
2415
- join10(here, "pg-sync-worker.ts"),
2416
- join10(here, "pg-sync-worker.js"),
2417
- join10(here, "..", "db", "pg-sync-worker.js")
2506
+ join11(here, "pg-sync-worker.ts"),
2507
+ join11(here, "pg-sync-worker.js"),
2508
+ join11(here, "..", "db", "pg-sync-worker.js")
2418
2509
  ];
2419
2510
  for (const candidate of candidates)
2420
- if (existsSync11(candidate))
2511
+ if (existsSync12(candidate))
2421
2512
  return candidate;
2422
2513
  return candidates[0];
2423
2514
  }
@@ -2874,7 +2965,7 @@ __export(exports_billing, {
2874
2965
  syncGeminiBilling: () => syncGeminiBilling,
2875
2966
  syncAnthropicBilling: () => syncAnthropicBilling
2876
2967
  });
2877
- import { readFileSync as readFileSync8 } from "fs";
2968
+ import { readFileSync as readFileSync9 } from "fs";
2878
2969
  function getAnthropicAdminKey() {
2879
2970
  return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
2880
2971
  }
@@ -3101,7 +3192,7 @@ async function syncGeminiBilling(db, opts = {}) {
3101
3192
  const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
3102
3193
  const fromDateStr = toISODate(start);
3103
3194
  const toDateStr = toISODate(end);
3104
- const rows = parseBillingRows(readFileSync8(exportPath, "utf-8"));
3195
+ const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
3105
3196
  clearBillingRange(db, "gemini", fromDateStr, toDateStr);
3106
3197
  const updatedAt = new Date().toISOString();
3107
3198
  let totalUsd = 0;
@@ -3165,16 +3256,16 @@ var init_open_projects = __esm(() => {
3165
3256
  });
3166
3257
 
3167
3258
  // src/lib/config.ts
3168
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
3169
- import { dirname as dirname2, join as join11 } from "path";
3259
+ import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
3260
+ import { dirname as dirname2, join as join12 } from "path";
3170
3261
  function getConfigPath() {
3171
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join11(getDataDir(), "config.json");
3262
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
3172
3263
  }
3173
3264
  function loadConfig() {
3174
3265
  try {
3175
3266
  const configPath = getConfigPath();
3176
- if (existsSync12(configPath)) {
3177
- const raw = readFileSync9(configPath, "utf-8");
3267
+ if (existsSync13(configPath)) {
3268
+ const raw = readFileSync10(configPath, "utf-8");
3178
3269
  return { ...DEFAULTS, ...JSON.parse(raw) };
3179
3270
  }
3180
3271
  } catch {}
@@ -3319,9 +3410,9 @@ function isAgent(value) {
3319
3410
  // src/ingest/claude.ts
3320
3411
  init_database();
3321
3412
  init_pricing();
3322
- import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
3323
- import { homedir as homedir2 } from "os";
3324
- import { join as join2, basename } from "path";
3413
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
3414
+ import { homedir as homedir3 } from "os";
3415
+ import { join as join3, basename } from "path";
3325
3416
 
3326
3417
  // src/lib/savings.ts
3327
3418
  function periodWhere2(period, column) {
@@ -3477,8 +3568,319 @@ function defaultCostBasisForAgent(agent) {
3477
3568
  return "estimated";
3478
3569
  }
3479
3570
 
3571
+ // src/lib/accounts-store.ts
3572
+ import { existsSync as existsSync2, readFileSync } from "fs";
3573
+ import { homedir as homedir2 } from "os";
3574
+ import { join as join2 } from "path";
3575
+ import { z } from "zod";
3576
+ import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
3577
+ var ACCOUNTS_APP = "accounts";
3578
+ var 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");
3579
+ var profileSchema = z.object({
3580
+ name: z.string(),
3581
+ tool: z.string(),
3582
+ email: z.string().optional(),
3583
+ displayName: z.string().optional(),
3584
+ dir: z.string(),
3585
+ description: z.string().optional(),
3586
+ createdAt: z.string(),
3587
+ lastUsedAt: z.string().optional()
3588
+ });
3589
+ var toolDefSchema = z.object({
3590
+ id: z.string(),
3591
+ label: z.string(),
3592
+ envVar: z.string(),
3593
+ defaultDir: z.string(),
3594
+ bin: z.string()
3595
+ });
3596
+ var storeSchema = z.object({
3597
+ version: z.literal(1),
3598
+ current: z.record(z.string(), z.string()).default({}),
3599
+ applied: z.record(z.string(), z.string()).default({}),
3600
+ toolLocks: z.record(z.string(), z.string()).default({}),
3601
+ profiles: z.array(profileSchema).default([]),
3602
+ tools: z.array(toolDefSchema).default([])
3603
+ });
3604
+ function validateEnvPath(value, label) {
3605
+ const trimmed = value.trim();
3606
+ if (!trimmed || trimmed.includes("\x00") || /[\r\n]/.test(trimmed))
3607
+ throw new Error(`invalid ${label}`);
3608
+ return trimmed;
3609
+ }
3610
+ function accountsHome() {
3611
+ const override = process.env.ACCOUNTS_HOME;
3612
+ if (override && override.trim())
3613
+ return validateEnvPath(override, "ACCOUNTS_HOME");
3614
+ return join2(homedir2(), ".hasna", "accounts");
3615
+ }
3616
+ function storePath() {
3617
+ const override = process.env.ACCOUNTS_STORE_PATH;
3618
+ if (override && override.trim())
3619
+ return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
3620
+ return join2(accountsHome(), "accounts.json");
3621
+ }
3622
+ var EMPTY_STORE = {
3623
+ version: 1,
3624
+ current: {},
3625
+ applied: {},
3626
+ toolLocks: {},
3627
+ profiles: [],
3628
+ tools: []
3629
+ };
3630
+ function parseStoreFile() {
3631
+ const path = storePath();
3632
+ if (!existsSync2(path))
3633
+ return structuredClone(EMPTY_STORE);
3634
+ let raw;
3635
+ try {
3636
+ raw = JSON.parse(readFileSync(path, "utf8"));
3637
+ } catch (err) {
3638
+ throw new Error(`could not parse store at ${path}: ${err instanceof Error ? err.message : String(err)}`);
3639
+ }
3640
+ const parsed = storeSchema.safeParse(raw);
3641
+ if (!parsed.success) {
3642
+ throw new Error(`invalid store at ${path}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
3643
+ }
3644
+ return parsed.data;
3645
+ }
3646
+ function loadAppliedMap() {
3647
+ const applied = {};
3648
+ for (const [toolId, name] of Object.entries(parseStoreFile().applied)) {
3649
+ if (name && profileNameSchema.safeParse(name).success)
3650
+ applied[toolId] = name;
3651
+ }
3652
+ return applied;
3653
+ }
3654
+ function appliedProfileName(toolId) {
3655
+ return loadAppliedMap()[toolId];
3656
+ }
3657
+ var BUILTIN_TOOLS = [
3658
+ { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
3659
+ { id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
3660
+ { id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
3661
+ { id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
3662
+ { id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
3663
+ { id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
3664
+ { id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
3665
+ { id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
3666
+ { id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
3667
+ { id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
3668
+ { id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
3669
+ { id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
3670
+ ];
3671
+
3672
+ class LocalStore {
3673
+ transport = "local";
3674
+ async listProfiles(tool) {
3675
+ const profiles = parseStoreFile().profiles;
3676
+ const filtered = tool ? profiles.filter((profile) => profile.tool === tool) : profiles;
3677
+ return filtered.slice().sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
3678
+ }
3679
+ async findProfile(name, tool) {
3680
+ const matches = parseStoreFile().profiles.filter((profile) => profile.name === name && (!tool || profile.tool === tool));
3681
+ return matches.length === 1 ? matches[0] : undefined;
3682
+ }
3683
+ async currentProfile(tool) {
3684
+ const store = parseStoreFile();
3685
+ const name = store.current[tool];
3686
+ if (!name)
3687
+ return;
3688
+ return store.profiles.find((profile) => profile.name === name && profile.tool === tool);
3689
+ }
3690
+ async listTools() {
3691
+ const custom = parseStoreFile().tools;
3692
+ const byId = new Map;
3693
+ for (const tool of BUILTIN_TOOLS)
3694
+ byId.set(tool.id, tool);
3695
+ for (const tool of custom)
3696
+ byId.set(tool.id, tool);
3697
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
3698
+ }
3699
+ }
3700
+ var RETIRED_STORAGE_MODE_KEYS = [
3701
+ "HASNA_ACCOUNTS_STORAGE_MODE",
3702
+ "ACCOUNTS_STORAGE_MODE",
3703
+ "HASNA_ACCOUNTS_MODE",
3704
+ "ACCOUNTS_MODE"
3705
+ ];
3706
+ function assertNoRetiredStorageMode(env) {
3707
+ const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
3708
+ if (!legacyKey)
3709
+ return;
3710
+ 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.`);
3711
+ }
3712
+ var RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
3713
+
3714
+ class AccountsApiError extends Error {
3715
+ status;
3716
+ body;
3717
+ constructor(method, path, status, body) {
3718
+ super(`accounts cloud request failed: ${method} ${path} -> ${status}`);
3719
+ this.name = "AccountsApiError";
3720
+ this.status = status;
3721
+ this.body = body;
3722
+ }
3723
+ }
3724
+ function isNotFound(err) {
3725
+ return Boolean(err && typeof err === "object" && err.status === 404);
3726
+ }
3727
+ function sleep(ms) {
3728
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3729
+ }
3730
+ async function cloudRequest(baseUrl, apiKey, path, query) {
3731
+ const rel = path.startsWith("/") ? path : `/${path}`;
3732
+ const url = query && Object.keys(query).length > 0 ? `${baseUrl}${rel}?${new URLSearchParams(query).toString()}` : `${baseUrl}${rel}`;
3733
+ let lastError;
3734
+ for (let attempt = 0;attempt < 3; attempt += 1) {
3735
+ const controller = new AbortController;
3736
+ const timer = setTimeout(() => controller.abort(), 30000);
3737
+ let res;
3738
+ try {
3739
+ res = await fetch(url, {
3740
+ method: "GET",
3741
+ headers: {
3742
+ "x-api-key": apiKey,
3743
+ Authorization: `Bearer ${apiKey}`,
3744
+ Accept: "application/json"
3745
+ },
3746
+ signal: controller.signal
3747
+ });
3748
+ } catch (error) {
3749
+ lastError = error instanceof Error ? error : new Error(String(error));
3750
+ clearTimeout(timer);
3751
+ if (attempt < 2) {
3752
+ const backoff = Math.min(2000, 200 * 2 ** attempt);
3753
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
3754
+ await sleep(backoff + jitter);
3755
+ }
3756
+ continue;
3757
+ } finally {
3758
+ clearTimeout(timer);
3759
+ }
3760
+ const text = await res.text();
3761
+ let parsed;
3762
+ if (text.length > 0) {
3763
+ try {
3764
+ parsed = JSON.parse(text);
3765
+ } catch {
3766
+ parsed = text;
3767
+ }
3768
+ }
3769
+ if (!res.ok) {
3770
+ lastError = new AccountsApiError("GET", rel, res.status, parsed);
3771
+ if (attempt < 2 && RETRY_STATUSES.includes(res.status)) {
3772
+ const backoff = Math.min(2000, 200 * 2 ** attempt);
3773
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
3774
+ await sleep(backoff + jitter);
3775
+ continue;
3776
+ }
3777
+ break;
3778
+ }
3779
+ return parsed;
3780
+ }
3781
+ throw lastError;
3782
+ }
3783
+ function toProfile(account) {
3784
+ const profile = {
3785
+ name: String(account.name),
3786
+ tool: String(account.tool),
3787
+ dir: typeof account.dir === "string" ? account.dir : "",
3788
+ createdAt: String(account.createdAt)
3789
+ };
3790
+ if (typeof account.email === "string")
3791
+ profile.email = account.email;
3792
+ if (typeof account.displayName === "string")
3793
+ profile.displayName = account.displayName;
3794
+ if (typeof account.description === "string")
3795
+ profile.description = account.description;
3796
+ if (typeof account.lastUsedAt === "string")
3797
+ profile.lastUsedAt = account.lastUsedAt;
3798
+ return profile;
3799
+ }
3800
+
3801
+ class ApiStore {
3802
+ baseUrl;
3803
+ apiKey;
3804
+ transport = "api";
3805
+ constructor(baseUrl, apiKey) {
3806
+ this.baseUrl = baseUrl;
3807
+ this.apiKey = apiKey;
3808
+ }
3809
+ async listProfiles(tool) {
3810
+ const raw = await cloudRequest(this.baseUrl, this.apiKey, "/accounts", tool ? { tool } : undefined);
3811
+ const accounts = Array.isArray(raw?.accounts) ? raw.accounts : [];
3812
+ return accounts.map((account) => toProfile(account)).sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
3813
+ }
3814
+ async findProfile(name, tool) {
3815
+ if (tool) {
3816
+ try {
3817
+ const account = await cloudRequest(this.baseUrl, this.apiKey, `/accounts/${encodeURIComponent(tool)}/${encodeURIComponent(name)}`);
3818
+ return account ? toProfile(account) : undefined;
3819
+ } catch (err) {
3820
+ if (isNotFound(err))
3821
+ return;
3822
+ throw err;
3823
+ }
3824
+ }
3825
+ const matches = (await this.listProfiles()).filter((profile) => profile.name === name);
3826
+ return matches.length === 1 ? matches[0] : undefined;
3827
+ }
3828
+ async currentProfile(tool) {
3829
+ let current;
3830
+ try {
3831
+ const raw = await cloudRequest(this.baseUrl, this.apiKey, `/current/${encodeURIComponent(tool)}`);
3832
+ current = raw && typeof raw === "object" ? raw : null;
3833
+ } catch (err) {
3834
+ if (isNotFound(err))
3835
+ return;
3836
+ throw err;
3837
+ }
3838
+ if (!current || typeof current.name !== "string")
3839
+ return;
3840
+ return this.findProfile(current.name, tool);
3841
+ }
3842
+ async listTools() {
3843
+ const raw = await cloudRequest(this.baseUrl, this.apiKey, "/tools");
3844
+ const tools = Array.isArray(raw?.tools) ? raw.tools : [];
3845
+ const custom = [];
3846
+ for (const item of tools) {
3847
+ if (item === null || typeof item !== "object")
3848
+ continue;
3849
+ const record = item;
3850
+ if (record.builtin !== false)
3851
+ continue;
3852
+ const parsed = toolDefSchema.safeParse(item);
3853
+ if (!parsed.success) {
3854
+ throw new Error(`invalid custom tool "${String(record.id)}" returned by accounts-serve: ` + parsed.error.issues.map((issue) => issue.message).join("; "));
3855
+ }
3856
+ custom.push(parsed.data);
3857
+ }
3858
+ const byId = new Map;
3859
+ for (const tool of BUILTIN_TOOLS)
3860
+ byId.set(tool.id, tool);
3861
+ for (const tool of custom)
3862
+ byId.set(tool.id, tool);
3863
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
3864
+ }
3865
+ }
3866
+ function resolveStore(env = process.env, options = {}) {
3867
+ assertNoRetiredStorageMode(env);
3868
+ const clientEnv = env;
3869
+ try {
3870
+ const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
3871
+ if (credential === null)
3872
+ return new LocalStore;
3873
+ const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
3874
+ credentials: credential ? { ...options.credentials, apiKey: credential.apiKey } : options.credentials
3875
+ });
3876
+ return new ApiStore(resolution.baseUrl, credential.apiKey);
3877
+ } catch (err) {
3878
+ console.warn(`accounts: API configuration unusable (${err instanceof Error ? err.message : String(err)}); using local store`);
3879
+ return new LocalStore;
3880
+ }
3881
+ }
3882
+
3480
3883
  // src/lib/accounts.ts
3481
- import { appliedProfileName, resolveStore } from "@hasna/accounts";
3482
3884
  var AGENT_ACCOUNT_TOOLS = {
3483
3885
  claude: ["claude"],
3484
3886
  takumi: ["takumi", "claude"],
@@ -3550,11 +3952,11 @@ async function profileForEnvDir(store, tool, env) {
3550
3952
  const normalized = normalizeDir(configuredDir);
3551
3953
  return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
3552
3954
  }
3553
- async function resolveAccountForAgent(agent, env = process.env) {
3955
+ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
3554
3956
  const override = envOverride(agent, env);
3555
3957
  if (override)
3556
3958
  return override;
3557
- const store = resolveStore(env);
3959
+ const store = resolveStore(env, options);
3558
3960
  const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
3559
3961
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
3560
3962
  const tool = tools.get(toolId);
@@ -3592,8 +3994,8 @@ function withAccount(record, account) {
3592
3994
  function autoDetectProject(cwd, projects) {
3593
3995
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
3594
3996
  }
3595
- var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
3596
- var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
3997
+ var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
3998
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
3597
3999
  function dirNameToPath(dirName) {
3598
4000
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
3599
4001
  }
@@ -3603,9 +4005,9 @@ function collectJsonlFiles(projectDir) {
3603
4005
  try {
3604
4006
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
3605
4007
  if (entry.isDirectory())
3606
- walk(join2(dir, entry.name));
4008
+ walk(join3(dir, entry.name));
3607
4009
  else if (entry.name.endsWith(".jsonl"))
3608
- files.push(join2(dir, entry.name));
4010
+ files.push(join3(dir, entry.name));
3609
4011
  }
3610
4012
  } catch {}
3611
4013
  }
@@ -3619,7 +4021,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
3619
4021
  return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
3620
4022
  }
3621
4023
  async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
3622
- if (!existsSync2(projectsDir)) {
4024
+ if (!existsSync3(projectsDir)) {
3623
4025
  if (verbose)
3624
4026
  console.log(`${agentName} projects dir not found:`, projectsDir);
3625
4027
  return { files: 0, requests: 0, sessions: 0 };
@@ -3630,9 +4032,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
3630
4032
  const touchedSessions = new Set;
3631
4033
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
3632
4034
  const account = await resolveAccountForAgent(agentName);
4035
+ const ingestState = loadIngestState(db, agentName);
3633
4036
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3634
4037
  for (const projectDirEntry of projectDirs) {
3635
- const projectDirPath = join2(projectsDir, projectDirEntry.name);
4038
+ const projectDirPath = join3(projectsDir, projectDirEntry.name);
3636
4039
  const projectPath = dirNameToPath(projectDirEntry.name);
3637
4040
  const jsonlFiles = collectJsonlFiles(projectDirPath);
3638
4041
  for (const filePath of jsonlFiles) {
@@ -3643,12 +4046,12 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
3643
4046
  } catch {
3644
4047
  continue;
3645
4048
  }
3646
- const processed = getIngestState(db, agentName, stateKey);
4049
+ const processed = ingestState.get(stateKey) ?? null;
3647
4050
  if (processed === fileMtime)
3648
4051
  continue;
3649
4052
  let lines;
3650
4053
  try {
3651
- lines = readFileSync(filePath, "utf-8").split(`
4054
+ lines = readFileSync2(filePath, "utf-8").split(`
3652
4055
  `).filter((l) => l.trim());
3653
4056
  } catch {
3654
4057
  continue;
@@ -3768,14 +4171,14 @@ function supportsClaudeDataResidencyPricing(model) {
3768
4171
  // src/ingest/codex.ts
3769
4172
  init_database();
3770
4173
  init_pricing();
3771
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3772
- import { homedir as homedir3 } from "os";
3773
- import { join as join3, basename as basename2 } from "path";
4174
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
4175
+ import { homedir as homedir4 } from "os";
4176
+ import { join as join4, basename as basename2 } from "path";
3774
4177
  import { Database as BunDatabase } from "bun:sqlite";
3775
- var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
3776
- var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
3777
- var DEFAULT_CODEWITH_DB_PATH = join3(homedir3(), ".codewith", "state_5.sqlite");
3778
- var DEFAULT_CODEWITH_CONFIG_PATH = join3(homedir3(), ".codewith", "config.toml");
4178
+ var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
4179
+ var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
4180
+ var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
4181
+ var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
3779
4182
  var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
3780
4183
  function codexDbPath() {
3781
4184
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
@@ -3817,10 +4220,10 @@ function codexSources() {
3817
4220
  });
3818
4221
  }
3819
4222
  function readCodexModel(configPath = codexConfigPath()) {
3820
- if (!existsSync3(configPath))
4223
+ if (!existsSync4(configPath))
3821
4224
  return "gpt-5-codex";
3822
4225
  try {
3823
- const content = readFileSync2(configPath, "utf-8");
4226
+ const content = readFileSync3(configPath, "utf-8");
3824
4227
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
3825
4228
  return match?.[1] ?? "gpt-5-codex";
3826
4229
  } catch {
@@ -3858,12 +4261,12 @@ function openCodexDb(dbPath, verbose) {
3858
4261
  return null;
3859
4262
  }
3860
4263
  function readTokenEvents(rolloutPath) {
3861
- if (!rolloutPath || !existsSync3(rolloutPath))
4264
+ if (!rolloutPath || !existsSync4(rolloutPath))
3862
4265
  return [];
3863
4266
  const fallbackUsages = new Map;
3864
4267
  let fallbackTimestamp;
3865
4268
  let aggregate = null;
3866
- for (const line of readFileSync2(rolloutPath, "utf-8").split(`
4269
+ for (const line of readFileSync3(rolloutPath, "utf-8").split(`
3867
4270
  `)) {
3868
4271
  if (!line.trim())
3869
4272
  continue;
@@ -3939,7 +4342,7 @@ async function ingestCodex(db, verbose = false) {
3939
4342
  let requests = 0;
3940
4343
  const account = await resolveAccountForAgent("codex");
3941
4344
  for (const source of codexSources()) {
3942
- if (!existsSync3(source.dbPath)) {
4345
+ if (!existsSync4(source.dbPath)) {
3943
4346
  if (verbose)
3944
4347
  console.log(`${source.label} DB not found:`, source.dbPath);
3945
4348
  continue;
@@ -4020,11 +4423,11 @@ async function ingestCodex(db, verbose = false) {
4020
4423
  // src/ingest/gemini.ts
4021
4424
  init_database();
4022
4425
  init_pricing();
4023
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
4024
- import { homedir as homedir4 } from "os";
4025
- import { join as join4, basename as basename3 } from "path";
4026
- var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
4027
- var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
4426
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
4427
+ import { homedir as homedir5 } from "os";
4428
+ import { join as join5, basename as basename3 } from "path";
4429
+ var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
4430
+ var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
4028
4431
  function geminiTmpDir() {
4029
4432
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
4030
4433
  }
@@ -4041,12 +4444,12 @@ function numberField(...values) {
4041
4444
  function listProjectDirs(...roots) {
4042
4445
  const dirs = new Set;
4043
4446
  for (const root of roots) {
4044
- if (!existsSync4(root))
4447
+ if (!existsSync5(root))
4045
4448
  continue;
4046
4449
  try {
4047
4450
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
4048
4451
  if (entry.isDirectory())
4049
- dirs.add(join4(root, entry.name));
4452
+ dirs.add(join5(root, entry.name));
4050
4453
  }
4051
4454
  } catch {}
4052
4455
  }
@@ -4057,17 +4460,17 @@ function projectRoot(projectDir, chatData) {
4057
4460
  return chatData.projectPath;
4058
4461
  if (chatData.project_path)
4059
4462
  return chatData.project_path;
4060
- const rootFile = join4(projectDir, ".project_root");
4463
+ const rootFile = join5(projectDir, ".project_root");
4061
4464
  try {
4062
- if (existsSync4(rootFile))
4063
- return readFileSync3(rootFile, "utf-8").trim();
4465
+ if (existsSync5(rootFile))
4466
+ return readFileSync4(rootFile, "utf-8").trim();
4064
4467
  } catch {}
4065
4468
  return "";
4066
4469
  }
4067
4470
  async function ingestGemini(db, verbose) {
4068
4471
  const tmpDir = geminiTmpDir();
4069
4472
  const historyDir = geminiHistoryDir();
4070
- if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
4473
+ if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
4071
4474
  if (verbose)
4072
4475
  console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
4073
4476
  return { sessions: 0, requests: 0 };
@@ -4079,17 +4482,17 @@ async function ingestGemini(db, verbose) {
4079
4482
  const account = await resolveAccountForAgent("gemini");
4080
4483
  const projectDirs = listProjectDirs(tmpDir, historyDir);
4081
4484
  for (const projectDir of projectDirs) {
4082
- const chatsDir = join4(projectDir, "chats");
4083
- if (!existsSync4(chatsDir))
4485
+ const chatsDir = join5(projectDir, "chats");
4486
+ if (!existsSync5(chatsDir))
4084
4487
  continue;
4085
4488
  let chatFiles = [];
4086
4489
  try {
4087
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
4490
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
4088
4491
  } catch {
4089
4492
  continue;
4090
4493
  }
4091
4494
  for (const filePath of chatFiles) {
4092
- const stateKey = filePath.replace(homedir4(), "~");
4495
+ const stateKey = filePath.replace(homedir5(), "~");
4093
4496
  let fileMtime = "0";
4094
4497
  try {
4095
4498
  fileMtime = statSync3(filePath).mtimeMs.toString();
@@ -4101,7 +4504,7 @@ async function ingestGemini(db, verbose) {
4101
4504
  continue;
4102
4505
  let chatData;
4103
4506
  try {
4104
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
4507
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
4105
4508
  } catch {
4106
4509
  continue;
4107
4510
  }
@@ -4179,15 +4582,15 @@ async function ingestGemini(db, verbose) {
4179
4582
  // src/ingest/opencode.ts
4180
4583
  init_database();
4181
4584
  init_pricing();
4182
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
4183
- import { homedir as homedir5 } from "os";
4184
- import { join as join5 } from "path";
4185
- var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
4585
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
4586
+ import { homedir as homedir6 } from "os";
4587
+ import { join as join6 } from "path";
4588
+ var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
4186
4589
  function walkJsonFiles(dir, acc = []) {
4187
- if (!existsSync5(dir))
4590
+ if (!existsSync6(dir))
4188
4591
  return acc;
4189
4592
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
4190
- const p = join5(dir, entry.name);
4593
+ const p = join6(dir, entry.name);
4191
4594
  if (entry.isDirectory())
4192
4595
  walkJsonFiles(p, acc);
4193
4596
  else if (entry.name.endsWith(".json"))
@@ -4207,7 +4610,7 @@ function parseSessionIdFromPath(filePath) {
4207
4610
  return null;
4208
4611
  }
4209
4612
  async function ingestOpenCode(db, verbose = false) {
4210
- const messageDir = join5(OPENCODE_STORAGE, "message");
4613
+ const messageDir = join6(OPENCODE_STORAGE, "message");
4211
4614
  const files = walkJsonFiles(messageDir);
4212
4615
  let requests = 0;
4213
4616
  const touched = new Set;
@@ -4222,7 +4625,7 @@ async function ingestOpenCode(db, verbose = false) {
4222
4625
  continue;
4223
4626
  let parsed;
4224
4627
  try {
4225
- parsed = JSON.parse(readFileSync4(file, "utf-8"));
4628
+ parsed = JSON.parse(readFileSync5(file, "utf-8"));
4226
4629
  } catch {
4227
4630
  continue;
4228
4631
  }
@@ -4409,15 +4812,15 @@ async function ingestCursor(db, verbose = false) {
4409
4812
 
4410
4813
  // src/ingest/pi.ts
4411
4814
  init_database();
4412
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
4413
- import { homedir as homedir6 } from "os";
4414
- import { join as join6 } from "path";
4415
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
4815
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
4816
+ import { homedir as homedir7 } from "os";
4817
+ import { join as join7 } from "path";
4818
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
4416
4819
  function walkSessions(dir, acc = []) {
4417
- if (!existsSync6(dir))
4820
+ if (!existsSync7(dir))
4418
4821
  return acc;
4419
4822
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
4420
- const p = join6(dir, entry.name);
4823
+ const p = join7(dir, entry.name);
4421
4824
  if (entry.isDirectory())
4422
4825
  walkSessions(p, acc);
4423
4826
  else if (entry.name.endsWith(".json"))
@@ -4439,7 +4842,7 @@ async function ingestPi(db, verbose = false) {
4439
4842
  continue;
4440
4843
  let data;
4441
4844
  try {
4442
- data = JSON.parse(readFileSync5(file, "utf-8"));
4845
+ data = JSON.parse(readFileSync6(file, "utf-8"));
4443
4846
  } catch {
4444
4847
  continue;
4445
4848
  }
@@ -4503,10 +4906,10 @@ async function ingestPi(db, verbose = false) {
4503
4906
 
4504
4907
  // src/ingest/hermes.ts
4505
4908
  init_database();
4506
- import { existsSync as existsSync7, statSync as statSync6 } from "fs";
4507
- import { homedir as homedir7 } from "os";
4508
- import { join as join7 } from "path";
4509
- var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
4909
+ import { existsSync as existsSync8, statSync as statSync6 } from "fs";
4910
+ import { homedir as homedir8 } from "os";
4911
+ import { join as join8 } from "path";
4912
+ var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
4510
4913
  function mapCostBasis(billingMode) {
4511
4914
  if (billingMode === "subscription")
4512
4915
  return "subscription_included";
@@ -4515,7 +4918,7 @@ function mapCostBasis(billingMode) {
4515
4918
  return defaultCostBasisForAgent("hermes");
4516
4919
  }
4517
4920
  async function ingestHermes(db, verbose = false) {
4518
- if (!existsSync7(HERMES_DB)) {
4921
+ if (!existsSync8(HERMES_DB)) {
4519
4922
  return { sessions: 0, requests: 0 };
4520
4923
  }
4521
4924
  const { Database: Sqlite } = await import("bun:sqlite");
@@ -4595,14 +4998,81 @@ function statSyncSafe(path) {
4595
4998
  // src/ingest/loops.ts
4596
4999
  init_database();
4597
5000
  init_pricing();
4598
- import { existsSync as existsSync8 } from "fs";
4599
- import { homedir as homedir8 } from "os";
4600
- import { join as join8 } from "path";
5001
+ import { existsSync as existsSync9 } from "fs";
5002
+ import { homedir as homedir9 } from "os";
5003
+ import { join as join9 } from "path";
4601
5004
  import { Database as BunDatabase2 } from "bun:sqlite";
4602
- var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
5005
+ import { homedir as pathsResolverHomedir2 } from "os";
5006
+ import { join as pathsResolverJoin2 } from "path";
5007
+ var PATHS_RESOLVER_KIND_ENV2 = {
5008
+ config: "HASNA_CONFIG_HOME",
5009
+ data: "HASNA_DATA_HOME",
5010
+ state: "HASNA_STATE_HOME",
5011
+ cache: "HASNA_CACHE_HOME"
5012
+ };
5013
+ var PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5014
+ function pathsResolverAssertApp2(app) {
5015
+ if (typeof app !== "string" || app.length === 0) {
5016
+ throw new TypeError("paths: app must be a non-empty string");
5017
+ }
5018
+ if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
5019
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
5020
+ }
5021
+ }
5022
+ function pathsResolverAssertKind2(kind) {
5023
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
5024
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
5025
+ }
5026
+ }
5027
+ function pathsResolverBaseDir2(kind, options) {
5028
+ pathsResolverAssertKind2(kind);
5029
+ const env = options.env ?? process.env;
5030
+ const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
5031
+ if (typeof override === "string" && override.length > 0)
5032
+ return override;
5033
+ const home = options.home ?? pathsResolverHomedir2();
5034
+ const platform2 = options.platform ?? process.platform;
5035
+ if (platform2 === "darwin") {
5036
+ switch (kind) {
5037
+ case "config":
5038
+ case "data":
5039
+ return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
5040
+ case "cache":
5041
+ return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
5042
+ case "state":
5043
+ return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
5044
+ }
5045
+ }
5046
+ switch (kind) {
5047
+ case "config":
5048
+ return pathsResolverJoin2(home, ".config", "hasna");
5049
+ case "data":
5050
+ return pathsResolverJoin2(home, ".local", "share", "hasna");
5051
+ case "state":
5052
+ return pathsResolverJoin2(home, ".local", "state", "hasna");
5053
+ case "cache":
5054
+ return pathsResolverJoin2(home, ".cache", "hasna");
5055
+ }
5056
+ }
5057
+ function pathsResolverResolve2(kind, options) {
5058
+ pathsResolverAssertApp2(options.app);
5059
+ const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
5060
+ return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
5061
+ }
5062
+ function dataDir2(options) {
5063
+ return pathsResolverResolve2("data", options);
5064
+ }
4603
5065
  var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
5066
+ function defaultLoopsDbPath() {
5067
+ const resolved = dataDir2({
5068
+ app: "loops",
5069
+ home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
5070
+ });
5071
+ const legacy = join9(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
5072
+ return existsSync9(join9(resolved, "loops.db")) ? join9(resolved, "loops.db") : legacy;
5073
+ }
4604
5074
  function loopsDbPath() {
4605
- return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
5075
+ return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
4606
5076
  }
4607
5077
  function loopsModel() {
4608
5078
  return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
@@ -4691,7 +5161,7 @@ function readLoopGoalRuns(loopsDb) {
4691
5161
  }
4692
5162
  async function ingestLoops(db, verbose = false) {
4693
5163
  const dbPath = loopsDbPath();
4694
- if (!existsSync8(dbPath)) {
5164
+ if (!existsSync9(dbPath)) {
4695
5165
  if (verbose)
4696
5166
  console.log("OpenLoops DB not found:", dbPath);
4697
5167
  return { sessions: 0, requests: 0 };
@@ -4781,30 +5251,30 @@ async function ingestLoops(db, verbose = false) {
4781
5251
 
4782
5252
  // src/ingest/claude-quota.ts
4783
5253
  init_database();
4784
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
5254
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4785
5255
 
4786
5256
  // src/lib/paths.ts
4787
- import { homedir as homedir9 } from "os";
4788
- import { join as join9 } from "path";
4789
- function getHomeDir() {
4790
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
5257
+ import { homedir as homedir10 } from "os";
5258
+ import { join as join10 } from "path";
5259
+ function getHomeDir2() {
5260
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
4791
5261
  }
4792
5262
  function agentPaths() {
4793
- const home = getHomeDir();
5263
+ const home = getHomeDir2();
4794
5264
  return {
4795
- claudeProjects: join9(home, ".claude", "projects"),
4796
- claudeCredentials: join9(home, ".claude", ".credentials.json"),
4797
- takumiProjects: join9(home, ".takumi", "projects"),
4798
- codexDir: join9(home, ".codex"),
4799
- codexDb: join9(home, ".codex", "state_5.sqlite"),
4800
- codexAuth: join9(home, ".codex", "auth.json"),
4801
- codexConfig: join9(home, ".codex", "config.toml"),
4802
- geminiTmp: join9(home, ".gemini", "tmp"),
4803
- geminiHistory: join9(home, ".gemini", "history"),
4804
- opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
4805
- piSessions: join9(home, ".pi", "agent", "sessions"),
4806
- hermesDir: join9(home, ".hermes"),
4807
- hermesDb: join9(home, ".hermes", "state.db")
5265
+ claudeProjects: join10(home, ".claude", "projects"),
5266
+ claudeCredentials: join10(home, ".claude", ".credentials.json"),
5267
+ takumiProjects: join10(home, ".takumi", "projects"),
5268
+ codexDir: join10(home, ".codex"),
5269
+ codexDb: join10(home, ".codex", "state_5.sqlite"),
5270
+ codexAuth: join10(home, ".codex", "auth.json"),
5271
+ codexConfig: join10(home, ".codex", "config.toml"),
5272
+ geminiTmp: join10(home, ".gemini", "tmp"),
5273
+ geminiHistory: join10(home, ".gemini", "history"),
5274
+ opencodeMessages: join10(home, ".local", "share", "opencode", "storage", "message"),
5275
+ piSessions: join10(home, ".pi", "agent", "sessions"),
5276
+ hermesDir: join10(home, ".hermes"),
5277
+ hermesDb: join10(home, ".hermes", "state.db")
4808
5278
  };
4809
5279
  }
4810
5280
 
@@ -4816,10 +5286,10 @@ function readClaudeToken() {
4816
5286
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
4817
5287
  if (fromEnv)
4818
5288
  return { token: fromEnv };
4819
- if (!existsSync9(CREDENTIALS_PATH))
5289
+ if (!existsSync10(CREDENTIALS_PATH))
4820
5290
  return null;
4821
5291
  try {
4822
- const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
5292
+ const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
4823
5293
  const oauth = creds.claudeAiOauth;
4824
5294
  if (!oauth?.accessToken)
4825
5295
  return null;
@@ -4955,17 +5425,17 @@ async function ingestClaudeQuota(db, verbose = false) {
4955
5425
 
4956
5426
  // src/ingest/codex-quota.ts
4957
5427
  init_database();
4958
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
5428
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4959
5429
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
4960
5430
  function readCodexAuth() {
4961
5431
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
4962
5432
  if (fromEnv)
4963
5433
  return { token: fromEnv, authMode: "chatgpt" };
4964
5434
  const authPath = agentPaths().codexAuth;
4965
- if (!existsSync10(authPath))
5435
+ if (!existsSync11(authPath))
4966
5436
  return null;
4967
5437
  try {
4968
- const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
5438
+ const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
4969
5439
  const token = auth.tokens?.access_token;
4970
5440
  if (!token)
4971
5441
  return null;
@@ -5950,7 +6420,7 @@ init_database();
5950
6420
 
5951
6421
  // src/lib/serve-auth.ts
5952
6422
  function getServeApiToken() {
5953
- return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
6423
+ return process.env["HASNA_ECONOMY_API_TOKEN"];
5954
6424
  }
5955
6425
  function getServeBindHost() {
5956
6426
  const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
@@ -5974,13 +6444,11 @@ function isAuthorizedRequest(req, path) {
5974
6444
 
5975
6445
  // src/server/serve.ts
5976
6446
  import { randomUUID } from "crypto";
5977
- import { existsSync as existsSync13 } from "fs";
5978
- import { resolve, sep } from "path";
5979
6447
  // package.json
5980
6448
  var package_default = {
5981
6449
  name: "@hasna/economy",
5982
- version: "0.3.27",
5983
- 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",
6450
+ version: "0.4.0",
6451
+ 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",
5984
6452
  repository: {
5985
6453
  type: "git",
5986
6454
  url: "git+https://github.com/hasna/economy.git"
@@ -6011,7 +6479,7 @@ var package_default = {
6011
6479
  files: [
6012
6480
  "dist",
6013
6481
  "docs",
6014
- "dashboard/dist",
6482
+ "postinstall.js",
6015
6483
  "LICENSE",
6016
6484
  "README.md",
6017
6485
  "CHANGELOG.md",
@@ -6020,12 +6488,11 @@ var package_default = {
6020
6488
  "CODE_OF_CONDUCT.md"
6021
6489
  ],
6022
6490
  scripts: {
6023
- 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",
6491
+ 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",
6024
6492
  "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
6025
6493
  "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
6026
6494
  "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
6027
6495
  "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
6028
- "build:dashboard": "cd dashboard && bun run build",
6029
6496
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
6030
6497
  prepack: "bun run build && bun run artifact-scan",
6031
6498
  typecheck: "tsc --noEmit",
@@ -6034,7 +6501,7 @@ var package_default = {
6034
6501
  "dev:mcp": "bun run src/mcp/index.ts",
6035
6502
  "dev:serve": "bun run src/server/index.ts",
6036
6503
  "dev:otel": "bun run src/otel/index.ts",
6037
- postinstall: "mkdir -p $HOME/.hasna/economy/training 2>/dev/null || true"
6504
+ postinstall: "node postinstall.js"
6038
6505
  },
6039
6506
  keywords: [
6040
6507
  "economy",
@@ -6055,11 +6522,8 @@ var package_default = {
6055
6522
  access: "public"
6056
6523
  },
6057
6524
  dependencies: {
6058
- "@hasna/accounts": "0.2.23",
6059
- "@hasna/agent-registry": "^0.1.0",
6060
- "@hasna/contracts": "0.13.4",
6525
+ "@hasna/contracts": "1.0.2",
6061
6526
  "@hasna/events": "^0.1.16",
6062
- "@hasna/mcp-harness": "^0.1.0",
6063
6527
  "@hasna/projects": "1.0.0",
6064
6528
  "@modelcontextprotocol/sdk": "^1.12.1",
6065
6529
  chalk: "^5.4.1",
@@ -6068,7 +6532,7 @@ var package_default = {
6068
6532
  zod: "^3.24.2"
6069
6533
  },
6070
6534
  devDependencies: {
6071
- "@types/bun": "latest",
6535
+ "@types/bun": "1.3.14",
6072
6536
  "@types/pg": "^8.20.0",
6073
6537
  "bun-types": "latest",
6074
6538
  typescript: "^5.7.2"
@@ -7746,7 +8210,6 @@ var APP = "economy";
7746
8210
  var ECONOMY_WRITE_SCOPE = `${APP}:write`;
7747
8211
  var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
7748
8212
  var SYNC_SOURCES = ["all", ...AGENTS, "loops"];
7749
- var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
7750
8213
  function json(data, status = 200) {
7751
8214
  return new Response(JSON.stringify(data), {
7752
8215
  status,
@@ -7799,38 +8262,6 @@ function requiredScopesForRequest(method, path) {
7799
8262
  function stringArray(value) {
7800
8263
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
7801
8264
  }
7802
- function dashboardPath(root, pathname) {
7803
- let decoded;
7804
- try {
7805
- decoded = decodeURIComponent(pathname);
7806
- } catch {
7807
- return null;
7808
- }
7809
- const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
7810
- const rootPath = resolve(root);
7811
- const filePath = resolve(rootPath, relativePath);
7812
- return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
7813
- }
7814
- var FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
7815
- function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
7816
- return async function fetch2(req) {
7817
- const url = new URL(req.url);
7818
- if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
7819
- return apiHandler(req);
7820
- }
7821
- if (existsSync13(dashboardDir)) {
7822
- const filePath = dashboardPath(dashboardDir, url.pathname);
7823
- if (filePath && existsSync13(filePath)) {
7824
- return new Response(Bun.file(filePath));
7825
- }
7826
- const indexPath = dashboardPath(dashboardDir, "/");
7827
- if (indexPath && existsSync13(indexPath)) {
7828
- return new Response(Bun.file(indexPath));
7829
- }
7830
- }
7831
- return apiHandler(req);
7832
- };
7833
- }
7834
8265
  function applyFields(obj, fields) {
7835
8266
  if (!fields || fields.length === 0)
7836
8267
  return obj;
@@ -8354,7 +8785,7 @@ function startServer(port = 3456, options = {}) {
8354
8785
  const server = Bun.serve({
8355
8786
  port,
8356
8787
  hostname: hostname2,
8357
- fetch: createServerFetch(apiHandler, options.dashboardDir)
8788
+ fetch: apiHandler
8358
8789
  });
8359
8790
  const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
8360
8791
  log(`economy-serve listening on ${address}`);