@hasna/economy 0.3.8 → 0.3.10

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  All notable changes to this repository are tracked here. This project follows semantic versioning for published npm packages when practical.
4
4
 
5
+ ## @hasna/economy 0.3.10 - 2026-08-15
6
+
7
+ Two PRs merged after `0.3.9`; neither is published yet.
8
+
9
+ - **#32** `fix(economy)` — `breakdown --since` now actually filters the billing
10
+ window it reports, and `doctor` stops certifying drift between two zeroes: when
11
+ no provider billing was imported, or the imported total is zero, drift is
12
+ reported as UNKNOWN instead of a false `0.0%` green tick.
13
+ - **#33** `feat(contracts)` — satisfy the contracts `published_artifact_gate`:
14
+ `prepack` builds into `dist` and runs `contracts artifact-scan dist`, so a
15
+ packed tarball is scanned before it reaches the registry.
16
+
17
+ ## @hasna/economy 0.3.9 - 2026-08-04
18
+
19
+ Release-only bump. Four PRs merged after `0.3.8` and were never published, because
20
+ this repo has no workflow that runs `npm publish` — the registry sat at `0.3.8`
21
+ while `main` accumulated a contracts migration, a data-correctness fix and a
22
+ metadata correction.
23
+
24
+ **Breaking for server operators.** `HASNA_ECONOMY_STORAGE_MODE` (and the
25
+ `ECONOMY_STORAGE_MODE` alias) are retired for server backend selection. They are
26
+ now *rejected at startup with a migration hint* rather than normalized, so a
27
+ half-migrated deployment fails loudly instead of quietly serving the wrong store.
28
+ The server data backend is `sqlite | postgresql`, selected by the presence of
29
+ `HASNA_ECONOMY_DATABASE_URL` alone. If you set `HASNA_ECONOMY_STORAGE_MODE=cloud`
30
+ to reach Postgres, unset it and rely on the database URL.
31
+
32
+ - **#27** `fix(client)` — hard-fail a half-applied cloud flip instead of silently
33
+ serving local data. An `API_URL` set *without* an `API_KEY` previously resolved
34
+ to `local` with no warning, byte-identical to an unconfigured host: the CLI
35
+ served the local SQLite store while the operator had pointed it at the cloud
36
+ API — a different dataset rendered as plausible spend numbers. It now reports
37
+ `misconfigured` and refuses. An unconfigured machine (neither variable set)
38
+ stays silently local; that negative control is covered by a test.
39
+ - **#28** `fix(contracts)` — migrate `storage.mode` to `storage.backend` for
40
+ contract kit `0.9.0`; declare `hosting` and `serviceSurfaces` in
41
+ `hasna.contract.json`. Bumps `@hasna/contracts` `^0.4.2` -> `^0.9.0`.
42
+ - **#29** `fix(server)` — make the runtime speak the `0.9.0` backend vocabulary it
43
+ declares; `isCloudMode()` is replaced by `resolveEconomyServerBackend()` and
44
+ `isPostgresBackend()` (both internal — neither was ever exported from the
45
+ package root).
46
+ - **#30** `fix(package)` — describe Gemini CLI support as legacy rather than
47
+ active, and name the Gemini API billing ingest separately. Google retired the
48
+ Gemini CLI on 2026-06-18.
49
+
5
50
  ## @hasna/economy 0.3.7 - 2026-07-24
6
51
 
7
52
  Reconciliation release: `main` had diverged from the published npm line. The `0.3.x`
@@ -1 +1 @@
1
- {"version":3,"file":"extras.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/extras.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAgDnC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyS/D;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyC5D"}
1
+ {"version":3,"file":"extras.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/extras.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAiDnC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2S/D;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyC5D"}
package/dist/cli/index.js CHANGED
@@ -649,6 +649,7 @@ __export(exports_database, {
649
649
  deleteGoal: () => deleteGoal,
650
650
  deleteBudget: () => deleteBudget,
651
651
  dedupeRequests: () => dedupeRequests,
652
+ countBillingRecords: () => countBillingRecords,
652
653
  clearBillingRange: () => clearBillingRange,
653
654
  bulkIngest: () => bulkIngest
654
655
  });
