@hasna/economy 0.3.10 → 0.3.12

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
@@ -76,15 +76,15 @@ var init_sqlite_adapter = () => {};
76
76
  // src/lib/pricing.ts
77
77
  var exports_pricing = {};
78
78
  __export(exports_pricing, {
79
- normalizeModelName: () => normalizeModelName,
80
- getPricingFromRows: () => getPricingFromRows,
81
- getPricingFromDb: () => getPricingFromDb,
82
- getPricing: () => getPricing,
83
- estimateCostFromRows: () => estimateCostFromRows,
84
- ensurePricingSeeded: () => ensurePricingSeeded,
85
- computeCostFromDb: () => computeCostFromDb,
79
+ DEFAULT_PRICING: () => DEFAULT_PRICING,
86
80
  computeCost: () => computeCost,
87
- DEFAULT_PRICING: () => DEFAULT_PRICING
81
+ computeCostFromDb: () => computeCostFromDb,
82
+ ensurePricingSeeded: () => ensurePricingSeeded,
83
+ estimateCostFromRows: () => estimateCostFromRows,
84
+ getPricing: () => getPricing,
85
+ getPricingFromDb: () => getPricingFromDb,
86
+ getPricingFromRows: () => getPricingFromRows,
87
+ normalizeModelName: () => normalizeModelName
88
88
  });
