chain-insights 0.68.2 → 0.69.1

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.
Files changed (43) hide show
  1. package/README.md +1 -0
  2. package/dist/apps/claude-view.html +39 -38
  3. package/dist/cli.cjs +31 -5
  4. package/dist/cli.mjs +31 -5
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/dex-registry-05wAhSa0.mjs +383 -0
  7. package/dist/dex-registry-05wAhSa0.mjs.map +1 -0
  8. package/dist/dex-registry-KOTJhAQf.cjs +411 -0
  9. package/dist/dex-tool-DIKN4dXB.cjs +536 -0
  10. package/dist/dex-tool-DpQKS7Te.mjs +497 -0
  11. package/dist/dex-tool-DpQKS7Te.mjs.map +1 -0
  12. package/dist/{format-DB2IH2U5.mjs → format-CSesEBvS.mjs} +4 -4
  13. package/dist/format-CSesEBvS.mjs.map +1 -0
  14. package/dist/{format-BjZuYDr8.cjs → format-DfhRQq4L.cjs} +3 -3
  15. package/dist/{graph-schema-DHd2fGbr.mjs → graph-schema-C7LgLhlY.mjs} +11 -4
  16. package/dist/graph-schema-C7LgLhlY.mjs.map +1 -0
  17. package/dist/{graph-schema-C_7aZ8RL.cjs → graph-schema-DHZi7kUu.cjs} +10 -3
  18. package/dist/mcp-proxy.cjs +113 -79
  19. package/dist/mcp-proxy.d.cts +4 -0
  20. package/dist/mcp-proxy.d.cts.map +1 -1
  21. package/dist/mcp-proxy.d.mts +4 -0
  22. package/dist/mcp-proxy.d.mts.map +1 -1
  23. package/dist/mcp-proxy.mjs +110 -79
  24. package/dist/mcp-proxy.mjs.map +1 -1
  25. package/dist/{tool-visibility-B4RAFMaB.cjs → tool-visibility-BopFnGsT.cjs} +5 -0
  26. package/dist/{tool-visibility-kclgGKL5.mjs → tool-visibility-Dq-Ruj-8.mjs} +6 -1
  27. package/dist/tool-visibility-Dq-Ruj-8.mjs.map +1 -0
  28. package/docs/mcp-proxy.md +71 -0
  29. package/docs/stability.md +3 -2
  30. package/package.json +3 -2
  31. package/registry/README.md +133 -0
  32. package/registry/robinhood.json +769 -0
  33. package/registry/robinhood.md +102 -0
  34. package/skills/chain-insights-dex/SKILL.md +104 -96
  35. package/skills/chain-insights-dex/agents/openai.yaml +1 -1
  36. package/skills/chain-insights-dex/reference.md +8 -27
  37. package/skills/chain-insights-dex-gauge/SKILL.md +6 -4
  38. package/skills/chain-insights-dex-launchpad/SKILL.md +6 -3
  39. package/skills/chain-insights-dex-vault/SKILL.md +4 -2
  40. package/skills/chain-insights-virtuals/SKILL.md +8 -4
  41. package/dist/format-DB2IH2U5.mjs.map +0 -1
  42. package/dist/graph-schema-DHd2fGbr.mjs.map +0 -1
  43. package/dist/tool-visibility-kclgGKL5.mjs.map +0 -1
