@hasna/economy 0.3.10 → 0.3.15

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/dist/cli/index.js CHANGED
@@ -591,68 +591,6 @@ var init_pricing = __esm(() => {
591
591
  });
592
592
 
593
593
  // src/db/database.ts
594
- var exports_database = {};
595
- __export(exports_database, {
596
- upsertUsageSnapshot: () => upsertUsageSnapshot,
597
- upsertSubscription: () => upsertSubscription,
598
- upsertSession: () => upsertSession,
599
- upsertRequest: () => upsertRequest,
600
- upsertProject: () => upsertProject,
601
- upsertModelPricing: () => upsertModelPricing,
602
- upsertGoal: () => upsertGoal,
603
- upsertCostCenter: () => upsertCostCenter,
604
- upsertBudget: () => upsertBudget,
605
- upsertBillingDaily: () => upsertBillingDaily,
606
- setIngestState: () => setIngestState,
607
- seedModelPricing: () => seedModelPricing,
608
- rollupSession: () => rollupSession,
609
- queryZeroCostTokenizedModels: () => queryZeroCostTokenizedModels,
610
- queryUsageSnapshots: () => queryUsageSnapshots,
611
- queryTopSessions: () => queryTopSessions,
612
- querySummary: () => querySummary,
613
- querySessions: () => querySessions,
614
- queryRequestsSince: () => queryRequestsSince,
615
- queryProjectBreakdownSince: () => queryProjectBreakdownSince,
616
- queryProjectBreakdown: () => queryProjectBreakdown,
617
- queryModelBreakdownSince: () => queryModelBreakdownSince,
618
- queryModelBreakdown: () => queryModelBreakdown,
619
- queryHourlyBreakdown: () => queryHourlyBreakdown,
620
- queryDailyBreakdown: () => queryDailyBreakdown,
621
- queryCostCenterBreakdown: () => queryCostCenterBreakdown,
622
- queryBillingSummary: () => queryBillingSummary,
623
- queryAgentBreakdownSince: () => queryAgentBreakdownSince,
624
- queryAgentBreakdown: () => queryAgentBreakdown,
625
- queryAccountBreakdownSince: () => queryAccountBreakdownSince,
626
- queryAccountBreakdown: () => queryAccountBreakdown,
627
- openDatabase: () => openDatabase,
628
- listSubscriptions: () => listSubscriptions,
629
- listProjects: () => listProjects,
630
- listModelPricing: () => listModelPricing,
631
- listMachines: () => listMachines,
632
- listMachineRegistry: () => listMachineRegistry,
633
- listGoals: () => listGoals,
634
- listBudgets: () => listBudgets,
635
- insertFeedback: () => insertFeedback,
636
- getSessionDetail: () => getSessionDetail,
637
- getProject: () => getProject,
638
- getModelPricing: () => getModelPricing,
639
- getMachineId: () => getMachineId,
640
- getIngestState: () => getIngestState,
641
- getGoalStatuses: () => getGoalStatuses,
642
- getDbPath: () => getDbPath,
643
- getDataDir: () => getDataDir,
644
- getCostCenter: () => getCostCenter,
645
- getBudgetStatuses: () => getBudgetStatuses,
646
- deleteSubscription: () => deleteSubscription,
647
- deleteProject: () => deleteProject,
648
- deleteModelPricing: () => deleteModelPricing,
649
- deleteGoal: () => deleteGoal,
650
- deleteBudget: () => deleteBudget,
651
- dedupeRequests: () => dedupeRequests,
652
- countBillingRecords: () => countBillingRecords,
653
- clearBillingRange: () => clearBillingRange,
654
- bulkIngest: () => bulkIngest
655
- });
656
594
  import { execFileSync } from "child_process";
657
595
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
658
596
  import { hostname, platform } from "os";
