@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 +45 -0
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +129 -47
- package/dist/db/cloud.d.ts +24 -4
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +7 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +35 -4
- package/dist/lib/billing-diff.d.ts +39 -0
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/contracts-client/transport.d.ts.map +1 -1
- package/dist/mcp/index.js +35 -4
- package/dist/openapi.d.ts.map +1 -1
- package/dist/server/index.js +238 -48
- package/dist/server/serve.d.ts.map +1 -1
- package/docs/configuration.md +4 -2
- package/package.json +5 -3
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;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
7089
|
-
type: "string"
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
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
|
-
"
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
8649
|
-
return
|
|
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(
|
|
8825
|
+
return json(healthEnvelope("ok"));
|
|
8752
8826
|
}
|
|
8753
8827
|
if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
|
|
8754
|
-
return json(
|
|
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(
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
12390
|
-
|
|
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);
|
package/dist/db/cloud.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
export declare function
|
|
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
|
/**
|
package/dist/db/cloud.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/db/database.d.ts
CHANGED
|
@@ -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":"AAAA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAM/D,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,sBAAsB,EACtB,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,KAAK,EACL,aAAa,EACd,MAAM,mBAAmB,CAAA;AAyB1B,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAkBnC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAIlC;AAkCD,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAwBxE;AAoSD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAUvE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAwBrE;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAmBzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CA8BnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,aAAkB,GAAG,cAAc,EAAE,CAuBxF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAOvG;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,UAAQ,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,WAAW,CAuC5H;AAED,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,SAAK,GAAG,sBAAsB,EAAE,CAc/F;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAUlE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CA4E5G;AA0KD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAShH;AA+FD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CA+EhH;AAiFD,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,QAAQ,EACZ,MAAM,GAAE,MAAc,EACtB,MAAM,GAAE;IAAE,IAAI,CAAC,EAAE,cAAc,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACvE,mBAAmB,EAAE,CAqFvB;AAOD,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAS5G;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAetF;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAWtF;AAED,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAqG1F;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,SAAK,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAUvI;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiB7I;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKzE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAI5E;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAG3D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAW/D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,EAAE,CA+B9D;AAID,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI;IACtC,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;IACpB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CASzD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,EAAE,CAE9C;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,UAAU,EAAE,CA6B1D;AAID,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7F;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAEhF;AAID,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAUzE;AAID,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAA;IACzC,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAKxE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAExG;AAiBD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAM5H;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxE;AAID,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACzC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAK/E;AAID,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,GAAG,WAAW,EAAE,CA4ChF;AAID,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,GAAG,IAAI,CAexE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAElF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAE/D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAkBvO;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,mBAAmB,EAAE,YAAY,GAAG,IAAI,CASpG;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,mBAAmB,EAAE,YAAY,EAAE,CAE1F;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEjE;AAID,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,IAAI,CAAC,OAAO,mBAAmB,EAAE,aAAa,EAAE,IAAI,GAAG,YAAY,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAChH,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3D,OAAO,mBAAmB,EAAE,aAAa,EAAE,CAQ7C;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,mBAAmB,EAAE,eAAe,EAAE,CAE/F;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAqBnD;AAoJD,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAuC3H"}
|
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
|
|
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
|
|
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,
|
|
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":"AAmCA,OAAO,EAAkC,KAAK,GAAG,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvF,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,MAAM,WAAW,sBAAsB;IACrC,sCAAsC;IACtC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yCAAyC;IACzC,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,CAY3E;AAUD,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWlD;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,YAAY,CAAC;AAEzD,MAAM,WAAW,yBAAyB;IACxC,+CAA+C;IAC/C,SAAS,EAAE,mBAAmB,CAAC;IAC/B,iDAAiD;IACjD,IAAI,EAAE,WAAW,CAAC;IAClB,8EAA8E;IAC9E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,2DAA2D;IAC3D,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,qEAAqE;IACrE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,GAAiB,GAAG,yBAAyB,
|
|
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
|
|
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
|
|
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 {
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
5898
|
-
type: "string"
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
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
|
-
"
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
7446
|
-
return
|
|
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(
|
|
7738
|
+
return json(healthEnvelope("ok"));
|
|
7560
7739
|
}
|
|
7561
7740
|
if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
|
|
7562
|
-
return json(
|
|
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(
|
|
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 :
|
|
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
|
|
8064
|
-
|
|
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
|
|
8069
|
-
version print { status, version,
|
|
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
|
-
|
|
8117
|
-
|
|
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;
|
|
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"}
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex,
|
|
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.
|
|
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",
|