@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
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAw1BjC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CA81BjC"}
@@ -595,7 +595,59 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
595
595
  import { hostname, platform } from "os";
596
596
  import { homedir } from "os";
597
597
  import { join, resolve } from "path";
598
- import { dataDir } from "@hasna/paths";
598
+ import { homedir as pathsResolverHomedir } from "os";
599
+ import { join as pathsResolverJoin } from "path";
600
+ function pathsResolverAssertApp(app) {
601
+ if (typeof app !== "string" || app.length === 0) {
602
+ throw new TypeError("paths: app must be a non-empty string");
603
+ }
604
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
605
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
606
+ }
607
+ }
608
+ function pathsResolverAssertKind(kind) {
609
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
610
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
611
+ }
612
+ }
613
+ function pathsResolverBaseDir(kind, options) {
614
+ pathsResolverAssertKind(kind);
615
+ const env = options.env ?? process.env;
616
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
617
+ if (typeof override === "string" && override.length > 0)
618
+ return override;
619
+ const home = options.home ?? pathsResolverHomedir();
620
+ const platform2 = options.platform ?? process.platform;
621
+ if (platform2 === "darwin") {
622
+ switch (kind) {
623
+ case "config":
624
+ case "data":
625
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
626
+ case "cache":
627
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
628
+ case "state":
629
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
630
+ }
631
+ }
632
+ switch (kind) {
633
+ case "config":
634
+ return pathsResolverJoin(home, ".config", "hasna");
635
+ case "data":
636
+ return pathsResolverJoin(home, ".local", "share", "hasna");
637
+ case "state":
638
+ return pathsResolverJoin(home, ".local", "state", "hasna");
639
+ case "cache":
640
+ return pathsResolverJoin(home, ".cache", "hasna");
641
+ }
642
+ }
643
+ function pathsResolverResolve(kind, options) {
644
+ pathsResolverAssertApp(options.app);
645
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
646
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
647
+ }
648
+ function dataDir(options) {
649
+ return pathsResolverResolve("data", options);
650
+ }
599
651
  function normalizeMachineId(value) {
600
652
  const id = value?.trim().toLowerCase().split(".")[0];
601
653
  return id && id.length > 0 ? id : null;
@@ -618,7 +670,7 @@ function macHostMachineId() {
618
670
  return null;
619
671
  }
620
672
  function getMachineId() {
621
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
673
+ const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
622
674
  if (envMachine)
623
675
  return envMachine;
624
676
  const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
@@ -1067,8 +1119,16 @@ function seedModelPricing(db, defaults) {
1067
1119
  });
1068
1120
  }
1069
1121
  }
1122
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
1070
1123
  var init_database = __esm(() => {
1071
1124
  init_sqlite_adapter();
1125
+ PATHS_RESOLVER_KIND_ENV = {
1126
+ config: "HASNA_CONFIG_HOME",
1127
+ data: "HASNA_DATA_HOME",
1128
+ state: "HASNA_STATE_HOME",
1129
+ cache: "HASNA_CACHE_HOME"
1130
+ };
1131
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1072
1132
  });
1073
1133
 
1074
1134
  // src/otel/index.ts
@@ -1413,11 +1473,334 @@ async function ingestOtelRows(db, rows) {
1413
1473
  rollupSession(db, sessionId);
1414
1474
  return { requests, sessions: sessions.size };
1415
1475
  }