@@ -1066,9 +1004,6 @@ function upsertCostCenter(db, center) {
1066
1004
  VALUES (?, ?, ?, ?, ?, ?, ?)
1067
1005
  `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
1068
1006
  }
1069
- function getCostCenter(db, id) {
1070
- return db.prepare(`SELECT * FROM cost_centers WHERE id = ?`).get(id);
1071
- }
1072
1007
  function upsertRequest(db, req) {
1073
1008
  const now = req.updated_at ?? new Date().toISOString();
1074
1009
  db.prepare(`
@@ -2190,6 +2125,7 @@ function ingestSpecs() {
2190
2125
  "timestamp",
2191
2126
  "source_request_id",
2192
2127
  "machine_id",
2128
+ "cost_center_id",
2193
2129
  "attribution_tag",
2194
2130
  "account_key",
2195
2131
  "account_tool",
@@ -2216,6 +2152,7 @@ function ingestSpecs() {
2216
2152
  strOr(r["timestamp"]),
2217
2153
  r["source_request_id"] == null ? null : strOr(r["source_request_id"]),
2218
2154
  strOr(r["machine_id"]),
2155
+ r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
2219
2156
  strOr(r["attribution_tag"]),
2220
2157
  strOr(r["account_key"]),
2221
2158
  strOr(r["account_tool"]),
@@ -2240,6 +2177,7 @@ function ingestSpecs() {
2240
2177
  "total_tokens",
2241
2178
  "request_count",
2242
2179
  "machine_id",
2180
+ "cost_center_id",
2243
2181
  "attribution_tag",
2244
2182
  "account_key",
2245
2183
  "account_tool",
@@ -2260,6 +2198,7 @@ function ingestSpecs() {
2260
2198
  numOr(r["total_tokens"]),
2261
2199
  numOr(r["request_count"]),
2262
2200
  strOr(r["machine_id"]),
2201
+ r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
2263
2202
  strOr(r["attribution_tag"]),
2264
2203
  strOr(r["account_key"]),
2265
2204
  strOr(r["account_tool"]),
@@ -3441,448 +3380,14 @@ function queryModelEfficiency(db) {
3441
3380
  FROM requests GROUP BY model ORDER BY cost DESC`).all();
3442
3381
  }
3443
3382
 
3444
- // src/lib/contracts-client/mode.ts
3445
- function normalizeStorageMode(value) {
3446
- const normalized = value.trim().toLowerCase().replace(/-/g, "_");
3447
- if (normalized === "local")
3448
- return { mode: "local", deprecatedAlias: null };
3449
- if (normalized === "cloud")
3450
- return { mode: "cloud", deprecatedAlias: null };
3451
- if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
3452
- return { mode: "cloud", deprecatedAlias: normalized };
3453
- }
3454
- throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
3455
- }
3456
- function envToken(name) {
3457
- return name.toUpperCase().replace(/-/g, "_");
3458
- }
3459
- var DEPRECATED_STORAGE_MODE_ALIASES;
3460
- var init_mode = __esm(() => {
3461
- DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
3462
- });
3463
-
3464
- // src/lib/contracts-client/transport.ts
3465
- function defaultCloudBaseUrl(name) {
3466
- return `https://${name}.hasna.xyz`;
3467
- }
3468
- function clientTransportEnvKeys(name) {
3469
- const token = envToken(name);
3470
- return {
3471
- modeKeys: [
3472
- `HASNA_${token}_STORAGE_MODE`,
3473
- `HASNA_${token}_MODE`,
3474
- `${token}_STORAGE_MODE`,
3475
- `${token}_MODE`
3476
- ],
3477
- apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
3478
- apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
3479
- };
3480
- }
3481
- function firstEnv(env, keys) {
3482
- for (const key of keys) {
3483
- const value = env[key]?.trim();
3484
- if (value)
3485
- return { key, value };
3486
- }
3487
- return null;
3488
- }
3489
- function toV1BaseUrl(apiUrl) {
3490
- const url = new URL(apiUrl);
3491
- if (url.protocol !== "http:" && url.protocol !== "https:") {
3492
- throw new Error("API URL must use http or https.");
3493
- }
3494
- let path = url.pathname.replace(/\/+$/, "");
3495
- if (path.endsWith("/v1"))
3496
- path = path.slice(0, -"/v1".length);
3497
- url.pathname = `${path}/v1`;
3498
- url.search = "";
3499
- url.hash = "";
3500
- return url.toString().replace(/\/+$/, "");
3501
- }
3502
- function resolveClientTransport(name, env = process.env) {
3503
- const keys = clientTransportEnvKeys(name);
3504
- const modeHit = firstEnv(env, keys.modeKeys);
3505
- const urlHit = firstEnv(env, keys.apiUrlKeys);
3506
- const keyHit = firstEnv(env, keys.apiKeyKeys);
3507
- let mode = "local";
3508
- let deprecatedAlias = null;
3509
- let modeSource = "default";
3510
- const warnings = [];
3511
- if (modeHit) {
3512
- const normalized = normalizeStorageMode(modeHit.value);
3513
- mode = normalized.mode;
3514
- deprecatedAlias = normalized.deprecatedAlias;
3515
- modeSource = modeHit.key;
3516
- if (deprecatedAlias) {
3517
- warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
3518
- }
3519
- } else if (urlHit && keyHit) {
3520
- mode = "cloud";
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
- };
3536
- }
3537
- if (mode === "local") {
3538
- return {
3539
- transport: "local",
3540
- mode,
3541
- deprecatedAlias,
3542
- modeSource,
3543
- baseUrl: null,
3544
- apiUrlSource: null,
3545
- apiKeyPresent: Boolean(keyHit),
3546
- apiKeySource: keyHit ? keyHit.key : null,
3547
- misconfigured: false,
3548
- warning: warnings.length > 0 ? warnings.join(" ") : null
3549
- };
3550
- }
3551
- if (!keyHit) {
3552
- warnings.push(`${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud; using local store. Set ${keys.apiKeyKeys[0]} to enable the cloud client.`);
3553
- return {
3554
- transport: "local",
3555
- mode,
3556
- deprecatedAlias,
3557
- modeSource,
3558
- baseUrl: null,
3559
- apiUrlSource: null,
3560
- apiKeyPresent: false,
3561
- apiKeySource: null,
3562
- misconfigured: true,
3563
- warning: warnings.join(" ")
3564
- };
3565
- }
3566
- const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
3567
- const apiUrlSource = urlHit ? urlHit.key : "default";
3568
- let baseUrl;
3569
- try {
3570
- baseUrl = toV1BaseUrl(rawUrl);
3571
- } catch (error) {
3572
- const message = error instanceof Error ? error.message : String(error);
3573
- warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
3574
- return {
3575
- transport: "local",
3576
- mode,
3577
- deprecatedAlias,
3578
- modeSource,
3579
- baseUrl: null,
3580
- apiUrlSource: null,
3581
- apiKeyPresent: true,
3582
- apiKeySource: keyHit.key,
3583
- misconfigured: true,
3584
- warning: warnings.join(" ")
3585
- };
3586
- }
3587
- return {
3588
- transport: "cloud-http",
3589
- mode,
3590
- deprecatedAlias,
3591
- modeSource,
3592
- baseUrl,
3593
- apiUrlSource,
3594
- apiKeyPresent: true,
3595
- apiKeySource: keyHit.key,
3596
- misconfigured: false,
3597
- warning: warnings.length > 0 ? warnings.join(" ") : null
3598
- };
3599
- }
3600
- function appendQuery(path, query) {
3601
- if (!query)
3602
- return path;
3603
- const params = query instanceof URLSearchParams ? query : new URLSearchParams;
3604
- if (!(query instanceof URLSearchParams)) {
3605
- for (const [key, value] of Object.entries(query)) {
3606
- if (value === null || value === undefined)
3607
- continue;
3608
- if (Array.isArray(value)) {
3609
- for (const v of value)
3610
- params.append(key, String(v));
3611
- } else {
3612
- params.append(key, String(value));
3613
- }
3614
- }
3615
- }
3616
- const qs = params.toString();
3617
- if (!qs)
3618
- return path;
3619
- return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
3620
- }
3621
- function createHasnaHttpTransport(options) {
3622
- const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
3623
- const base = options.baseUrl.replace(/\/+$/, "");
3624
- const timeoutMs = options.timeoutMs ?? 30000;
3625
- const sleep = options.sleepImpl ?? defaultSleep;
3626
- const defaultRetry = options.retry;
3627
- function resolveRetry(callRetry) {
3628
- const chosen = callRetry !== undefined ? callRetry : defaultRetry;
3629
- if (chosen === false)
3630
- return null;
3631
- const r = chosen ?? {};
3632
- return {
3633
- retries: r.retries ?? 2,
3634
- baseDelayMs: r.baseDelayMs ?? 200,
3635
- maxDelayMs: r.maxDelayMs ?? 2000,
3636
- retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
3637
- };
3638
- }
3639
- async function once(method, rel, url, body, opts) {
3640
- const headers = {
3641
- "x-api-key": options.apiKey,
3642
- Authorization: `Bearer ${options.apiKey}`,
3643
- Accept: "application/json",
3644
- ...options.headers ?? {},
3645
- ...opts.headers ?? {}
3646
- };
3647
- if (opts.idempotencyKey)
3648
- headers["Idempotency-Key"] = opts.idempotencyKey;
3649
- const init = { method, headers };
3650
- if (body !== undefined) {
3651
- headers["Content-Type"] = "application/json";
3652
- init.body = JSON.stringify(body);
3653
- }
3654
- const controller = new AbortController;
3655
- const onAbort = () => controller.abort();
3656
- if (opts.signal) {
3657
- if (opts.signal.aborted)
3658
- controller.abort();
3659
- else
3660
- opts.signal.addEventListener("abort", onAbort, { once: true });
3661
- }
3662
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
3663
- init.signal = controller.signal;
3664
- let response;
3665
- try {
3666
- response = await fetchImpl(url, init);
3667
- } catch (error) {
3668
- const err = error instanceof Error ? error : new Error(String(error));
3669
- if (opts.signal?.aborted)
3670
- return { ok: false, retryable: false, error: err };
3671
- return { ok: false, retryable: true, error: err };
3672
- } finally {
3673
- clearTimeout(timer);
3674
- if (opts.signal)
3675
- opts.signal.removeEventListener("abort", onAbort);
3676
- }
3677
- const text = await response.text();
3678
- let parsed = undefined;
3679
- if (text.length > 0) {
3680
- try {
3681
- parsed = JSON.parse(text);
3682
- } catch {
3683
- parsed = text;
3684
- }
3685
- }
3686
- if (!response.ok) {
3687
- const retry = resolveRetry(opts.retry);
3688
- const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
3689
- return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
3690
- }
3691
- return { ok: true, value: parsed };
3692
- }
3693
- async function request(method, path, body, opts = {}) {
3694
- const upper = method.toUpperCase();
3695
- const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
3696
- const url = `${base}${rel}`;
3697
- const retry = resolveRetry(opts.retry);
3698
- const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
3699
- const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
3700
- let last = null;
3701
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3702
- const result = await once(upper, rel, url, body, opts);
3703
- if (result.ok)
3704
- return result.value;
3705
- last = result;
3706
- const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
3707
- if (!canRetry)
3708
- break;
3709
- const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
3710
- const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
3711
- await sleep(backoff + jitter);
3712
- }
3713
- throw last.error;
3714
- }
3715
- return {
3716
- baseUrl: base,
3717
- request,
3718
- get: (path, opts) => request("GET", path, undefined, opts),
3719
- post: (path, body, opts) => request("POST", path, body, opts),
3720
- put: (path, body, opts) => request("PUT", path, body, opts),
3721
- patch: (path, body, opts) => request("PATCH", path, body, opts),
3722
- del: (path, body, opts) => request("DELETE", path, body, opts)
3723
- };
3724
- }
3725
- function createClientTransport(name, env = process.env, overrides) {
3726
- const resolution = resolveClientTransport(name, env);
3727
- if (resolution.misconfigured) {
3728
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
3729
- }
3730
- if (resolution.transport === "local" || !resolution.baseUrl) {
3731
- return { transport: "local", client: null, resolution };
3732
- }
3733
- const keys = clientTransportEnvKeys(name);
3734
- const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
3735
- if (!apiKey) {
3736
- throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
3737
- }
3738
- return {
3739
- transport: "cloud-http",
3740
- client: createHasnaHttpTransport({
3741
- name,
3742
- baseUrl: resolution.baseUrl,
3743
- apiKey,
3744
- ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
3745
- ...overrides?.headers ? { headers: overrides.headers } : {},
3746
- ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
3747
- ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
3748
- ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
3749
- }),
3750
- resolution
3751
- };
3752
- }
3753
- var HasnaHttpError, DEFAULT_RETRY_STATUSES, IDEMPOTENT_METHODS, defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3754
- var init_transport = __esm(() => {
3755
- init_mode();
3756
- HasnaHttpError = class HasnaHttpError extends Error {
3757
- status;
3758
- method;
3759
- path;
3760
- body;
3761
- constructor(method, path, status, body) {
3762
- super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
3763
- this.name = "HasnaHttpError";
3764
- this.status = status;
3765
- this.method = method;
3766
- this.path = path;
3767
- this.body = body;
3768
- }
3769
- };
3770
- DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
3771
- IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
3772
- });
3773
-
3774
3383
  // src/lib/contracts-client/storage.ts
3775
- function resourcePath(resource) {
3776
- const trimmed = resource.replace(/^\/+|\/+$/g, "");
3777
- if (!trimmed)
3778
- throw new Error("resource must be a non-empty path segment");
3779
- return `/${trimmed}`;
3780
- }
3781
- function entityPath(resource, id) {
3782
- if (id === undefined || id === null || `${id}`.length === 0) {
3783
- throw new Error("id must be a non-empty string");
3784
- }
3785
- return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
3786
- }
3787
- function newIdempotencyKey() {
3788
- const g = globalThis;
3789
- if (g.crypto?.randomUUID)
3790
- return g.crypto.randomUUID();
3791
- return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
3792
- }
3793
- function extractItems(raw) {
3794
- if (Array.isArray(raw))
3795
- return raw;
3796
- if (raw && typeof raw === "object") {
3797
- const obj = raw;
3798
- for (const key of ["items", "data", "results", "rows", "records"]) {
3799
- if (Array.isArray(obj[key]))
3800
- return obj[key];
3801
- }
3802
- }
3803
- return [];
3804
- }
3805
- function extractTotal(raw) {
3806
- if (raw && typeof raw === "object") {
3807
- const obj = raw;
3808
- for (const key of ["total", "count", "totalCount", "total_count"]) {
3809
- if (typeof obj[key] === "number")
3810
- return obj[key];
3811
- }
3812
- }
3813
- return null;
3814
- }
3815
- function extractCursor(raw) {
3816
- if (raw && typeof raw === "object") {
3817
- const obj = raw;
3818
- for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
3819
- if (typeof obj[key] === "string")
3820
- return obj[key];
3821
- }
3822
- }
3823
- return null;
3824
- }
3825
- function createHasnaStorageClient(name, transport) {
3826
- return {
3827
- name,
3828
- baseUrl: transport.baseUrl,
3829
- transport,
3830
- async list(resource, options = {}) {
3831
- const raw = await transport.get(resourcePath(resource), options);
3832
- return {
3833
- items: extractItems(raw),
3834
- total: extractTotal(raw),
3835
- cursor: extractCursor(raw),
3836
- raw
3837
- };
3838
- },
3839
- async get(resource, id, options = {}) {
3840
- try {
3841
- return await transport.get(entityPath(resource, id), options);
3842
- } catch (error) {
3843
- if (error instanceof HasnaHttpError && error.status === 404)
3844
- return null;
3845
- throw error;
3846
- }
3847
- },
3848
- async create(resource, body, options = {}) {
3849
- const { idempotencyKey, ...rest } = options;
3850
- return transport.post(resourcePath(resource), body, {
3851
- ...rest,
3852
- idempotencyKey: idempotencyKey ?? newIdempotencyKey()
3853
- });
3854
- },
3855
- async update(resource, id, patch, options = {}) {
3856
- const { method = "PATCH", idempotencyKey, ...rest } = options;
3857
- const call = method === "PUT" ? transport.put : transport.patch;
3858
- return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
3859
- },
3860
- async delete(resource, id, options = {}) {
3861
- try {
3862
- await transport.del(entityPath(resource, id), undefined, options);
3863
- } catch (error) {
3864
- if (error instanceof HasnaHttpError && error.status === 404)
3865
- return;
3866
- throw error;
3867
- }
3868
- }
3869
- };
3870
- }
3871
- function resolveStorageClient(name, env = process.env, overrides) {
3872
- const wired = createClientTransport(name, env, overrides);
3873
- if (wired.transport === "cloud-http") {
3874
- return { transport: "cloud-http", client: createHasnaStorageClient(name, wired.client) };
3875
- }
3876
- return { transport: "local", client: null };
3877
- }
3878
- var init_storage = __esm(() => {
3879
- init_transport();
3880
- });
3384
+ import { resolveStorageClient } from "@hasna/contracts/client/storage";
3385
+ var init_storage = () => {};
3881
3386
 
3882
3387
  // src/lib/cloud-storage.ts
3883
3388
  function resolveEconomyCloudStorage(env = process.env, overrides) {
3884
3389
  const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
3885
- return resolved.transport === "cloud-http" ? { active: true, client: resolved.client } : { active: false, client: null };
3390
+ return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
3886
3391
  }
3887
3392
  function economyCloudStorage(env = process.env) {
3888
3393
  if (cache === undefined)
@@ -6144,11 +5649,11 @@ async function syncAll(db, opts = {}) {
6144
5649
  const all = !anySpecific;
6145
5650
  const result = { deduped: 0 };
6146
5651
  if (all || opts.claude) {
6147
- result.claude = await ingestClaude(db, opts.verbose);
5652
+ result.claude = await ingestClaude(db, opts.verbose, opts.projectsDir);
6148
5653
  result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
6149
5654
  }
6150
5655
  if (all || opts.takumi)
6151
- result.takumi = await ingestTakumi(db, opts.verbose);
5656
+ result.takumi = await ingestTakumi(db, opts.verbose, opts.projectsDir);
6152
5657
  if (all || opts.codex) {
6153
5658
  result.codex = await ingestCodex(db, opts.verbose);
6154
5659
  result.codexQuota = await ingestCodexQuota(db, opts.verbose);
@@ -6451,7 +5956,7 @@ __export(exports_open_projects, {
6451
5956
  async function syncOpenProjectsRegistry(db, listActiveProjects) {
6452
5957
  let listProjects2 = listActiveProjects;
6453
5958
  if (!listProjects2) {
6454
- const projectsApi = await import("@hasna/projects");
5959
+ const projectsApi = await import(PROJECTS_MODULE);
6455
5960
  listProjects2 = projectsApi.listProjects;
6456
5961
  }
6457
5962
  const projects = listProjects2({ status: "active", limit: 5000 });
@@ -6475,7 +5980,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
6475
5980
  }
6476
5981
  return { imported, skipped };
6477
5982
  }
6478
- var resolveProjectPath = (project) => project.primary_path || project.path || "";
5983
+ var resolveProjectPath = (project) => project.primary_path || project.path || "", PROJECTS_MODULE = "@hasna/projects";
6479
5984
  var init_open_projects = __esm(() => {
6480
5985
  init_database();
6481
5986
  });
@@ -6489,9 +5994,9 @@ __export(exports_config, {
6489
5994
  getConfigValue: () => getConfigValue
6490
5995
  });
6491
5996
  import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
6492
- import { dirname as dirname2, join as join13 } from "path";
5997
+ import { dirname as dirname2, join as join14 } from "path";
6493
5998
  function getConfigPath() {
6494
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
5999
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join14(getDataDir(), "config.json");
6495
6000
  }
6496
6001
  function loadConfig2() {
6497
6002
  try {
@@ -6880,7 +6385,7 @@ function sqliteToPostgres(sql) {
6880
6385
  // src/db/sync-pg.ts
6881
6386
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
6882
6387
  import { fileURLToPath } from "url";
6883
- import { dirname as dirname3, join as join14 } from "path";
6388
+ import { dirname as dirname3, join as join15 } from "path";
6884
6389
  import { existsSync as existsSync15 } from "fs";
6885
6390
  function translateSqliteDates(sql) {
6886
6391
  let s = sql;
@@ -6906,9 +6411,9 @@ function translateSqliteDates(sql) {
6906
6411
  function resolveWorkerPath() {
6907
6412
  const here = dirname3(fileURLToPath(import.meta.url));
6908
6413
  const candidates = [
6909
- join14(here, "pg-sync-worker.ts"),
6910
- join14(here, "pg-sync-worker.js"),
6911
- join14(here, "..", "db", "pg-sync-worker.js")
6414
+ join15(here, "pg-sync-worker.ts"),
6415
+ join15(here, "pg-sync-worker.js"),
6416
+ join15(here, "..", "db", "pg-sync-worker.js")
6912
6417
  ];
6913
6418
  for (const candidate of candidates)
6914
6419
  if (existsSync15(candidate))
@@ -7014,13 +6519,18 @@ var init_pg_migrate = () => {};
7014
6519
  var init_pg_migrations = () => {};
7015
6520
 
7016
6521
  // src/db/cloud.ts
7017
- import { assertNoLegacyStorageMode } from "@hasna/contracts/server-backend";
7018
6522
  import pg2 from "pg";
6523
+ function assertNoRetiredBackendKey(env) {
6524
+ const legacyKey = RETIRED_BACKEND_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
6525
+ if (!legacyKey)
6526
+ return;
6527
+ throw new Error(`${legacyKey} was removed. Delete the retired variable; ` + "set HASNA_ECONOMY_DATABASE_URL to select the postgresql server backend, " + "or leave it unset for sqlite.");
6528
+ }
7019
6529
  function getCloudDatabaseUrl(env = process.env) {
7020
6530
  return env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || env["ECONOMY_DATABASE_URL"]?.trim() || env["DATABASE_URL"]?.trim() || undefined;
7021
6531
  }
7022
6532
  function resolveEconomyServerBackend(env = process.env) {
7023
- assertNoLegacyStorageMode("economy", env);
6533
+ assertNoRetiredBackendKey(env);
7024
6534
  return getCloudDatabaseUrl(env) ? "postgresql" : "sqlite";
7025
6535
  }
7026
6536
  function isPostgresBackend(env = process.env) {
@@ -7059,10 +6569,17 @@ function authClientFromPool(pool) {
7059
6569
  }
7060
6570
  };
7061
6571
  }
6572
+ var RETIRED_BACKEND_KEYS;
7062
6573
  var init_cloud = __esm(() => {
7063
6574
  init_sync_pg();
7064
6575
  init_pg_migrate();
7065
6576
  init_pg_migrations();
6577
+ RETIRED_BACKEND_KEYS = [
6578
+ "HASNA_ECONOMY_STORAGE_MODE",
6579
+ "HASNA_ECONOMY_MODE",
6580
+ "ECONOMY_STORAGE_MODE",
6581
+ "ECONOMY_MODE"
6582
+ ];
7066
6583
  });
7067
6584
 
7068
6585
  // src/openapi.ts
@@ -7076,13 +6593,9 @@ var init_openapi = __esm(() => {
7076
6593
  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."
7077
6594
  },
7078
6595
  servers: [
7079
- {
7080
- url: "https://economy.hasna.xyz",
7081
- description: "prod"
7082
- },
7083
6596
  {
7084
6597
  url: "http://localhost:3456",
7085
- description: "local"
6598
+ description: "local (self-hosted default; set your own URL when deploying)"
7086
6599
  }
7087
6600
  ],
7088
6601
  security: [
@@ -9101,7 +8614,24 @@ function createHandler(db, options = {}) {
9101
8614
  const body = await jsonBody(req);
9102
8615
  if (!body)
9103
8616
  return err("invalid JSON body");
9104
- return ok(bulkIngest(db, body));
8617
+ const result = bulkIngest(db, body);
8618
+ const reqRows = Array.isArray(body?.["requests"]) ? body["requests"] : [];
8619
+ const touchedSessions = new Set;
8620
+ for (const r of reqRows) {
8621
+ const sid = r?.["session_id"];
8622
+ if (typeof sid === "string" && sid)
8623
+ touchedSessions.add(sid);
8624
+ }
8625
+ for (const sid of touchedSessions) {
8626
+ const hasReq = db.prepare(`SELECT COUNT(*) AS c FROM requests WHERE session_id = ?`).get(sid);
8627
+ if (Number(hasReq?.c ?? 0) > 0)
8628
+ rollupSession(db, sid);
8629
+ }
8630
+ try {
8631
+ const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
8632
+ await checkAndFireWebhooks2(db);
8633
+ } catch {}
8634
+ return ok(result);
9105
8635
  }
9106
8636
  if (path === "/api/usage" && method === "GET") {
9107
8637
  const period = url.searchParams.get("period") ?? "month";
@@ -9290,7 +8820,7 @@ function startServer(port = 3456, options = {}) {
9290
8820
  handlerOptions.authenticator = verifyApiKey({
9291
8821
  app: APP,
9292
8822
  signingSecret,
9293
- isRevoked: keys.statusChecker(),
8823
+ keyStatus: keys.keyStatus,
9294
8824
  audit: (event) => log(JSON.stringify({ evt: "api_auth", outcome: event.outcome, kid: event.kid, reason: event.reason, path: event.path, status: event.status }))
9295
8825
  });
9296
8826
  } else if (!isLocalHost(hostname2)) {
@@ -9357,7 +8887,7 @@ import chalk6 from "chalk";
9357
8887
  import { execFileSync as execFileSync3 } from "child_process";
9358
8888
  import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
9359
8889
  import { tmpdir, arch } from "os";
9360
- import { join as join15 } from "path";
8890
+ import { join as join16 } from "path";
9361
8891
  function getArch() {
9362
8892
  return arch() === "arm64" ? "arm64" : "x86_64";
9363
8893
  }
@@ -9399,8 +8929,8 @@ async function menubarInstall(opts) {
9399
8929
  console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
9400
8930
  process.exit(1);
9401
8931
  }
9402
- const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
9403
- const extractDir = join15(tmpdir(), "economy-bar-extracted");
8932
+ const zipPath = join16(tmpdir(), `economy-bar-${cpuArch}.zip`);
8933
+ const extractDir = join16(tmpdir(), "economy-bar-extracted");
9404
8934
  console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
9405
8935
  try {
9406
8936
  const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
@@ -9420,7 +8950,7 @@ async function menubarInstall(opts) {
9420
8950
  execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
9421
8951
  if (isInstalled())
9422
8952
  rmSync(APP_PATH, { recursive: true, force: true });
9423
- cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
8953
+ cpSync(join16(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
9424
8954
  try {
9425
8955
  execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
9426
8956
  } catch {}
@@ -11075,7 +10605,7 @@ function registerExtendedCommands(program) {
11075
10605
  console.log(" 7. Linux status: economy tui --watch | economy waybar");
11076
10606
  console.log();
11077
10607
  console.log(" Self-hosted/cloud mode (shared cloud API \u2014 reads/writes route to it):");
11078
- console.log(" a. API URL: export HASNA_ECONOMY_API_URL=https://economy.hasna.xyz/v1");
10608
+ console.log(" a. API URL: export HASNA_ECONOMY_API_URL=https://your-economy-host.example.com/v1");
11079
10609
  console.log(" b. API key: export HASNA_ECONOMY_API_KEY=<bearer key> (never a DB DSN)");
11080
10610
  console.log();
11081
10611
  });
@@ -11296,14 +10826,171 @@ init_billing();
11296
10826
  init_package_metadata();
11297
10827
  init_pricing();
11298
10828
  init_store();
11299
- init_billing_diff();
11300
- init_transport();
11301
10829
  import { execSync } from "child_process";
10830
+
10831
+ // src/lib/cloud-ingest.ts
10832
+ init_database();
10833
+ init_pricing();
10834
+ init_sync_all();
10835
+ init_billing();
10836
+ import { join as join13 } from "path";
10837
+
10838
+ // src/lib/sync-maintenance.ts
10839
+ init_database();
10840
+ function backfillMachineId(db) {
10841
+ const machine = getMachineId();
10842
+ const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
10843
+ const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
10844
+ return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
10845
+ }
10846
+ async function recalculateZeroCostRequests(db) {
10847
+ const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
10848
+ const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens
10849
+ FROM requests
10850
+ WHERE cost_usd = 0
10851
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
10852
+ let fixed = 0;
10853
+ for (const r of zeroRows) {
10854
+ const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
10855
+ const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
10856
+ if (cost > 0) {
10857
+ db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
10858
+ fixed++;
10859
+ }
10860
+ }
10861
+ if (fixed > 0) {
10862
+ const touchedSessions = new Set(zeroRows.map((r) => {
10863
+ const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
10864
+ return row?.session_id;
10865
+ }).filter(Boolean));
10866
+ for (const sid of touchedSessions)
10867
+ rollupSession(db, sid);
10868
+ }
10869
+ return { fixed, total: zeroRows.length, buckets: queryZeroCostTokenizedModels(db, 8) };
10870
+ }
10871
+
10872
+ // src/lib/cloud-ingest.ts
10873
+ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
10874
+ function getIngestCachePath() {
10875
+ if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
10876
+ return process.env["HASNA_ECONOMY_INGEST_CACHE"];
10877
+ return join13(getDataDir(), "ingest-cache.db");
10878
+ }
10879
+ function copyIngestState(from, to) {
10880
+ const rows = from.prepare(`SELECT source, key, value FROM ingest_state`).all();
10881
+ to.prepare(`DELETE FROM ingest_state`).run();
10882
+ const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
10883
+ for (const row of rows)
10884
+ insert.run(row.source, row.key, row.value);
10885
+ }
10886
+ function exportIngestRows(db) {
10887
+ const body = {};
10888
+ for (const table2 of CLOUD_INGEST_TABLES) {
10889
+ const rows = db.prepare(`SELECT * FROM ${table2}`).all();
10890
+ if (rows.length > 0)
10891
+ body[table2] = rows;
10892
+ }
10893
+ return body;
10894
+ }
10895
+ function unwrapEnvelope(raw) {
10896
+ if (raw && typeof raw === "object" && "data" in raw) {
10897
+ const data = raw.data;
10898
+ return data && typeof data === "object" ? data : {};
10899
+ }
10900
+ return raw ?? {};
10901
+ }
10902
+ async function postIngest(cloud, body) {
10903
+ const raw = await cloud.client.transport.post("/ingest", body);
10904
+ const payload = unwrapEnvelope(raw);
10905
+ return {
10906
+ ingested: payload["ingested"] ?? {},
10907
+ total: Number(payload["total"] ?? 0)
10908
+ };
10909
+ }
10910
+ async function syncAllToCloud(cloud, opts = {}) {
10911
+ const scratch = openDatabase(":memory:", true);
10912
+ ensurePricingSeeded(scratch);
10913
+ const cache2 = openDatabase(opts.cachePath ?? getIngestCachePath(), true);
10914
+ try {
10915
+ if (opts.force)
10916
+ cache2.prepare(`DELETE FROM ingest_state`).run();
10917
+ copyIngestState(cache2, scratch);
10918
+ const result = await syncAll(scratch, opts);
10919
+ if (opts.backfillMachine)
10920
+ backfillMachineId(scratch);
10921
+ if (opts.recalculate)
10922
+ await recalculateZeroCostRequests(scratch);
10923
+ const body = exportIngestRows(scratch);
10924
+ if (Object.keys(body).length === 0) {
10925
+ copyIngestState(scratch, cache2);
10926
+ return { ...result, posted: false };
10927
+ }
10928
+ const response = await postIngest(cloud, body);
10929
+ copyIngestState(scratch, cache2);
10930
+ return { ...result, posted: true, ingested: response.ingested, total: response.total };
10931
+ } finally {
10932
+ try {
10933
+ scratch.close();
10934
+ } catch {}
10935
+ try {
10936
+ cache2.close();
10937
+ } catch {}
10938
+ }
10939
+ }
10940
+ async function billingSyncToCloud(cloud, opts = {}) {
10941
+ const scratch = openDatabase(":memory:", true);
10942
+ try {
10943
+ const days = opts.days ?? 31;
10944
+ const providers = {};
10945
+ const attempts = [
10946
+ ["anthropic", () => syncAnthropicBilling(scratch, { days })],
10947
+ ["openai", () => syncOpenAIBilling(scratch, { days })],
10948
+ ["gemini", () => syncGeminiBilling(scratch, { days })]
10949
+ ];
10950
+ const anySelected = opts.anthropic || opts.openai || opts.gemini;
10951
+ let totalUsd = 0;
10952
+ for (const [provider, run] of attempts) {
10953
+ if (anySelected && !opts[provider])
10954
+ continue;
10955
+ {
10956
+ try {
10957
+ const r = await run();
10958
+ providers[provider] = { ok: true, days: r.days, totalUsd: r.totalUsd };
10959
+ totalUsd += r.totalUsd;
10960
+ } catch (e) {
10961
+ providers[provider] = { ok: false, error: e instanceof Error ? e.message : String(e) };
10962
+ }
10963
+ }
10964
+ }
10965
+ const body = exportIngestRows(scratch);
10966
+ if (Object.keys(body).length === 0)
10967
+ return { providers, totalUsd, posted: false };
10968
+ const response = await postIngest(cloud, body);
10969
+ return { providers, totalUsd, posted: true, ingested: response.ingested, total: response.total };
10970
+ } finally {
10971
+ try {
10972
+ scratch.close();
10973
+ } catch {}
10974
+ }
10975
+ }
10976
+
10977
+ // src/cli/index.ts
10978
+ init_cloud_storage();
10979
+ init_billing_diff();
10980
+
10981
+ // src/lib/contracts-client/transport.ts
10982
+ import { HasnaHttpError } from "@hasna/contracts/client";
10983
+
10984
+ // src/cli/index.ts
11302
10985
  var program = new Command;
11303
10986
  program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
11304
10987
  async function autoSync(opts = {}) {
11305
- if (isCloudStore())
10988
+ if (isCloudStore()) {
10989
+ const cloud = economyCloudStorage();
10990
+ if (cloud.active)
10991
+ await syncAllToCloud(cloud, opts);
11306
10992
  return;
10993
+ }
11307
10994
  const db = openDatabase();
11308
10995
  ensurePricingSeeded(db);
11309
10996
  await syncAll(db, opts);
@@ -11518,7 +11205,34 @@ program.action(async () => {
11518
11205
  });
11519
11206
  program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("--loops", "Only ingest OpenLoops orchestration/judge token usage").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
11520
11207
  if (isCloudStore()) {
11521
- console.log(chalk7.yellow("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)"));
11208
+ const cloud = economyCloudStorage();
11209
+ if (!cloud.active) {
11210
+ console.log(chalk7.yellow("cloud mode: sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
11211
+ return;
11212
+ }
11213
+ const result2 = await syncAllToCloud(cloud, {
11214
+ claude: opts.claude,
11215
+ takumi: opts.takumi,
11216
+ codex: opts.codex,
11217
+ gemini: opts.gemini,
11218
+ opencode: opts.opencode,
11219
+ cursor: opts.cursor,
11220
+ pi: opts.pi,
11221
+ hermes: opts.hermes,
11222
+ loops: opts.loops,
11223
+ verbose: opts.verbose,
11224
+ force: opts.force,
11225
+ backfillMachine: opts.backfillMachine,
11226
+ recalculate: opts.recalculate
11227
+ });
11228
+ if (result2.posted) {
11229
+ const parts = Object.entries(result2.ingested ?? {}).filter(([, n]) => n > 0).map(([table2, n]) => `${n} ${table2}`);
11230
+ console.log(chalk7.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
11231
+ } else {
11232
+ console.log(chalk7.dim("\u2713 no new provider data to push (mtime cache current)"));
11233
+ }
11234
+ console.log(chalk7.bold.green(`
11235
+ \u2713 Sync complete`));
11522
11236
  return;
11523
11237
  }
11524
11238
  const db = openDatabase();
@@ -11557,34 +11271,14 @@ program.command("sync").description("Ingest cost data from all coding agents").o
11557
11271
  console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
11558
11272
  if (opts.backfillMachine) {
11559
11273
  const machine = getMachineId();
11560
- const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
11561
- const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
11562
- console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
11274
+ const r = backfillMachineId(db);
11275
+ console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${r.requests} requests, ${r.sessions} sessions`));
11563
11276
  }
11564
11277
  if (opts.recalculate) {
11565
- const { computeCostFromDb: computeCostFromDb2, getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
11566
- const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
11567
- let fixed = 0;
11568
- for (const r of zeroRows) {
11569
- const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
11570
- const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
11571
- if (cost > 0) {
11572
- db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
11573
- fixed++;
11574
- }
11575
- }
11576
- if (fixed > 0) {
11577
- const touchedSessions = new Set(zeroRows.map((r) => {
11578
- const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
11579
- return row?.session_id;
11580
- }).filter(Boolean));
11581
- const { rollupSession: rollupSession2 } = await Promise.resolve().then(() => (init_database(), exports_database));
11582
- for (const sid of touchedSessions) {
11583
- rollupSession2(db, sid);
11584
- }
11585
- }
11586
- console.log(chalk7.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
11587
- const zeroCostBuckets = queryZeroCostTokenizedModels(db, 8);
11278
+ const { getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
11279
+ const r = await recalculateZeroCostRequests(db);
11280
+ console.log(chalk7.cyan(`\u2192 Recalculated: ${r.fixed}/${r.total} zero-cost requests now have pricing`));
11281
+ const zeroCostBuckets = r.buckets;
11588
11282
  if (zeroCostBuckets.length > 0) {
11589
11283
  console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
11590
11284
  for (const row of zeroCostBuckets) {
@@ -12410,13 +12104,26 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
12410
12104
  });
12411
12105
  var billingCmd = program.command("billing").description("Pull actual billing from provider billing sources (ground truth)");
12412
12106
  billingCmd.command("sync").description("Sync actual billing from Anthropic, OpenAI, and Gemini billing sources").option("--days <n>", "Days of history to fetch", "31").option("--anthropic", "Only sync Anthropic").option("--openai", "Only sync OpenAI").option("--gemini", "Only sync Gemini").action(async (opts) => {
12413
- if (isCloudStore()) {
12414
- console.log(chalk7.yellow("cloud mode: billing ingest is a local-only operation; nothing to sync (reads route to the cloud API)"));
12415
- return;
12416
- }
12417
12107
  const days = parsePositiveCliInteger(opts.days ?? "31", "--days");
12418
12108
  if (days > 366)
12419
12109
  fail("--days must be between 1 and 366");
12110
+ if (isCloudStore()) {
12111
+ const cloud = economyCloudStorage();
12112
+ if (!cloud.active) {
12113
+ console.log(chalk7.yellow("cloud mode: billing sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
12114
+ return;
12115
+ }
12116
+ const result = await billingSyncToCloud(cloud, { days, anthropic: opts.anthropic, openai: opts.openai, gemini: opts.gemini });
12117
+ for (const [provider, p] of Object.entries(result.providers)) {
12118
+ if (p.ok)
12119
+ console.log(chalk7.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
12120
+ else
12121
+ console.log(chalk7.red(`\u2717 ${provider}: ${p.error}`));
12122
+ }
12123
+ if (result.posted)
12124
+ console.log(chalk7.green(`\u2713 pushed ${result.total} billing rows to the shared API`));
12125
+ return;
12126
+ }
12420
12127
  const db = openDatabase();
12421
12128
  const doAll = !opts.anthropic && !opts.openai && !opts.gemini;
12422
12129
  if (opts.anthropic || doAll) {