@hasna/economy 0.3.28 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +6 -13
  4. package/SECURITY.md +1 -1
  5. package/dist/cli/commands/extras.d.ts.map +1 -1
  6. package/dist/cli/index.js +1135 -853
  7. package/dist/db/cloud.d.ts +16 -8
  8. package/dist/db/cloud.d.ts.map +1 -1
  9. package/dist/db/database.d.ts +19 -0
  10. package/dist/db/database.d.ts.map +1 -1
  11. package/dist/index.js +198 -19
  12. package/dist/ingest/claude.d.ts.map +1 -1
  13. package/dist/ingest/loops.d.ts +9 -0
  14. package/dist/ingest/loops.d.ts.map +1 -1
  15. package/dist/lib/accounts-store.d.ts +83 -0
  16. package/dist/lib/accounts-store.d.ts.map +1 -0
  17. package/dist/lib/accounts.d.ts +2 -1
  18. package/dist/lib/accounts.d.ts.map +1 -1
  19. package/dist/lib/api-display-url.d.ts +19 -0
  20. package/dist/lib/api-display-url.d.ts.map +1 -0
  21. package/dist/lib/autosync-gate.d.ts +15 -0
  22. package/dist/lib/autosync-gate.d.ts.map +1 -0
  23. package/dist/lib/cloud-ingest.d.ts +15 -1
  24. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  25. package/dist/lib/cloud-storage.d.ts +166 -10
  26. package/dist/lib/cloud-storage.d.ts.map +1 -1
  27. package/dist/lib/gatherer.d.ts.map +1 -1
  28. package/dist/lib/serve-auth.d.ts.map +1 -1
  29. package/dist/lib/store/index.d.ts +6 -3
  30. package/dist/lib/store/index.d.ts.map +1 -1
  31. package/dist/lib/test-hermetic-accounts.d.ts +3 -2
  32. package/dist/lib/test-hermetic-accounts.d.ts.map +1 -1
  33. package/dist/lib/test-hermetic-fleet-env.d.ts +4 -0
  34. package/dist/lib/test-hermetic-fleet-env.d.ts.map +1 -0
  35. package/dist/mcp/agent-registry.d.ts +85 -0
  36. package/dist/mcp/agent-registry.d.ts.map +1 -0
  37. package/dist/mcp/harness.d.ts +26 -0
  38. package/dist/mcp/harness.d.ts.map +1 -0
  39. package/dist/mcp/index.js +1340 -302
  40. package/dist/mcp/server.d.ts.map +1 -1
  41. package/dist/otel/index.js +430 -16
  42. package/dist/server/index.js +548 -155
  43. package/dist/server/serve.d.ts +0 -2
  44. package/dist/server/serve.d.ts.map +1 -1
  45. package/docs/cli.md +2 -3
  46. package/docs/configuration.md +23 -10
  47. package/docs/ingestion.md +1 -1
  48. package/docs/mcp.md +4 -2
  49. package/docs/otel.md +8 -2
  50. package/docs/rest-api.md +2 -2
  51. package/package.json +5 -11
  52. package/postinstall.js +52 -1
  53. package/dashboard/README.md +0 -26
  54. package/dist/cli/brains.d.ts +0 -3
  55. package/dist/cli/brains.d.ts.map +0 -1
@@ -596,7 +596,59 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
596
596
  import { hostname, platform } from "os";
597
597
  import { homedir } from "os";
598
598
  import { join, resolve } from "path";
