@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/mcp/index.js CHANGED
@@ -1145,6 +1145,21 @@ function querySummary(db, period, machine, allMachines = false, agent) {
1145
1145
  period
1146
1146
  };
1147
1147
  }
1148
+ function queryZeroCostTokenizedModels(db, limit = 10) {
1149
+ return db.prepare(`
1150
+ SELECT agent,
1151
+ model,
1152
+ COUNT(*) as requests,
1153
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1154
+ MAX(timestamp) as last_seen
1155
+ FROM requests
1156
+ WHERE cost_usd = 0
1157
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
1158
+ GROUP BY agent, model
1159
+ ORDER BY requests DESC, total_tokens DESC
1160
+ LIMIT ?
1161
+ `).all(limit);
1162
+ }
1148
1163
  function queryModelBreakdown(db) {
1149
1164
  return db.prepare(`
1150
1165
  SELECT model, agent,
@@ -3873,11 +3888,11 @@ async function syncAll(db, opts = {}) {
3873
3888
  const all = !anySpecific;
3874
3889
  const result = { deduped: 0 };
3875
3890
  if (all || opts.claude) {
3876
- result.claude = await ingestClaude(db, opts.verbose);
3891
+ result.claude = await ingestClaude(db, opts.verbose, opts.projectsDir);
3877
3892
  result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
3878
3893
  }
3879
3894
  if (all || opts.takumi)
3880
- result.takumi = await ingestTakumi(db, opts.verbose);
3895
+ result.takumi = await ingestTakumi(db, opts.verbose, opts.projectsDir);
3881
3896
  if (all || opts.codex) {
3882
3897
  result.codex = await ingestCodex(db, opts.verbose);
3883
3898
  result.codexQuota = await ingestCodexQuota(db, opts.verbose);
@@ -3899,6 +3914,156 @@ async function syncAll(db, opts = {}) {
3899
3914
  return result;
3900
3915
  }
3901
3916
 
3917
+ // src/lib/cloud-ingest.ts
3918
+ init_database();
3919
+ init_pricing();
3920
+ import { join as join10 } from "path";
3921
+
3922
+ // src/ingest/billing.ts
3923
+ init_database();
3924
+
3925
+ // src/lib/sync-maintenance.ts
3926
+ init_database();
3927
+ function backfillMachineId(db) {
3928
+ const machine = getMachineId();
3929
+ const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
3930
+ const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
3931
+ return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
3932
+ }
3933
+ async function recalculateZeroCostRequests(db) {
3934
+ const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
3935
+ 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
3936
+ FROM requests
3937
+ WHERE cost_usd = 0
3938
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
3939
+ let fixed = 0;
3940
+ for (const r of zeroRows) {
3941
+ const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
3942
+ const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
3943
+ if (cost > 0) {
3944
+ db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
3945
+ fixed++;
3946
+ }
3947
+ }
3948
+ if (fixed > 0) {
3949
+ const touchedSessions = new Set(zeroRows.map((r) => {
3950
+ const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
3951
+ return row?.session_id;
3952
+ }).filter(Boolean));
3953
+ for (const sid of touchedSessions)
3954
+ rollupSession(db, sid);
3955
+ }
3956
+ return { fixed, total: zeroRows.length, buckets: queryZeroCostTokenizedModels(db, 8) };
3957
+ }
3958
+
3959
+ // src/lib/cloud-ingest.ts
3960
+ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
3961
+ function getIngestCachePath() {
3962
+ if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
3963
+ return process.env["HASNA_ECONOMY_INGEST_CACHE"];
3964
+ return join10(getDataDir(), "ingest-cache.db");
3965
+ }
3966
+ function copyIngestState(from, to) {
3967
+ const rows = from.prepare(`SELECT source, key, value FROM ingest_state`).all();
3968
+ to.prepare(`DELETE FROM ingest_state`).run();
3969
+ const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
3970
+ for (const row of rows)
3971
+ insert.run(row.source, row.key, row.value);
3972
+ }
3973
+ function exportIngestRows(db) {
3974
+ const body = {};
3975
+ for (const table of CLOUD_INGEST_TABLES) {
3976
+ const rows = db.prepare(`SELECT * FROM ${table}`).all();
3977
+ if (rows.length > 0)
3978
+ body[table] = rows;
3979
+ }
3980
+ return body;
3981
+ }
3982
+ function unwrapEnvelope(raw) {
3983
+ if (raw && typeof raw === "object" && "data" in raw) {
3984
+ const data = raw.data;
3985
+ return data && typeof data === "object" ? data : {};
3986
+ }
3987
+ return raw ?? {};
3988
+ }
3989
+ async function postIngest(cloud, body) {
3990
+ const raw = await cloud.client.transport.post("/ingest", body);
3991
+ const payload = unwrapEnvelope(raw);
3992
+ return {
3993
+ ingested: payload["ingested"] ?? {},
3994
+ total: Number(payload["total"] ?? 0)
3995
+ };
3996
+ }
3997
+ async function syncAllToCloud(cloud, opts = {}) {
3998
+ const scratch = openDatabase(":memory:", true);
3999
+ ensurePricingSeeded(scratch);
4000
+ const cache = openDatabase(opts.cachePath ?? getIngestCachePath(), true);
4001
+ try {
4002
+ if (opts.force)
4003
+ cache.prepare(`DELETE FROM ingest_state`).run();
4004
+ copyIngestState(cache, scratch);
4005
+ const result = await syncAll(scratch, opts);
4006
+ if (opts.backfillMachine)
4007
+ backfillMachineId(scratch);
4008
+ if (opts.recalculate)
4009
+ await recalculateZeroCostRequests(scratch);
4010
+ const body = exportIngestRows(scratch);
4011
+ if (Object.keys(body).length === 0) {
4012
+ copyIngestState(scratch, cache);
4013
+ return { ...result, posted: false };
4014
+ }
4015
+ const response = await postIngest(cloud, body);
4016
+ copyIngestState(scratch, cache);
4017
+ return { ...result, posted: true, ingested: response.ingested, total: response.total };
4018
+ } finally {
4019
+ try {
4020
+ scratch.close();
4021
+ } catch {}
4022
+ try {
4023
+ cache.close();
4024
+ } catch {}
4025
+ }
4026
+ }
4027
+
4028
+ // src/lib/contracts-client/storage.ts
4029
+ import { resolveStorageClient } from "@hasna/contracts/client/storage";
4030
+
4031
+ // src/lib/cloud-storage.ts
4032
+ var ECONOMY_APP = "economy";
4033
+ var cache;
4034
+ function resolveEconomyCloudStorage(env = process.env, overrides) {
4035
+ const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
4036
+ return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
4037
+ }
4038
+ function economyCloudStorage(env = process.env) {
4039
+ if (cache === undefined)
4040
+ cache = resolveEconomyCloudStorage(env);
4041
+ return cache;
4042
+ }
4043
+ function cleanQuery(query) {
4044
+ if (!query)
4045
+ return;
4046
+ const out = {};
4047
+ for (const [k, v] of Object.entries(query)) {
4048
+ if (v !== undefined && v !== null && v !== "")
4049
+ out[k] = v;
4050
+ }
4051
+ return Object.keys(out).length > 0 ? out : undefined;
4052
+ }
4053
+ async function cloudListItems(storage, resource, query) {
4054
+ const cleaned = cleanQuery(query);
4055
+ const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
4056
+ return res.items;
4057
+ }
4058
+ async function cloudObject(storage, path, query) {
4059
+ const cleaned = cleanQuery(query);
4060
+ const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
4061
+ if (raw && typeof raw === "object" && "data" in raw) {
4062
+ return raw.data;
4063
+ }
4064
+ return raw;
4065
+ }
4066
+
3902
4067
  // src/lib/agents.ts
3903
4068
  var AGENTS = [
3904
4069
  "claude",
@@ -4736,472 +4901,6 @@ function queryModelEfficiency(db) {
4736
4901
  FROM requests GROUP BY model ORDER BY cost DESC`).all();
4737
4902
  }
