@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/CHANGELOG.md +52 -2
- package/dist/cli/index.js +267 -560
- package/dist/db/cloud.d.ts +1 -3
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/index.js +2 -429
- package/dist/lib/cloud-ingest.d.ts +57 -0
- package/dist/lib/cloud-ingest.d.ts.map +1 -0
- package/dist/lib/cloud-storage.d.ts +5 -5
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/contracts-client/index.d.ts.map +1 -1
- package/dist/lib/contracts-client/storage.d.ts +2 -82
- package/dist/lib/contracts-client/storage.d.ts.map +1 -1
- package/dist/lib/contracts-client/transport.d.ts +1 -147
- package/dist/lib/contracts-client/transport.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts +15 -0
- package/dist/lib/sync-maintenance.d.ts.map +1 -0
- package/dist/mcp/index.js +194 -471
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/server/index.js +42 -13
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/cli.md +3 -3
- package/docs/mcp.md +1 -1
- package/package.json +6 -6
- package/dist/lib/contracts-client/mode.d.ts +0 -20
- package/dist/lib/contracts-client/mode.d.ts.map +0 -1
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));
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"
|
|
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"}
|