599
- import { dataDir } from "@hasna/paths";
599
+ import { homedir as pathsResolverHomedir } from "os";
600
+ import { join as pathsResolverJoin } from "path";
601
+ function pathsResolverAssertApp(app) {
602
+ if (typeof app !== "string" || app.length === 0) {
603
+ throw new TypeError("paths: app must be a non-empty string");
604
+ }
605
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
606
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
607
+ }
608
+ }
609
+ function pathsResolverAssertKind(kind) {
610
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
611
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
612
+ }
613
+ }
614
+ function pathsResolverBaseDir(kind, options) {
615
+ pathsResolverAssertKind(kind);
616
+ const env = options.env ?? process.env;
617
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
618
+ if (typeof override === "string" && override.length > 0)
619
+ return override;
620
+ const home = options.home ?? pathsResolverHomedir();
621
+ const platform2 = options.platform ?? process.platform;
622
+ if (platform2 === "darwin") {
623
+ switch (kind) {
624
+ case "config":
625
+ case "data":
626
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
627
+ case "cache":
628
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
629
+ case "state":
630
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
631
+ }
632
+ }
633
+ switch (kind) {
634
+ case "config":
635
+ return pathsResolverJoin(home, ".config", "hasna");
636
+ case "data":
637
+ return pathsResolverJoin(home, ".local", "share", "hasna");
638
+ case "state":
639
+ return pathsResolverJoin(home, ".local", "state", "hasna");
640
+ case "cache":
641
+ return pathsResolverJoin(home, ".cache", "hasna");
642
+ }
643
+ }
644
+ function pathsResolverResolve(kind, options) {
645
+ pathsResolverAssertApp(options.app);
646
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
647
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
648
+ }
649
+ function dataDir(options) {
650
+ return pathsResolverResolve("data", options);
651
+ }
600
652
  function normalizeMachineId(value) {
601
653
  const id = value?.trim().toLowerCase().split(".")[0];
602
654
  return id && id.length > 0 ? id : null;
@@ -619,7 +671,7 @@ function macHostMachineId() {
619
671
  return null;
620
672
  }
621
673
  function getMachineId() {
622
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
674
+ const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
623
675
  if (envMachine)
624
676
  return envMachine;
625
677
  const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
@@ -1926,6 +1978,10 @@ function getIngestState(db, source, key) {
1926
1978
  const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
1927
1979
  return row?.value ?? null;
1928
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
+ }
1929
1985
  function setIngestState(db, source, key, value) {
1930
1986
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1931
1987
  }
@@ -2364,7 +2420,7 @@ function bulkIngest(db, body) {
2364
2420
  const total = Object.values(ingested).reduce((a, b) => a + b, 0);
2365
2421
  return { ingested, total };
2366
2422
  }
2367
- var numOr = (v, d = 0) => {
2423
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, numOr = (v, d = 0) => {
2368
2424
  if (v == null)
2369
2425
  return d;
2370
2426
  const num = Number(v);
@@ -2372,6 +2428,13 @@ var numOr = (v, d = 0) => {
2372
2428
  }, strOr = (v, d = "") => v == null ? d : String(v);
2373
2429
  var init_database = __esm(() => {
2374
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]+)*$/;
2375
2438
  });
2376
2439
 
2377
2440
  // src/db/dialect.ts
@@ -2414,8 +2477,8 @@ function sqliteToPostgres(sql) {
2414
2477
  // src/db/sync-pg.ts
2415
2478
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
2416
2479
  import { fileURLToPath } from "url";
2417
- import { dirname, join as join10 } from "path";
2418
- import { existsSync as existsSync11 } from "fs";
2480
+ import { dirname, join as join11 } from "path";
2481
+ import { existsSync as existsSync12 } from "fs";
2419
2482
  function translateSqliteDates(sql) {
2420
2483
  let s = sql;
2421
2484
  const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
@@ -2440,12 +2503,12 @@ function translateSqliteDates(sql) {
2440
2503
  function resolveWorkerPath() {
2441
2504
  const here = dirname(fileURLToPath(import.meta.url));
2442
2505
  const candidates = [
2443
- join10(here, "pg-sync-worker.ts"),
2444
- join10(here, "pg-sync-worker.js"),
2445
- 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")
2446
2509
  ];
2447
2510
  for (const candidate of candidates)
2448
- if (existsSync11(candidate))
2511
+ if (existsSync12(candidate))
2449
2512
  return candidate;
2450
2513
  return candidates[0];
2451
2514
  }
@@ -2902,7 +2965,7 @@ __export(exports_billing, {
2902
2965
  syncGeminiBilling: () => syncGeminiBilling,
2903
2966
  syncAnthropicBilling: () => syncAnthropicBilling
2904
2967
  });
2905
- import { readFileSync as readFileSync8 } from "fs";
2968
+ import { readFileSync as readFileSync9 } from "fs";
2906
2969
  function getAnthropicAdminKey() {
2907
2970
  return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
2908
2971
  }
@@ -3129,7 +3192,7 @@ async function syncGeminiBilling(db, opts = {}) {
3129
3192
  const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
3130
3193
  const fromDateStr = toISODate(start);
3131
3194
  const toDateStr = toISODate(end);
3132
- const rows = parseBillingRows(readFileSync8(exportPath, "utf-8"));
3195
+ const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
3133
3196
  clearBillingRange(db, "gemini", fromDateStr, toDateStr);
3134
3197
  const updatedAt = new Date().toISOString();
3135
3198
  let totalUsd = 0;
@@ -3193,16 +3256,16 @@ var init_open_projects = __esm(() => {
3193
3256
  });
3194
3257
 
3195
3258
  // src/lib/config.ts
3196
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
3197
- 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";
3198
3261
  function getConfigPath() {
3199
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join11(getDataDir(), "config.json");
3262
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
3200
3263
  }
3201
3264
  function loadConfig() {
3202
3265
  try {
3203
3266
  const configPath = getConfigPath();
3204
- if (existsSync12(configPath)) {
3205
- const raw = readFileSync9(configPath, "utf-8");
3267
+ if (existsSync13(configPath)) {
3268
+ const raw = readFileSync10(configPath, "utf-8");
3206
3269
  return { ...DEFAULTS, ...JSON.parse(raw) };
3207
3270
  }
3208
3271
  } catch {}
@@ -3347,9 +3410,9 @@ function isAgent(value) {
3347
3410
  // src/ingest/claude.ts
3348
3411
  init_database();
3349
3412
  init_pricing();
3350
- import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
3351
- import { homedir as homedir2 } from "os";
3352
- 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";
3353
3416
 
3354
3417
  // src/lib/savings.ts
3355
3418
  function periodWhere2(period, column) {
@@ -3505,8 +3568,319 @@ function defaultCostBasisForAgent(agent) {
3505
3568
  return "estimated";
3506
3569
  }
3507
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
+
3508
3883
  // src/lib/accounts.ts
3509
- import { appliedProfileName, resolveStore } from "@hasna/accounts";
3510
3884
  var AGENT_ACCOUNT_TOOLS = {
3511
3885
  claude: ["claude"],
3512
3886
  takumi: ["takumi", "claude"],
@@ -3578,11 +3952,11 @@ async function profileForEnvDir(store, tool, env) {
3578
3952
  const normalized = normalizeDir(configuredDir);
3579
3953
  return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
3580
3954
  }
3581
- async function resolveAccountForAgent(agent, env = process.env) {
3955
+ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
3582
3956
  const override = envOverride(agent, env);
3583
3957
  if (override)
3584
3958
  return override;
3585
- const store = resolveStore(env);
3959
+ const store = resolveStore(env, options);
3586
3960
  const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
3587
3961
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
3588
3962
  const tool = tools.get(toolId);
@@ -3620,8 +3994,8 @@ function withAccount(record, account) {
3620
3994
  function autoDetectProject(cwd, projects) {
3621
3995
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
3622
3996
  }
3623
- var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
3624
- 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");
3625
3999
  function dirNameToPath(dirName) {
3626
4000
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
3627
4001
  }
@@ -3631,9 +4005,9 @@ function collectJsonlFiles(projectDir) {
3631
4005
  try {
3632
4006
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
3633
4007
  if (entry.isDirectory())
3634
- walk(join2(dir, entry.name));
4008
+ walk(join3(dir, entry.name));
3635
4009
  else if (entry.name.endsWith(".jsonl"))
3636
- files.push(join2(dir, entry.name));
4010
+ files.push(join3(dir, entry.name));
3637
4011
  }
3638
4012
  } catch {}
3639
4013
  }
@@ -3647,7 +4021,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
3647
4021
  return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
3648
4022
  }
3649
4023
  async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
3650
- if (!existsSync2(projectsDir)) {
4024
+ if (!existsSync3(projectsDir)) {
3651
4025
  if (verbose)
3652
4026
  console.log(`${agentName} projects dir not found:`, projectsDir);
3653
4027
  return { files: 0, requests: 0, sessions: 0 };
@@ -3658,9 +4032,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
3658
4032
  const touchedSessions = new Set;
3659
4033
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
3660
4034
  const account = await resolveAccountForAgent(agentName);
4035
+ const ingestState = loadIngestState(db, agentName);
3661
4036
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3662
4037
  for (const projectDirEntry of projectDirs) {
3663
- const projectDirPath = join2(projectsDir, projectDirEntry.name);
4038
+ const projectDirPath = join3(projectsDir, projectDirEntry.name);
3664
4039
  const projectPath = dirNameToPath(projectDirEntry.name);
3665
4040
  const jsonlFiles = collectJsonlFiles(projectDirPath);
3666
4041
  for (const filePath of jsonlFiles) {
@@ -3671,12 +4046,12 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
3671
4046
  } catch {
3672
4047
  continue;
3673
4048
  }
3674
- const processed = getIngestState(db, agentName, stateKey);
4049
+ const processed = ingestState.get(stateKey) ?? null;
3675
4050
  if (processed === fileMtime)
3676
4051
  continue;
3677
4052
  let lines;
3678
4053
  try {
3679
- lines = readFileSync(filePath, "utf-8").split(`
4054
+ lines = readFileSync2(filePath, "utf-8").split(`
3680
4055
  `).filter((l) => l.trim());
3681
4056
  } catch {
3682
4057
  continue;
@@ -3796,14 +4171,14 @@ function supportsClaudeDataResidencyPricing(model) {
3796
4171
  // src/ingest/codex.ts
3797
4172
  init_database();
3798
4173
  init_pricing();
3799
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3800
- import { homedir as homedir3 } from "os";
3801
- 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";
3802
4177
  import { Database as BunDatabase } from "bun:sqlite";
3803
- var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
3804
- var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
3805
- var DEFAULT_CODEWITH_DB_PATH = join3(homedir3(), ".codewith", "state_5.sqlite");
3806
- 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");
3807
4182
  var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
3808
4183
  function codexDbPath() {
3809
4184
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
@@ -3845,10 +4220,10 @@ function codexSources() {
3845
4220
  });
3846
4221
  }
3847
4222
  function readCodexModel(configPath = codexConfigPath()) {
3848
- if (!existsSync3(configPath))
4223
+ if (!existsSync4(configPath))
3849
4224
  return "gpt-5-codex";
3850
4225
  try {
3851
- const content = readFileSync2(configPath, "utf-8");
4226
+ const content = readFileSync3(configPath, "utf-8");
3852
4227
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
3853
4228
  return match?.[1] ?? "gpt-5-codex";
3854
4229
  } catch {
@@ -3886,12 +4261,12 @@ function openCodexDb(dbPath, verbose) {
3886
4261
  return null;
3887
4262
  }
3888
4263
  function readTokenEvents(rolloutPath) {
3889
- if (!rolloutPath || !existsSync3(rolloutPath))
4264
+ if (!rolloutPath || !existsSync4(rolloutPath))
3890
4265
  return [];
3891
4266
  const fallbackUsages = new Map;
3892
4267
  let fallbackTimestamp;
3893
4268
  let aggregate = null;
3894
- for (const line of readFileSync2(rolloutPath, "utf-8").split(`
4269
+ for (const line of readFileSync3(rolloutPath, "utf-8").split(`
3895
4270
  `)) {
3896
4271
  if (!line.trim())
3897
4272
  continue;
@@ -3967,7 +4342,7 @@ async function ingestCodex(db, verbose = false) {
3967
4342
  let requests = 0;
3968
4343
  const account = await resolveAccountForAgent("codex");
3969
4344
  for (const source of codexSources()) {
3970
- if (!existsSync3(source.dbPath)) {
4345
+ if (!existsSync4(source.dbPath)) {
3971
4346
  if (verbose)
3972
4347
  console.log(`${source.label} DB not found:`, source.dbPath);
3973
4348
  continue;
@@ -4048,11 +4423,11 @@ async function ingestCodex(db, verbose = false) {
4048
4423
  // src/ingest/gemini.ts
4049
4424
  init_database();
4050
4425
  init_pricing();
4051
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
4052
- import { homedir as homedir4 } from "os";
4053
- import { join as join4, basename as basename3 } from "path";
4054
- var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
4055
- 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");
4056
4431
  function geminiTmpDir() {
4057
4432
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
4058
4433
  }
@@ -4069,12 +4444,12 @@ function numberField(...values) {
4069
4444
  function listProjectDirs(...roots) {
4070
4445
  const dirs = new Set;
4071
4446
  for (const root of roots) {
4072
- if (!existsSync4(root))
4447
+ if (!existsSync5(root))
4073
4448
  continue;
4074
4449
  try {
4075
4450
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
4076
4451
  if (entry.isDirectory())
4077
- dirs.add(join4(root, entry.name));
4452
+ dirs.add(join5(root, entry.name));
4078
4453
  }
4079
4454
  } catch {}
4080
4455
  }
@@ -4085,17 +4460,17 @@ function projectRoot(projectDir, chatData) {
4085
4460
  return chatData.projectPath;
4086
4461
  if (chatData.project_path)
4087
4462
  return chatData.project_path;
4088
- const rootFile = join4(projectDir, ".project_root");
4463
+ const rootFile = join5(projectDir, ".project_root");
4089
4464
  try {
4090
- if (existsSync4(rootFile))
4091
- return readFileSync3(rootFile, "utf-8").trim();
4465
+ if (existsSync5(rootFile))
4466
+ return readFileSync4(rootFile, "utf-8").trim();
4092
4467
  } catch {}
4093
4468
  return "";
4094
4469
  }
4095
4470
  async function ingestGemini(db, verbose) {
4096
4471
  const tmpDir = geminiTmpDir();
4097
4472
  const historyDir = geminiHistoryDir();
4098
- if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
4473
+ if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
4099
4474
  if (verbose)
4100
4475
  console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
4101
4476
  return { sessions: 0, requests: 0 };
@@ -4107,17 +4482,17 @@ async function ingestGemini(db, verbose) {
4107
4482
  const account = await resolveAccountForAgent("gemini");
4108
4483
  const projectDirs = listProjectDirs(tmpDir, historyDir);
4109
4484
  for (const projectDir of projectDirs) {
4110
- const chatsDir = join4(projectDir, "chats");
4111
- if (!existsSync4(chatsDir))
4485
+ const chatsDir = join5(projectDir, "chats");
4486
+ if (!existsSync5(chatsDir))
4112
4487
  continue;
4113
4488
  let chatFiles = [];
4114
4489
  try {
4115
- 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));
4116
4491
  } catch {
4117
4492
  continue;
4118
4493
  }
4119
4494
  for (const filePath of chatFiles) {
4120
- const stateKey = filePath.replace(homedir4(), "~");
4495
+ const stateKey = filePath.replace(homedir5(), "~");
4121
4496
  let fileMtime = "0";
4122
4497
  try {
4123
4498
  fileMtime = statSync3(filePath).mtimeMs.toString();
@@ -4129,7 +4504,7 @@ async function ingestGemini(db, verbose) {
4129
4504
  continue;
4130
4505
  let chatData;
4131
4506
  try {
4132
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
4507
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
4133
4508
  } catch {
4134
4509
  continue;
4135
4510
  }
@@ -4207,15 +4582,15 @@ async function ingestGemini(db, verbose) {
4207
4582
  // src/ingest/opencode.ts
4208
4583
  init_database();
4209
4584
  init_pricing();
4210
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
4211
- import { homedir as homedir5 } from "os";
4212
- import { join as join5 } from "path";
4213
- 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");
4214
4589
  function walkJsonFiles(dir, acc = []) {
4215
- if (!existsSync5(dir))
4590
+ if (!existsSync6(dir))
4216
4591
  return acc;
4217
4592
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
4218
- const p = join5(dir, entry.name);
4593
+ const p = join6(dir, entry.name);
4219
4594
  if (entry.isDirectory())
4220
4595
  walkJsonFiles(p, acc);
4221
4596
  else if (entry.name.endsWith(".json"))
@@ -4235,7 +4610,7 @@ function parseSessionIdFromPath(filePath) {
4235
4610
  return null;
4236
4611
  }
4237
4612
  async function ingestOpenCode(db, verbose = false) {
4238
- const messageDir = join5(OPENCODE_STORAGE, "message");
4613
+ const messageDir = join6(OPENCODE_STORAGE, "message");
4239
4614
  const files = walkJsonFiles(messageDir);
4240
4615
  let requests = 0;
4241
4616
  const touched = new Set;
@@ -4250,7 +4625,7 @@ async function ingestOpenCode(db, verbose = false) {
4250
4625
  continue;
4251
4626
  let parsed;
4252
4627
  try {
4253
- parsed = JSON.parse(readFileSync4(file, "utf-8"));
4628
+ parsed = JSON.parse(readFileSync5(file, "utf-8"));
4254
4629
  } catch {
4255
4630
  continue;
4256
4631
  }
@@ -4437,15 +4812,15 @@ async function ingestCursor(db, verbose = false) {
4437
4812
 
4438
4813
  // src/ingest/pi.ts
4439
4814
  init_database();
4440
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
4441
- import { homedir as homedir6 } from "os";
4442
- import { join as join6 } from "path";
4443
- 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");
4444
4819
  function walkSessions(dir, acc = []) {
4445
- if (!existsSync6(dir))
4820
+ if (!existsSync7(dir))
4446
4821
  return acc;
4447
4822
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
4448
- const p = join6(dir, entry.name);
4823
+ const p = join7(dir, entry.name);
4449
4824
  if (entry.isDirectory())
4450
4825
  walkSessions(p, acc);
4451
4826
  else if (entry.name.endsWith(".json"))
@@ -4467,7 +4842,7 @@ async function ingestPi(db, verbose = false) {
4467
4842
  continue;
4468
4843
  let data;
4469
4844
  try {
4470
- data = JSON.parse(readFileSync5(file, "utf-8"));
4845
+ data = JSON.parse(readFileSync6(file, "utf-8"));
4471
4846
  } catch {
4472
4847
  continue;
4473
4848
  }
@@ -4531,10 +4906,10 @@ async function ingestPi(db, verbose = false) {
4531
4906
 
4532
4907
  // src/ingest/hermes.ts
4533
4908
  init_database();
4534
- import { existsSync as existsSync7, statSync as statSync6 } from "fs";
4535
- import { homedir as homedir7 } from "os";
4536
- import { join as join7 } from "path";
4537
- 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");
4538
4913
  function mapCostBasis(billingMode) {
4539
4914
  if (billingMode === "subscription")
4540
4915
  return "subscription_included";
@@ -4543,7 +4918,7 @@ function mapCostBasis(billingMode) {
4543
4918
  return defaultCostBasisForAgent("hermes");
4544
4919
  }
4545
4920
  async function ingestHermes(db, verbose = false) {
4546
- if (!existsSync7(HERMES_DB)) {
4921
+ if (!existsSync8(HERMES_DB)) {
4547
4922
  return { sessions: 0, requests: 0 };
4548
4923
  }
4549
4924
  const { Database: Sqlite } = await import("bun:sqlite");
@@ -4623,19 +4998,78 @@ function statSyncSafe(path) {
4623
4998
  // src/ingest/loops.ts
4624
4999
  init_database();
4625
5000
  init_pricing();
4626
- import { existsSync as existsSync8 } from "fs";
4627
- import { homedir as homedir8 } from "os";
4628
- 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";
4629
5004
  import { Database as BunDatabase2 } from "bun:sqlite";
4630
- import { dataDir as dataDir2 } from "@hasna/paths";
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
+ }
4631
5065
  var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
4632
5066
  function defaultLoopsDbPath() {
4633
5067
  const resolved = dataDir2({
4634
5068
  app: "loops",
4635
5069
  home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
4636
5070
  });
4637
- const legacy = join8(process.env["HOME"] || process.env["USERPROFILE"] || homedir8(), ".hasna", "loops", "loops.db");
4638
- return existsSync8(join8(resolved, "loops.db")) ? join8(resolved, "loops.db") : legacy;
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;
4639
5073
  }
4640
5074
  function loopsDbPath() {
4641
5075
  return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
@@ -4727,7 +5161,7 @@ function readLoopGoalRuns(loopsDb) {
4727
5161
  }
4728
5162
  async function ingestLoops(db, verbose = false) {
4729
5163
  const dbPath = loopsDbPath();
4730
- if (!existsSync8(dbPath)) {
5164
+ if (!existsSync9(dbPath)) {
4731
5165
  if (verbose)
4732
5166
  console.log("OpenLoops DB not found:", dbPath);
4733
5167
  return { sessions: 0, requests: 0 };
@@ -4817,30 +5251,30 @@ async function ingestLoops(db, verbose = false) {
4817
5251
 
4818
5252
  // src/ingest/claude-quota.ts
4819
5253
  init_database();
4820
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
5254
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4821
5255
 
4822
5256
  // src/lib/paths.ts
4823
- import { homedir as homedir9 } from "os";
4824
- import { join as join9 } from "path";
5257
+ import { homedir as homedir10 } from "os";
5258
+ import { join as join10 } from "path";
4825
5259
  function getHomeDir2() {
4826
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
5260
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
4827
5261
  }
4828
5262
  function agentPaths() {
4829
5263
  const home = getHomeDir2();
4830
5264
  return {
4831
- claudeProjects: join9(home, ".claude", "projects"),
4832
- claudeCredentials: join9(home, ".claude", ".credentials.json"),
4833
- takumiProjects: join9(home, ".takumi", "projects"),
4834
- codexDir: join9(home, ".codex"),
4835
- codexDb: join9(home, ".codex", "state_5.sqlite"),
4836
- codexAuth: join9(home, ".codex", "auth.json"),
4837
- codexConfig: join9(home, ".codex", "config.toml"),
4838
- geminiTmp: join9(home, ".gemini", "tmp"),
4839
- geminiHistory: join9(home, ".gemini", "history"),
4840
- opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
4841
- piSessions: join9(home, ".pi", "agent", "sessions"),
4842
- hermesDir: join9(home, ".hermes"),
4843
- 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")
4844
5278
  };
4845
5279
  }
4846
5280
 
@@ -4852,10 +5286,10 @@ function readClaudeToken() {
4852
5286
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
4853
5287
  if (fromEnv)
4854
5288
  return { token: fromEnv };
4855
- if (!existsSync9(CREDENTIALS_PATH))
5289
+ if (!existsSync10(CREDENTIALS_PATH))
4856
5290
  return null;
4857
5291
  try {
4858
- const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
5292
+ const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
4859
5293
  const oauth = creds.claudeAiOauth;
4860
5294
  if (!oauth?.accessToken)
4861
5295
  return null;
@@ -4991,17 +5425,17 @@ async function ingestClaudeQuota(db, verbose = false) {
4991
5425
 
4992
5426
  // src/ingest/codex-quota.ts
4993
5427
  init_database();
4994
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
5428
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4995
5429
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
4996
5430
  function readCodexAuth() {
4997
5431
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
4998
5432
  if (fromEnv)
4999
5433
  return { token: fromEnv, authMode: "chatgpt" };
5000
5434
  const authPath = agentPaths().codexAuth;
5001
- if (!existsSync10(authPath))
5435
+ if (!existsSync11(authPath))
5002
5436
  return null;
5003
5437
  try {
5004
- const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
5438
+ const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
5005
5439
  const token = auth.tokens?.access_token;
5006
5440
  if (!token)
5007
5441
  return null;
@@ -5986,7 +6420,7 @@ init_database();
5986
6420
 
5987
6421
  // src/lib/serve-auth.ts
5988
6422
  function getServeApiToken() {
5989
- return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
6423
+ return process.env["HASNA_ECONOMY_API_TOKEN"];
5990
6424
  }
5991
6425
  function getServeBindHost() {
5992
6426
  const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
@@ -6010,13 +6444,11 @@ function isAuthorizedRequest(req, path) {
6010
6444
 
6011
6445
  // src/server/serve.ts
6012
6446
  import { randomUUID } from "crypto";
6013
- import { existsSync as existsSync13 } from "fs";
6014
- import { resolve as resolve2, sep } from "path";
6015
6447
  // package.json
6016
6448
  var package_default = {
6017
6449
  name: "@hasna/economy",
6018
- version: "0.3.28",
6019
- 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",
6020
6452
  repository: {
6021
6453
  type: "git",
6022
6454
  url: "git+https://github.com/hasna/economy.git"
@@ -6047,7 +6479,6 @@ var package_default = {
6047
6479
  files: [
6048
6480
  "dist",
6049
6481
  "docs",
6050
- "dashboard/dist",
6051
6482
  "postinstall.js",
6052
6483
  "LICENSE",
6053
6484
  "README.md",
@@ -6057,12 +6488,11 @@ var package_default = {
6057
6488
  "CODE_OF_CONDUCT.md"
6058
6489
  ],
6059
6490
  scripts: {
6060
- 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",
6061
6492
  "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
6062
6493
  "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
6063
6494
  "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
6064
6495
  "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
6065
- "build:dashboard": "cd dashboard && bun run build",
6066
6496
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
6067
6497
  prepack: "bun run build && bun run artifact-scan",
6068
6498
  typecheck: "tsc --noEmit",
@@ -6092,12 +6522,8 @@ var package_default = {
6092
6522
  access: "public"
6093
6523
  },
6094
6524
  dependencies: {
6095
- "@hasna/accounts": "0.2.23",
6096
- "@hasna/agent-registry": "^0.1.0",
6097
- "@hasna/contracts": "0.13.4",
6525
+ "@hasna/contracts": "1.0.2",
6098
6526
  "@hasna/events": "^0.1.16",
6099
- "@hasna/mcp-harness": "^0.1.0",
6100
- "@hasna/paths": "0.1.0",
6101
6527
  "@hasna/projects": "1.0.0",
6102
6528
  "@modelcontextprotocol/sdk": "^1.12.1",
6103
6529
  chalk: "^5.4.1",
@@ -6106,7 +6532,7 @@ var package_default = {
6106
6532
  zod: "^3.24.2"
6107
6533
  },
6108
6534
  devDependencies: {
6109
- "@types/bun": "latest",
6535
+ "@types/bun": "1.3.14",
6110
6536
  "@types/pg": "^8.20.0",
6111
6537
  "bun-types": "latest",
6112
6538
  typescript: "^5.7.2"
@@ -7784,7 +8210,6 @@ var APP = "economy";
7784
8210
  var ECONOMY_WRITE_SCOPE = `${APP}:write`;
7785
8211
  var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
7786
8212
  var SYNC_SOURCES = ["all", ...AGENTS, "loops"];
7787
- var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
7788
8213
  function json(data, status = 200) {
7789
8214
  return new Response(JSON.stringify(data), {
7790
8215
  status,
@@ -7837,38 +8262,6 @@ function requiredScopesForRequest(method, path) {
7837
8262
  function stringArray(value) {
7838
8263
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
7839
8264
  }
7840
- function dashboardPath(root, pathname) {
7841
- let decoded;
7842
- try {
7843
- decoded = decodeURIComponent(pathname);
7844
- } catch {
7845
- return null;
7846
- }
7847
- const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
7848
- const rootPath = resolve2(root);
7849
- const filePath = resolve2(rootPath, relativePath);
7850
- return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
7851
- }
7852
- var FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
7853
- function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
7854
- return async function fetch2(req) {
7855
- const url = new URL(req.url);
7856
- if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
7857
- return apiHandler(req);
7858
- }
7859
- if (existsSync13(dashboardDir)) {
7860
- const filePath = dashboardPath(dashboardDir, url.pathname);
7861
- if (filePath && existsSync13(filePath)) {
7862
- return new Response(Bun.file(filePath));
7863
- }
7864
- const indexPath = dashboardPath(dashboardDir, "/");
7865
- if (indexPath && existsSync13(indexPath)) {
7866
- return new Response(Bun.file(indexPath));
7867
- }
7868
- }
7869
- return apiHandler(req);
7870
- };
7871
- }
7872
8265
  function applyFields(obj, fields) {
7873
8266
  if (!fields || fields.length === 0)
7874
8267
  return obj;
@@ -8392,7 +8785,7 @@ function startServer(port = 3456, options = {}) {
8392
8785
  const server = Bun.serve({
8393
8786
  port,
8394
8787
  hostname: hostname2,
8395
- fetch: createServerFetch(apiHandler, options.dashboardDir)
8788
+ fetch: apiHandler
8396
8789
  });
8397
8790
  const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
8398
8791
  log(`economy-serve listening on ${address}`);