4738
4903
 
4739
- // src/lib/contracts-client/mode.ts
4740
- var DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
4741
- function normalizeStorageMode(value) {
4742
- const normalized = value.trim().toLowerCase().replace(/-/g, "_");
4743
- if (normalized === "local")
4744
- return { mode: "local", deprecatedAlias: null };
4745
- if (normalized === "cloud")
4746
- return { mode: "cloud", deprecatedAlias: null };
4747
- if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
4748
- return { mode: "cloud", deprecatedAlias: normalized };
4749
- }
4750
- throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
4751
- }
4752
- function envToken(name) {
4753
- return name.toUpperCase().replace(/-/g, "_");
4754
- }
4755
-
4756
- // src/lib/contracts-client/transport.ts
4757
- function defaultCloudBaseUrl(name) {
4758
- return `https://${name}.hasna.xyz`;
4759
- }
4760
- function clientTransportEnvKeys(name) {
4761
- const token = envToken(name);
4762
- return {
4763
- modeKeys: [
4764
- `HASNA_${token}_STORAGE_MODE`,
4765
- `HASNA_${token}_MODE`,
4766
- `${token}_STORAGE_MODE`,
4767
- `${token}_MODE`
4768
- ],
4769
- apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
4770
- apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
4771
- };
4772
- }
4773
- function firstEnv(env, keys) {
4774
- for (const key of keys) {
4775
- const value = env[key]?.trim();
4776
- if (value)
4777
- return { key, value };
4778
- }
4779
- return null;
4780
- }
4781
- function toV1BaseUrl(apiUrl) {
4782
- const url = new URL(apiUrl);
4783
- if (url.protocol !== "http:" && url.protocol !== "https:") {
4784
- throw new Error("API URL must use http or https.");
4785
- }
4786
- let path = url.pathname.replace(/\/+$/, "");
4787
- if (path.endsWith("/v1"))
4788
- path = path.slice(0, -"/v1".length);
4789
- url.pathname = `${path}/v1`;
4790
- url.search = "";
4791
- url.hash = "";
4792
- return url.toString().replace(/\/+$/, "");
4793
- }
4794
- function resolveClientTransport(name, env = process.env) {
4795
- const keys = clientTransportEnvKeys(name);
4796
- const modeHit = firstEnv(env, keys.modeKeys);
4797
- const urlHit = firstEnv(env, keys.apiUrlKeys);
4798
- const keyHit = firstEnv(env, keys.apiKeyKeys);
4799
- let mode = "local";
4800
- let deprecatedAlias = null;
4801
- let modeSource = "default";
4802
- const warnings = [];
4803
- if (modeHit) {
4804
- const normalized = normalizeStorageMode(modeHit.value);
4805
- mode = normalized.mode;
4806
- deprecatedAlias = normalized.deprecatedAlias;
4807
- modeSource = modeHit.key;
4808
- if (deprecatedAlias) {
4809
- warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
4810
- }
4811
- } else if (urlHit && keyHit) {
4812
- mode = "cloud";
4813
- modeSource = `${urlHit.key}+${keyHit.key}`;
4814
- } else if (urlHit) {
4815
- warnings.push(`${urlHit.key} is set but no API key is present (${keys.apiKeyKeys[0]}). ` + `Refusing to fall back to the local store, which would silently serve a different dataset. ` + `Set ${keys.apiKeyKeys[0]} to enable the cloud client, or set ${keys.modeKeys[0]}=local ` + `to use the local store deliberately.`);
4816
- return {
4817
- transport: "local",
4818
- mode: "local",
4819
- deprecatedAlias,
4820
- modeSource: urlHit.key,
4821
- baseUrl: null,
4822
- apiUrlSource: null,
4823
- apiKeyPresent: false,
4824
- apiKeySource: null,
4825
- misconfigured: true,
4826
- warning: warnings.join(" ")
4827
- };
4828
- }
4829
- if (mode === "local") {
4830
- return {
4831
- transport: "local",
4832
- mode,
4833
- deprecatedAlias,
4834
- modeSource,
4835
- baseUrl: null,
4836
- apiUrlSource: null,
4837
- apiKeyPresent: Boolean(keyHit),
4838
- apiKeySource: keyHit ? keyHit.key : null,
4839
- misconfigured: false,
4840
- warning: warnings.length > 0 ? warnings.join(" ") : null
4841
- };
4842
- }
4843
- if (!keyHit) {
4844
- 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.`);
4845
- return {
4846
- transport: "local",
4847
- mode,
4848
- deprecatedAlias,
4849
- modeSource,
4850
- baseUrl: null,
4851
- apiUrlSource: null,
4852
- apiKeyPresent: false,
4853
- apiKeySource: null,
4854
- misconfigured: true,
4855
- warning: warnings.join(" ")
4856
- };
4857
- }
4858
- const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
4859
- const apiUrlSource = urlHit ? urlHit.key : "default";
4860
- let baseUrl;
4861
- try {
4862
- baseUrl = toV1BaseUrl(rawUrl);
4863
- } catch (error) {
4864
- const message = error instanceof Error ? error.message : String(error);
4865
- warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
4866
- return {
4867
- transport: "local",
4868
- mode,
4869
- deprecatedAlias,
4870
- modeSource,
4871
- baseUrl: null,
4872
- apiUrlSource: null,
4873
- apiKeyPresent: true,
4874
- apiKeySource: keyHit.key,
4875
- misconfigured: true,
4876
- warning: warnings.join(" ")
4877
- };
4878
- }
4879
- return {
4880
- transport: "cloud-http",
4881
- mode,
4882
- deprecatedAlias,
4883
- modeSource,
4884
- baseUrl,
4885
- apiUrlSource,
4886
- apiKeyPresent: true,
4887
- apiKeySource: keyHit.key,
4888
- misconfigured: false,
4889
- warning: warnings.length > 0 ? warnings.join(" ") : null
4890
- };
4891
- }
4892
-
4893
- class HasnaHttpError extends Error {
4894
- status;
4895
- method;
4896
- path;
4897
- body;
4898
- constructor(method, path, status, body) {
4899
- super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
4900
- this.name = "HasnaHttpError";
4901
- this.status = status;
4902
- this.method = method;
4903
- this.path = path;
4904
- this.body = body;
4905
- }
4906
- }
4907
- var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
4908
- var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4909
- function appendQuery(path, query) {
4910
- if (!query)
4911
- return path;
4912
- const params = query instanceof URLSearchParams ? query : new URLSearchParams;
4913
- if (!(query instanceof URLSearchParams)) {
4914
- for (const [key, value] of Object.entries(query)) {
4915
- if (value === null || value === undefined)
4916
- continue;
4917
- if (Array.isArray(value)) {
4918
- for (const v of value)
4919
- params.append(key, String(v));
4920
- } else {
4921
- params.append(key, String(value));
4922
- }
4923
- }
4924
- }
4925
- const qs = params.toString();
4926
- if (!qs)
4927
- return path;
4928
- return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
4929
- }
4930
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4931
- function createHasnaHttpTransport(options) {
4932
- const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
4933
- const base = options.baseUrl.replace(/\/+$/, "");
4934
- const timeoutMs = options.timeoutMs ?? 30000;
4935
- const sleep = options.sleepImpl ?? defaultSleep;
4936
- const defaultRetry = options.retry;
4937
- function resolveRetry(callRetry) {
4938
- const chosen = callRetry !== undefined ? callRetry : defaultRetry;
4939
- if (chosen === false)
4940
- return null;
4941
- const r = chosen ?? {};
4942
- return {
4943
- retries: r.retries ?? 2,
4944
- baseDelayMs: r.baseDelayMs ?? 200,
4945
- maxDelayMs: r.maxDelayMs ?? 2000,
4946
- retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
4947
- };
4948
- }
4949
- async function once(method, rel, url, body, opts) {
4950
- const headers = {
4951
- "x-api-key": options.apiKey,
4952
- Authorization: `Bearer ${options.apiKey}`,
4953
- Accept: "application/json",
4954
- ...options.headers ?? {},
4955
- ...opts.headers ?? {}
4956
- };
4957
- if (opts.idempotencyKey)
4958
- headers["Idempotency-Key"] = opts.idempotencyKey;
4959
- const init = { method, headers };
4960
- if (body !== undefined) {
4961
- headers["Content-Type"] = "application/json";
4962
- init.body = JSON.stringify(body);
4963
- }
4964
- const controller = new AbortController;
4965
- const onAbort = () => controller.abort();
4966
- if (opts.signal) {
4967
- if (opts.signal.aborted)
4968
- controller.abort();
4969
- else
4970
- opts.signal.addEventListener("abort", onAbort, { once: true });
4971
- }
4972
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
4973
- init.signal = controller.signal;
4974
- let response;
4975
- try {
4976
- response = await fetchImpl(url, init);
4977
- } catch (error) {
4978
- const err = error instanceof Error ? error : new Error(String(error));
4979
- if (opts.signal?.aborted)
4980
- return { ok: false, retryable: false, error: err };
4981
- return { ok: false, retryable: true, error: err };
4982
- } finally {
4983
- clearTimeout(timer);
4984
- if (opts.signal)
4985
- opts.signal.removeEventListener("abort", onAbort);
4986
- }
4987
- const text = await response.text();
4988
- let parsed = undefined;
4989
- if (text.length > 0) {
4990
- try {
4991
- parsed = JSON.parse(text);
4992
- } catch {
4993
- parsed = text;
4994
- }
4995
- }
4996
- if (!response.ok) {
4997
- const retry = resolveRetry(opts.retry);
4998
- const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
4999
- return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
5000
- }
5001
- return { ok: true, value: parsed };
5002
- }
5003
- async function request(method, path, body, opts = {}) {
5004
- const upper = method.toUpperCase();
5005
- const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
5006
- const url = `${base}${rel}`;
5007
- const retry = resolveRetry(opts.retry);
5008
- const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
5009
- const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
5010
- let last = null;
5011
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
5012
- const result = await once(upper, rel, url, body, opts);
5013
- if (result.ok)
5014
- return result.value;
5015
- last = result;
5016
- const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
5017
- if (!canRetry)
5018
- break;
5019
- const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
5020
- const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
5021
- await sleep(backoff + jitter);
5022
- }
5023
- throw last.error;
5024
- }
5025
- return {
5026
- baseUrl: base,
5027
- request,
5028
- get: (path, opts) => request("GET", path, undefined, opts),
5029
- post: (path, body, opts) => request("POST", path, body, opts),
5030
- put: (path, body, opts) => request("PUT", path, body, opts),
5031
- patch: (path, body, opts) => request("PATCH", path, body, opts),
5032
- del: (path, body, opts) => request("DELETE", path, body, opts)
5033
- };
5034
- }
5035
- function createClientTransport(name, env = process.env, overrides) {
5036
- const resolution = resolveClientTransport(name, env);
5037
- if (resolution.misconfigured) {
5038
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
5039
- }
5040
- if (resolution.transport === "local" || !resolution.baseUrl) {
5041
- return { transport: "local", client: null, resolution };
5042
- }
5043
- const keys = clientTransportEnvKeys(name);
5044
- const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
5045
- if (!apiKey) {
5046
- throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
5047
- }
5048
- return {
5049
- transport: "cloud-http",
5050
- client: createHasnaHttpTransport({
5051
- name,
5052
- baseUrl: resolution.baseUrl,
5053
- apiKey,
5054
- ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
5055
- ...overrides?.headers ? { headers: overrides.headers } : {},
5056
- ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
5057
- ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
5058
- ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
5059
- }),
5060
- resolution
5061
- };
5062
- }
5063
-
5064
- // src/lib/contracts-client/storage.ts
5065
- function resourcePath(resource) {
5066
- const trimmed = resource.replace(/^\/+|\/+$/g, "");
5067
- if (!trimmed)
5068
- throw new Error("resource must be a non-empty path segment");
5069
- return `/${trimmed}`;
5070
- }
5071
- function entityPath(resource, id) {
5072
- if (id === undefined || id === null || `${id}`.length === 0) {
5073
- throw new Error("id must be a non-empty string");
5074
- }
5075
- return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
5076
- }
5077
- function newIdempotencyKey() {
5078
- const g = globalThis;
5079
- if (g.crypto?.randomUUID)
5080
- return g.crypto.randomUUID();
5081
- return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
5082
- }
5083
- function extractItems(raw) {
5084
- if (Array.isArray(raw))
5085
- return raw;
5086
- if (raw && typeof raw === "object") {
5087
- const obj = raw;
5088
- for (const key of ["items", "data", "results", "rows", "records"]) {
5089
- if (Array.isArray(obj[key]))
5090
- return obj[key];
5091
- }
5092
- }
5093
- return [];
5094
- }
5095
- function extractTotal(raw) {
5096
- if (raw && typeof raw === "object") {
5097
- const obj = raw;
5098
- for (const key of ["total", "count", "totalCount", "total_count"]) {
5099
- if (typeof obj[key] === "number")
5100
- return obj[key];
5101
- }
5102
- }
5103
- return null;
5104
- }
5105
- function extractCursor(raw) {
5106
- if (raw && typeof raw === "object") {
5107
- const obj = raw;
5108
- for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
5109
- if (typeof obj[key] === "string")
5110
- return obj[key];
5111
- }
5112
- }
5113
- return null;
5114
- }
5115
- function createHasnaStorageClient(name, transport) {
5116
- return {
5117
- name,
5118
- baseUrl: transport.baseUrl,
5119
- transport,
5120
- async list(resource, options = {}) {
5121
- const raw = await transport.get(resourcePath(resource), options);
5122
- return {
5123
- items: extractItems(raw),
5124
- total: extractTotal(raw),
5125
- cursor: extractCursor(raw),
5126
- raw
5127
- };
5128
- },
5129
- async get(resource, id, options = {}) {
5130
- try {
5131
- return await transport.get(entityPath(resource, id), options);
5132
- } catch (error) {
5133
- if (error instanceof HasnaHttpError && error.status === 404)
5134
- return null;
5135
- throw error;
5136
- }
5137
- },
5138
- async create(resource, body, options = {}) {
5139
- const { idempotencyKey, ...rest } = options;
5140
- return transport.post(resourcePath(resource), body, {
5141
- ...rest,
5142
- idempotencyKey: idempotencyKey ?? newIdempotencyKey()
5143
- });
5144
- },
5145
- async update(resource, id, patch, options = {}) {
5146
- const { method = "PATCH", idempotencyKey, ...rest } = options;
5147
- const call = method === "PUT" ? transport.put : transport.patch;
5148
- return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
5149
- },
5150
- async delete(resource, id, options = {}) {
5151
- try {
5152
- await transport.del(entityPath(resource, id), undefined, options);
5153
- } catch (error) {
5154
- if (error instanceof HasnaHttpError && error.status === 404)
5155
- return;
5156
- throw error;
5157
- }
5158
- }
5159
- };
5160
- }
5161
- function resolveStorageClient(name, env = process.env, overrides) {
5162
- const wired = createClientTransport(name, env, overrides);
5163
- if (wired.transport === "cloud-http") {
5164
- return { transport: "cloud-http", client: createHasnaStorageClient(name, wired.client) };
5165
- }
5166
- return { transport: "local", client: null };
5167
- }
5168
-
5169
- // src/lib/cloud-storage.ts
5170
- var ECONOMY_APP = "economy";
5171
- var cache;
5172
- function resolveEconomyCloudStorage(env = process.env, overrides) {
5173
- const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
5174
- return resolved.transport === "cloud-http" ? { active: true, client: resolved.client } : { active: false, client: null };
5175
- }
5176
- function economyCloudStorage(env = process.env) {
5177
- if (cache === undefined)
5178
- cache = resolveEconomyCloudStorage(env);
5179
- return cache;
5180
- }
5181
- function cleanQuery(query) {
5182
- if (!query)
5183
- return;
5184
- const out = {};
5185
- for (const [k, v] of Object.entries(query)) {
5186
- if (v !== undefined && v !== null && v !== "")
5187
- out[k] = v;
5188
- }
5189
- return Object.keys(out).length > 0 ? out : undefined;
5190
- }
5191
- async function cloudListItems(storage, resource, query) {
5192
- const cleaned = cleanQuery(query);
5193
- const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
5194
- return res.items;
5195
- }
5196
- async function cloudObject(storage, path, query) {
5197
- const cleaned = cleanQuery(query);
5198
- const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
5199
- if (raw && typeof raw === "object" && "data" in raw) {
5200
- return raw.data;
5201
- }
5202
- return raw;
5203
- }
5204
-
5205
4904
  // src/lib/store/index.ts
5206
4905
  class LocalStore {
5207
4906
  transport = "local";
@@ -5995,11 +5694,35 @@ function buildServer() {
5995
5694
  `));