1476
+
1477
+ // src/lib/cloud-ingest.ts
1478
+ init_database();
1479
+ init_pricing();
1480
+
1481
+ // src/ingest/claude.ts
1482
+ init_database();
1483
+ init_pricing();
1484
+ import { homedir as homedir3 } from "os";
1485
+ import { join as join3, basename } from "path";
1486
+
1487
+ // src/lib/accounts-store.ts
1488
+ import { homedir as homedir2 } from "os";
1489
+ import { join as join2 } from "path";
1490
+ import { z } from "zod";
1491
+ import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
1492
+ 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");
1493
+ var profileSchema = z.object({
1494
+ name: z.string(),
1495
+ tool: z.string(),
1496
+ email: z.string().optional(),
1497
+ displayName: z.string().optional(),
1498
+ dir: z.string(),
1499
+ description: z.string().optional(),
1500
+ createdAt: z.string(),
1501
+ lastUsedAt: z.string().optional()
1502
+ });
1503
+ var toolDefSchema = z.object({
1504
+ id: z.string(),
1505
+ label: z.string(),
1506
+ envVar: z.string(),
1507
+ defaultDir: z.string(),
1508
+ bin: z.string()
1509
+ });
1510
+ var storeSchema = z.object({
1511
+ version: z.literal(1),
1512
+ current: z.record(z.string(), z.string()).default({}),
1513
+ applied: z.record(z.string(), z.string()).default({}),
1514
+ toolLocks: z.record(z.string(), z.string()).default({}),
1515
+ profiles: z.array(profileSchema).default([]),
1516
+ tools: z.array(toolDefSchema).default([])
1517
+ });
1518
+ var BUILTIN_TOOLS = [
1519
+ { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
1520
+ { id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
1521
+ { id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
1522
+ { id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
1523
+ { id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
1524
+ { id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
1525
+ { id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
1526
+ { id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
1527
+ { id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
1528
+ { id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
1529
+ { id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
1530
+ { id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
1531
+ ];
1532
+
1533
+ // src/ingest/claude.ts
1534
+ var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
1535
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
1536
+
1537
+ // src/ingest/codex.ts
1538
+ import { homedir as homedir4 } from "os";
1539
+ import { join as join4, basename as basename2 } from "path";
1540
+ init_database();
1541
+ init_pricing();
1542
+ var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
1543
+ var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
1544
+ var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
1545
+ var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
1546
+
1547
+ // src/ingest/gemini.ts
1548
+ init_database();
1549
+ init_pricing();
1550
+ import { homedir as homedir5 } from "os";
1551
+ import { join as join5, basename as basename3 } from "path";
1552
+ var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
1553
+ var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
1554
+
1555
+ // src/ingest/opencode.ts
1556
+ init_database();
1557
+ init_pricing();
1558
+ import { homedir as homedir6 } from "os";
1559
+ import { join as join6 } from "path";
1560
+ var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
1561
+
1562
+ // src/ingest/cursor.ts
1563
+ init_database();
1564
+
1565
+ // src/ingest/pi.ts
1566
+ init_database();
1567
+ import { homedir as homedir7 } from "os";
1568
+ import { join as join7 } from "path";
1569
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
1570
+
1571
+ // src/ingest/hermes.ts
1572
+ init_database();
1573
+ import { homedir as homedir8 } from "os";
1574
+ import { join as join8 } from "path";
1575
+ var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
1576
+
1577
+ // src/ingest/loops.ts
1578
+ init_database();
1579
+ init_pricing();
1580
+
1581
+ // src/ingest/claude-quota.ts
1582
+ init_database();
1583
+
1584
+ // src/lib/paths.ts
1585
+ import { homedir as homedir9 } from "os";
1586
+ import { join as join9 } from "path";
1587
+ function getHomeDir2() {
1588
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
1589
+ }
1590
+ function agentPaths() {
1591
+ const home = getHomeDir2();
1592
+ return {
1593
+ claudeProjects: join9(home, ".claude", "projects"),
1594
+ claudeCredentials: join9(home, ".claude", ".credentials.json"),
1595
+ takumiProjects: join9(home, ".takumi", "projects"),
1596
+ codexDir: join9(home, ".codex"),
1597
+ codexDb: join9(home, ".codex", "state_5.sqlite"),
1598
+ codexAuth: join9(home, ".codex", "auth.json"),
1599
+ codexConfig: join9(home, ".codex", "config.toml"),
1600
+ geminiTmp: join9(home, ".gemini", "tmp"),
1601
+ geminiHistory: join9(home, ".gemini", "history"),
1602
+ opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
1603
+ piSessions: join9(home, ".pi", "agent", "sessions"),
1604
+ hermesDir: join9(home, ".hermes"),
1605
+ hermesDb: join9(home, ".hermes", "state.db")
1606
+ };
1607
+ }
1608
+
1609
+ // src/ingest/claude-quota.ts
1610
+ var CREDENTIALS_PATH = agentPaths().claudeCredentials;
1611
+
1612
+ // src/ingest/codex-quota.ts
1613
+ init_database();
1614
+ var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
1615
+
1616
+ // src/lib/sync-all.ts
1617
+ init_database();
1618
+
1619
+ // src/ingest/billing.ts
1620
+ init_database();
1621
+
1622
+ // src/lib/sync-maintenance.ts
1623
+ init_database();
1624
+
1625
+ // src/lib/cloud-ingest.ts
1626
+ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
1627
+ function exportIngestRows(db) {
1628
+ const body = {};
1629
+ for (const table of CLOUD_INGEST_TABLES) {
1630
+ const rows = db.prepare(`SELECT * FROM ${table}`).all();
1631
+ if (rows.length > 0)
1632
+ body[table] = rows;
1633
+ }
1634
+ return body;
1635
+ }
1636
+ function unwrapEnvelope(raw) {
1637
+ if (raw && typeof raw === "object" && "data" in raw) {
1638
+ const data = raw.data;
1639
+ return data && typeof data === "object" ? data : {};
1640
+ }
1641
+ return raw ?? {};
1642
+ }
1643
+ async function postIngest(cloud, body) {
1644
+ const raw = await cloud.client.transport.post("/ingest", body);
1645
+ const payload = unwrapEnvelope(raw);
1646
+ return {
1647
+ ingested: payload["ingested"] ?? {},
1648
+ total: Number(payload["total"] ?? 0)
1649
+ };
1650
+ }
1651
+ async function pushIngestRows(cloud, scratch) {
1652
+ const body = exportIngestRows(scratch);
1653
+ if (Object.keys(body).length === 0)
1654
+ return { posted: false, ingested: {}, total: 0 };
1655
+ const response = await postIngest(cloud, body);
1656
+ return { posted: true, ingested: response.ingested, total: response.total };
1657
+ }
1658
+
1659
+ // src/lib/cloud-storage.ts
1660
+ import {
1661
+ appConfigDiskValue,
1662
+ ClientTransportConfigurationError,
1663
+ clientTransportEnvKeys,
1664
+ createClientTransport,
1665
+ credentialDiskSources,
1666
+ credentialOverrideEnvKey,
1667
+ credentialPointerEnvKey,
1668
+ CREDENTIAL_PROFILE_ENV_KEY,
1669
+ keychainConfigValue
1670
+ } from "@hasna/contracts/client";
1671
+ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
1672
+ var ECONOMY_APP = "economy";
1673
+ var RETIRED_STORAGE_MODE_KEYS = [
1674
+ "HASNA_ECONOMY_STORAGE_MODE",
1675
+ "HASNA_ECONOMY_MODE",
1676
+ "ECONOMY_STORAGE_MODE",
1677
+ "ECONOMY_MODE"
1678
+ ];
1679
+ function assertNoRetiredStorageMode(env) {
1680
+ const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
1681
+ if (!legacyKey)
1682
+ return;
1683
+ throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
1684
+ }
1685
+ var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
1686
+ function envFlagSet(env, key) {
1687
+ const raw = env[key];
1688
+ if (raw === undefined)
1689
+ return false;
1690
+ const value = raw.trim().toLowerCase();
1691
+ return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
1692
+ }
1693
+ function localStorageExplicitlyOptedIn(env) {
1694
+ return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
1695
+ }
1696
+ function economyAuthorityEnvKeys() {
1697
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
1698
+ return [
1699
+ ...keys.apiUrlKeys,
1700
+ ...keys.apiKeyKeys,
1701
+ credentialOverrideEnvKey(ECONOMY_APP),
1702
+ credentialPointerEnvKey(ECONOMY_APP),
1703
+ CREDENTIAL_PROFILE_ENV_KEY
1704
+ ];
1705
+ }
1706
+ function hasEconomyEnvAuthorityIntent(env) {
1707
+ return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
1708
+ }
1709
+ function hostedAuthorityConfigured(env, options = {}) {
1710
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
1711
+ if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
1712
+ return true;
1713
+ if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
1714
+ return true;
1715
+ return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
1716
+ }
1717
+ function economyResolverEnv(env) {
1718
+ const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
1719
+ if (blanks.length === 0)
1720
+ return env;
1721
+ const next = { ...env };
1722
+ for (const key of blanks)
1723
+ delete next[key];
1724
+ return next;
1725
+ }
1726
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
1727
+ function isAmbientEconomyEnv(env) {
1728
+ if (typeof process !== "undefined" && env === process.env)
1729
+ return true;
1730
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
1731
+ }
1732
+ function economyResolverInputs(env, credentials = {}) {
1733
+ const normalised = economyResolverEnv(env);
1734
+ if (normalised === env)
1735
+ return { env: normalised, credentials };
1736
+ const keychain = { ...credentials.keychain };
1737
+ if (keychain.enabled === undefined && keychain.run === undefined) {
1738
+ keychain.enabled = isAmbientEconomyEnv(env);
1739
+ }
1740
+ return { env: normalised, credentials: { ...credentials, keychain } };
1741
+ }
1742
+ function resolveEconomyStorageClient(name, env, overrides = {}) {
1743
+ const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
1744
+ const wired = createClientTransport(name, resolverEnv, {
1745
+ credentials,
1746
+ ...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
1747
+ ...overrides.headers ? { headers: overrides.headers } : {},
1748
+ ...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
1749
+ ...overrides.retry !== undefined ? { retry: overrides.retry } : {},
1750
+ ...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
1751
+ });
1752
+ return {
1753
+ transport: "http",
1754
+ client: createHasnaStorageClient(name, wired.client),
1755
+ resolution: wired.resolution
1756
+ };
1757
+ }
1758
+ function localEconomyNotice() {
1759
+ return `economy: local mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
1760
+ }
1761
+ function credentialRequiredError(env, cause) {
1762
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
1763
+ const clientEnv = env;
1764
+ const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
1765
+ const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
1766
+ return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
1767
+ }
1768
+ var cache;
1769
+ var localNoticePrinted = false;
1770
+ function resolveEconomyCloudStorage(env = process.env, overrides) {
1771
+ assertNoRetiredStorageMode(env);
1772
+ const clientEnv = env;
1773
+ if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
1774
+ return { active: false, client: null };
1775
+ }
1776
+ let resolved;
1777
+ try {
1778
+ resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
1779
+ } catch (error) {
1780
+ if (error instanceof ClientTransportConfigurationError) {
1781
+ throw credentialRequiredError(env, error);
1782
+ }
1783
+ throw error;
1784
+ }
1785
+ return { active: true, client: resolved.client };
1786
+ }
1787
+ function economyCloudStorage(env = process.env) {
1788
+ if (cache === undefined) {
1789
+ cache = resolveEconomyCloudStorage(env);
1790
+ if (!cache.active && !localNoticePrinted) {
1791
+ localNoticePrinted = true;
1792
+ if (typeof process !== "undefined")
1793
+ process.stderr.write(`${localEconomyNotice()}
1794
+ `);
1795
+ }
1796
+ }
1797
+ return cache;
1798
+ }
1416
1799
  // package.json
1417
1800
  var package_default = {
1418
1801
  name: "@hasna/economy",
1419
- version: "0.3.28",
1420
- 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",
1802
+ version: "0.4.0",
1803
+ 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",
1421
1804
  repository: {
1422
1805
  type: "git",
1423
1806
  url: "git+https://github.com/hasna/economy.git"
@@ -1448,7 +1831,6 @@ var package_default = {
1448
1831
  files: [
1449
1832
  "dist",
1450
1833
  "docs",
1451
- "dashboard/dist",
1452
1834
  "postinstall.js",
1453
1835
  "LICENSE",
1454
1836
  "README.md",
@@ -1458,12 +1840,11 @@ var package_default = {
1458
1840
  "CODE_OF_CONDUCT.md"
1459
1841
  ],
1460
1842
  scripts: {
1461
- 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",
1843
+ 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",
1462
1844
  "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
1463
1845
  "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
1464
1846
  "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
1465
1847
  "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
1466
- "build:dashboard": "cd dashboard && bun run build",
1467
1848
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
1468
1849
  prepack: "bun run build && bun run artifact-scan",
1469
1850
  typecheck: "tsc --noEmit",
@@ -1493,12 +1874,8 @@ var package_default = {
1493
1874
  access: "public"
1494
1875
  },
1495
1876
  dependencies: {
1496
- "@hasna/accounts": "0.2.23",
1497
- "@hasna/agent-registry": "^0.1.0",
1498
- "@hasna/contracts": "0.13.4",
1877
+ "@hasna/contracts": "1.0.2",
1499
1878
  "@hasna/events": "^0.1.16",
1500
- "@hasna/mcp-harness": "^0.1.0",
1501
- "@hasna/paths": "0.1.0",
1502
1879
  "@hasna/projects": "1.0.0",
1503
1880
  "@modelcontextprotocol/sdk": "^1.12.1",
1504
1881
  chalk: "^5.4.1",
@@ -1507,7 +1884,7 @@ var package_default = {
1507
1884
  zod: "^3.24.2"
1508
1885
  },
1509
1886
  devDependencies: {
1510
- "@types/bun": "latest",
1887
+ "@types/bun": "1.3.14",
1511
1888
  "@types/pg": "^8.20.0",
1512
1889
  "bun-types": "latest",
1513
1890
  typescript: "^5.7.2"
@@ -1547,7 +1924,7 @@ if (args.includes("--version") || args.includes("-V")) {
1547
1924
  if (args.includes("--help") || args.includes("-h")) {
1548
1925
  console.log(`Usage: economy-otel [options]
1549
1926
 
1550
- OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into economy.db
1927
+ OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into Economy
1551
1928
 
1552
1929
  Options:
1553
1930
  -p, --port <port> Port to bind (default: ECONOMY_OTEL_PORT or 4318)
@@ -1558,6 +1935,13 @@ Environment:
1558
1935
  ECONOMY_OTEL_PORT Override the default port (4318)
1559
1936
  ECONOMY_OTEL_BIND Bind address (default: 127.0.0.1)
1560
1937
 
1938
+ Storage (decided at startup, before the listener binds):
1939
+ A credential resolved by @hasna/contracts (Keychain item
1940
+ hasna.credentials.economy.api-key, ~/.hasna/economy/config/credentials,
1941
+ HASNA_ECONOMY_API_KEY) forwards every accepted payload to the hosted
1942
+ API's /v1/ingest. HASNA_ECONOMY_LOCAL=1 writes the on-box SQLite store
1943
+ instead. With neither the sidecar fails closed (exit 1, no listener).
1944
+
1561
1945
  Endpoints:
1562
1946
  POST /v1/metrics OTLP JSON metrics
1563
1947
  POST /ingest Simplified single-event JSON
@@ -1565,7 +1949,28 @@ Endpoints:
1565
1949
  process.exit(0);
1566
1950
  }
1567
1951
  var port = resolvePort(args);
1568
- var db = openDatabase();
1952
+ function resolveStorage() {
1953
+ try {
1954
+ return economyCloudStorage();
1955
+ } catch (error) {
1956
+ console.error(`economy-otel: ${error instanceof Error ? error.message : String(error)}`);
1957
+ process.exit(1);
1958
+ }
1959
+ }
1960
+ var storage = resolveStorage();
1961
+ var localDb = storage.active ? null : openDatabase();
1962
+ async function forwardRows(cloud, rows) {
1963
+ const scratch = openDatabase(":memory:", true);
1964
+ try {
1965
+ const result = await ingestOtelRows(scratch, rows);
1966
+ const pushed = await pushIngestRows(cloud, scratch);
1967
+ return { ...result, forwarded: pushed.total };
1968
+ } finally {
1969
+ try {
1970
+ scratch.close();
1971
+ } catch {}
1972
+ }
1973
+ }
1569
1974
  var server = Bun.serve({
1570
1975
  port,
1571
1976
  hostname: process.env["ECONOMY_OTEL_BIND"] ?? "127.0.0.1",
@@ -1590,7 +1995,16 @@ var server = Bun.serve({
1590
1995
  if (rows.length === 0) {
1591
1996
  return Response.json({ ingested: 0, message: "no matching metrics" });
1592
1997
  }
1593
- const result = await ingestOtelRows(db, rows);
1998
+ if (storage.active) {
1999
+ try {
2000
+ const result2 = await forwardRows(storage, rows);
2001
+ return Response.json({ ingested: result2.requests, sessions: result2.sessions, forwarded: result2.forwarded });
2002
+ } catch (error) {
2003
+ const reason = error instanceof Error ? error.message : String(error);
2004
+ return Response.json({ error: `forward to the economy API failed: ${reason}` }, { status: 502 });
2005
+ }
2006
+ }
2007
+ const result = await ingestOtelRows(localDb, rows);
1594
2008
  return Response.json({ ingested: result.requests, sessions: result.sessions });
1595
2009
  }
1596
2010
  });