@@ -0,0 +1,411 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
+ const require_cache_location = require("./cache-location-CBVzUIxq.cjs");
3
+ let node_url = require("node:url");
4
+ let node_path = require("node:path");
5
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
6
+ let node_fs = require("node:fs");
7
+ let node_fs_promises = require("node:fs/promises");
8
+ //#region src/mcp/dex-registry.ts
9
+ /**
10
+ * The exchange registry, as the proxy tool meta_dex_lookup reads it.
11
+ *
12
+ * The reviewed table lives in this repository, registry/<network>.md, and its
13
+ * served copy registry/<network>.json is checked in CI
14
+ * (scripts/pin-dex-registry.mjs). The proxy reads the copy three ways, in this
15
+ * order:
16
+ *
17
+ * cached ~/.chain-insights/cache/dex-registry-<network>.json, younger than one hour
18
+ * fetched the copy on main, https://raw.githubusercontent.com/chainswarm/chain-insights/main/registry/<network>.json,
19
+ * within 3 s, at most once an hour per network and never two in flight
20
+ * packaged registry/<network>.json of the installed package, when the fetch fails and no cache exists
21
+ *
22
+ * A fetched file is used only when it passes the same rules the pin script
23
+ * applies. A malformed file never replaces a good cache: the stale cache keeps
24
+ * answering and one line names the fault. So a proxy that never reaches GitHub
25
+ * behaves like the skill memo of its own version, and a row merged on main
26
+ * reaches every running proxy within the hour, with no release.
27
+ *
28
+ * matchDex turns a name into one exchange: normalise, exact on key, display and
29
+ * aliases, then within two letters of an alias of four or more characters. One
30
+ * winner is matched, a tie is listed, never guessed, and no hit names every
31
+ * covered exchange.
32
+ */
33
+ var dex_registry_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
34
+ DEX_REGISTRY_FETCH_BUDGET_MS: () => DEX_REGISTRY_FETCH_BUDGET_MS,
35
+ DEX_REGISTRY_PUBLIC_URL: () => DEX_REGISTRY_PUBLIC_URL,
36
+ DEX_REGISTRY_SCHEMA: () => DEX_REGISTRY_SCHEMA,
37
+ DEX_REGISTRY_TTL_MS: () => DEX_REGISTRY_TTL_MS,
38
+ DEX_REGISTRY_URL_ENV: () => DEX_REGISTRY_URL_ENV,
39
+ DexRegistryError: () => DexRegistryError,
40
+ NEAR_DISTANCE: () => 2,
41
+ NEAR_MIN_ALIAS_LENGTH: () => 4,
42
+ ROLES: () => ROLES,
43
+ TRAIL_TYPES: () => TRAIL_TYPES,
44
+ damerauLevenshtein: () => damerauLevenshtein,
45
+ dexRegistryCachePath: () => dexRegistryCachePath,
46
+ dexRegistryUrl: () => dexRegistryUrl,
47
+ loadRegistry: () => loadRegistry,
48
+ matchDex: () => matchDex,
49
+ normaliseName: () => normaliseName,
50
+ normalizeDexNetwork: () => normalizeDexNetwork,
51
+ packagedRegistryDir: () => packagedRegistryDir,
52
+ readPackagedRegistry: () => readPackagedRegistry,
53
+ registryFault: () => registryFault
54
+ });
55
+ const DEX_REGISTRY_SCHEMA = "chain-insights.dex-registry.v1";
56
+ /** How long a cached registry stays fresh. CACHE_TTL_MS (24 h) stays the lifetime of the schema caches. */
57
+ const DEX_REGISTRY_TTL_MS = 36e5;
58
+ /** The most a fetch of the public file may take on the call path. */
59
+ const DEX_REGISTRY_FETCH_BUDGET_MS = 3e3;
60
+ const DEX_REGISTRY_URL_ENV = "CHAIN_INSIGHTS_DEX_REGISTRY_URL";
61
+ const DEX_REGISTRY_PUBLIC_URL = "https://raw.githubusercontent.com/chainswarm/chain-insights/main/registry/{network}.json";
62
+ const TRAIL_TYPES = [
63
+ "plain",
64
+ "vault",
65
+ "gauge",
66
+ "launchpad"
67
+ ];
68
+ const ROLES = [
69
+ "factory",
70
+ "manager",
71
+ "router"
72
+ ];
73
+ /** A refusal with its own words for the caller: a network the registry does not cover. */
74
+ var DexRegistryError = class extends Error {
75
+ constructor(message) {
76
+ super(message);
77
+ this.name = "DexRegistryError";
78
+ }
79
+ };
80
+ const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
81
+ const POOL_RE = /^0x(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
82
+ const DEX_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
83
+ const NETWORK_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
84
+ function isRecord(value) {
85
+ return !!value && typeof value === "object" && !Array.isArray(value);
86
+ }
87
+ const isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
88
+ /** The network as a lower-case identifier; it becomes part of a file name and a URL. */
89
+ function normalizeDexNetwork(raw) {
90
+ const network = typeof raw === "string" ? raw.trim().toLowerCase() : "";
91
+ if (!network) throw new DexRegistryError("invalid_network: network is required. Pass the identifier of a served network, for example robinhood.");
92
+ if (!NETWORK_RE.test(network)) throw new DexRegistryError(`invalid_network: "${String(raw)}" is not a network identifier`);
93
+ return network;
94
+ }
95
+ /**
96
+ * The fault of a registry document, or null when it is valid for the network:
97
+ * the rules of registry/README.md, the same ones the pin script applies.
98
+ */
99
+ function registryFault(value, network) {
100
+ if (!isRecord(value)) return "the file is not a JSON object";
101
+ if (value.schema !== "chain-insights.dex-registry.v1") return `schema is ${JSON.stringify(value.schema)}, not ${DEX_REGISTRY_SCHEMA}`;
102
+ if (value.network !== network) return `network is ${JSON.stringify(value.network)}, not ${network}`;
103
+ if (!Number.isInteger(value.registry_version) || value.registry_version < 0) return "registry_version is not a whole number";
104
+ if (typeof value.recorded_on !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value.recorded_on)) return "recorded_on is not a YYYY-MM-DD day";
105
+ if (!Array.isArray(value.rows) || value.rows.length === 0) return "rows is empty";
106
+ const keys = /* @__PURE__ */ new Set();
107
+ for (const [index, row] of value.rows.entries()) {
108
+ const at = `rows[${index}]`;
109
+ if (!isRecord(row)) return `${at} is not an object`;
110
+ if (typeof row.dex !== "string" || !DEX_RE.test(row.dex)) return `${at}.dex is not a key: ${String(row.dex)}`;
111
+ if (keys.has(row.dex)) return `${at}.dex ${row.dex} is listed twice`;
112
+ keys.add(row.dex);
113
+ if (typeof row.display !== "string" || row.display.trim() === "") return `${at} (${row.dex}) has no display`;
114
+ if (!isStringList(row.aliases)) return `${at} (${row.dex}) aliases is not a list of names`;
115
+ if (!TRAIL_TYPES.includes(row.trail_type)) return `${at} (${row.dex}) trail_type ${String(row.trail_type)} is not plain, vault, gauge or launchpad`;
116
+ if (!isStringList(row.adapters)) return `${at} (${row.dex}) adapters is not a list`;
117
+ if (!isRecord(row.roles)) return `${at} (${row.dex}) roles is not a map`;
118
+ for (const [address, role] of Object.entries(row.roles)) {
119
+ if (!ADDRESS_RE.test(address)) return `${at} (${row.dex}) address ${address} is not lowercase 0x plus 40 hex characters`;
120
+ if (!ROLES.includes(role)) return `${at} (${row.dex}) role ${String(role)} is not factory, manager or router`;
121
+ }
122
+ for (const field of ["factories", "routers"]) {
123
+ const list = row[field];
124
+ if (!isStringList(list)) return `${at} (${row.dex}) ${field} is not a list`;
125
+ for (const address of list) {
126
+ if (!ADDRESS_RE.test(address)) return `${at} (${row.dex}) address ${address} is not lowercase 0x plus 40 hex characters`;
127
+ if (!(address in row.roles)) return `${at} (${row.dex}) ${field} names ${address}, which roles lacks`;
128
+ }
129
+ }
130
+ if (row.factories.length === 0) return `${at} (${row.dex}) has no factory or manager`;
131
+ if (row.example_pool !== null && (typeof row.example_pool !== "string" || !POOL_RE.test(row.example_pool))) return `${at} (${row.dex}) example_pool ${String(row.example_pool)} is not a pool`;
132
+ if (row.pool_floor !== null && (!Number.isInteger(row.pool_floor) || row.pool_floor < 0)) return `${at} (${row.dex}) pool_floor is not a whole number`;
133
+ if (!Number.isInteger(row.since_block) || row.since_block < 0) return `${at} (${row.dex}) since_block is not a block`;
134
+ if (typeof row.source !== "string" || !row.source.startsWith("https://")) return `${at} (${row.dex}) source is not an https link`;
135
+ }
136
+ return null;
137
+ }
138
+ /** The URL of the served copy: the public file on main, or the override with {network} filled in. */
139
+ function dexRegistryUrl(network, env = process.env) {
140
+ const template = env["CHAIN_INSIGHTS_DEX_REGISTRY_URL"]?.trim() || "https://raw.githubusercontent.com/chainswarm/chain-insights/main/registry/{network}.json";
141
+ if (template.includes("{network}")) return template.replaceAll("{network}", network);
142
+ return template.endsWith("/") ? `${template}${network}.json` : template;
143
+ }
144
+ function dexRegistryCachePath(network) {
145
+ return node_path.default.join(require_cache_location.chainInsightsHome(), "cache", `dex-registry-${network}.json`);
146
+ }
147
+ /** The registry/ folder of the installed package: next to dist/, or the repository root when run from source. */
148
+ function packagedRegistryDir() {
149
+ const candidates = [(0, node_url.fileURLToPath)(new URL("../registry", require("url").pathToFileURL(__filename).href)), (0, node_url.fileURLToPath)(new URL("../../registry", require("url").pathToFileURL(__filename).href))];
150
+ return candidates.find((candidate) => (0, node_fs.existsSync)(node_path.default.join(candidate, "robinhood.json"))) ?? candidates[0];
151
+ }
152
+ /** The packaged copy of a network, or null when the package carries none. A packaged copy that fails the rules is a defect: it throws. */
153
+ function readPackagedRegistry(network, dir = packagedRegistryDir()) {
154
+ let parsed;
155
+ try {
156
+ parsed = JSON.parse((0, node_fs.readFileSync)(node_path.default.join(dir, `${network}.json`), "utf8"));
157
+ } catch {
158
+ return null;
159
+ }
160
+ const fault = registryFault(parsed, network);
161
+ if (fault) throw new Error(`the packaged exchange registry registry/${network}.json is malformed: ${fault}`);
162
+ return parsed;
163
+ }
164
+ const documentOf = (file) => ({
165
+ schema: file.schema,
166
+ network: file.network,
167
+ registry_version: file.registry_version,
168
+ recorded_on: file.recorded_on,
169
+ rows: file.rows
170
+ });
171
+ async function readCache(network) {
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(await (0, node_fs_promises.readFile)(dexRegistryCachePath(network), "utf8"));
175
+ } catch {
176
+ return null;
177
+ }
178
+ if (!isRecord(parsed) || registryFault(parsed, network) !== null) return null;
179
+ if (typeof parsed.checked_at_ms !== "number" || typeof parsed.cached_at_ms !== "number") return null;
180
+ if (parsed.source !== "fetched" && parsed.source !== "packaged") return null;
181
+ return parsed;
182
+ }
183
+ async function writeCache(file) {
184
+ const target = dexRegistryCachePath(file.network);
185
+ await (0, node_fs_promises.mkdir)(node_path.default.dirname(target), { recursive: true });
186
+ const scratch = `${target}.${process.pid}.tmp`;
187
+ await (0, node_fs_promises.writeFile)(scratch, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
188
+ await (0, node_fs_promises.rename)(scratch, target);
189
+ }
190
+ async function fetchRegistry(network, options) {
191
+ const url = dexRegistryUrl(network, options.env);
192
+ const fetchImpl = options.fetch ?? fetch;
193
+ let response;
194
+ try {
195
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(options.budgetMs ?? 3e3) });
196
+ } catch (err) {
197
+ return { fault: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}` };
198
+ }
199
+ if (response.status !== 200) return { fault: `${url} answered HTTP ${response.status}` };
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(await response.text());
203
+ } catch {
204
+ return { fault: `${url} is not JSON` };
205
+ }
206
+ const fault = registryFault(parsed, network);
207
+ if (fault) return { fault: `${url} is malformed: ${fault}` };
208
+ return { document: parsed };
209
+ }
210
+ const inFlight = /* @__PURE__ */ new Map();
211
+ const defaultLog = (line) => {
212
+ process.stderr.write(`[chain-insights] dex-registry: ${line}\n`);
213
+ };
214
+ /**
215
+ * The registry of one network: the fresh cache, else the public file, else the
216
+ * stale cache, else the packaged copy. A packaged copy with a higher registry
217
+ * version than the cache or the public file wins, so an upgrade is never
218
+ * answered from an older file. Never throws for a network the package covers.
219
+ */
220
+ async function loadRegistry(rawNetwork, options = {}) {
221
+ const network = normalizeDexNetwork(rawNetwork);
222
+ const now = options.now ?? Date.now;
223
+ const log = options.log ?? defaultLog;
224
+ const packaged = readPackagedRegistry(network, options.packagedDir);
225
+ const newer = (document) => packaged !== null && packaged.registry_version > document.registry_version;
226
+ const fromPackaged = () => ({
227
+ document: packaged,
228
+ source: "packaged",
229
+ loaded_at_ms: now()
230
+ });
231
+ const cache = await readCache(network);
232
+ const age = cache ? now() - cache.checked_at_ms : Infinity;
233
+ if (cache && age >= 0 && age < 36e5) {
234
+ if (newer(cache)) return fromPackaged();
235
+ return {
236
+ document: documentOf(cache),
237
+ source: cache.source === "fetched" ? "cached" : "packaged",
238
+ loaded_at_ms: now()
239
+ };
240
+ }
241
+ const running = inFlight.get(network);
242
+ if (running) return running;
243
+ const load = (async () => {
244
+ const outcome = await fetchRegistry(network, options);
245
+ const checked = now();
246
+ const save = async (file) => {
247
+ try {
248
+ await writeCache(file);
249
+ } catch (err) {
250
+ log(`the cache ${dexRegistryCachePath(network)} could not be written: ${err instanceof Error ? err.message : String(err)}`);
251
+ }
252
+ };
253
+ if ("document" in outcome) {
254
+ const { document } = outcome;
255
+ await save({
256
+ ...document,
257
+ cached_at_ms: checked,
258
+ checked_at_ms: checked,
259
+ source: "fetched"
260
+ });
261
+ if (newer(document)) return fromPackaged();
262
+ return {
263
+ document,
264
+ source: "fetched",
265
+ loaded_at_ms: checked
266
+ };
267
+ }
268
+ log(outcome.fault);
269
+ if (cache) {
270
+ await save({
271
+ ...cache,
272
+ checked_at_ms: checked
273
+ });
274
+ if (newer(cache)) return fromPackaged();
275
+ return {
276
+ document: documentOf(cache),
277
+ source: cache.source === "fetched" ? "cached" : "packaged",
278
+ loaded_at_ms: checked
279
+ };
280
+ }
281
+ if (!packaged) throw new DexRegistryError(`network_not_covered: the exchange registry covers no network "${network}", and ${outcome.fault}`);
282
+ await save({
283
+ ...packaged,
284
+ cached_at_ms: checked,
285
+ checked_at_ms: checked,
286
+ source: "packaged"
287
+ });
288
+ return fromPackaged();
289
+ })();
290
+ inFlight.set(network, load);
291
+ try {
292
+ return await load;
293
+ } finally {
294
+ inFlight.delete(network);
295
+ }
296
+ }
297
+ /** Lower case, with every space, ".", "-" and "_" removed: "Uni v4" and "uni-v4" are "univ4". */
298
+ function normaliseName(name) {
299
+ return name.toLowerCase().replace(/[\s.\-_]+/g, "");
300
+ }
301
+ /** The optimal string alignment distance: insertions, deletions, substitutions and swaps of two neighbours. */
302
+ function damerauLevenshtein(a, b) {
303
+ const rows = a.length + 1;
304
+ const cols = b.length + 1;
305
+ const d = Array.from({ length: rows }, (_, i) => Array.from({ length: cols }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
306
+ for (let i = 1; i < rows; i += 1) for (let j = 1; j < cols; j += 1) {
307
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
308
+ let best = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
309
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, d[i - 2][j - 2] + 1);
310
+ d[i][j] = best;
311
+ }
312
+ return d[a.length][b.length];
313
+ }
314
+ /**
315
+ * One name against the registry rows:
316
+ * one exact hit matched
317
+ * several exact hits candidates at distance 0, matched null
318
+ * no exact hit, one nearest exchange matched, took_for the name
319
+ * no exact hit, a tie at the nearest candidates, matched null
320
+ * nothing within two letters matched null, no candidates
321
+ */
322
+ function matchDex(name, rows) {
323
+ const covered = rows.map((row) => row.display);
324
+ const wanted = normaliseName(name);
325
+ if (!wanted) return {
326
+ matched: null,
327
+ candidates: [],
328
+ covered
329
+ };
330
+ const exact = rows.filter((row) => [
331
+ row.dex,
332
+ row.display,
333
+ ...row.aliases
334
+ ].some((written) => normaliseName(written) === wanted));
335
+ if (exact.length === 1) return {
336
+ matched: exact[0],
337
+ candidates: [],
338
+ covered
339
+ };
340
+ if (exact.length > 1) return {
341
+ matched: null,
342
+ candidates: exact.map((row) => ({
343
+ dex: row.dex,
344
+ display: row.display,
345
+ distance: 0
346
+ })),
347
+ covered
348
+ };
349
+ const near = [];
350
+ for (const row of rows) {
351
+ const distances = row.aliases.map(normaliseName).filter((alias) => alias.length >= 4).map((alias) => damerauLevenshtein(wanted, alias));
352
+ const distance = Math.min(...distances);
353
+ if (distance <= 2) near.push({
354
+ row,
355
+ distance
356
+ });
357
+ }
358
+ if (near.length === 0) return {
359
+ matched: null,
360
+ candidates: [],
361
+ covered
362
+ };
363
+ const least = Math.min(...near.map((hit) => hit.distance));
364
+ const nearest = near.filter((hit) => hit.distance === least);
365
+ if (nearest.length === 1) return {
366
+ matched: nearest[0].row,
367
+ took_for: name.trim(),
368
+ candidates: [],
369
+ covered
370
+ };
371
+ return {
372
+ matched: null,
373
+ candidates: nearest.map(({ row, distance }) => ({
374
+ dex: row.dex,
375
+ display: row.display,
376
+ distance
377
+ })),
378
+ covered
379
+ };
380
+ }
381
+ //#endregion
382
+ Object.defineProperty(exports, "DexRegistryError", {
383
+ enumerable: true,
384
+ get: function() {
385
+ return DexRegistryError;
386
+ }
387
+ });
388
+ Object.defineProperty(exports, "dex_registry_exports", {
389
+ enumerable: true,
390
+ get: function() {
391
+ return dex_registry_exports;
392
+ }
393
+ });
394
+ Object.defineProperty(exports, "loadRegistry", {
395
+ enumerable: true,
396
+ get: function() {
397
+ return loadRegistry;
398
+ }
399
+ });
400
+ Object.defineProperty(exports, "matchDex", {
401
+ enumerable: true,
402
+ get: function() {
403
+ return matchDex;
404
+ }
405
+ });
406
+ Object.defineProperty(exports, "normalizeDexNetwork", {
407
+ enumerable: true,
408
+ get: function() {
409
+ return normalizeDexNetwork;
410
+ }
411
+ });