89
89
  function normalizeModelName(raw) {
90
90
  return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
@@ -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)
@@ -4340,12 +3845,12 @@ function isAuthorizedRequest(req, path) {
4340
3845
  // src/cli/commands/tui.ts
4341
3846
  var exports_tui = {};
4342
3847
  __export(exports_tui, {
4343
- runTui: () => runTui,
4344
- printWaybarJson: () => printWaybarJson,
4345
- printStatusLine: () => printStatusLine,
4346
- gatherStatusData: () => gatherStatusData,
3848
+ buildStatusLine: () => buildStatusLine,
4347
3849
  buildWaybarJson: () => buildWaybarJson,
4348
- buildStatusLine: () => buildStatusLine
3850
+ gatherStatusData: () => gatherStatusData,
3851
+ printStatusLine: () => printStatusLine,
3852
+ printWaybarJson: () => printWaybarJson,
3853
+ runTui: () => runTui
4349
3854
  });
4350
3855
  import chalk3 from "chalk";
4351
3856
  function fmt(usd) {
@@ -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);
@@ -6186,9 +5691,9 @@ var init_sync_all = __esm(() => {
6186
5691
  // src/ingest/billing.ts
6187
5692
  var exports_billing = {};
6188
5693
  __export(exports_billing, {
6189
- syncOpenAIBilling: () => syncOpenAIBilling,
5694
+ syncAnthropicBilling: () => syncAnthropicBilling,
6190
5695
  syncGeminiBilling: () => syncGeminiBilling,
6191
- syncAnthropicBilling: () => syncAnthropicBilling
5696
+ syncOpenAIBilling: () => syncOpenAIBilling
6192
5697
  });
6193
5698
  import { readFileSync as readFileSync10 } from "fs";
6194
5699
  function getAnthropicAdminKey() {
@@ -6483,15 +5988,15 @@ var init_open_projects = __esm(() => {
6483
5988
  // src/lib/config.ts
6484
5989
  var exports_config = {};
6485
5990
  __export(exports_config, {
6486
- setConfigValue: () => setConfigValue,
6487
- saveConfig: () => saveConfig2,
5991
+ getConfigValue: () => getConfigValue,
6488
5992
  loadConfig: () => loadConfig2,
6489
- getConfigValue: () => getConfigValue
5993
+ saveConfig: () => saveConfig2,
5994
+ setConfigValue: () => setConfigValue
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
@@ -8709,10 +8226,10 @@ var init_openapi = __esm(() => {
8709
8226
  // src/server/serve.ts
8710
8227
  var exports_serve = {};
8711
8228
  __export(exports_serve, {
8712
- startServer: () => startServer,
8713
- openApiDocument: () => openApiDocument,
8229
+ createHandler: () => createHandler,
8714
8230
  createServerFetch: () => createServerFetch,
8715
- createHandler: () => createHandler
8231
+ openApiDocument: () => openApiDocument,
8232
+ startServer: () => startServer
8716
8233
  });
8717
8234
  import { randomUUID as randomUUID2 } from "crypto";
8718
8235
  import { existsSync as existsSync16 } from "fs";
@@ -9101,7 +8618,24 @@ function createHandler(db, options = {}) {
9101
8618
  const body = await jsonBody(req);
9102
8619
  if (!body)
9103
8620
  return err("invalid JSON body");
9104
- return ok(bulkIngest(db, body));
8621
+ const result = bulkIngest(db, body);
8622
+ const reqRows = Array.isArray(body?.["requests"]) ? body["requests"] : [];
8623
+ const touchedSessions = new Set;
8624
+ for (const r of reqRows) {
8625
+ const sid = r?.["session_id"];
8626
+ if (typeof sid === "string" && sid)
8627
+ touchedSessions.add(sid);
8628
+ }
8629
+ for (const sid of touchedSessions) {
8630
+ const hasReq = db.prepare(`SELECT COUNT(*) AS c FROM requests WHERE session_id = ?`).get(sid);
8631
+ if (Number(hasReq?.c ?? 0) > 0)
8632
+ rollupSession(db, sid);
8633
+ }
8634
+ try {
8635
+ const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
8636
+ await checkAndFireWebhooks2(db);
8637
+ } catch {}
8638
+ return ok(result);
9105
8639
  }
9106
8640
  if (path === "/api/usage" && method === "GET") {
9107
8641
  const period = url.searchParams.get("period") ?? "month";
@@ -9348,16 +8882,16 @@ var init_serve = __esm(() => {
9348
8882
  // src/cli/commands/menubar.ts
9349
8883
  var exports_menubar = {};
9350
8884
  __export(exports_menubar, {
9351
- menubarUninstall: () => menubarUninstall,
9352
- menubarStop: () => menubarStop,
8885
+ menubarInstall: () => menubarInstall,
9353
8886
  menubarStart: () => menubarStart,
9354
- menubarInstall: () => menubarInstall
8887
+ menubarStop: () => menubarStop,
8888
+ menubarUninstall: () => menubarUninstall
9355
8889
  });
9356
8890
  import chalk6 from "chalk";
9357
8891
  import { execFileSync as execFileSync3 } from "child_process";
9358
8892
  import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
9359
8893
  import { tmpdir, arch } from "os";
9360
- import { join as join15 } from "path";
8894
+ import { join as join16 } from "path";
9361
8895
  function getArch() {
9362
8896
  return arch() === "arm64" ? "arm64" : "x86_64";
9363
8897
  }
@@ -9399,8 +8933,8 @@ async function menubarInstall(opts) {
9399
8933
  console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
9400
8934
  process.exit(1);
9401
8935
  }
9402
- const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
9403
- const extractDir = join15(tmpdir(), "economy-bar-extracted");
8936
+ const zipPath = join16(tmpdir(), `economy-bar-${cpuArch}.zip`);
8937
+ const extractDir = join16(tmpdir(), "economy-bar-extracted");
9404
8938
  console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
9405
8939
  try {
9406
8940
  const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
@@ -9420,7 +8954,7 @@ async function menubarInstall(opts) {
9420
8954
  execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
9421
8955
  if (isInstalled())
9422
8956
  rmSync(APP_PATH, { recursive: true, force: true });
9423
- cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
8957
+ cpSync(join16(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
9424
8958
  try {
9425
8959
  execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
9426
8960
  } catch {}
@@ -11296,14 +10830,171 @@ init_billing();
11296
10830
  init_package_metadata();
11297
10831
  init_pricing();
11298
10832
  init_store();
11299
- init_billing_diff();
11300
- init_transport();
11301
10833
  import { execSync } from "child_process";
10834
+
10835
+ // src/lib/cloud-ingest.ts
10836
+ init_database();
10837
+ init_pricing();
10838
+ init_sync_all();
10839
+ init_billing();
10840
+ import { join as join13 } from "path";
10841
+
10842
+ // src/lib/sync-maintenance.ts
10843
+ init_database();
10844
+ function backfillMachineId(db) {
10845
+ const machine = getMachineId();
10846
+ const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
10847
+ const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
10848
+ return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
10849
+ }
10850
+ async function recalculateZeroCostRequests(db) {
10851
+ const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
10852
+ 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
10853
+ FROM requests
10854
+ WHERE cost_usd = 0
10855
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
10856
+ let fixed = 0;
10857
+ for (const r of zeroRows) {
10858
+ const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
10859
+ const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
10860
+ if (cost > 0) {
10861
+ db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
10862
+ fixed++;
10863
+ }
10864
+ }
10865
+ if (fixed > 0) {
10866
+ const touchedSessions = new Set(zeroRows.map((r) => {
10867
+ const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
10868
+ return row?.session_id;
10869
+ }).filter(Boolean));
10870
+ for (const sid of touchedSessions)
10871
+ rollupSession(db, sid);
10872
+ }
10873
+ return { fixed, total: zeroRows.length, buckets: queryZeroCostTokenizedModels(db, 8) };
10874
+ }
10875
+
10876
+ // src/lib/cloud-ingest.ts
10877
+ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
10878
+ function getIngestCachePath() {
10879
+ if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
10880
+ return process.env["HASNA_ECONOMY_INGEST_CACHE"];
10881
+ return join13(getDataDir(), "ingest-cache.db");
10882
+ }
10883
+ function copyIngestState(from, to) {
10884
+ const rows = from.prepare(`SELECT source, key, value FROM ingest_state`).all();
10885
+ to.prepare(`DELETE FROM ingest_state`).run();
10886
+ const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
10887
+ for (const row of rows)
10888
+ insert.run(row.source, row.key, row.value);
10889
+ }
10890
+ function exportIngestRows(db) {
10891
+ const body = {};
10892
+ for (const table2 of CLOUD_INGEST_TABLES) {
10893
+ const rows = db.prepare(`SELECT * FROM ${table2}`).all();
10894
+ if (rows.length > 0)
10895
+ body[table2] = rows;
10896
+ }
10897
+ return body;
10898
+ }
10899
+ function unwrapEnvelope(raw) {
10900
+ if (raw && typeof raw === "object" && "data" in raw) {
10901
+ const data = raw.data;
10902
+ return data && typeof data === "object" ? data : {};
10903
+ }
10904
+ return raw ?? {};
10905
+ }
10906
+ async function postIngest(cloud, body) {
10907
+ const raw = await cloud.client.transport.post("/ingest", body);
10908
+ const payload = unwrapEnvelope(raw);
10909
+ return {
10910
+ ingested: payload["ingested"] ?? {},
10911
+ total: Number(payload["total"] ?? 0)
10912
+ };
10913
+ }
10914
+ async function syncAllToCloud(cloud, opts = {}) {
10915
+ const scratch = openDatabase(":memory:", true);
10916
+ ensurePricingSeeded(scratch);
10917
+ const cache2 = openDatabase(opts.cachePath ?? getIngestCachePath(), true);
10918
+ try {
10919
+ if (opts.force)
10920
+ cache2.prepare(`DELETE FROM ingest_state`).run();
10921
+ copyIngestState(cache2, scratch);
10922
+ const result = await syncAll(scratch, opts);
10923
+ if (opts.backfillMachine)
10924
+ backfillMachineId(scratch);
10925
+ if (opts.recalculate)
10926
+ await recalculateZeroCostRequests(scratch);
10927
+ const body = exportIngestRows(scratch);
10928
+ if (Object.keys(body).length === 0) {
10929
+ copyIngestState(scratch, cache2);
10930
+ return { ...result, posted: false };
10931
+ }
10932
+ const response = await postIngest(cloud, body);
10933
+ copyIngestState(scratch, cache2);
10934
+ return { ...result, posted: true, ingested: response.ingested, total: response.total };
10935
+ } finally {
10936
+ try {
10937
+ scratch.close();
10938
+ } catch {}
10939
+ try {
10940
+ cache2.close();
10941
+ } catch {}
10942
+ }
10943
+ }
10944
+ async function billingSyncToCloud(cloud, opts = {}) {
10945
+ const scratch = openDatabase(":memory:", true);
10946
+ try {
10947
+ const days = opts.days ?? 31;
10948
+ const providers = {};
10949
+ const attempts = [
10950
+ ["anthropic", () => syncAnthropicBilling(scratch, { days })],
10951
+ ["openai", () => syncOpenAIBilling(scratch, { days })],
10952
+ ["gemini", () => syncGeminiBilling(scratch, { days })]
10953
+ ];
10954
+ const anySelected = opts.anthropic || opts.openai || opts.gemini;
10955
+ let totalUsd = 0;
10956
+ for (const [provider, run] of attempts) {
10957
+ if (anySelected && !opts[provider])
10958
+ continue;
10959
+ {
10960
+ try {
10961
+ const r = await run();
10962
+ providers[provider] = { ok: true, days: r.days, totalUsd: r.totalUsd };
10963
+ totalUsd += r.totalUsd;
10964
+ } catch (e) {
10965
+ providers[provider] = { ok: false, error: e instanceof Error ? e.message : String(e) };
10966
+ }
10967
+ }
10968
+ }
10969
+ const body = exportIngestRows(scratch);
10970
+ if (Object.keys(body).length === 0)
10971
+ return { providers, totalUsd, posted: false };
10972
+ const response = await postIngest(cloud, body);
10973
+ return { providers, totalUsd, posted: true, ingested: response.ingested, total: response.total };
10974
+ } finally {
10975
+ try {
10976
+ scratch.close();
10977
+ } catch {}
10978
+ }
10979
+ }
10980
+
10981
+ // src/cli/index.ts
10982
+ init_cloud_storage();
10983
+ init_billing_diff();
10984
+
10985
+ // src/lib/contracts-client/transport.ts
10986
+ import { HasnaHttpError } from "@hasna/contracts/client";
10987
+
10988
+ // src/cli/index.ts
11302
10989
  var program = new Command;
11303
10990
  program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
11304
10991
  async function autoSync(opts = {}) {
11305
- if (isCloudStore())
10992
+ if (isCloudStore()) {
10993
+ const cloud = economyCloudStorage();
10994
+ if (cloud.active)
10995
+ await syncAllToCloud(cloud, opts);
11306
10996
  return;
10997
+ }
11307
10998
  const db = openDatabase();
11308
10999
  ensurePricingSeeded(db);
11309
11000
  await syncAll(db, opts);
@@ -11518,7 +11209,34 @@ program.action(async () => {
11518
11209
  });
11519
11210
  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
11211
  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)"));
11212
+ const cloud = economyCloudStorage();
11213
+ if (!cloud.active) {
11214
+ console.log(chalk7.yellow("cloud mode: sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
11215
+ return;
11216
+ }
11217
+ const result2 = await syncAllToCloud(cloud, {
11218
+ claude: opts.claude,
11219
+ takumi: opts.takumi,
11220
+ codex: opts.codex,
11221
+ gemini: opts.gemini,
11222
+ opencode: opts.opencode,
11223
+ cursor: opts.cursor,
11224
+ pi: opts.pi,
11225
+ hermes: opts.hermes,
11226
+ loops: opts.loops,
11227
+ verbose: opts.verbose,
11228
+ force: opts.force,
11229
+ backfillMachine: opts.backfillMachine,
11230
+ recalculate: opts.recalculate
11231
+ });
11232
+ if (result2.posted) {
11233
+ const parts = Object.entries(result2.ingested ?? {}).filter(([, n]) => n > 0).map(([table2, n]) => `${n} ${table2}`);
11234
+ console.log(chalk7.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
11235
+ } else {
11236
+ console.log(chalk7.dim("\u2713 no new provider data to push (mtime cache current)"));
11237
+ }
11238
+ console.log(chalk7.bold.green(`
11239
+ \u2713 Sync complete`));
11522
11240
  return;
11523
11241
  }
11524
11242
  const db = openDatabase();
@@ -11557,34 +11275,14 @@ program.command("sync").description("Ingest cost data from all coding agents").o
11557
11275
  console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
11558
11276
  if (opts.backfillMachine) {
11559
11277
  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`));
11278
+ const r = backfillMachineId(db);
11279
+ console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${r.requests} requests, ${r.sessions} sessions`));
11563
11280
  }
11564
11281
  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);
11282
+ const { getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
11283
+ const r = await recalculateZeroCostRequests(db);
11284
+ console.log(chalk7.cyan(`\u2192 Recalculated: ${r.fixed}/${r.total} zero-cost requests now have pricing`));
11285
+ const zeroCostBuckets = r.buckets;
11588
11286
  if (zeroCostBuckets.length > 0) {
11589
11287
  console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
11590
11288
  for (const row of zeroCostBuckets) {
@@ -12410,13 +12108,26 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
12410
12108
  });
12411
12109
  var billingCmd = program.command("billing").description("Pull actual billing from provider billing sources (ground truth)");
12412
12110
  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
12111
  const days = parsePositiveCliInteger(opts.days ?? "31", "--days");
12418
12112
  if (days > 366)
12419
12113
  fail("--days must be between 1 and 366");
12114
+ if (isCloudStore()) {
12115
+ const cloud = economyCloudStorage();
12116
+ if (!cloud.active) {
12117
+ console.log(chalk7.yellow("cloud mode: billing sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
12118
+ return;
12119
+ }
12120
+ const result = await billingSyncToCloud(cloud, { days, anthropic: opts.anthropic, openai: opts.openai, gemini: opts.gemini });
12121
+ for (const [provider, p] of Object.entries(result.providers)) {
12122
+ if (p.ok)
12123
+ console.log(chalk7.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
12124
+ else
12125
+ console.log(chalk7.red(`\u2717 ${provider}: ${p.error}`));
12126
+ }
12127
+ if (result.posted)
12128
+ console.log(chalk7.green(`\u2713 pushed ${result.total} billing rows to the shared API`));
12129
+ return;
12130
+ }
12420
12131
  const db = openDatabase();
12421
12132
  const doAll = !opts.anthropic && !opts.openai && !opts.gemini;
12422
12133
  if (opts.anthropic || doAll) {