5996
5695
  });
5997
5696
  server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z.enum([...SYNC_SOURCES]).optional(), json: z.boolean().optional() }, async ({ sources, json }) => {
5998
- if (isCloudStore()) {
5999
- return text("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)");
6000
- }
6001
5697
  const selected = sources ?? "all";
6002
5698
  const opts = selected === "all" ? {} : { [selected]: true };
5699
+ if (isCloudStore()) {
5700
+ const cloud = economyCloudStorage();
5701
+ if (!cloud.active) {
5702
+ return text("cloud mode: sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)");
5703
+ }
5704
+ const result2 = await syncAllToCloud(cloud, opts);
5705
+ if (json)
5706
+ return text(JSON.stringify(result2, null, 2));
5707
+ const lines2 = [`sync: ${selected}`];
5708
+ if (result2.posted) {
5709
+ const parts = Object.entries(result2.ingested ?? {}).filter(([, n]) => n > 0).map(([table, n]) => `${table}:${n}`);
5710
+ lines2.push(`pushed: ${parts.join(", ")} (${result2.total} rows)`);
5711
+ } else {
5712
+ lines2.push("no new provider data to push (mtime cache current)");
5713
+ }
5714
+ lines2.push(`deduped: ${result2.deduped}`);
5715
+ for (const [source, value] of Object.entries(result2)) {
5716
+ if (source === "deduped" || source === "posted" || source === "ingested" || source === "total")
5717
+ continue;
5718
+ if (value == null)
5719
+ continue;
5720
+ lines2.push(`${source}: ${compactObject(value)}`);
5721
+ }
5722
+ lines2.push("Use json=true for the full sync result.");
5723
+ return text(lines2.join(`
5724
+ `));
5725
+ }
6003
5726
  const result = await syncAll(localDb(), opts);
6004
5727
  if (json)
6005
5728
  return text(JSON.stringify(result, null, 2));
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAm0BjC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAw1BjC"}