@@ -2001,9 +2002,11 @@ function upsertBillingDaily(db, row) {
2001
2002
  function clearBillingRange(db, provider, fromDate, toDate) {
2002
2003
  db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
2003
2004
  }
2005
+ function billingPeriodWhere(period) {
2006
+ return period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
2007
+ }
2004
2008
  function queryBillingSummary(db, period) {
2005
- const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
2006
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
2009
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
2007
2010
  const by_provider = {};
2008
2011
  let total = 0;
2009
2012
  for (const r of rows) {
@@ -2012,6 +2015,10 @@ function queryBillingSummary(db, period) {
2012
2015
  }
2013
2016
  return { total_usd: total, by_provider };
2014
2017
  }
2018
+ function countBillingRecords(db, period) {
2019
+ const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
2020
+ return row?.c ?? 0;
2021
+ }
2015
2022
  function getSessionDetail(db, id) {
2016
2023
  const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
2017
2024
  if (!session)
@@ -2601,11 +2608,20 @@ function defaultCostBasisForAgent(agent) {
2601
2608
  }
2602
2609
 
2603
2610
  // src/lib/billing-diff.ts
2611
+ function billingDeltaPct(estimatedUsd, actualUsd) {
2612
+ if (!(actualUsd > 0))
2613
+ return null;
2614
+ return (estimatedUsd - actualUsd) / actualUsd * 100;
2615
+ }
2604
2616
  function queryBillingDiff(db, period, thresholdPct = 15) {
2605
2617
  const estimated = querySummary(db, period, undefined, true);
2606
2618
  const actual = queryBillingSummary(db, period);
2619
+ const billingRecords = countBillingRecords(db, period);
2607
2620
  const delta = estimated.total_usd - actual.total_usd;
2608
- const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
2621
+ const measuredPct = billingDeltaPct(estimated.total_usd, actual.total_usd);
2622
+ const incomparableReason = measuredPct !== null ? null : billingRecords === 0 ? "no_billing_records" : "zero_actual_billing";
2623
+ const comparable = incomparableReason === null;
2624
+ const deltaPct = measuredPct ?? 0;
2609
2625
  const agentRows = db.prepare(`
2610
2626
  SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
2611
2627
  FROM requests
@@ -2632,11 +2648,24 @@ function queryBillingDiff(db, period, thresholdPct = 15) {
2632
2648
  delta_usd: delta,
2633
2649
  delta_pct: deltaPct,
2634
2650
  threshold_pct: thresholdPct,
2635
- is_alert: Math.abs(deltaPct) > thresholdPct,
2651
+ is_alert: comparable && Math.abs(deltaPct) > thresholdPct,
2652
+ comparable,
2653
+ incomparable_reason: incomparableReason,
2636
2654
  by_agent,
2637
2655
  by_provider: actual.by_provider
2638
2656
  };
2639
2657
  }
2658
+ function billingDriftCheck(diff) {
2659
+ if (diff.comparable) {
2660
+ return {
2661
+ ok: !diff.is_alert,
2662
+ msg: `billing drift ${diff.period}: ${Math.abs(diff.delta_pct).toFixed(1)}%`
2663
+ };
2664
+ }
2665
+ const estimated = `estimated $${diff.estimated_usd.toFixed(2)}`;
2666
+ const detail = diff.incomparable_reason === "no_billing_records" ? `no provider billing records imported for this period (${estimated}); run: economy billing sync` : `provider billing records exist but total $0.00 (${estimated})`;
2667
+ return { ok: false, msg: `billing drift ${diff.period}: UNKNOWN \u2014 ${detail}` };
2668
+ }
2640
2669
  function periodWhere3(period, column) {
2641
2670
  switch (period) {
2642
2671
  case "today":
@@ -3490,6 +3519,20 @@ function resolveClientTransport(name, env = process.env) {
3490
3519
  } else if (urlHit && keyHit) {
3491
3520
  mode = "cloud";
3492
3521
  modeSource = `${urlHit.key}+${keyHit.key}`;
3522
+ } else if (urlHit) {
3523
+ warnings.push(`${urlHit.key} is set but no API key is present (${keys.apiKeyKeys[0]}). ` + `Refusing to fall back to the local store, which would silently serve a different dataset. ` + `Set ${keys.apiKeyKeys[0]} to enable the cloud client, or set ${keys.modeKeys[0]}=local ` + `to use the local store deliberately.`);
3524
+ return {
3525
+ transport: "local",
3526
+ mode: "local",
3527
+ deprecatedAlias,
3528
+ modeSource: urlHit.key,
3529
+ baseUrl: null,
3530
+ apiUrlSource: null,
3531
+ apiKeyPresent: false,
3532
+ apiKeySource: null,
3533
+ misconfigured: true,
3534
+ warning: warnings.join(" ")
3535
+ };
3493
3536
  }
3494
3537
  if (mode === "local") {
3495
3538
  return {
@@ -6971,17 +7014,17 @@ var init_pg_migrate = () => {};
6971
7014
  var init_pg_migrations = () => {};
6972
7015
 
6973
7016
  // src/db/cloud.ts
7017
+ import { assertNoLegacyStorageMode } from "@hasna/contracts/server-backend";
6974
7018
  import pg2 from "pg";
6975
- function getCloudDatabaseUrl() {
6976
- return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
7019
+ function getCloudDatabaseUrl(env = process.env) {
7020
+ return env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || env["ECONOMY_DATABASE_URL"]?.trim() || env["DATABASE_URL"]?.trim() || undefined;
6977
7021
  }
6978
- function isCloudMode() {
6979
- const mode = process.env["HASNA_ECONOMY_STORAGE_MODE"]?.trim().toLowerCase();
6980
- if (mode === "cloud")
6981
- return true;
6982
- if (mode === "local")
6983
- return false;
6984
- return Boolean(getCloudDatabaseUrl());
7022
+ function resolveEconomyServerBackend(env = process.env) {
7023
+ assertNoLegacyStorageMode("economy", env);
7024
+ return getCloudDatabaseUrl(env) ? "postgresql" : "sqlite";
7025
+ }
7026
+ function isPostgresBackend(env = process.env) {
7027
+ return resolveEconomyServerBackend(env) === "postgresql";
6985
7028
  }
6986
7029
  function resolveSigningSecret() {
6987
7030
  return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
@@ -7030,7 +7073,7 @@ var init_openapi = __esm(() => {
7030
7073
  info: {
7031
7074
  title: "@hasna/economy self-hosted API",
7032
7075
  version: "0.0.0",
7033
- description: "AI coding cost tracker \u2014 self-hosted control plane. Cloud mode is PURE REMOTE per Amendment A1: the serve process reads/writes the shared RDS Postgres directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
7076
+ description: "AI coding cost tracker \u2014 self-hosted control plane. The server data backend is sqlite | postgresql, selected by the presence of a database URL; with postgresql the serve process reads AND writes PostgreSQL directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
7034
7077
  },
7035
7078
  servers: [
7036
7079
  {
@@ -7076,27 +7119,61 @@ var init_openapi = __esm(() => {
7076
7119
  "data"
7077
7120
  ]
7078
7121
  },
7079
- Foundation: {
7122
+ Health: {
7080
7123
  type: "object",
7081
7124
  properties: {
7082
7125
  status: {
7083
- type: "string"
7126
+ type: "string",
7127
+ enum: [
7128
+ "ok",
7129
+ "degraded",
7130
+ "unavailable"
7131
+ ]
7084
7132
  },
7085
7133
  version: {
7086
7134
  type: "string"
7087
7135
  },
7088
- mode: {
7089
- type: "string"
7090
- },
7091
- service: {
7092
- type: "string"
7136
+ backend: {
7137
+ type: "string",
7138
+ enum: [
7139
+ "sqlite",
7140
+ "postgresql"
7141
+ ]
7093
7142
  }
7094
7143
  },
7095
7144
  required: [
7096
7145
  "status",
7097
7146
  "version",
7098
- "mode"
7099
- ]
7147
+ "backend"
7148
+ ],
7149
+ additionalProperties: false
7150
+ },
7151
+ Ready: {
7152
+ type: "object",
7153
+ properties: {
7154
+ ready: {
7155
+ type: "boolean"
7156
+ },
7157
+ reason: {
7158
+ type: "string"
7159
+ }
7160
+ },
7161
+ required: [
7162
+ "ready"
7163
+ ],
7164
+ additionalProperties: false
7165
+ },
7166
+ Version: {
7167
+ type: "object",
7168
+ properties: {
7169
+ version: {
7170
+ type: "string"
7171
+ }
7172
+ },
7173
+ required: [
7174
+ "version"
7175
+ ],
7176
+ additionalProperties: false
7100
7177
  },
7101
7178
  Error: {
7102
7179
  type: "object",
@@ -7379,7 +7456,7 @@ var init_openapi = __esm(() => {
7379
7456
  content: {
7380
7457
  "application/json": {
7381
7458
  schema: {
7382
- $ref: "#/components/schemas/Foundation"
7459
+ $ref: "#/components/schemas/Health"
7383
7460
  }
7384
7461
  }
7385
7462
  }
@@ -7398,7 +7475,7 @@ var init_openapi = __esm(() => {
7398
7475
  content: {
7399
7476
  "application/json": {
7400
7477
  schema: {
7401
- $ref: "#/components/schemas/Foundation"
7478
+ $ref: "#/components/schemas/Ready"
7402
7479
  }
7403
7480
  }
7404
7481
  }
@@ -7408,7 +7485,7 @@ var init_openapi = __esm(() => {
7408
7485
  content: {
7409
7486
  "application/json": {
7410
7487
  schema: {
7411
- $ref: "#/components/schemas/Foundation"
7488
+ $ref: "#/components/schemas/Ready"
7412
7489
  }
7413
7490
  }
7414
7491
  }
@@ -7427,7 +7504,7 @@ var init_openapi = __esm(() => {
7427
7504
  content: {
7428
7505
  "application/json": {
7429
7506
  schema: {
7430
- $ref: "#/components/schemas/Foundation"
7507
+ $ref: "#/components/schemas/Version"
7431
7508
  }
7432
7509
  }
7433
7510
  }
@@ -8645,11 +8722,8 @@ function openApiDocument() {
8645
8722
  const spec = openApiSpec;
8646
8723
  return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
8647
8724
  }
8648
- function serveMode() {
8649
- return isCloudMode() ? "self_hosted" : "local";
8650
- }
8651
- function foundationEnvelope(status, extra = {}) {
8652
- return { status, version: packageMetadata.version, mode: serveMode(), service: "economy", ...extra };
8725
+ function healthEnvelope(status) {
8726
+ return { status, version: packageMetadata.version, backend: resolveEconomyServerBackend() };
8653
8727
  }
8654
8728
  function json(data, status = 200) {
8655
8729
  return new Response(JSON.stringify(data), {
@@ -8748,14 +8822,14 @@ function createHandler(db, options = {}) {
8748
8822
  if (method === "OPTIONS")
8749
8823
  return new Response(null, { status: 204, headers: CORS });
8750
8824
  if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
8751
- return json(foundationEnvelope("ok"));
8825
+ return json(healthEnvelope("ok"));
8752
8826
  }
8753
8827
  if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
8754
- return json(foundationEnvelope("ok"));
8828
+ return json({ version: packageMetadata.version });
8755
8829
  }
8756
8830
  if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
8757
8831
  const result = options.readyCheck ? await options.readyCheck() : { ready: true };
8758
- return json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), result.ready ? 200 : 503);
8832
+ return json(result.detail ? { ready: result.ready, reason: result.detail } : { ready: result.ready }, result.ready ? 200 : 503);
8759
8833
  }
8760
8834
  if (method === "GET" && rawPath === "/openapi.json") {
8761
8835
  return json(openApiDocument());
@@ -8894,14 +8968,14 @@ function createHandler(db, options = {}) {
8894
8968
  if (by === "project")
8895
8969
  return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
8896
8970
  if (by === "agent")
8897
- return ok(queryAgentBreakdown(db, period, machine));
8971
+ return ok(since ? queryAgentBreakdownSince(db, since) : queryAgentBreakdown(db, period, machine));
8898
8972
  if (by === "account")
8899
- return ok(queryAccountBreakdown(db, period, machine));
8973
+ return ok(since ? queryAccountBreakdownSince(db, since) : queryAccountBreakdown(db, period, machine));
8900
8974
  if (by === "cost-center")
8901
- return ok(queryCostCenterBreakdown(db, period, { machine }));
8975
+ return ok(queryCostCenterBreakdown(db, period, { machine, since }));
8902
8976
  if (["loop", "app", "repo", "service", "team"].includes(by))
8903
- return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
8904
- return ok(queryModelBreakdown(db));
8977
+ return ok(queryCostCenterBreakdown(db, period, { kind: by, machine, since }));
8978
+ return ok(since ? queryModelBreakdownSince(db, since) : queryModelBreakdown(db));
8905
8979
  }
8906
8980
  if (path === "/api/budgets" && method === "GET") {
8907
8981
  return ok(getBudgetStatuses(db));
@@ -9201,7 +9275,7 @@ function isLocalHost(host) {
9201
9275
  return ["127.0.0.1", "localhost", "::1"].includes(host);
9202
9276
  }
9203
9277
  function startServer(port = 3456, options = {}) {
9204
- const cloud = options.db ? false : isCloudMode();
9278
+ const cloud = options.db ? false : isPostgresBackend();
9205
9279
  const hostname2 = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
9206
9280
  const log = options.log ?? console.log;
9207
9281
  const handlerOptions = {};
@@ -10808,6 +10882,7 @@ function printCompletion(shell) {
10808
10882
 
10809
10883
  // src/cli/commands/extras.ts
10810
10884
  init_paths();
10885
+ init_billing_diff();
10811
10886
  function fmt2(usd) {
10812
10887
  return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
10813
10888
  }
@@ -10977,9 +11052,7 @@ function registerExtendedCommands(program) {
10977
11052
  if (removed > 0)
10978
11053
  checks.push({ ok: true, msg: `deduped ${removed} duplicate requests` });
10979
11054
  }
10980
- const diff = await store.billingDiff("month");
10981
- const drift = Math.abs(diff.delta_pct) / 100;
10982
- checks.push({ ok: !diff.is_alert, msg: `billing drift month: ${(drift * 100).toFixed(1)}%` });
11055
+ checks.push(billingDriftCheck(await store.billingDiff("month")));
10983
11056
  console.log();
10984
11057
  console.log(chalk4.bold.cyan(" Economy Doctor"));
10985
11058
  console.log();
@@ -11043,7 +11116,11 @@ function registerExtendedCommands(program) {
11043
11116
  console.log(chalk4.bold.cyan(` Billing diff \u2014 ${period}`));
11044
11117
  console.log(` Estimated: ${fmt2(diff.estimated_usd)}`);
11045
11118
  console.log(` Actual: ${fmt2(diff.actual_usd)}`);
11046
- console.log(` Delta: ${fmt2(Math.abs(diff.delta_usd))} (${diff.delta_usd >= 0 ? "+" : ""}${diff.delta_pct.toFixed(1)}%)`);
11119
+ const deltaPct = diff.comparable ? `${diff.delta_usd >= 0 ? "+" : ""}${diff.delta_pct.toFixed(1)}%` : "n/a";
11120
+ console.log(` Delta: ${fmt2(Math.abs(diff.delta_usd))} (${deltaPct})`);
11121
+ if (!diff.comparable)
11122
+ console.log(chalk4.yellow(`
11123
+ ${billingDriftCheck(diff).msg}`));
11047
11124
  if (diff.is_alert)
11048
11125
  console.log(chalk4.yellow(`
11049
11126
  Suggestion: economy sync --recalculate && economy billing sync`));
@@ -11219,6 +11296,7 @@ init_billing();
11219
11296
  init_package_metadata();
11220
11297
  init_pricing();
11221
11298
  init_store();
11299
+ init_billing_diff();
11222
11300
  init_transport();
11223
11301
  import { execSync } from "child_process";
11224
11302
  var program = new Command;
@@ -12386,8 +12464,12 @@ billingCmd.command("show").description("Show actual billing totals vs our estima
12386
12464
  console.log(` ${chalk7.bold("Actual total:")} ${fmt4(actual.total_usd)}`);
12387
12465
  console.log(` ${chalk7.dim("Our estimate:")} ${fmt4(estimated.total_usd)}`);
12388
12466
  const diff = estimated.total_usd - actual.total_usd;
12389
- const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
12390
- console.log(` ${chalk7.dim("Difference:")} ${fmt4(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
12467
+ const pct = billingDeltaPct(estimated.total_usd, actual.total_usd);
12468
+ const rendered = pct === null ? "n/a" : `${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%`;
12469
+ console.log(` ${chalk7.dim("Difference:")} ${fmt4(Math.abs(diff))} (${rendered})`);
12470
+ if (pct === null) {
12471
+ console.log(chalk7.yellow(` ${chalk7.dim("Note:")} no provider billing recorded for ${period} \u2014 drift is UNKNOWN, not zero.`));
12472
+ }
12391
12473
  console.log();
12392
12474
  });
12393
12475
  registerBrainsCommand(program);
@@ -1,10 +1,30 @@
1
1
  import type { AuthQueryClient } from '@hasna/contracts/auth';
2
+ import type { ServerDataBackend } from '@hasna/contracts/schemas';
2
3
  import pg from 'pg';
3
4
  import type { DbAdapter, SqliteAdapter as Database } from './sqlite-adapter.js';
4
- /** Resolve the cloud Postgres DSN from the standard env aliases. */
5
- export declare function getCloudDatabaseUrl(): string | undefined;
6
- /** True when the serve should run in cloud (RDS-direct) mode. */
7
- export declare function isCloudMode(): boolean;
5
+ /** The environment shape the backend resolver reads. */
6
+ type Env = Record<string, string | undefined>;
7
+ /** Resolve the Postgres DSN from the standard env aliases. */
8
+ export declare function getCloudDatabaseUrl(env?: Env): string | undefined;
9
+ /**
10
+ * Resolve the server data backend from database configuration ALONE.
11
+ *
12
+ * `@hasna/contracts` 0.9.0 removed the deployment-mode axis: the only switch is
13
+ * the server's data backend, `sqlite | postgresql`, and a present database URL
14
+ * is what selects `postgresql`. Retired `STORAGE_MODE` / `MODE` variables are
15
+ * rejected with a migration hint rather than normalized or silently mapped
16
+ * (CONTRACT.md section 2), so a half-migrated deployment fails loudly at startup
17
+ * instead of quietly serving the wrong store.
18
+ *
19
+ * The rejection is delegated to the contract package so the migration hint stays
20
+ * identical to the one the `server_backend_configuration` conformance gate emits.
21
+ * DSN resolution stays local because economy also honours the bare `DATABASE_URL`
22
+ * alias, which the contract's own resolver does not read — deferring to it
23
+ * wholesale would silently downgrade such a deployment to sqlite.
24
+ */
25
+ export declare function resolveEconomyServerBackend(env?: Env): ServerDataBackend;
26
+ /** True when the serve reads and writes PostgreSQL directly. */
27
+ export declare function isPostgresBackend(env?: Env): boolean;
8
28
  /** HMAC signing secret for API-key verification (server-held, never in a token). */
9
29
  export declare function resolveSigningSecret(): string | undefined;
10
30
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/db/cloud.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAK/E,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAOxD;AAED,iEAAiE;AACjE,wBAAgB,WAAW,IAAI,OAAO,CAKrC;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,qBAAwB,GAAG,QAAQ,CASvE;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,GAAG,qBAAwB,GAAG,EAAE,CAAC,IAAI,CAWpE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,eAAe,CAcjE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,YAAY,EAAE,SAAS,EAAE,CAAA"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/db/cloud.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAK/E,wDAAwD;AACxD,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAE7C,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,GAAiB,GAAG,MAAM,GAAG,SAAS,CAO9E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,GAAE,GAAiB,GAAG,iBAAiB,CAGrF;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,GAAiB,GAAG,OAAO,CAEjE;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,qBAAwB,GAAG,QAAQ,CASvE;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,GAAG,qBAAwB,GAAG,EAAE,CAAC,IAAI,CAWpE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,eAAe,CAcjE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,YAAY,EAAE,SAAS,EAAE,CAAA"}
@@ -89,6 +89,13 @@ export declare function queryBillingSummary(db: Database, period: Period): {
89
89
  total_usd: number;
90
90
  by_provider: Record<string, number>;
91
91
  };
92
+ /**
93
+ * How many provider billing rows exist for a period. A $0.00 actual total is
94
+ * ambiguous on its own — it is produced both by "billing agrees with telemetry
95
+ * at zero" and by "billing was never imported" — and only this count separates
96
+ * them.
97
+ */
98
+ export declare function countBillingRecords(db: Database, period: Period): number;
92
99
  export interface SessionDetail {
93
100
  session: Record<string, unknown>;
94
101
  requests: Array<Record<string, unknown>>;
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1825,9 +1825,11 @@ function queryRequestsSince(db, since) {
1825
1825
  function insertFeedback(db, record) {
1826
1826
  db.prepare(`INSERT INTO feedback (message, email, category, version, machine_id) VALUES (?, ?, ?, ?, ?)`).run(record.message, record.email ?? null, record.category ?? "general", record.version ?? null, record.machine_id ?? null);
1827
1827
  }
1828
+ function billingPeriodWhere(period) {
1829
+ return period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1830
+ }
1828
1831
  function queryBillingSummary(db, period) {
1829
- const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1830
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1832
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
1831
1833
  const by_provider = {};
1832
1834
  let total = 0;
1833
1835
  for (const r of rows) {
@@ -1836,6 +1838,10 @@ function queryBillingSummary(db, period) {
1836
1838
  }
1837
1839
  return { total_usd: total, by_provider };
1838
1840
  }
1841
+ function countBillingRecords(db, period) {
1842
+ const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
1843
+ return row?.c ?? 0;
1844
+ }
1839
1845
  function getSessionDetail(db, id) {
1840
1846
  const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
1841
1847
  if (!session)
@@ -2156,11 +2162,20 @@ var PROVIDER_TO_AGENT = {
2156
2162
  gemini: "gemini",
2157
2163
  google: "gemini"
2158
2164
  };
2165
+ function billingDeltaPct(estimatedUsd, actualUsd) {
2166
+ if (!(actualUsd > 0))
2167
+ return null;
2168
+ return (estimatedUsd - actualUsd) / actualUsd * 100;
2169
+ }
2159
2170
  function queryBillingDiff(db, period, thresholdPct = 15) {
2160
2171
  const estimated = querySummary(db, period, undefined, true);
2161
2172
  const actual = queryBillingSummary(db, period);
2173
+ const billingRecords = countBillingRecords(db, period);
2162
2174
  const delta = estimated.total_usd - actual.total_usd;
2163
- const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
2175
+ const measuredPct = billingDeltaPct(estimated.total_usd, actual.total_usd);
2176
+ const incomparableReason = measuredPct !== null ? null : billingRecords === 0 ? "no_billing_records" : "zero_actual_billing";
2177
+ const comparable = incomparableReason === null;
2178
+ const deltaPct = measuredPct ?? 0;
2164
2179
  const agentRows = db.prepare(`
2165
2180
  SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
2166
2181
  FROM requests
@@ -2187,7 +2202,9 @@ function queryBillingDiff(db, period, thresholdPct = 15) {
2187
2202
  delta_usd: delta,
2188
2203
  delta_pct: deltaPct,
2189
2204
  threshold_pct: thresholdPct,
2190
- is_alert: Math.abs(deltaPct) > thresholdPct,
2205
+ is_alert: comparable && Math.abs(deltaPct) > thresholdPct,
2206
+ comparable,
2207
+ incomparable_reason: incomparableReason,
2191
2208
  by_agent,
2192
2209
  by_provider: actual.by_provider
2193
2210
  };
@@ -3029,6 +3046,20 @@ function resolveClientTransport(name, env = process.env) {
3029
3046
  } else if (urlHit && keyHit) {
3030
3047
  mode = "cloud";
3031
3048
  modeSource = `${urlHit.key}+${keyHit.key}`;
3049
+ } else if (urlHit) {
3050
+ warnings.push(`${urlHit.key} is set but no API key is present (${keys.apiKeyKeys[0]}). ` + `Refusing to fall back to the local store, which would silently serve a different dataset. ` + `Set ${keys.apiKeyKeys[0]} to enable the cloud client, or set ${keys.modeKeys[0]}=local ` + `to use the local store deliberately.`);
3051
+ return {
3052
+ transport: "local",
3053
+ mode: "local",
3054
+ deprecatedAlias,
3055
+ modeSource: urlHit.key,
3056
+ baseUrl: null,
3057
+ apiUrlSource: null,
3058
+ apiKeyPresent: false,
3059
+ apiKeySource: null,
3060
+ misconfigured: true,
3061
+ warning: warnings.join(" ")
3062
+ };
3032
3063
  }
3033
3064
  if (mode === "local") {
3034
3065
  return {
@@ -7,6 +7,25 @@ export interface BillingDiffRow {
7
7
  delta_usd: number;
8
8
  delta_pct: number;
9
9
  }
10
+ /**
11
+ * Why a drift percentage could not be computed. `null` means it could.
12
+ *
13
+ * - `no_billing_records` — no provider billing rows exist for the period at
14
+ * all. Nothing was imported; there is no "actual" to compare against.
15
+ * - `zero_actual_billing` — rows exist but total $0.00, so the denominator is
16
+ * still absent even though the import ran.
17
+ */
18
+ export type BillingDiffIncomparableReason = 'no_billing_records' | 'zero_actual_billing';
19
+ /**
20
+ * Estimated-vs-actual drift as a percentage, or `null` when there is no actual
21
+ * to divide by.
22
+ *
23
+ * Returning `null` rather than `0` is the point. Three call sites independently
24
+ * wrote `actual > 0 ? (delta / actual) * 100 : 0`, and every one of them then
25
+ * rendered the fallback as a measured "0.0%" — reporting perfect agreement for
26
+ * the case where nothing was there to agree with.
27
+ */
28
+ export declare function billingDeltaPct(estimatedUsd: number, actualUsd: number): number | null;
10
29
  export interface BillingDiffSummary {
11
30
  period: Period;
12
31
  estimated_usd: number;
@@ -15,8 +34,28 @@ export interface BillingDiffSummary {
15
34
  delta_pct: number;
16
35
  threshold_pct: number;
17
36
  is_alert: boolean;
37
+ /**
38
+ * Whether `delta_pct` is a measurement at all. False means the two sides
39
+ * could not be compared, and `delta_pct` carries 0 only because the type is
40
+ * a number — read this before reporting the percentage as agreement.
41
+ */
42
+ comparable: boolean;
43
+ incomparable_reason: BillingDiffIncomparableReason | null;
18
44
  by_agent: BillingDiffRow[];
19
45
  by_provider: Record<string, number>;
20
46
  }
21
47
  export declare function queryBillingDiff(db: Database, period: Period, thresholdPct?: number): BillingDiffSummary;
48
+ /**
49
+ * The health verdict `economy doctor` reports for billing drift.
50
+ *
51
+ * Kept beside the diff rather than in the CLI because the interesting case is
52
+ * a semantic one: an unmeasurable drift must not render as a measured 0.0%.
53
+ * A check that cannot distinguish "the sides agree" from "there is nothing to
54
+ * compare" cannot fail, and this one is consulted to decide whether the rest
55
+ * of the tool's numbers are trustworthy.
56
+ */
57
+ export declare function billingDriftCheck(diff: BillingDiffSummary): {
58
+ ok: boolean;
59
+ msg: string;
60
+ };
22
61
  //# sourceMappingURL=billing-diff.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"billing-diff.d.ts","sourceRoot":"","sources":["../../src/lib/billing-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAExE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAS/C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,EACd,YAAY,SAAK,GAChB,kBAAkB,CAsCpB"}
1
+ {"version":3,"file":"billing-diff.d.ts","sourceRoot":"","sources":["../../src/lib/billing-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAExE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAS/C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,6BAA6B,GAAG,oBAAoB,GAAG,qBAAqB,CAAA;AAExF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGtF;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB,mBAAmB,EAAE,6BAA6B,GAAG,IAAI,CAAA;IACzD,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,EACd,YAAY,SAAK,GAChB,kBAAkB,CAsDpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAYxF"}
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/lib/contracts-client/transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/lib/contracts-client/transport.ts"],"names":[],"mappings":"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"}
package/dist/mcp/index.js CHANGED
@@ -1895,9 +1895,11 @@ function queryRequestsSince(db, since) {
1895
1895
  function insertFeedback(db, record) {
1896
1896
  db.prepare(`INSERT INTO feedback (message, email, category, version, machine_id) VALUES (?, ?, ?, ?, ?)`).run(record.message, record.email ?? null, record.category ?? "general", record.version ?? null, record.machine_id ?? null);
1897
1897
  }
1898
+ function billingPeriodWhere(period) {
1899
+ return period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1900
+ }
1898
1901
  function queryBillingSummary(db, period) {
1899
- const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1900
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1902
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
1901
1903
  const by_provider = {};
1902
1904
  let total = 0;
1903
1905
  for (const r of rows) {
@@ -1906,6 +1908,10 @@ function queryBillingSummary(db, period) {
1906
1908
  }
1907
1909
  return { total_usd: total, by_provider };
1908
1910
  }
1911
+ function countBillingRecords(db, period) {
1912
+ const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
1913
+ return row?.c ?? 0;
1914
+ }
1909
1915
  function getSessionDetail(db, id) {
1910
1916
  const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
1911
1917
  if (!session)
@@ -3921,11 +3927,20 @@ var PROVIDER_TO_AGENT = {
3921
3927
  gemini: "gemini",
3922
3928
  google: "gemini"
3923
3929
  };
3930
+ function billingDeltaPct(estimatedUsd, actualUsd) {
3931
+ if (!(actualUsd > 0))
3932
+ return null;
3933
+ return (estimatedUsd - actualUsd) / actualUsd * 100;
3934
+ }
3924
3935
  function queryBillingDiff(db, period, thresholdPct = 15) {
3925
3936
  const estimated = querySummary(db, period, undefined, true);
3926
3937
  const actual = queryBillingSummary(db, period);
3938
+ const billingRecords = countBillingRecords(db, period);
3927
3939
  const delta = estimated.total_usd - actual.total_usd;
3928
- const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
3940
+ const measuredPct = billingDeltaPct(estimated.total_usd, actual.total_usd);
3941
+ const incomparableReason = measuredPct !== null ? null : billingRecords === 0 ? "no_billing_records" : "zero_actual_billing";
3942
+ const comparable = incomparableReason === null;
3943
+ const deltaPct = measuredPct ?? 0;
3929
3944
  const agentRows = db.prepare(`
3930
3945
  SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
3931
3946
  FROM requests
@@ -3952,7 +3967,9 @@ function queryBillingDiff(db, period, thresholdPct = 15) {
3952
3967
  delta_usd: delta,
3953
3968
  delta_pct: deltaPct,
3954
3969
  threshold_pct: thresholdPct,
3955
- is_alert: Math.abs(deltaPct) > thresholdPct,
3970
+ is_alert: comparable && Math.abs(deltaPct) > thresholdPct,
3971
+ comparable,
3972
+ incomparable_reason: incomparableReason,
3956
3973
  by_agent,
3957
3974
  by_provider: actual.by_provider
3958
3975
  };
@@ -4794,6 +4811,20 @@ function resolveClientTransport(name, env = process.env) {
4794
4811
  } else if (urlHit && keyHit) {
4795
4812
  mode = "cloud";
4796
4813
  modeSource = `${urlHit.key}+${keyHit.key}`;
4814
+ } else if (urlHit) {
4815
+ warnings.push(`${urlHit.key} is set but no API key is present (${keys.apiKeyKeys[0]}). ` + `Refusing to fall back to the local store, which would silently serve a different dataset. ` + `Set ${keys.apiKeyKeys[0]} to enable the cloud client, or set ${keys.modeKeys[0]}=local ` + `to use the local store deliberately.`);
4816
+ return {
4817
+ transport: "local",
4818
+ mode: "local",
4819
+ deprecatedAlias,
4820
+ modeSource: urlHit.key,
4821
+ baseUrl: null,
4822
+ apiUrlSource: null,
4823
+ apiKeyPresent: false,
4824
+ apiKeySource: null,
4825
+ misconfigured: true,
4826
+ warning: warnings.join(" ")
4827
+ };
4797
4828
  }
4798
4829
  if (mode === "local") {
4799
4830
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAikD/C,CAAA;AACD,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAmmD/C,CAAA;AACD,eAAe,WAAW,CAAA"}
@@ -1643,6 +1643,136 @@ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1643
1643
  function queryProjectBreakdownSince(db, since, machine) {
1644
1644
  return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1645
1645
  }
1646
+ function queryAgentBreakdownSince(db, since) {
1647
+ return db.prepare(`
1648
+ SELECT agent,
1649
+ COUNT(DISTINCT session_id) as sessions,
1650
+ COUNT(*) as requests,
1651
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1652
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1653
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1654
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1655
+ MAX(timestamp) as last_active
1656
+ FROM requests
1657
+ WHERE timestamp >= ?
1658
+ GROUP BY agent
1659
+ ORDER BY api_equivalent_usd DESC
1660
+ `).all(since);
1661
+ }
1662
+ function queryModelBreakdownSince(db, since) {
1663
+ return db.prepare(`
1664
+ SELECT model, agent,
1665
+ COUNT(*) as requests,
1666
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1667
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1668
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1669
+ COALESCE(SUM(cost_usd), 0) as cost_usd
1670
+ FROM requests WHERE timestamp >= ?
1671
+ GROUP BY model, agent ORDER BY cost_usd DESC
1672
+ `).all(since);
1673
+ }
1674
+ function queryAccountBreakdownSince(db, since) {
1675
+ return db.prepare(`
1676
+ WITH request_rows AS (
1677
+ SELECT
1678
+ r.session_id as session_id,
1679
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1680
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
1681
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
1682
+ LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
1683
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1684
+ 1 as requests,
1685
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1686
+ COALESCE(r.cost_usd, 0) as cost_usd,
1687
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1688
+ r.timestamp as last_active
1689
+ FROM requests r
1690
+ LEFT JOIN sessions s ON s.id = r.session_id
1691
+ WHERE r.timestamp >= ?
1692
+ AND (
1693
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1694
+ OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1695
+ OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1696
+ OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1697
+ )
1698
+ ),
1699
+ session_only_rows AS (
1700
+ SELECT
1701
+ s.id as session_id,
1702
+ COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1703
+ s.account_key as raw_account_key,
1704
+ s.account_name as raw_account_name,
1705
+ LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
1706
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1707
+ COALESCE(s.request_count, 0) as requests,
1708
+ COALESCE(s.total_tokens, 0) as total_tokens,
1709
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1710
+ 'estimated' as cost_basis,
1711
+ s.started_at as last_active
1712
+ FROM sessions s
1713
+ WHERE s.started_at >= ?
1714
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1715
+ AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1716
+ ),
1717
+ normalized AS (
1718
+ SELECT
1719
+ CASE
1720
+ WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1721
+ WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1722
+ ELSE raw_account_key
1723
+ END as account_key,
1724
+ account_agent as account_tool,
1725
+ raw_account_name as account_name,
1726
+ raw_account_email as account_email,
1727
+ account_source,
1728
+ session_id,
1729
+ requests,
1730
+ total_tokens,
1731
+ cost_usd,
1732
+ cost_basis,
1733
+ last_active
1734
+ FROM request_rows
1735
+ UNION ALL
1736
+ SELECT
1737
+ CASE
1738
+ WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1739
+ WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1740
+ ELSE raw_account_key
1741
+ END as account_key,
1742
+ account_agent as account_tool,
1743
+ raw_account_name as account_name,
1744
+ raw_account_email as account_email,
1745
+ account_source,
1746
+ session_id,
1747
+ requests,
1748
+ total_tokens,
1749
+ cost_usd,
1750
+ cost_basis,
1751
+ last_active
1752
+ FROM session_only_rows
1753
+ )
1754
+ SELECT account_key,
1755
+ account_tool,
1756
+ COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
1757
+ NULLIF(account_email, '') as account_email,
1758
+ COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
1759
+ COUNT(DISTINCT session_id) as sessions,
1760
+ COALESCE(SUM(requests), 0) as requests,
1761
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1762
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1763
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1764
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1765
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1766
+ COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1767
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1768
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1769
+ MAX(last_active) as last_active
1770
+ FROM normalized
1771
+ WHERE account_key != ''
1772
+ GROUP BY account_key, account_tool, account_email
1773
+ ORDER BY api_equivalent_usd DESC
1774
+ `).all(since, since);
1775
+ }
1646
1776
  function queryDailyBreakdown(db, days = 30, machine) {
1647
1777
  const machineClause = machine ? " AND machine_id = ?" : "";
1648
1778
  const params = machine ? [`-${days}`, machine] : [`-${days}`];
@@ -1786,9 +1916,11 @@ function upsertBillingDaily(db, row) {
1786
1916
  function clearBillingRange(db, provider, fromDate, toDate) {
1787
1917
  db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
1788
1918
  }
1919
+ function billingPeriodWhere(period) {
1920
+ return period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1921
+ }
1789
1922
  function queryBillingSummary(db, period) {
1790
- const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1791
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1923
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
1792
1924
  const by_provider = {};
1793
1925
  let total = 0;
1794
1926
  for (const r of rows) {
@@ -1797,6 +1929,10 @@ function queryBillingSummary(db, period) {
1797
1929
  }
1798
1930
  return { total_usd: total, by_provider };
1799
1931
  }
1932
+ function countBillingRecords(db, period) {
1933
+ const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
1934
+ return row?.c ?? 0;
1935
+ }
1800
1936
  function listMachines(db, period = "all") {
1801
1937
  const rWhere = requestPeriodWhere(period);
1802
1938
  const sWhere = sessionPeriodWhere(period);
@@ -2653,24 +2789,25 @@ var init_pg_migrations = __esm(() => {
2653
2789
  var exports_cloud = {};
2654
2790
  __export(exports_cloud, {
2655
2791
  resolveSigningSecret: () => resolveSigningSecret,
2792
+ resolveEconomyServerBackend: () => resolveEconomyServerBackend,
2656
2793
  openCloudDatabase: () => openCloudDatabase,
2657
- isCloudMode: () => isCloudMode,
2794
+ isPostgresBackend: () => isPostgresBackend,
2658
2795
  getCloudDatabaseUrl: () => getCloudDatabaseUrl,
2659
2796
  createCloudPool: () => createCloudPool,
2660
2797
  cloudMigrations: () => cloudMigrations,
2661
2798
  authClientFromPool: () => authClientFromPool
2662
2799
  });
2800
+ import { assertNoLegacyStorageMode } from "@hasna/contracts/server-backend";
2663
2801
  import pg2 from "pg";
2664
- function getCloudDatabaseUrl() {
2665
- return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
2802
+ function getCloudDatabaseUrl(env = process.env) {
2803
+ return env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || env["ECONOMY_DATABASE_URL"]?.trim() || env["DATABASE_URL"]?.trim() || undefined;
2666
2804
  }
2667
- function isCloudMode() {
2668
- const mode = process.env["HASNA_ECONOMY_STORAGE_MODE"]?.trim().toLowerCase();
2669
- if (mode === "cloud")
2670
- return true;
2671
- if (mode === "local")
2672
- return false;
2673
- return Boolean(getCloudDatabaseUrl());
2805
+ function resolveEconomyServerBackend(env = process.env) {
2806
+ assertNoLegacyStorageMode("economy", env);
2807
+ return getCloudDatabaseUrl(env) ? "postgresql" : "sqlite";
2808
+ }
2809
+ function isPostgresBackend(env = process.env) {
2810
+ return resolveEconomyServerBackend(env) === "postgresql";
2674
2811
  }
2675
2812
  function resolveSigningSecret() {
2676
2813
  return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
@@ -5728,11 +5865,20 @@ var PROVIDER_TO_AGENT = {
5728
5865
  gemini: "gemini",
5729
5866
  google: "gemini"
5730
5867
  };
5868
+ function billingDeltaPct(estimatedUsd, actualUsd) {
5869
+ if (!(actualUsd > 0))
5870
+ return null;
5871
+ return (estimatedUsd - actualUsd) / actualUsd * 100;
5872
+ }
5731
5873
  function queryBillingDiff(db, period, thresholdPct = 15) {
5732
5874
  const estimated = querySummary(db, period, undefined, true);
5733
5875
  const actual = queryBillingSummary(db, period);
5876
+ const billingRecords = countBillingRecords(db, period);
5734
5877
  const delta = estimated.total_usd - actual.total_usd;
5735
- const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
5878
+ const measuredPct = billingDeltaPct(estimated.total_usd, actual.total_usd);
5879
+ const incomparableReason = measuredPct !== null ? null : billingRecords === 0 ? "no_billing_records" : "zero_actual_billing";
5880
+ const comparable = incomparableReason === null;
5881
+ const deltaPct = measuredPct ?? 0;
5736
5882
  const agentRows = db.prepare(`
5737
5883
  SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
5738
5884
  FROM requests
@@ -5759,7 +5905,9 @@ function queryBillingDiff(db, period, thresholdPct = 15) {
5759
5905
  delta_usd: delta,
5760
5906
  delta_pct: deltaPct,
5761
5907
  threshold_pct: thresholdPct,
5762
- is_alert: Math.abs(deltaPct) > thresholdPct,
5908
+ is_alert: comparable && Math.abs(deltaPct) > thresholdPct,
5909
+ comparable,
5910
+ incomparable_reason: incomparableReason,
5763
5911
  by_agent,
5764
5912
  by_provider: actual.by_provider
5765
5913
  };
@@ -5839,7 +5987,7 @@ var openApiSpec = {
5839
5987
  info: {
5840
5988
  title: "@hasna/economy self-hosted API",
5841
5989
  version: "0.0.0",
5842
- description: "AI coding cost tracker \u2014 self-hosted control plane. Cloud mode is PURE REMOTE per Amendment A1: the serve process reads/writes the shared RDS Postgres directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
5990
+ description: "AI coding cost tracker \u2014 self-hosted control plane. The server data backend is sqlite | postgresql, selected by the presence of a database URL; with postgresql the serve process reads AND writes PostgreSQL directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
5843
5991
  },
5844
5992
  servers: [
5845
5993
  {
@@ -5885,27 +6033,61 @@ var openApiSpec = {
5885
6033
  "data"
5886
6034
  ]
5887
6035
  },
5888
- Foundation: {
6036
+ Health: {
5889
6037
  type: "object",
5890
6038
  properties: {
5891
6039
  status: {
5892
- type: "string"
6040
+ type: "string",
6041
+ enum: [
6042
+ "ok",
6043
+ "degraded",
6044
+ "unavailable"
6045
+ ]
5893
6046
  },
5894
6047
  version: {
5895
6048
  type: "string"
5896
6049
  },
5897
- mode: {
5898
- type: "string"
5899
- },
5900
- service: {
5901
- type: "string"
6050
+ backend: {
6051
+ type: "string",
6052
+ enum: [
6053
+ "sqlite",
6054
+ "postgresql"
6055
+ ]
5902
6056
  }
5903
6057
  },
5904
6058
  required: [
5905
6059
  "status",
5906
6060
  "version",
5907
- "mode"
5908
- ]
6061
+ "backend"
6062
+ ],
6063
+ additionalProperties: false
6064
+ },
6065
+ Ready: {
6066
+ type: "object",
6067
+ properties: {
6068
+ ready: {
6069
+ type: "boolean"
6070
+ },
6071
+ reason: {
6072
+ type: "string"
6073
+ }
6074
+ },
6075
+ required: [
6076
+ "ready"
6077
+ ],
6078
+ additionalProperties: false
6079
+ },
6080
+ Version: {
6081
+ type: "object",
6082
+ properties: {
6083
+ version: {
6084
+ type: "string"
6085
+ }
6086
+ },
6087
+ required: [
6088
+ "version"
6089
+ ],
6090
+ additionalProperties: false
5909
6091
  },
5910
6092
  Error: {
5911
6093
  type: "object",
@@ -6188,7 +6370,7 @@ var openApiSpec = {
6188
6370
  content: {
6189
6371
  "application/json": {
6190
6372
  schema: {
6191
- $ref: "#/components/schemas/Foundation"
6373
+ $ref: "#/components/schemas/Health"
6192
6374
  }
6193
6375
  }
6194
6376
  }
@@ -6207,7 +6389,7 @@ var openApiSpec = {
6207
6389
  content: {
6208
6390
  "application/json": {
6209
6391
  schema: {
6210
- $ref: "#/components/schemas/Foundation"
6392
+ $ref: "#/components/schemas/Ready"
6211
6393
  }
6212
6394
  }
6213
6395
  }
@@ -6217,7 +6399,7 @@ var openApiSpec = {
6217
6399
  content: {
6218
6400
  "application/json": {
6219
6401
  schema: {
6220
- $ref: "#/components/schemas/Foundation"
6402
+ $ref: "#/components/schemas/Ready"
6221
6403
  }
6222
6404
  }
6223
6405
  }
@@ -6236,7 +6418,7 @@ var openApiSpec = {
6236
6418
  content: {
6237
6419
  "application/json": {
6238
6420
  schema: {
6239
- $ref: "#/components/schemas/Foundation"
6421
+ $ref: "#/components/schemas/Version"
6240
6422
  }
6241
6423
  }
6242
6424
  }
@@ -7442,11 +7624,8 @@ function openApiDocument() {
7442
7624
  const spec = openApiSpec;
7443
7625
  return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
7444
7626
  }
7445
- function serveMode() {
7446
- return isCloudMode() ? "self_hosted" : "local";
7447
- }
7448
- function foundationEnvelope(status, extra = {}) {
7449
- return { status, version: packageMetadata.version, mode: serveMode(), service: "economy", ...extra };
7627
+ function healthEnvelope(status) {
7628
+ return { status, version: packageMetadata.version, backend: resolveEconomyServerBackend() };
7450
7629
  }
7451
7630
  var CORS = {
7452
7631
  "Access-Control-Allow-Origin": "*",
@@ -7556,14 +7735,14 @@ function createHandler(db, options = {}) {
7556
7735
  if (method === "OPTIONS")
7557
7736
  return new Response(null, { status: 204, headers: CORS });
7558
7737
  if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
7559
- return json(foundationEnvelope("ok"));
7738
+ return json(healthEnvelope("ok"));
7560
7739
  }
7561
7740
  if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
7562
- return json(foundationEnvelope("ok"));
7741
+ return json({ version: packageMetadata.version });
7563
7742
  }
7564
7743
  if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
7565
7744
  const result = options.readyCheck ? await options.readyCheck() : { ready: true };
7566
- return json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), result.ready ? 200 : 503);
7745
+ return json(result.detail ? { ready: result.ready, reason: result.detail } : { ready: result.ready }, result.ready ? 200 : 503);
7567
7746
  }
7568
7747
  if (method === "GET" && rawPath === "/openapi.json") {
7569
7748
  return json(openApiDocument());
@@ -7702,14 +7881,14 @@ function createHandler(db, options = {}) {
7702
7881
  if (by === "project")
7703
7882
  return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
7704
7883
  if (by === "agent")
7705
- return ok(queryAgentBreakdown(db, period, machine));
7884
+ return ok(since ? queryAgentBreakdownSince(db, since) : queryAgentBreakdown(db, period, machine));
7706
7885
  if (by === "account")
7707
- return ok(queryAccountBreakdown(db, period, machine));
7886
+ return ok(since ? queryAccountBreakdownSince(db, since) : queryAccountBreakdown(db, period, machine));
7708
7887
  if (by === "cost-center")
7709
- return ok(queryCostCenterBreakdown(db, period, { machine }));
7888
+ return ok(queryCostCenterBreakdown(db, period, { machine, since }));
7710
7889
  if (["loop", "app", "repo", "service", "team"].includes(by))
7711
- return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
7712
- return ok(queryModelBreakdown(db));
7890
+ return ok(queryCostCenterBreakdown(db, period, { kind: by, machine, since }));
7891
+ return ok(since ? queryModelBreakdownSince(db, since) : queryModelBreakdown(db));
7713
7892
  }
7714
7893
  if (path === "/api/budgets" && method === "GET") {
7715
7894
  return ok(getBudgetStatuses(db));
@@ -8009,7 +8188,7 @@ function isLocalHost(host) {
8009
8188
  return ["127.0.0.1", "localhost", "::1"].includes(host);
8010
8189
  }
8011
8190
  function startServer(port = 3456, options = {}) {
8012
- const cloud = options.db ? false : isCloudMode();
8191
+ const cloud = options.db ? false : isPostgresBackend();
8013
8192
  const hostname2 = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
8014
8193
  const log = options.log ?? console.log;
8015
8194
  const handlerOptions = {};
@@ -8060,13 +8239,18 @@ function printHelp() {
8060
8239
  console.log(`Usage: economy-serve [command] [options]
8061
8240
 
8062
8241
  REST API server for ${packageMetadata.name}
8063
- Foundation probes: GET /health, /ready, /version -> { status, version, mode }
8064
- Versioned API: /v1/* (API-key auth via @hasna/contracts in cloud mode)
8242
+ Foundation probes: GET /health -> { status, version, backend }
8243
+ GET /ready -> { ready, reason? }
8244
+ GET /version -> { version }
8245
+ Versioned API: /v1/* (API-key auth via @hasna/contracts)
8246
+
8247
+ The server data backend is sqlite | postgresql, selected by the presence of
8248
+ HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL).
8065
8249
 
8066
8250
  Commands:
8067
8251
  (default) start the HTTP server
8068
- migrate apply the cloud Postgres schema + api_keys, then exit
8069
- version print { status, version, mode }
8252
+ migrate apply the Postgres schema + api_keys, then exit
8253
+ version print { status, version, backend }
8070
8254
 
8071
8255
  Options:
8072
8256
  -p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
@@ -8113,8 +8297,14 @@ async function runMigrate() {
8113
8297
  var args = process.argv.slice(2);
8114
8298
  var sub = args[0];
8115
8299
  if (sub === "version") {
8116
- console.log(JSON.stringify({ status: "ok", version: packageMetadata.version, mode: process.env["HASNA_ECONOMY_STORAGE_MODE"] === "cloud" ? "self_hosted" : "local" }));
8117
- process.exit(0);
8300
+ try {
8301
+ const { resolveEconomyServerBackend: resolveEconomyServerBackend2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
8302
+ console.log(JSON.stringify({ status: "ok", version: packageMetadata.version, backend: resolveEconomyServerBackend2() }));
8303
+ process.exit(0);
8304
+ } catch (error) {
8305
+ console.error(error instanceof Error ? error.message : String(error));
8306
+ process.exit(1);
8307
+ }
8118
8308
  }
8119
8309
  if (sub === "migrate") {
8120
8310
  runMigrate().catch((error) => {
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AA8CxE,gFAAgF;AAChF,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGzD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CACV,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAC7F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAuBD,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AA6ED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,MAAM,WAAW,cAAc;IAC7B;;wEAEoE;IACpE,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,IACxC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA+e/D;AAMD,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAkDvG"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AA+CxE,gFAAgF;AAChF,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGzD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CACV,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAC7F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAuBD,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AA6ED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,MAAM,WAAW,cAAc;IAC7B;;wEAEoE;IACpE,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,IACxC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA0f/D;AAMD,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAkDvG"}
@@ -62,7 +62,7 @@ The server serves `dashboard/dist` and falls back to its `index.html` for non-AP
62
62
 
63
63
  ## Self-hosted server
64
64
 
65
- The server switches to direct Postgres mode when `HASNA_ECONOMY_STORAGE_MODE=cloud` or when a DSN is present:
65
+ The server backend follows the database URL alone — `postgresql` when one of these is set, `sqlite` when none is:
66
66
 
67
67
  ```text
68
68
  HASNA_ECONOMY_DATABASE_URL
@@ -70,7 +70,9 @@ ECONOMY_DATABASE_URL
70
70
  DATABASE_URL
71
71
  ```
72
72
 
73
- Apply migrations with `economy-serve migrate`. `ECONOMY_PG_POOL_MAX` defaults to 5. A non-loopback cloud server also requires one of `HASNA_ECONOMY_API_SIGNING_KEY`, `HASNA_API_SIGNING_KEY`, or `API_KEY_SIGNING_SECRET`; API keys are then verified by `@hasna/contracts`. The signing secret belongs only on the server.
73
+ `HASNA_ECONOMY_STORAGE_MODE` (and `HASNA_ECONOMY_MODE`, `ECONOMY_STORAGE_MODE`, `ECONOMY_MODE`) no longer selects a backend: the server refuses to start and prints a migration hint. Delete it and set a DSN instead. This is server-only — the CLI/MCP client still reads the mode variable described above.
74
+
75
+ Apply migrations with `economy-serve migrate`. `ECONOMY_PG_POOL_MAX` defaults to 5. A non-loopback server also requires one of `HASNA_ECONOMY_API_SIGNING_KEY`, `HASNA_API_SIGNING_KEY`, or `API_KEY_SIGNING_SECRET`; API keys are then verified by `@hasna/contracts`. The signing secret belongs only on the server.
74
76
 
75
77
  See [REST API authentication](rest-api.md#authentication) for request headers and open probes.
76
78
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.3.8",
4
- "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
3
+ "version": "0.3.10",
4
+ "description": "AI coding cost tracker — 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",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/hasna/economy.git"
@@ -43,6 +43,8 @@
43
43
  "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
44
44
  "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
45
45
  "build:dashboard": "cd dashboard && bun run build",
46
+ "artifact-scan": "contracts artifact-scan dist",
47
+ "prepack": "bun run build && bun run artifact-scan",
46
48
  "typecheck": "tsc --noEmit",
47
49
  "test": "bun test",
48
50
  "dev:cli": "bun run src/cli/index.ts",
@@ -72,7 +74,7 @@
72
74
  "dependencies": {
73
75
  "@hasna/accounts": "0.2.23",
74
76
  "@hasna/agent-registry": "^0.1.0",
75
- "@hasna/contracts": "^0.4.2",
77
+ "@hasna/contracts": "^0.9.0",
76
78
  "@hasna/events": "^0.1.7",
77
79
  "@hasna/mcp-harness": "^0.1.0",
78
80
  "@hasna/projects": "0.1.95",