chain-insights 0.65.1 → 0.66.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.
- package/dist/cli.cjs +7 -7
- package/dist/cli.mjs +7 -7
- package/dist/mcp-proxy.cjs +2 -2
- package/dist/mcp-proxy.mjs +2 -2
- package/dist/{print-result-CZ2cH30Z.mjs → print-result-CJ8d7lmz.mjs} +80 -3
- package/dist/print-result-CJ8d7lmz.mjs.map +1 -0
- package/dist/{print-result--j99yoCV.cjs → print-result-qVhhuSdo.cjs} +79 -2
- package/dist/{tool-visibility-BppZZD77.cjs → tool-visibility-B4RAFMaB.cjs} +1 -1
- package/dist/{tool-visibility-C5IgMQuu.mjs → tool-visibility-kclgGKL5.mjs} +2 -2
- package/dist/{tool-visibility-C5IgMQuu.mjs.map → tool-visibility-kclgGKL5.mjs.map} +1 -1
- package/dist/{transport-error--eFoC7tP.mjs → transport-error-BP6bZVDs.mjs} +2 -2
- package/dist/{transport-error--eFoC7tP.mjs.map → transport-error-BP6bZVDs.mjs.map} +1 -1
- package/dist/{transport-error-YlHlPEWJ.cjs → transport-error-nSrFW47U.cjs} +1 -1
- package/dist/{update-CdMN-5BW.cjs → update-BDCDRozR.cjs} +1 -1
- package/dist/{update-DRZfepx8.mjs → update-S6j1hT_S.mjs} +2 -2
- package/dist/{update-DRZfepx8.mjs.map → update-S6j1hT_S.mjs.map} +1 -1
- package/docs/graph-tools.md +7 -0
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +42 -1
- package/dist/print-result-CZ2cH30Z.mjs.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_print_result = require("./print-result
|
|
2
|
+
const require_print_result = require("./print-result-qVhhuSdo.cjs");
|
|
3
3
|
let commander = require("commander");
|
|
4
4
|
let node_child_process = require("node:child_process");
|
|
5
5
|
let node_url = require("node:url");
|
|
@@ -94,7 +94,7 @@ async function withGraphMcpClient(name, fn) {
|
|
|
94
94
|
const { loadConfig } = await Promise.resolve().then(() => require("./config-BxtMoo9c.cjs")).then((n) => n.config_exports);
|
|
95
95
|
const config = await loadConfig();
|
|
96
96
|
const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await Promise.resolve().then(() => require("./client-Ypw9BNbn.cjs")).then((n) => n.client_exports);
|
|
97
|
-
const { toGraphMcpEndpointError } = await Promise.resolve().then(() => require("./transport-error-
|
|
97
|
+
const { toGraphMcpEndpointError } = await Promise.resolve().then(() => require("./transport-error-nSrFW47U.cjs"));
|
|
98
98
|
const endpoint = resolveGraphMcpEndpoint(config);
|
|
99
99
|
const paymentFetch = await createConfiguredGraphMcpFetch(config);
|
|
100
100
|
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
@@ -219,7 +219,7 @@ program.command("status").description("Show toolkit status and configuration").a
|
|
|
219
219
|
});
|
|
220
220
|
program.command("update").description("Check npmjs for a newer Chain Insights release and update this CLI").option("--check", "Only check for a newer release").option("--dry-run", "Print the update command without running it").action(async (opts) => {
|
|
221
221
|
try {
|
|
222
|
-
const { checkForUpdate, runPackageUpdate } = await Promise.resolve().then(() => require("./update-
|
|
222
|
+
const { checkForUpdate, runPackageUpdate } = await Promise.resolve().then(() => require("./update-BDCDRozR.cjs"));
|
|
223
223
|
const result = await checkForUpdate();
|
|
224
224
|
if (result.error) throw new Error(`Could not check npmjs for updates: ${result.error}`);
|
|
225
225
|
if (!result.updateAvailable || !result.latestVersion) {
|
|
@@ -499,7 +499,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
499
499
|
try {
|
|
500
500
|
const { loadSchema, saveSchema } = await Promise.resolve().then(() => require("./schema-cache-DwddPLcY.cjs"));
|
|
501
501
|
const { formatToolsTable } = await Promise.resolve().then(() => require("./format-BjZuYDr8.cjs"));
|
|
502
|
-
const { visibleRemoteTools } = await Promise.resolve().then(() => require("./tool-visibility-
|
|
502
|
+
const { visibleRemoteTools } = await Promise.resolve().then(() => require("./tool-visibility-B4RAFMaB.cjs")).then((n) => n.tool_visibility_exports);
|
|
503
503
|
const { loadConfig } = await Promise.resolve().then(() => require("./config-BxtMoo9c.cjs")).then((n) => n.config_exports);
|
|
504
504
|
const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await Promise.resolve().then(() => require("./client-Ypw9BNbn.cjs")).then((n) => n.client_exports);
|
|
505
505
|
const config = await loadConfig();
|
|
@@ -529,7 +529,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
529
529
|
})).addCommand(createCliCommand("call").description("Call a low-level MCP tool directly (advanced)").option("--json", "Print machine-readable JSON output").argument("<tool>", "Tool name to call").argument("[args...]", "Key=value arguments (e.g. address=0x1234... network=robinhood)").action(async (tool, rawArgs, opts) => {
|
|
530
530
|
try {
|
|
531
531
|
const { parseMcpCallArgs } = await Promise.resolve().then(() => require("./call-args-DhJiv6T8.cjs"));
|
|
532
|
-
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await Promise.resolve().then(() => require("./tool-visibility-
|
|
532
|
+
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await Promise.resolve().then(() => require("./tool-visibility-B4RAFMaB.cjs")).then((n) => n.tool_visibility_exports);
|
|
533
533
|
let args = parseMcpCallArgs(rawArgs);
|
|
534
534
|
assertPublicMcpToolName(tool);
|
|
535
535
|
validatePublicMcpToolArguments(tool, args);
|
|
@@ -595,7 +595,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
595
595
|
}
|
|
596
596
|
const { graphToolRequestOptions } = await Promise.resolve().then(() => require("./request-timeout-CYmQF8N7.cjs"));
|
|
597
597
|
const requestOptions = graphToolRequestOptions(tool);
|
|
598
|
-
const { withoutUtcProperties } = await Promise.resolve().then(() => require("./print-result
|
|
598
|
+
const { withoutUtcProperties } = await Promise.resolve().then(() => require("./print-result-qVhhuSdo.cjs")).then((n) => n.timestamp_text_exports);
|
|
599
599
|
args = withoutUtcProperties(tool, args);
|
|
600
600
|
const result = requestOptions ? await client.callTool({
|
|
601
601
|
name: tool,
|
|
@@ -610,7 +610,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
610
610
|
});
|
|
611
611
|
});
|
|
612
612
|
} catch (err) {
|
|
613
|
-
const { formatMcpCallError } = await Promise.resolve().then(() => require("./tool-visibility-
|
|
613
|
+
const { formatMcpCallError } = await Promise.resolve().then(() => require("./tool-visibility-B4RAFMaB.cjs")).then((n) => n.tool_visibility_exports);
|
|
614
614
|
console.error(formatMcpCallError(tool, err));
|
|
615
615
|
process.exit(1);
|
|
616
616
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as PACKAGE_VERSION, r as printMcpTextContent, s as PACKAGE_INFO } from "./print-result-
|
|
1
|
+
import { c as PACKAGE_VERSION, r as printMcpTextContent, s as PACKAGE_INFO } from "./print-result-CJ8d7lmz.mjs";
|
|
2
2
|
import { Command, Option } from "commander";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -92,7 +92,7 @@ async function withGraphMcpClient(name, fn) {
|
|
|
92
92
|
const { loadConfig } = await import("./config-9MabJh5R.mjs").then((n) => n.t);
|
|
93
93
|
const config = await loadConfig();
|
|
94
94
|
const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await import("./client-gsLi528K.mjs").then((n) => n.r);
|
|
95
|
-
const { toGraphMcpEndpointError } = await import("./transport-error
|
|
95
|
+
const { toGraphMcpEndpointError } = await import("./transport-error-BP6bZVDs.mjs");
|
|
96
96
|
const endpoint = resolveGraphMcpEndpoint(config);
|
|
97
97
|
const paymentFetch = await createConfiguredGraphMcpFetch(config);
|
|
98
98
|
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
@@ -217,7 +217,7 @@ program.command("status").description("Show toolkit status and configuration").a
|
|
|
217
217
|
});
|
|
218
218
|
program.command("update").description("Check npmjs for a newer Chain Insights release and update this CLI").option("--check", "Only check for a newer release").option("--dry-run", "Print the update command without running it").action(async (opts) => {
|
|
219
219
|
try {
|
|
220
|
-
const { checkForUpdate, runPackageUpdate } = await import("./update-
|
|
220
|
+
const { checkForUpdate, runPackageUpdate } = await import("./update-S6j1hT_S.mjs");
|
|
221
221
|
const result = await checkForUpdate();
|
|
222
222
|
if (result.error) throw new Error(`Could not check npmjs for updates: ${result.error}`);
|
|
223
223
|
if (!result.updateAvailable || !result.latestVersion) {
|
|
@@ -497,7 +497,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
497
497
|
try {
|
|
498
498
|
const { loadSchema, saveSchema } = await import("./schema-cache-BT-kHy_r.mjs");
|
|
499
499
|
const { formatToolsTable } = await import("./format-DB2IH2U5.mjs");
|
|
500
|
-
const { visibleRemoteTools } = await import("./tool-visibility-
|
|
500
|
+
const { visibleRemoteTools } = await import("./tool-visibility-kclgGKL5.mjs").then((n) => n.a);
|
|
501
501
|
const { loadConfig } = await import("./config-9MabJh5R.mjs").then((n) => n.t);
|
|
502
502
|
const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await import("./client-gsLi528K.mjs").then((n) => n.r);
|
|
503
503
|
const config = await loadConfig();
|
|
@@ -527,7 +527,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
527
527
|
})).addCommand(createCliCommand("call").description("Call a low-level MCP tool directly (advanced)").option("--json", "Print machine-readable JSON output").argument("<tool>", "Tool name to call").argument("[args...]", "Key=value arguments (e.g. address=0x1234... network=robinhood)").action(async (tool, rawArgs, opts) => {
|
|
528
528
|
try {
|
|
529
529
|
const { parseMcpCallArgs } = await import("./call-args-Bdw2sV4n.mjs");
|
|
530
|
-
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await import("./tool-visibility-
|
|
530
|
+
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await import("./tool-visibility-kclgGKL5.mjs").then((n) => n.a);
|
|
531
531
|
let args = parseMcpCallArgs(rawArgs);
|
|
532
532
|
assertPublicMcpToolName(tool);
|
|
533
533
|
validatePublicMcpToolArguments(tool, args);
|
|
@@ -593,7 +593,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
593
593
|
}
|
|
594
594
|
const { graphToolRequestOptions } = await import("./request-timeout-lBBe0mGf.mjs");
|
|
595
595
|
const requestOptions = graphToolRequestOptions(tool);
|
|
596
|
-
const { withoutUtcProperties } = await import("./print-result-
|
|
596
|
+
const { withoutUtcProperties } = await import("./print-result-CJ8d7lmz.mjs").then((n) => n.i);
|
|
597
597
|
args = withoutUtcProperties(tool, args);
|
|
598
598
|
const result = requestOptions ? await client.callTool({
|
|
599
599
|
name: tool,
|
|
@@ -608,7 +608,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
608
608
|
});
|
|
609
609
|
});
|
|
610
610
|
} catch (err) {
|
|
611
|
-
const { formatMcpCallError } = await import("./tool-visibility-
|
|
611
|
+
const { formatMcpCallError } = await import("./tool-visibility-kclgGKL5.mjs").then((n) => n.a);
|
|
612
612
|
console.error(formatMcpCallError(tool, err));
|
|
613
613
|
process.exit(1);
|
|
614
614
|
}
|
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
3
|
-
const require_print_result = require("./print-result
|
|
3
|
+
const require_print_result = require("./print-result-qVhhuSdo.cjs");
|
|
4
4
|
const require_client = require("./client-Ypw9BNbn.cjs");
|
|
5
|
-
const require_tool_visibility = require("./tool-visibility-
|
|
5
|
+
const require_tool_visibility = require("./tool-visibility-B4RAFMaB.cjs");
|
|
6
6
|
const require_usage_status = require("./usage-status-BdwwgYsu.cjs");
|
|
7
7
|
const require_subscription_status = require("./subscription-status-ru99UCPZ.cjs");
|
|
8
8
|
const require_capabilities = require("./capabilities-DGavTSoW.cjs");
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as withTimestampTextResult, c as PACKAGE_VERSION, o as withoutUtcProperties } from "./print-result-
|
|
1
|
+
import { a as withTimestampTextResult, c as PACKAGE_VERSION, o as withoutUtcProperties } from "./print-result-CJ8d7lmz.mjs";
|
|
2
2
|
import { t as PaymentRequiredError } from "./client-gsLi528K.mjs";
|
|
3
|
-
import { i as isAppOnlyTool, n as PUBLIC_MCP_TOOL_ALLOWED_ARGS, r as PUBLIC_MCP_TOOL_REQUIRED_ARGS, t as HIDDEN_REMOTE_TOOL_NAMES } from "./tool-visibility-
|
|
3
|
+
import { i as isAppOnlyTool, n as PUBLIC_MCP_TOOL_ALLOWED_ARGS, r as PUBLIC_MCP_TOOL_REQUIRED_ARGS, t as HIDDEN_REMOTE_TOOL_NAMES } from "./tool-visibility-kclgGKL5.mjs";
|
|
4
4
|
import { t as primitiveBackendUsageStatus } from "./usage-status-CXOnrq-s.mjs";
|
|
5
5
|
import { n as unavailableSubscriptionStatus } from "./subscription-status-DA9DCFrC.mjs";
|
|
6
6
|
import { r as mirrorGraphNetworkCapabilities } from "./capabilities-BBVDIECk.mjs";
|
|
@@ -50,6 +50,82 @@ function stripEthProperties(query) {
|
|
|
50
50
|
return query.replace(/\.((?:[A-Za-z_][A-Za-z0-9_]*_)?balance)_eth\b/g, ".$1");
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
|
+
//#region src/mcp/label-case.ts
|
|
54
|
+
const CANONICAL = new Map([
|
|
55
|
+
"Account",
|
|
56
|
+
"Address",
|
|
57
|
+
"Bridge",
|
|
58
|
+
"Bundler",
|
|
59
|
+
"Chain",
|
|
60
|
+
"Contract",
|
|
61
|
+
"EntryPoint",
|
|
62
|
+
"Exchange",
|
|
63
|
+
"Launchpad",
|
|
64
|
+
"Mixer",
|
|
65
|
+
"NFT",
|
|
66
|
+
"OnMoneyTrail",
|
|
67
|
+
"Paymaster",
|
|
68
|
+
"Poisoned",
|
|
69
|
+
"Pool",
|
|
70
|
+
"Protocol",
|
|
71
|
+
"Sanctioned",
|
|
72
|
+
"Scam",
|
|
73
|
+
"SmartAccount",
|
|
74
|
+
"Targeted",
|
|
75
|
+
"Token",
|
|
76
|
+
"Victim",
|
|
77
|
+
"Transaction",
|
|
78
|
+
"Block",
|
|
79
|
+
"Head",
|
|
80
|
+
"ADDED_LIQUIDITY",
|
|
81
|
+
"APPROVED",
|
|
82
|
+
"BRIDGED",
|
|
83
|
+
"BUNDLED",
|
|
84
|
+
"DEPLOYED_CONTRACT",
|
|
85
|
+
"FLOWS_TO",
|
|
86
|
+
"LINKED",
|
|
87
|
+
"OPERATED_BY",
|
|
88
|
+
"REMOVED_LIQUIDITY",
|
|
89
|
+
"SIGNED_AUTHORIZATION",
|
|
90
|
+
"SIGNED_FOR",
|
|
91
|
+
"SPONSORED",
|
|
92
|
+
"SWAPPED",
|
|
93
|
+
"TRANSFERRED",
|
|
94
|
+
"TRANSFER",
|
|
95
|
+
"BRIDGE_CROSSING",
|
|
96
|
+
"SWAP",
|
|
97
|
+
"LIQUIDITY_ADD",
|
|
98
|
+
"LIQUIDITY_REMOVE"
|
|
99
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
100
|
+
const IDENT = /[A-Za-z0-9_]/;
|
|
101
|
+
function prevNonSpace(text, from) {
|
|
102
|
+
let i = from;
|
|
103
|
+
while (i >= 0 && /\s/.test(text[i])) i--;
|
|
104
|
+
return i;
|
|
105
|
+
}
|
|
106
|
+
/** True when the `:` or `|` at `at` opens a label or link type, not a map value. */
|
|
107
|
+
function opensLabel(text, at) {
|
|
108
|
+
let i = prevNonSpace(text, at - 1);
|
|
109
|
+
if (i < 0) return false;
|
|
110
|
+
if (text[at] !== "|" && (text[i] === "(" || text[i] === "[")) return true;
|
|
111
|
+
if (!IDENT.test(text[i])) return false;
|
|
112
|
+
while (i >= 0 && IDENT.test(text[i])) i--;
|
|
113
|
+
const before = prevNonSpace(text, i);
|
|
114
|
+
if (before >= 0 && (text[before] === ":" || text[before] === "|")) return opensLabel(text, before);
|
|
115
|
+
return text[at] === ":" && before >= 0 && (text[before] === "(" || text[before] === "[");
|
|
116
|
+
}
|
|
117
|
+
function fixSegment(segment) {
|
|
118
|
+
return segment.replace(/([:|])(\s*)([A-Za-z_][A-Za-z0-9_]*)/g, (whole, mark, space, name, at) => {
|
|
119
|
+
const canonical = CANONICAL.get(name.toLowerCase());
|
|
120
|
+
if (!canonical || canonical === name || !opensLabel(segment, at)) return whole;
|
|
121
|
+
return `${mark}${space}${canonical}`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** The query with every known label and link type in the graph's case; quoted text is untouched. */
|
|
125
|
+
function fixLabelCase(query) {
|
|
126
|
+
return query.split(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[^`]*`)/).map((part, index) => index % 2 === 1 ? part : fixSegment(part)).join("");
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
53
129
|
//#region src/mcp/timestamp-text.ts
|
|
54
130
|
/**
|
|
55
131
|
* A UTC text twin beside every timestamp column of a graph answer.
|
|
@@ -102,10 +178,11 @@ function addTimestampText(row) {
|
|
|
102
178
|
* reference is rewritten to its millisecond column, `x.<column>_utc` to
|
|
103
179
|
* `x.<column>`; an alias after AS is not a property and stays as written, so
|
|
104
180
|
* `f.first_seen_timestamp_utc AS first_seen_utc` yields a `first_seen_utc`
|
|
105
|
-
* column that addTimestampText renders as text.
|
|
181
|
+
* column that addTimestampText renders as text. A label or link type in the
|
|
182
|
+
* wrong case is spelled the graph's way (label-case.ts).
|
|
106
183
|
*/
|
|
107
184
|
function stripUtcProperties(query) {
|
|
108
|
-
return stripEthProperties(query.replace(/\.([A-Za-z_][A-Za-z0-9_]*?)_utc\b/g, ".$1"));
|
|
185
|
+
return fixLabelCase(stripEthProperties(query.replace(/\.([A-Za-z_][A-Za-z0-9_]*?)_utc\b/g, ".$1")));
|
|
109
186
|
}
|
|
110
187
|
/** graph_query and graph_query_batch arguments with stripUtcProperties applied to every query. */
|
|
111
188
|
function withoutUtcProperties(toolName, args) {
|
|
@@ -348,4 +425,4 @@ function printMcpTextContent(result, options = {}) {
|
|
|
348
425
|
//#endregion
|
|
349
426
|
export { withTimestampTextResult as a, PACKAGE_VERSION as c, timestamp_text_exports as i, formatMcpToolError as n, withoutUtcProperties as o, printMcpTextContent as r, PACKAGE_INFO as s, McpToolError as t };
|
|
350
427
|
|
|
351
|
-
//# sourceMappingURL=print-result-
|
|
428
|
+
//# sourceMappingURL=print-result-CJ8d7lmz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"print-result-CJ8d7lmz.mjs","names":["isRecord"],"sources":["../src/version.ts","../src/mcp/balance-text.ts","../src/mcp/label-case.ts","../src/mcp/timestamp-text.ts","../src/mcp/print-result.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\n\nexport const PACKAGE_INFO = JSON.parse(\n readFileSync(new URL('../package.json', import.meta.url), 'utf8')\n) as { name: string; version: string }\n\nexport const PACKAGE_VERSION = PACKAGE_INFO.version\n","/**\n * An ETH text twin beside every native balance column of a chain answer.\n *\n * `USE chain` serves `balance` as wei in decimal text, exact and 16 to 22\n * digits long. A host without a shell (Claude Desktop, 2026-10-21) printed\n * `10003752747799400` to the user as the balance. A model that divides by\n * 10^18 by head misplaces the point, so the proxy writes `<column>_eth`\n * next to each one, the same way `<column>_utc` sits beside a timestamp.\n */\n\nconst BALANCE_KEY = /(^|_)balance$/\nconst WEI_TEXT = /^[0-9]{1,78}$/\nconst WEI_PER_ETH = 10n ** 18n\n\n/** `0.0100037527477994` for `10003752747799400` wei: exact, no exponent, no trailing zeros. */\nexport function ethText(wei: string): string {\n const value = BigInt(wei)\n const whole = value / WEI_PER_ETH\n const frac = (value % WEI_PER_ETH).toString().padStart(18, '0').replace(/0+$/, '')\n return frac === '' ? whole.toString() : `${whole}.${frac}`\n}\n\nfunction isWeiText(value: unknown): value is string {\n return typeof value === 'string' && WEI_TEXT.test(value)\n}\n\n/**\n * One row with a `<key>_eth` string after every wei balance column that has\n * no twin yet. A column the model itself named `*_eth` (an alias such as\n * `a.balance_eth AS held_eth`, see stripEthProperties) arrives as wei and is\n * rendered as ETH text in place: the model gets the column it asked for.\n */\nexport function addBalanceText(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(row)) {\n out[key] = key.endsWith('_eth') && isWeiText(value) ? ethText(value) : value\n if (BALANCE_KEY.test(key) && isWeiText(value) && !(`${key}_eth` in row)) out[`${key}_eth`] = ethText(value)\n }\n return out\n}\n\n/**\n * The `_eth` twin is added by the proxy; it is no chain property. A query\n * that names `a.balance_eth` is rewritten to `a.balance`, so the row comes\n * back and the twin is written beside it. An alias after AS stays as written.\n */\nexport function stripEthProperties(query: string): string {\n return query.replace(/\\.((?:[A-Za-z_][A-Za-z0-9_]*_)?balance)_eth\\b/g, '.$1')\n}\n","/**\n * A node label or link type written in the wrong case is spelled the graph's way.\n *\n * Labels and link types are case sensitive. `(a:address {address: \"0x…\"})`\n * matches nothing and the read answers zero rows with no error, so the model\n * tells the user the address has no activity. Claude Desktop did exactly that\n * for the basket router on 2026-10-21 (skill loop, basket round 8): its links\n * were there under `:Address`.\n *\n * Only a name that is a known label or link type, in another case, after the\n * `:` (or `|`) of a node or link pattern is rewritten. Text inside quotes and a\n * map value such as `{address: address}` stay as written.\n */\n\nconst NAMES = [\n // topology node labels\n 'Account', 'Address', 'Bridge', 'Bundler', 'Chain', 'Contract', 'EntryPoint', 'Exchange', 'Launchpad', 'Mixer',\n 'NFT', 'OnMoneyTrail', 'Paymaster', 'Poisoned', 'Pool', 'Protocol', 'Sanctioned', 'Scam', 'SmartAccount',\n 'Targeted', 'Token', 'Victim',\n // chain lookups\n 'Transaction', 'Block', 'Head',\n // topology link types\n 'ADDED_LIQUIDITY', 'APPROVED', 'BRIDGED', 'BUNDLED', 'DEPLOYED_CONTRACT', 'FLOWS_TO', 'LINKED', 'OPERATED_BY',\n 'REMOVED_LIQUIDITY', 'SIGNED_AUTHORIZATION', 'SIGNED_FOR', 'SPONSORED', 'SWAPPED', 'TRANSFERRED',\n // facts link types\n 'TRANSFER', 'BRIDGE_CROSSING', 'SWAP', 'LIQUIDITY_ADD', 'LIQUIDITY_REMOVE',\n]\n\nconst CANONICAL = new Map(NAMES.map((name) => [name.toLowerCase(), name]))\nconst IDENT = /[A-Za-z0-9_]/\n\nfunction prevNonSpace(text: string, from: number): number {\n let i = from\n while (i >= 0 && /\\s/.test(text[i])) i--\n return i\n}\n\n/** True when the `:` or `|` at `at` opens a label or link type, not a map value. */\nfunction opensLabel(text: string, at: number): boolean {\n let i = prevNonSpace(text, at - 1)\n if (i < 0) return false\n // `[:TRANSFER|swap]`: a `|` counts only after a link type of its own.\n if (text[at] !== '|' && (text[i] === '(' || text[i] === '[')) return true\n if (!IDENT.test(text[i])) return false\n while (i >= 0 && IDENT.test(text[i])) i--\n const before = prevNonSpace(text, i)\n // `(a:Address:contract)`: the name before this `:` is itself a label.\n if (before >= 0 && (text[before] === ':' || text[before] === '|')) return opensLabel(text, before)\n return text[at] === ':' && before >= 0 && (text[before] === '(' || text[before] === '[')\n}\n\nfunction fixSegment(segment: string): string {\n return segment.replace(/([:|])(\\s*)([A-Za-z_][A-Za-z0-9_]*)/g, (whole, mark: string, space: string, name: string, at: number) => {\n const canonical = CANONICAL.get(name.toLowerCase())\n if (!canonical || canonical === name || !opensLabel(segment, at)) return whole\n return `${mark}${space}${canonical}`\n })\n}\n\n/** The query with every known label and link type in the graph's case; quoted text is untouched. */\nexport function fixLabelCase(query: string): string {\n return query\n .split(/(\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|`[^`]*`)/)\n .map((part, index) => (index % 2 === 1 ? part : fixSegment(part)))\n .join('')\n}\n","/**\n * A UTC text twin beside every timestamp column of a graph answer.\n *\n * The graph serves integer milliseconds. A model that converts them by head\n * gets the time of day wrong about one time in three (loop 3, 2026-10-10:\n * four of twelve answers about one block named 00:15, 09:35, 06:35 or 09:15\n * for 23:55:43 UTC). Hosts without a shell (Claude Desktop) have no other\n * way to convert, so the proxy writes `<column>_utc` next to each one.\n */\n\nimport { addBalanceText, stripEthProperties } from './balance-text.js'\nimport { fixLabelCase } from './label-case.js'\n\nconst TIMESTAMP_KEY = /(_timestamp|_time|_at|_seen|_active)$/\nconst MIN_MS = 1_000_000_000_000 // 2001-09-09\nconst MAX_MS = 4_000_000_000_000 // 2096-10-02\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** `2026-08-05 23:55:43 UTC` for epoch milliseconds. */\nexport function utcText(ms: number): string {\n return `${new Date(ms).toISOString().slice(0, 19).replace('T', ' ')} UTC`\n}\n\nfunction isEpochMs(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= MIN_MS && value <= MAX_MS\n}\n\n/**\n * One row with a `<key>_utc` string after every epoch-millisecond timestamp\n * column. A column the model itself named `*_utc` (its query asked for the\n * twin as a property, see stripUtcProperties) arrives as milliseconds and is\n * rendered as UTC text in place: the model gets the column it asked for.\n */\nexport function addTimestampText(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(row)) {\n out[key] = key.endsWith('_utc') && isEpochMs(value) ? utcText(value) : value\n if (TIMESTAMP_KEY.test(key) && isEpochMs(value) && !(`${key}_utc` in row)) out[`${key}_utc`] = utcText(value)\n }\n return out\n}\n\n/**\n * The `_utc` twin is added to a reply by the proxy; it is no graph property.\n * Thirteen of 48 loop-5 rounds wrote `f.first_seen_timestamp_utc` into a\n * query anyway: topology answers null, a SWAP read is refused. The property\n * reference is rewritten to its millisecond column, `x.<column>_utc` to\n * `x.<column>`; an alias after AS is not a property and stays as written, so\n * `f.first_seen_timestamp_utc AS first_seen_utc` yields a `first_seen_utc`\n * column that addTimestampText renders as text. A label or link type in the\n * wrong case is spelled the graph's way (label-case.ts).\n */\nexport function stripUtcProperties(query: string): string {\n return fixLabelCase(stripEthProperties(query.replace(/\\.([A-Za-z_][A-Za-z0-9_]*?)_utc\\b/g, '.$1')))\n}\n\n/** graph_query and graph_query_batch arguments with stripUtcProperties applied to every query. */\nexport function withoutUtcProperties(toolName: string, args: Record<string, unknown>): Record<string, unknown> {\n if (toolName === 'graph_query' && typeof args.query === 'string') return { ...args, query: stripUtcProperties(args.query) }\n if (toolName === 'graph_query_batch' && Array.isArray(args.queries)) {\n return {\n ...args,\n queries: args.queries.map((entry) =>\n isRecord(entry) && typeof entry.query === 'string' ? { ...entry, query: stripUtcProperties(entry.query) } : entry\n ),\n }\n }\n return args\n}\n\nfunction mapResults(holder: unknown): unknown {\n if (!isRecord(holder) || !Array.isArray(holder.results)) return holder\n return {\n ...holder,\n results: holder.results.map((row) => (isRecord(row) ? addBalanceText(addTimestampText(row)) : row)),\n }\n}\n\n/**\n * The structured content of graph_query (facts.query.results) or\n * graph_query_batch (facts.queries[].results) with the UTC twins and the\n * ETH twins (balance-text.ts) added.\n * Anything else comes back as it was.\n */\nexport function withTimestampText(structuredContent: unknown): unknown {\n if (!isRecord(structuredContent) || !isRecord(structuredContent.facts)) return structuredContent\n const facts = structuredContent.facts\n if (!isRecord(facts.query) && !Array.isArray(facts.queries)) return structuredContent\n const next: Record<string, unknown> = { ...facts }\n if (isRecord(facts.query)) next.query = mapResults(facts.query)\n if (Array.isArray(facts.queries)) next.queries = facts.queries.map(mapResults)\n return { ...structuredContent, facts: next }\n}\n\ntype TextBlock = { type: string; text?: string }\n\n/**\n * A tool result whose structured content and single JSON text block both carry\n * the UTC twins. A text block that is not the JSON of the structured content\n * is left alone.\n */\nexport function withTimestampTextResult<T extends { content?: TextBlock[]; structuredContent?: unknown }>(result: T): T {\n const structured = withTimestampText(result.structuredContent)\n if (structured === result.structuredContent) return result\n const content = result.content ?? []\n const only = content.length === 1 && content[0]?.type === 'text' ? content[0] : null\n let parsed: unknown = undefined\n try {\n parsed = only?.text ? JSON.parse(only.text) : undefined\n } catch {\n parsed = undefined\n }\n const textIsStructured =\n parsed !== undefined && JSON.stringify(parsed) === JSON.stringify(result.structuredContent)\n return {\n ...result,\n structuredContent: structured,\n content: textIsStructured ? [{ ...only, text: JSON.stringify(structured) }] : content,\n }\n}\n","import { withTimestampTextResult } from './timestamp-text.js'\n\nexport interface McpTextResult {\n content?: Array<{ type: string; text?: string }>\n structuredContent?: unknown\n isError?: boolean\n}\n\n/**\n * The refusal envelope of the graph server, as the reply carries it in\n * `structuredContent.error_detail`. `class` is one of `refused`, `killed`,\n * `capacity` or `failed`, and it decides the caller's next move.\n */\nexport interface McpErrorDetail {\n code?: string\n rule?: string\n class?: string\n fix?: string\n example?: string\n}\n\nconst ERROR_DETAIL_FIELDS = ['code', 'rule', 'class', 'fix', 'example'] as const\n\n/**\n * A tool reply flagged `isError`: the server answered, and its answer is an\n * error. It is never a failure of the connection, so no caller may report it\n * as an unreachable endpoint. `message` is the server's own text and\n * `errorDetail` is the envelope of the reply, absent when the reply has none.\n */\nexport class McpToolError extends Error {\n readonly errorDetail?: McpErrorDetail\n\n constructor(message: string, errorDetail?: McpErrorDetail) {\n super(message)\n this.name = 'McpToolError'\n if (errorDetail) this.errorDetail = errorDetail\n }\n}\n\nexport interface McpPrintOptions {\n tool?: string\n json?: boolean\n}\n\n// What a switched-off or lagging chain layer means for the caller, in the line\n// under the server's text. The endpoint answered, so the endpoint is up, and\n// the other two layers do not depend on the chain node.\nconst CHAIN_UNAVAILABLE_MEANING =\n 'the chain layer is off or behind. The endpoint is up. USE topology and USE facts still work.'\n\n/**\n * How `cia mcp call` shows an McpToolError: the server's own text, then one\n * line built from its error_detail, `code <code> · class <class> · <fix>`. A\n * reply with no error_detail is the server text alone. For `chain_unavailable`\n * the line also says what the code means for the caller.\n */\nexport function formatMcpToolError(err: McpToolError): string {\n const detail = err.errorDetail\n if (!detail) return err.message\n const parts: string[] = []\n if (detail.code) parts.push(`code ${detail.code}`)\n if (detail.class) parts.push(`class ${detail.class}`)\n if (detail.fix) parts.push(detail.fix)\n if (detail.code === 'chain_unavailable') parts.push(CHAIN_UNAVAILABLE_MEANING)\n if (parts.length === 0) return err.message\n return `${err.message}\\n ${parts.join(' · ')}`\n}\n\ntype JsonRecord = Record<string, unknown>\n\nfunction isRecord(value: unknown): value is JsonRecord {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\n/** The text fields of `structuredContent.error_detail`, or undefined when it has none. */\nfunction errorDetailOf(structuredContent: unknown): McpErrorDetail | undefined {\n if (!isRecord(structuredContent) || !isRecord(structuredContent.error_detail)) return undefined\n const detail: McpErrorDetail = {}\n for (const field of ERROR_DETAIL_FIELDS) {\n const value = structuredContent.error_detail[field]\n if (typeof value === 'string') detail[field] = value\n }\n return Object.keys(detail).length > 0 ? detail : undefined\n}\n\nfunction displayValue(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n}\n\nfunction formatTable(rows: Array<JsonRecord>): string[] {\n if (rows.length === 0) return []\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))]\n const values = rows.map((row) => columns.map((column) => displayValue(row[column])))\n const widths = columns.map((column, index) =>\n Math.max(column.length, ...values.map((row) => row[index]?.length ?? 0))\n )\n const line = (row: string[]): string =>\n row\n .map((value, index) => (index === row.length - 1 ? value : value.padEnd(widths[index] ?? 0)))\n .join(' | ')\n return [line(columns), widths.map((width) => '-'.repeat(width)).join('-+-'), ...values.map(line)]\n}\n\nfunction formatGraphResult(value: JsonRecord, fallbackTool?: string): string | null {\n const facts = isRecord(value.facts) ? value.facts : undefined\n const query = facts && isRecord(facts.query) ? facts.query : undefined\n const queries = facts && Array.isArray(facts.queries) ? facts.queries : undefined\n if (!query && !queries) return null\n\n const lines = [`Tool: ${displayValue(value.tool) || fallbackTool || 'MCP result'}`]\n const subject = isRecord(value.subject)\n ? value.subject\n : facts && isRecord(facts.subject)\n ? facts.subject\n : undefined\n if (subject?.network !== undefined) lines.push(`Network: ${displayValue(subject.network)}`)\n\n if (query) {\n const results = Array.isArray(query.results) ? query.results.filter(isRecord) : []\n if (query.count !== undefined) lines.push(`Rows: ${displayValue(query.count)}`)\n else lines.push(`Rows: ${results.length}`)\n if (query.billable_units !== undefined) {\n lines.push(`Billed units: ${displayValue(query.billable_units)}`)\n }\n if (query.elapsed_ms !== undefined) lines.push(`Elapsed: ${displayValue(query.elapsed_ms)} ms`)\n if (query.truncated !== undefined) lines.push(`Truncated: ${displayValue(query.truncated)}`)\n if (query.error !== undefined) lines.push(`Error: ${displayValue(query.error)}`)\n if (results.length > 0) lines.push('', 'Results:', ...formatTable(results))\n else lines.push('', 'Results: none')\n }\n\n if (queries) {\n lines.push(`Queries: ${queries.length}`)\n for (const entry of queries) {\n if (!isRecord(entry)) continue\n const id = displayValue(entry.id) || 'query'\n const results = Array.isArray(entry.results) ? entry.results.filter(isRecord) : []\n lines.push('', `[${id}]`)\n if (entry.error !== undefined) lines.push(`Error: ${displayValue(entry.error)}`)\n lines.push(`Rows: ${displayValue(entry.count ?? results.length)}`)\n if (results.length > 0) lines.push(...formatTable(results))\n else lines.push('Results: none')\n }\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Formats one MCP text block for terminal output. JSON is indented when the\n * caller requests `--json`; graph query envelopes are rendered as a compact\n * result summary and table by default.\n */\nexport function formatMcpTextContent(\n text: string,\n tool?: string,\n options: Pick<McpPrintOptions, 'json'> = {}\n): string {\n let parsed: unknown\n try {\n parsed = JSON.parse(text) as unknown\n } catch {\n return text\n }\n\n if (options.json) {\n try {\n return JSON.stringify(parsed, null, 2)\n } catch {\n return text\n }\n }\n\n if (isRecord(parsed)) {\n return formatGraphResult(parsed, tool) ?? JSON.stringify(parsed, null, 2)\n }\n return displayValue(parsed)\n}\n\n/**\n * Prints the text blocks of an MCP tool result to stdout. When the result is\n * flagged `isError`, throws an McpToolError with the tool's error text instead\n * — MCP `callTool` returns tool errors as ordinary results (it does not\n * reject), so callers must surface them as failures (non-zero exit) rather than\n * printing to stdout and exiting 0. The error keeps the `error_detail` of the\n * reply, so a caller can tell a refused query from a dead endpoint.\n */\n/** The tools whose rows carry the `_utc` and `_eth` twins, the same two the proxy adds (src/mcp/proxy.ts). */\nfunction isGraphReadTool(tool?: string): boolean {\n return tool === 'graph_query' || tool === 'graph_query_batch'\n}\n\n/**\n * A graph reply with the twins the proxy writes for a host: `<column>_utc`\n * beside every timestamp and `<column>_eth` beside every wei balance. The\n * shell path printed the server row as it arrived (chain-insights #509), so a\n * person reading `cia mcp call` got the raw milliseconds and the raw wei.\n * The text block the server sends is the JSON of its structured content, so\n * both are rewritten together; a result with no structured content is read\n * from its text block first.\n */\nexport function withGraphTwins(result: McpTextResult, tool?: string): McpTextResult {\n if (!isGraphReadTool(tool) || result.isError) return result\n const only = (result.content ?? []).length === 1 && result.content?.[0]?.type === 'text' ? result.content[0] : null\n let structured = result.structuredContent\n if (structured === undefined && only?.text) {\n try {\n structured = JSON.parse(only.text) as unknown\n } catch {\n return result\n }\n }\n const twinned = withTimestampTextResult({ ...result, structuredContent: structured })\n return twinned.structuredContent === structured && result.structuredContent !== undefined ? result : twinned\n}\n\nexport function printMcpTextContent(result: McpTextResult, options: McpPrintOptions = {}): void {\n result = withGraphTwins(result, options.tool)\n const texts = (result.content ?? [])\n .filter((item) => item.type === 'text')\n .map((item) => item.text ?? '')\n\n if (result.isError) {\n throw new McpToolError(\n texts.join('\\n').trim() || 'MCP tool returned an error',\n errorDetailOf(result.structuredContent)\n )\n }\n\n for (const text of texts) console.log(formatMcpTextContent(text, options.tool, options))\n}\n"],"mappings":"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"}
|
|
@@ -50,6 +50,82 @@ function stripEthProperties(query) {
|
|
|
50
50
|
return query.replace(/\.((?:[A-Za-z_][A-Za-z0-9_]*_)?balance)_eth\b/g, ".$1");
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
|
+
//#region src/mcp/label-case.ts
|
|
54
|
+
const CANONICAL = new Map([
|
|
55
|
+
"Account",
|
|
56
|
+
"Address",
|
|
57
|
+
"Bridge",
|
|
58
|
+
"Bundler",
|
|
59
|
+
"Chain",
|
|
60
|
+
"Contract",
|
|
61
|
+
"EntryPoint",
|
|
62
|
+
"Exchange",
|
|
63
|
+
"Launchpad",
|
|
64
|
+
"Mixer",
|
|
65
|
+
"NFT",
|
|
66
|
+
"OnMoneyTrail",
|
|
67
|
+
"Paymaster",
|
|
68
|
+
"Poisoned",
|
|
69
|
+
"Pool",
|
|
70
|
+
"Protocol",
|
|
71
|
+
"Sanctioned",
|
|
72
|
+
"Scam",
|
|
73
|
+
"SmartAccount",
|
|
74
|
+
"Targeted",
|
|
75
|
+
"Token",
|
|
76
|
+
"Victim",
|
|
77
|
+
"Transaction",
|
|
78
|
+
"Block",
|
|
79
|
+
"Head",
|
|
80
|
+
"ADDED_LIQUIDITY",
|
|
81
|
+
"APPROVED",
|
|
82
|
+
"BRIDGED",
|
|
83
|
+
"BUNDLED",
|
|
84
|
+
"DEPLOYED_CONTRACT",
|
|
85
|
+
"FLOWS_TO",
|
|
86
|
+
"LINKED",
|
|
87
|
+
"OPERATED_BY",
|
|
88
|
+
"REMOVED_LIQUIDITY",
|
|
89
|
+
"SIGNED_AUTHORIZATION",
|
|
90
|
+
"SIGNED_FOR",
|
|
91
|
+
"SPONSORED",
|
|
92
|
+
"SWAPPED",
|
|
93
|
+
"TRANSFERRED",
|
|
94
|
+
"TRANSFER",
|
|
95
|
+
"BRIDGE_CROSSING",
|
|
96
|
+
"SWAP",
|
|
97
|
+
"LIQUIDITY_ADD",
|
|
98
|
+
"LIQUIDITY_REMOVE"
|
|
99
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
100
|
+
const IDENT = /[A-Za-z0-9_]/;
|
|
101
|
+
function prevNonSpace(text, from) {
|
|
102
|
+
let i = from;
|
|
103
|
+
while (i >= 0 && /\s/.test(text[i])) i--;
|
|
104
|
+
return i;
|
|
105
|
+
}
|
|
106
|
+
/** True when the `:` or `|` at `at` opens a label or link type, not a map value. */
|
|
107
|
+
function opensLabel(text, at) {
|
|
108
|
+
let i = prevNonSpace(text, at - 1);
|
|
109
|
+
if (i < 0) return false;
|
|
110
|
+
if (text[at] !== "|" && (text[i] === "(" || text[i] === "[")) return true;
|
|
111
|
+
if (!IDENT.test(text[i])) return false;
|
|
112
|
+
while (i >= 0 && IDENT.test(text[i])) i--;
|
|
113
|
+
const before = prevNonSpace(text, i);
|
|
114
|
+
if (before >= 0 && (text[before] === ":" || text[before] === "|")) return opensLabel(text, before);
|
|
115
|
+
return text[at] === ":" && before >= 0 && (text[before] === "(" || text[before] === "[");
|
|
116
|
+
}
|
|
117
|
+
function fixSegment(segment) {
|
|
118
|
+
return segment.replace(/([:|])(\s*)([A-Za-z_][A-Za-z0-9_]*)/g, (whole, mark, space, name, at) => {
|
|
119
|
+
const canonical = CANONICAL.get(name.toLowerCase());
|
|
120
|
+
if (!canonical || canonical === name || !opensLabel(segment, at)) return whole;
|
|
121
|
+
return `${mark}${space}${canonical}`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** The query with every known label and link type in the graph's case; quoted text is untouched. */
|
|
125
|
+
function fixLabelCase(query) {
|
|
126
|
+
return query.split(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[^`]*`)/).map((part, index) => index % 2 === 1 ? part : fixSegment(part)).join("");
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
53
129
|
//#region src/mcp/timestamp-text.ts
|
|
54
130
|
/**
|
|
55
131
|
* A UTC text twin beside every timestamp column of a graph answer.
|
|
@@ -102,10 +178,11 @@ function addTimestampText(row) {
|
|
|
102
178
|
* reference is rewritten to its millisecond column, `x.<column>_utc` to
|
|
103
179
|
* `x.<column>`; an alias after AS is not a property and stays as written, so
|
|
104
180
|
* `f.first_seen_timestamp_utc AS first_seen_utc` yields a `first_seen_utc`
|
|
105
|
-
* column that addTimestampText renders as text.
|
|
181
|
+
* column that addTimestampText renders as text. A label or link type in the
|
|
182
|
+
* wrong case is spelled the graph's way (label-case.ts).
|
|
106
183
|
*/
|
|
107
184
|
function stripUtcProperties(query) {
|
|
108
|
-
return stripEthProperties(query.replace(/\.([A-Za-z_][A-Za-z0-9_]*?)_utc\b/g, ".$1"));
|
|
185
|
+
return fixLabelCase(stripEthProperties(query.replace(/\.([A-Za-z_][A-Za-z0-9_]*?)_utc\b/g, ".$1")));
|
|
109
186
|
}
|
|
110
187
|
/** graph_query and graph_query_batch arguments with stripUtcProperties applied to every query. */
|
|
111
188
|
function withoutUtcProperties(toolName, args) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_print_result = require("./print-result
|
|
2
|
+
const require_print_result = require("./print-result-qVhhuSdo.cjs");
|
|
3
3
|
//#region src/mcp/tool-visibility.ts
|
|
4
4
|
var tool_visibility_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
5
5
|
HIDDEN_REMOTE_TOOL_NAMES: () => HIDDEN_REMOTE_TOOL_NAMES,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import { n as formatMcpToolError, t as McpToolError } from "./print-result-
|
|
2
|
+
import { n as formatMcpToolError, t as McpToolError } from "./print-result-CJ8d7lmz.mjs";
|
|
3
3
|
//#region src/mcp/tool-visibility.ts
|
|
4
4
|
var tool_visibility_exports = /* @__PURE__ */ __exportAll({
|
|
5
5
|
HIDDEN_REMOTE_TOOL_NAMES: () => HIDDEN_REMOTE_TOOL_NAMES,
|
|
@@ -86,4 +86,4 @@ function validatePublicMcpToolArguments(name, args) {
|
|
|
86
86
|
//#endregion
|
|
87
87
|
export { tool_visibility_exports as a, isAppOnlyTool as i, PUBLIC_MCP_TOOL_ALLOWED_ARGS as n, PUBLIC_MCP_TOOL_REQUIRED_ARGS as r, HIDDEN_REMOTE_TOOL_NAMES as t };
|
|
88
88
|
|
|
89
|
-
//# sourceMappingURL=tool-visibility-
|
|
89
|
+
//# sourceMappingURL=tool-visibility-kclgGKL5.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-visibility-
|
|
1
|
+
{"version":3,"file":"tool-visibility-kclgGKL5.mjs","names":[],"sources":["../src/mcp/tool-visibility.ts"],"sourcesContent":["import { McpToolError, formatMcpToolError } from './print-result.js'\n\nexport const HIDDEN_REMOTE_TOOL_NAMES = new Set([\n 'topup',\n 'address_risk',\n // Hidden until its verdict is fixed: never listed, never registered, never\n // called through the proxy. The remote graph endpoint does not serve it.\n 'aml_address_risk',\n 'trace_victim_funds',\n 'trace_suspect_funds',\n 'trace_deposit_sources',\n 'trace_funds',\n 'track_funds',\n // Retired tools: never exposed publicly. Built by concatenation so the\n // retired names do not appear as literals in this file.\n ['aml_trace_victim', '_funds'].join(''),\n ['aml_trace_suspect', '_funds'].join(''),\n ['aml_trace_deposit', '_sources'].join(''),\n 'network_capabilities',\n 'usage_status',\n 'subscription_status',\n 'balance',\n 'help',\n ['money', '_flows_between_exchanges'].join(''),\n 'address_connection_risk',\n])\n\nexport const PUBLIC_MCP_TOOL_REQUIRED_ARGS: Record<string, string[]> = {\n graph_query: ['query', 'network'],\n graph_query_batch: ['network', 'queries'],\n}\n\n// normalizeRemoteToolArguments FILTERS pass-through arguments to this list, so\n// an argument that is missing here is silently STRIPPED and the caller gets a\n// default-bounded result with no error and no warning. `time_scope` shipped\n// broken for exactly this reason. Every tunable search bound below\n// (per_address_limit, row_limit) must therefore be listed the moment it\n// appears on a tool schema, and is covered end-to-end through the proxy in\n// tests/mcp-proxy.test.ts.\nexport const PUBLIC_MCP_TOOL_ALLOWED_ARGS: Record<string, string[]> = {\n // `time_scope` narrows a `USE topology` query to a temporal-shard subset\n graph_query: ['query', 'network', 'time_scope'],\n graph_query_batch: ['network', 'queries', 'per_query_timeout_seconds', 'time_scope'],\n}\n\nexport function isHiddenRemoteToolName(name: string): boolean {\n return HIDDEN_REMOTE_TOOL_NAMES.has(name)\n}\n\n/**\n * True for an MCP Apps tool only a view may call: `_meta.ui.visibility` is set\n * and does not include \"model\" (for example graph_expand, visibility [\"app\"]).\n * Such a tool never appears in a tool list built for the model or the CLI.\n */\nexport function isAppOnlyTool(tool: { _meta?: Record<string, unknown> }): boolean {\n const ui = tool._meta?.ui\n if (!ui || typeof ui !== 'object' || Array.isArray(ui)) return false\n const visibility = (ui as Record<string, unknown>).visibility\n return Array.isArray(visibility) && !visibility.includes('model')\n}\n\nexport function visibleRemoteTools<T extends { name: string; _meta?: Record<string, unknown> }>(\n tools: T[]\n): T[] {\n return tools.filter((tool) => !isHiddenRemoteToolName(tool.name) && !isAppOnlyTool(tool))\n}\n\nexport function assertPublicMcpToolName(name: string): void {\n if (!isHiddenRemoteToolName(name)) return\n const replacement =\n name === 'network_capabilities'\n ? ' Use meta_network_capabilities instead.'\n : name === 'usage_status'\n ? ' Use meta_usage_status instead.'\n : name === 'subscription_status'\n ? ' Use meta_subscription_status instead.'\n : name === 'balance'\n ? ' Use wallet_balance instead.'\n : name === 'help'\n ? ' Use meta_help instead.'\n : ''\n throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${replacement}`)\n}\n\nexport function formatMcpCallError(tool: string, err: unknown): string {\n // A tool error with an envelope is the server's own answer: its text, then the\n // line of its error_detail. It is never matched against the unknown-tool\n // pattern below. A tool error with no envelope keeps the old reading, because\n // a server can answer an unknown tool as an error reply.\n if (err instanceof McpToolError && err.errorDetail) return formatMcpToolError(err)\n const message = err instanceof Error ? err.message : String(err)\n if (/\\bunknown tool\\b|\\btool\\b.*\\bnot found\\b/i.test(message)) {\n return `Unknown MCP tool \"${tool}\". Run \\`cia mcp tools --refresh\\` to list available tools.`\n }\n return message\n}\n\nexport function validatePublicMcpToolArguments(name: string, args: Record<string, unknown>): void {\n const allowedArgs = PUBLIC_MCP_TOOL_ALLOWED_ARGS[name]\n if (!allowedArgs) return\n\n const unsupportedArgs = Object.keys(args).filter((argName) => !allowedArgs.includes(argName))\n if (unsupportedArgs.length === 0) return\n\n throw new Error(\n [\n `Unsupported argument${unsupportedArgs.length === 1 ? '' : 's'} for ${name}: ${unsupportedArgs.join(', ')}.`,\n `Allowed arguments: ${allowedArgs.join(', ')}.`,\n ].join(' ')\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAEA,MAAa,2CAA2B,IAAI,IAAI;CAC9C;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CAGA,CAAC,oBAAoB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACtC,CAAC,qBAAqB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACvC,CAAC,qBAAqB,UAAU,CAAC,CAAC,KAAK,EAAE;CACzC;CACA;CACA;CACA;CACA;CACA,CAAC,SAAS,0BAA0B,CAAC,CAAC,KAAK,EAAE;CAC7C;AACF,CAAC;AAED,MAAa,gCAA0D;CACrE,aAAa,CAAC,SAAS,SAAS;CAChC,mBAAmB,CAAC,WAAW,SAAS;AAC1C;AASA,MAAa,+BAAyD;CAEpE,aAAa;EAAC;EAAS;EAAW;CAAY;CAC9C,mBAAmB;EAAC;EAAW;EAAW;EAA6B;CAAY;AACrF;AAEA,SAAgB,uBAAuB,MAAuB;CAC5D,OAAO,yBAAyB,IAAI,IAAI;AAC1C;;;;;;AAOA,SAAgB,cAAc,MAAoD;CAChF,MAAM,KAAK,KAAK,OAAO;CACvB,IAAI,CAAC,MAAM,OAAO,OAAO,YAAY,MAAM,QAAQ,EAAE,GAAG,OAAO;CAC/D,MAAM,aAAc,GAA+B;CACnD,OAAO,MAAM,QAAQ,UAAU,KAAK,CAAC,WAAW,SAAS,OAAO;AAClE;AAEA,SAAgB,mBACd,OACK;CACL,OAAO,MAAM,QAAQ,SAAS,CAAC,uBAAuB,KAAK,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC;AAC1F;AAEA,SAAgB,wBAAwB,MAAoB;CAC1D,IAAI,CAAC,uBAAuB,IAAI,GAAG;CAanC,MAAM,IAAI,MAAM,aAAa,KAAK,qCAXhC,SAAS,yBACL,4CACA,SAAS,iBACP,oCACA,SAAS,wBACP,2CACA,SAAS,YACP,iCACA,SAAS,SACP,4BACA,IACsE;AACtF;AAEA,SAAgB,mBAAmB,MAAc,KAAsB;CAKrE,IAAI,eAAe,gBAAgB,IAAI,aAAa,OAAO,mBAAmB,GAAG;CACjF,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,4CAA4C,KAAK,OAAO,GAC1D,OAAO,qBAAqB,KAAK;CAEnC,OAAO;AACT;AAEA,SAAgB,+BAA+B,MAAc,MAAqC;CAChG,MAAM,cAAc,6BAA6B;CACjD,IAAI,CAAC,aAAa;CAElB,MAAM,kBAAkB,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,YAAY,CAAC,YAAY,SAAS,OAAO,CAAC;CAC5F,IAAI,gBAAgB,WAAW,GAAG;CAElC,MAAM,IAAI,MACR,CACE,uBAAuB,gBAAgB,WAAW,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,gBAAgB,KAAK,IAAI,EAAE,IAC1G,sBAAsB,YAAY,KAAK,IAAI,EAAE,EAC/C,CAAC,CAAC,KAAK,GAAG,CACZ;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as McpToolError } from "./print-result-
|
|
1
|
+
import { t as McpToolError } from "./print-result-CJ8d7lmz.mjs";
|
|
2
2
|
//#region src/mcp/transport-error.ts
|
|
3
3
|
/** The error and its `cause` chain, bounded against a self-referential chain. */
|
|
4
4
|
function causeChain(err, depth = 0) {
|
|
@@ -58,4 +58,4 @@ function toGraphMcpEndpointError(err, endpoint) {
|
|
|
58
58
|
//#endregion
|
|
59
59
|
export { toGraphMcpEndpointError };
|
|
60
60
|
|
|
61
|
-
//# sourceMappingURL=transport-error
|
|
61
|
+
//# sourceMappingURL=transport-error-BP6bZVDs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport-error
|
|
1
|
+
{"version":3,"file":"transport-error-BP6bZVDs.mjs","names":[],"sources":["../src/mcp/transport-error.ts"],"sourcesContent":["// A failed request to the Chain Insights Graph endpoint surfaces from Node's\n// fetch as a bare `TypeError: fetch failed`, which says nothing about WHICH\n// endpoint was unreachable or WHY. `cia mcp call graph_query ...` against a\n// stale `graphMcpEndpoint` — for example a `debug` config pointing at a stopped\n// local `127.0.0.1:8012` server — therefore printed only \"fetch failed\", and a\n// user could not tell a dead endpoint from a bad query.\n//\n// describeGraphMcpTransportError maps a transport-level failure to an actionable\n// message that names the endpoint and the cause. A non-transport error (a 402,\n// a tool error, a validation error, an unknown-tool error) returns null and is\n// left untouched, so real backend messages still reach the caller verbatim.\n//\n// A tool error is the server answering, so it is never a transport failure. It\n// arrives as an McpToolError and is returned as it is. The words of a server's\n// own text are no evidence of a dead endpoint either: a refused chain lookup\n// says \"network_capabilities\" and a bad network argument says \"invalid_network\",\n// so the bare word \"network\" is not a transport marker.\n\nimport { McpToolError } from './print-result.js'\n\ninterface CauseLike {\n code?: unknown\n message?: unknown\n cause?: unknown\n}\n\n/** The error and its `cause` chain, bounded against a self-referential chain. */\nfunction causeChain(err: unknown, depth = 0): CauseLike[] {\n if (depth > 8 || err === null || typeof err !== 'object') return []\n const node = err as CauseLike\n return [node, ...causeChain(node.cause, depth + 1)]\n}\n\n// Node/undici transport error codes → plain-language cause.\nconst TRANSPORT_CODES: Record<string, string> = {\n ECONNREFUSED: 'connection refused',\n ECONNRESET: 'connection reset',\n ENOTFOUND: 'host not found',\n EAI_AGAIN: 'host not found (DNS lookup failed)',\n ETIMEDOUT: 'connection timed out',\n UND_ERR_CONNECT_TIMEOUT: 'connection timed out',\n UND_ERR_SOCKET: 'socket closed',\n EPROTO: 'TLS/protocol error',\n DEPTH_ZERO_SELF_SIGNED_CERT: 'TLS certificate not trusted',\n SELF_SIGNED_CERT_IN_CHAIN: 'TLS certificate not trusted',\n ERR_TLS_CERT_ALTNAME_INVALID: 'TLS certificate host mismatch',\n}\n\n/** Returns a plain-language cause when err is a transport failure, else null. */\nfunction transportDetail(err: unknown): string | null {\n if (err instanceof McpToolError) return null\n const chain = causeChain(err)\n if (chain.length === 0) return null\n let looksTransport = false\n for (const node of chain) {\n const code = typeof node.code === 'string' ? node.code : undefined\n if (code && TRANSPORT_CODES[code]) return TRANSPORT_CODES[code]\n const message = typeof node.message === 'string' ? node.message : ''\n for (const [knownCode, text] of Object.entries(TRANSPORT_CODES)) {\n if (message.includes(knownCode)) return text\n }\n if (/fetch failed|socket hang up|other side closed|and could not connect/i.test(message)) {\n looksTransport = true\n }\n }\n return looksTransport ? 'the endpoint is unreachable' : null\n}\n\nexport function describeGraphMcpTransportError(err: unknown, endpoint: string): string | null {\n const detail = transportDetail(err)\n if (!detail) return null\n return [\n `Could not reach the Chain Insights Graph endpoint ${endpoint} (${detail}).`,\n 'Check that the endpoint is correct and running — it is `graphMcpEndpoint` in',\n '~/.chain-insights/config.json — or override it for one run with the',\n 'CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT environment variable.',\n ].join(' ')\n}\n\n/**\n * Returns an endpoint-named Error when err is a transport failure, preserving\n * the original as `cause`; returns err unchanged otherwise. Callers throw the\n * result, so a real backend error surfaces verbatim while a dead endpoint is\n * self-explaining. An McpToolError is a reply of the server and always comes\n * back as it is.\n */\nexport function toGraphMcpEndpointError(err: unknown, endpoint: string): unknown {\n const message = describeGraphMcpTransportError(err, endpoint)\n return message === null ? err : new Error(message, { cause: err })\n}\n"],"mappings":";;;AA2BA,SAAS,WAAW,KAAc,QAAQ,GAAgB;CACxD,IAAI,QAAQ,KAAK,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO,CAAC;CAClE,MAAM,OAAO;CACb,OAAO,CAAC,MAAM,GAAG,WAAW,KAAK,OAAO,QAAQ,CAAC,CAAC;AACpD;AAGA,MAAM,kBAA0C;CAC9C,cAAc;CACd,YAAY;CACZ,WAAW;CACX,WAAW;CACX,WAAW;CACX,yBAAyB;CACzB,gBAAgB;CAChB,QAAQ;CACR,6BAA6B;CAC7B,2BAA2B;CAC3B,8BAA8B;AAChC;;AAGA,SAAS,gBAAgB,KAA6B;CACpD,IAAI,eAAe,cAAc,OAAO;CACxC,MAAM,QAAQ,WAAW,GAAG;CAC5B,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,IAAI,iBAAiB;CACrB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,KAAA;EACzD,IAAI,QAAQ,gBAAgB,OAAO,OAAO,gBAAgB;EAC1D,MAAM,UAAU,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU;EAClE,KAAK,MAAM,CAAC,WAAW,SAAS,OAAO,QAAQ,eAAe,GAC5D,IAAI,QAAQ,SAAS,SAAS,GAAG,OAAO;EAE1C,IAAI,uEAAuE,KAAK,OAAO,GACrF,iBAAiB;CAErB;CACA,OAAO,iBAAiB,gCAAgC;AAC1D;AAEA,SAAgB,+BAA+B,KAAc,UAAiC;CAC5F,MAAM,SAAS,gBAAgB,GAAG;CAClC,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO;EACL,qDAAqD,SAAS,IAAI,OAAO;EACzE;EACA;EACA;CACF,CAAC,CAAC,KAAK,GAAG;AACZ;;;;;;;;AASA,SAAgB,wBAAwB,KAAc,UAA2B;CAC/E,MAAM,UAAU,+BAA+B,KAAK,QAAQ;CAC5D,OAAO,YAAY,OAAO,MAAM,IAAI,MAAM,SAAS,EAAE,OAAO,IAAI,CAAC;AACnE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_print_result = require("./print-result
|
|
1
|
+
const require_print_result = require("./print-result-qVhhuSdo.cjs");
|
|
2
2
|
//#region src/mcp/transport-error.ts
|
|
3
3
|
/** The error and its `cause` chain, bounded against a self-referential chain. */
|
|
4
4
|
function causeChain(err, depth = 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_print_result = require("./print-result
|
|
1
|
+
const require_print_result = require("./print-result-qVhhuSdo.cjs");
|
|
2
2
|
let node_child_process = require("node:child_process");
|
|
3
3
|
//#region src/update.ts
|
|
4
4
|
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as PACKAGE_VERSION, s as PACKAGE_INFO } from "./print-result-
|
|
1
|
+
import { c as PACKAGE_VERSION, s as PACKAGE_INFO } from "./print-result-CJ8d7lmz.mjs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
//#region src/update.ts
|
|
4
4
|
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -108,4 +108,4 @@ function parseSemverCore(version) {
|
|
|
108
108
|
//#endregion
|
|
109
109
|
export { checkForUpdate, runPackageUpdate };
|
|
110
110
|
|
|
111
|
-
//# sourceMappingURL=update-
|
|
111
|
+
//# sourceMappingURL=update-S6j1hT_S.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-
|
|
1
|
+
{"version":3,"file":"update-S6j1hT_S.mjs","names":[],"sources":["../src/update.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process'\nimport type { Readable, Writable } from 'node:stream'\nimport { createInterface } from 'node:readline/promises'\nimport { stdin, stdout } from 'node:process'\nimport { PACKAGE_INFO, PACKAGE_VERSION } from './version.js'\n\nconst DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org'\nconst DEFAULT_TIMEOUT_MS = 3000\n\nexport interface UpdateCheckOptions {\n packageName?: string\n currentVersion?: string\n registryUrl?: string\n timeoutMs?: number\n fetchImpl?: typeof fetch\n}\n\nexport interface UpdateCheckResult {\n packageName: string\n currentVersion: string\n latestVersion?: string\n updateAvailable: boolean\n updateCommand: string\n error?: string\n}\n\nexport interface NpmInvocation {\n command: string\n args: string[]\n displayCommand: string\n}\n\nexport interface UpdatePromptOptions extends UpdateCheckOptions {\n input?: NodeJS.ReadStream & Readable\n output?: NodeJS.WriteStream & Writable\n env?: NodeJS.ProcessEnv\n}\n\nexport function compareSemverVersions(a: string, b: string): number {\n const left = parseSemverCore(a)\n const right = parseSemverCore(b)\n for (let i = 0; i < 3; i += 1) {\n if (left[i] > right[i]) return 1\n if (left[i] < right[i]) return -1\n }\n return 0\n}\n\nexport function resolveUpdateRegistryUrl(env: NodeJS.ProcessEnv = process.env): string {\n return env['CHAIN_INSIGHTS_NPM_REGISTRY_URL']?.trim() || DEFAULT_REGISTRY_URL\n}\n\nexport function buildPackageLatestUrl(\n packageName: string,\n registryUrl = DEFAULT_REGISTRY_URL\n): string {\n const base = registryUrl.endsWith('/') ? registryUrl.slice(0, -1) : registryUrl\n return `${base}/${encodeURIComponent(packageName)}/latest`\n}\n\nexport function resolveNpmUpdateInvocation(\n packageName = PACKAGE_INFO.name,\n env: NodeJS.ProcessEnv = process.env\n): NpmInvocation {\n const packageSpec = `${packageName}@latest`\n const displayCommand = `npm install -g ${packageSpec}`\n const npmExecPath = env['npm_execpath']?.trim()\n if (npmExecPath) {\n return {\n command: process.execPath,\n args: [npmExecPath, 'install', '-g', packageSpec],\n displayCommand,\n }\n }\n return {\n command: process.platform === 'win32' ? 'npm.cmd' : 'npm',\n args: ['install', '-g', packageSpec],\n displayCommand,\n }\n}\n\nexport async function fetchLatestPackageVersion(options: UpdateCheckOptions = {}): Promise<string> {\n const packageName = options.packageName ?? PACKAGE_INFO.name\n const registryUrl = options.registryUrl ?? resolveUpdateRegistryUrl()\n const fetchImpl = options.fetchImpl ?? fetch\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n\n try {\n const response = await fetchImpl(buildPackageLatestUrl(packageName, registryUrl), {\n headers: {\n accept: 'application/json',\n 'user-agent': `chain-insights/${PACKAGE_VERSION}`,\n },\n signal: controller.signal,\n })\n if (!response.ok) {\n throw new Error(`npmjs registry returned ${response.status}`)\n }\n const body = (await response.json()) as { version?: unknown }\n if (typeof body.version !== 'string' || !body.version.trim()) {\n throw new Error('npmjs registry response did not include a version')\n }\n return body.version.trim()\n } finally {\n clearTimeout(timeout)\n }\n}\n\nexport async function checkForUpdate(options: UpdateCheckOptions = {}): Promise<UpdateCheckResult> {\n const packageName = options.packageName ?? PACKAGE_INFO.name\n const currentVersion = options.currentVersion ?? PACKAGE_VERSION\n const updateCommand = resolveNpmUpdateInvocation(packageName).displayCommand\n try {\n const latestVersion = await fetchLatestPackageVersion(options)\n return {\n packageName,\n currentVersion,\n latestVersion,\n updateAvailable: compareSemverVersions(latestVersion, currentVersion) > 0,\n updateCommand,\n }\n } catch (err) {\n return {\n packageName,\n currentVersion,\n updateAvailable: false,\n updateCommand,\n error: (err as Error).message,\n }\n }\n}\n\nexport function runPackageUpdate(packageName = PACKAGE_INFO.name): void {\n const invocation = resolveNpmUpdateInvocation(packageName)\n const result = spawnSync(invocation.command, invocation.args, { stdio: 'inherit' })\n if (result.error) {\n throw result.error\n }\n if (result.status !== 0) {\n const detail = result.signal\n ? `signal ${result.signal}`\n : `exit code ${result.status ?? 'unknown'}`\n throw new Error(`Update failed with ${detail}`)\n }\n}\n\nexport function shouldPromptForUpdate(options: UpdatePromptOptions = {}): boolean {\n const env = options.env ?? process.env\n if (env['CHAIN_INSIGHTS_SKIP_UPDATE_CHECK'] === '1') return false\n if (env['CI'] === 'true') return false\n const input = options.input ?? stdin\n const output = options.output ?? stdout\n return input.isTTY === true && output.isTTY === true\n}\n\nexport async function maybePromptForUpdate(options: UpdatePromptOptions = {}): Promise<void> {\n if (!shouldPromptForUpdate(options)) return\n\n const check = await checkForUpdate(options)\n if (check.error || !check.updateAvailable || !check.latestVersion) return\n\n const output = options.output ?? stdout\n output.write(\n `\\nChain Insights ${check.latestVersion} is available (current ${check.currentVersion}).\\n`\n )\n output.write(`Update command: ${check.updateCommand}\\n`)\n\n const rl = createInterface({\n input: options.input ?? stdin,\n output,\n })\n try {\n const answer = await rl.question('Update now? [Y/n] ')\n if (!answer.trim() || answer.trim().toLowerCase().startsWith('y')) {\n try {\n runPackageUpdate(check.packageName)\n } catch (err) {\n output.write(`Update failed: ${(err as Error).message}\\n`)\n output.write('Workspace initialization is complete. Run `cia update` when ready.\\n')\n }\n } else {\n output.write('Skipped update. Run `cia update` when ready.\\n')\n }\n } finally {\n rl.close()\n }\n}\n\nfunction parseSemverCore(version: string): [number, number, number] {\n const match = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:[-+].*)?$/.exec(version)\n if (!match) throw new Error(`Invalid semver version: ${version}`)\n return [Number(match[1]), Number(match[2]), Number(match[3])]\n}\n"],"mappings":"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"}
|
package/docs/graph-tools.md
CHANGED
|
@@ -328,6 +328,7 @@ links of a pair add up to it.
|
|
|
328
328
|
| Property | Meaning |
|
|
329
329
|
| ---------------------- | ----------------------------------------------------------------------- |
|
|
330
330
|
| `asset` | the token's contract in lowercase, or `native` for the chain's own coin |
|
|
331
|
+
| `asset_key` | bookkeeping: the server's own number for the token, never a name to query by; it differs per server and changes on a rebuild |
|
|
331
332
|
| `token_standard` | the standard of the token |
|
|
332
333
|
| `tx_count` | transfers of that token between the pair |
|
|
333
334
|
| `amount_raw_sum` | exact total in base units, as text; not dollars |
|
|
@@ -370,6 +371,12 @@ tokens, send one search per token, in one `graph_query_batch`. A read anchored
|
|
|
370
371
|
on a full address needs neither bound. Tokens are not dollars, and a token is
|
|
371
372
|
named by its full contract.
|
|
372
373
|
|
|
374
|
+
An `asset` that names no token of the graph, or a symbol such as `USDC`, is
|
|
375
|
+
refused with `anchor_missing` whose fix names the contract: the graph holds no
|
|
376
|
+
token at that address. Check the contract, all 42 characters in lowercase, and
|
|
377
|
+
send the same query again with the right one. The server turns the contract
|
|
378
|
+
into its own `asset_key` itself; a query never needs the key.
|
|
379
|
+
|
|
373
380
|
## Role labels and flags
|
|
374
381
|
|
|
375
382
|
The graph labels an address with role words: `Exchange`, `Scam`, `Victim` and
|
package/package.json
CHANGED
|
@@ -188,6 +188,7 @@ Marks say what an address is. They sit beside the kind and are never an anchor:
|
|
|
188
188
|
- `:Mixer` is a mixer contract from the mixer registry: a pool, the router or the relayer of a Tornado-style mixer (Hearsay on robinhood). Its `labels` carry the word `Mixer` and `label_risk_levels` carries `high`.
|
|
189
189
|
- `:Protocol` is a protocol's own contract: a pool, a router, a position manager, a stock token's proxy. It is never given for a token name alone.
|
|
190
190
|
- `:SmartAccount` is a contract wallet, beside `:Account` or `:Contract`.
|
|
191
|
+
- A basket is a Robinhood stock-basket token: one token that holds several stock tokens in near-equal USD weights. One factory, `0xe78b036bd6da158cd61a3dac3f67e18f62ecddfd`, creates every basket, and one router, `0xb672a5d825f82dea8574711d1da696fcc48d2ae0`, mints and redeems them. A basket is `:Protocol` and `:Token`, and its `labels` carry the word `Basket`. The factory is `:Protocol` and its `labels` carry `BasketFactory`. The router has no mark of its own beyond `:Contract`. See [Baskets](#baskets-is-this-a-basket-and-what-does-it-trade).
|
|
191
192
|
|
|
192
193
|
**Ask by mark.** A question that names a kind of thing (mixers, launchpad pools, NFT collections, tokens of one standard) is a label match on the mark, a probe the server answers from the label index: nine `:Mixer` rows come back in 13 ms, a count of 2.4 million `:Token` in 4 s.
|
|
193
194
|
|
|
@@ -195,12 +196,49 @@ Marks say what an address is. They sit beside the kind and are never an anchor:
|
|
|
195
196
|
USE topology MATCH (a:Mixer) RETURN a.address AS address, labels(a) AS node_labels, a.labels AS labels LIMIT 25
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
Never write `MATCH (a:Address) WHERE a.labels CONTAINS "mixer"` with no address: `labels` is a list on every address, the scan touches all of them and the query times out. Read `a.labels` only beside an anchor, or beside a mark. The marks a lane serves, and a golden set of
|
|
199
|
+
Never write `MATCH (a:Address) WHERE a.labels CONTAINS "mixer"` with no address: `labels` is a list on every address, the scan touches all of them and the query times out. Read `a.labels` only beside an anchor, or beside a mark. The marks a lane serves, and a golden set of 41 addresses with the marks each must carry, are pinned in `tests/fixtures/graph-marks-golden.json`.
|
|
199
200
|
|
|
200
201
|
A token name from an explorer gives `:Token`, not `:Protocol`. `:Token` is a mark, not a stop: the walk guard below does not name it. One contract can carry both marks: a Uniswap v2 pair is itself an ERC20 token of its liquidity providers, so it is `:Pool` and, when the asset scan lists it, `:Token` too. Ask which tokens or pools an address touched with the mark: `WHERE t:Token` or `WHERE p:Pool`.
|
|
201
202
|
|
|
202
203
|
A topology link is a lifetime summary of a pair, and a facts row is one event. They are two things, so they keep two names: `SWAPPED`, `ADDED_LIQUIDITY` and `REMOVED_LIQUIDITY` on topology, `SWAP`, `LIQUIDITY_ADD` and `LIQUIDITY_REMOVE` on facts. Read the summary on topology, anchored on an address, then the events behind it on facts, by the pair and one day.
|
|
203
204
|
|
|
205
|
+
### Baskets: is this a basket, and what does it trade
|
|
206
|
+
|
|
207
|
+
A basket is known by two facts, and either one answers "is this a basket": the word `Basket` in its own `labels`, or the basket factory having created it. The word comes from the factory's creations, so it reaches every basket the factory made, old or new.
|
|
208
|
+
|
|
209
|
+
Is this address a basket? One read, anchored on the address:
|
|
210
|
+
|
|
211
|
+
```cypher
|
|
212
|
+
USE topology
|
|
213
|
+
MATCH (a:Address {address: "0x844e7f539ec3e90391b35843001e8624bef6eb83"})
|
|
214
|
+
RETURN a.address AS address, labels(a) AS node_labels, a.labels AS labels LIMIT 1
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The factory's own children are the other read. A row means the factory created the address:
|
|
218
|
+
|
|
219
|
+
```cypher
|
|
220
|
+
USE topology
|
|
221
|
+
MATCH (f:Address {address: "0xe78b036bd6da158cd61a3dac3f67e18f62ecddfd"})-[d:DEPLOYED_CONTRACT]->(p:Address {address: "0x844e7f539ec3e90391b35843001e8624bef6eb83"})
|
|
222
|
+
RETURN p.address AS address, d.block_height AS block_height, d.tx_id AS tx_id LIMIT 1
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
What does a basket trade? The stock tokens move between the router and the basket. Read one day of the pair on `USE facts`. A deposit has the router as sender, a redemption has the basket as sender. Each transaction lists its stock tokens in near-equal USD values:
|
|
226
|
+
|
|
227
|
+
```cypher
|
|
228
|
+
USE facts
|
|
229
|
+
MATCH (r:Address {address: "0xb672a5d825f82dea8574711d1da696fcc48d2ae0"})-[t:TRANSFER]->(b:Address {address: "0x844e7f539ec3e90391b35843001e8624bef6eb83"}) WHERE t.block_date = "2026-10-20"
|
|
230
|
+
RETURN t.tx_id AS tx_id, t.asset_contract AS asset_contract, t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT 50
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
For redemptions, flip the arrow. To see every token the pair ever moved in one read, use the `TRANSFERRED` links of the pair on topology (Tokens: one link per sender, receiver and token, above). Read `behind_transfers` on `Head` first: while it is large, those links are missing and the answer is empty, so use the facts day instead.
|
|
234
|
+
|
|
235
|
+
How to answer:
|
|
236
|
+
|
|
237
|
+
- "Is this a basket?" Say yes when the word `Basket` is there or the factory created it. Name the factory in full. If neither holds, say it is not one the factory made, and do not call it a basket.
|
|
238
|
+
- "What is in it?" The graph shows the stock tokens that moved between the basket and the router, not a balance. Say "the stock tokens it takes in and pays out", name each by its full contract, and never write a symbol you have not read. Say that the rows show the day you read, one deposit or one redemption each, and that a lifetime link is a total.
|
|
239
|
+
- "Who created these tokens?" The factory. List its children with a `DEPLOYED_CONTRACT` read anchored on the factory, with a literal `LIMIT`. Never scan `a.labels` for `Basket` with no anchor; ask the factory's children, or the `:Protocol` mark beside an anchor.
|
|
240
|
+
- "Which baskets went through the router on a day?" A facts read needs both addresses, so never send one with the router alone: the server refuses it. List the factory's children first (above), then send one `graph_query_batch` of the facts day, one query per basket and direction (router to basket, basket to router), each with its own `id`.
|
|
241
|
+
|
|
204
242
|
## Topology
|
|
205
243
|
|
|
206
244
|
`USE topology` is the address graph: `FLOWS_TO` money flow, swaps, liquidity,
|
|
@@ -366,6 +404,7 @@ RETURN c.network AS remote_network, c.address AS endpoint, x.events AS events LI
|
|
|
366
404
|
`FLOWS_TO` is the pair's total over every token. `TRANSFERRED` splits that total by token: one link for each sender, receiver and token. `FLOWS_TO` stays the pair total, and the `TRANSFERRED` links of a pair add up to it. A link carries these properties:
|
|
367
405
|
|
|
368
406
|
- `asset`: the token's contract, in lowercase and in full (42 characters), or `native` for the chain's own coin.
|
|
407
|
+
- `asset_key`: bookkeeping, the number the server keeps beside `asset`. Never name a token by it and never copy one into a query: it differs from server to server and changes when a graph is rebuilt. Write `asset` with the full contract; the server turns it into the key itself.
|
|
369
408
|
- `token_standard`: the standard of the token.
|
|
370
409
|
- `tx_count`: the number of transfers of that token between the pair.
|
|
371
410
|
- `amount_raw_sum`: the exact total in base units, as text. It is not dollars and not a number of whole tokens.
|
|
@@ -403,6 +442,8 @@ ORDER BY received_usd DESC LIMIT 10
|
|
|
403
442
|
|
|
404
443
|
A `TRANSFERRED` search over the whole chain, with no address anchor, must name one `asset` literal and a lower bound `t.last_seen_timestamp >=` joined by `AND`. Without both, the server refuses it with `transfer_search_unbounded` before it runs. An `OR` of two assets is refused the same way: send one search per asset, in one `graph_query_batch` when there are several. Read `behind_transfers` on `Head` first, as for any whole-chain search. A read anchored on a full address needs neither bound.
|
|
405
444
|
|
|
445
|
+
An `asset` that names no token of the graph, or a symbol such as `USDC`, is refused with `anchor_missing` whose fix names the contract: the graph holds no token at that address. That refusal is not a family behind and not a missing anchor: do not ask for an address. Check the contract, all 42 characters in lowercase, and send the same query again with the right one.
|
|
446
|
+
|
|
406
447
|
How to answer from these rows:
|
|
407
448
|
|
|
408
449
|
- Tokens are not dollars. Say "tokens worth about 183 USD", never "sent 183 USD". Head the column "Tokens worth (USD)".
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"print-result-CZ2cH30Z.mjs","names":["isRecord"],"sources":["../src/version.ts","../src/mcp/balance-text.ts","../src/mcp/timestamp-text.ts","../src/mcp/print-result.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\n\nexport const PACKAGE_INFO = JSON.parse(\n readFileSync(new URL('../package.json', import.meta.url), 'utf8')\n) as { name: string; version: string }\n\nexport const PACKAGE_VERSION = PACKAGE_INFO.version\n","/**\n * An ETH text twin beside every native balance column of a chain answer.\n *\n * `USE chain` serves `balance` as wei in decimal text, exact and 16 to 22\n * digits long. A host without a shell (Claude Desktop, 2026-10-21) printed\n * `10003752747799400` to the user as the balance. A model that divides by\n * 10^18 by head misplaces the point, so the proxy writes `<column>_eth`\n * next to each one, the same way `<column>_utc` sits beside a timestamp.\n */\n\nconst BALANCE_KEY = /(^|_)balance$/\nconst WEI_TEXT = /^[0-9]{1,78}$/\nconst WEI_PER_ETH = 10n ** 18n\n\n/** `0.0100037527477994` for `10003752747799400` wei: exact, no exponent, no trailing zeros. */\nexport function ethText(wei: string): string {\n const value = BigInt(wei)\n const whole = value / WEI_PER_ETH\n const frac = (value % WEI_PER_ETH).toString().padStart(18, '0').replace(/0+$/, '')\n return frac === '' ? whole.toString() : `${whole}.${frac}`\n}\n\nfunction isWeiText(value: unknown): value is string {\n return typeof value === 'string' && WEI_TEXT.test(value)\n}\n\n/**\n * One row with a `<key>_eth` string after every wei balance column that has\n * no twin yet. A column the model itself named `*_eth` (an alias such as\n * `a.balance_eth AS held_eth`, see stripEthProperties) arrives as wei and is\n * rendered as ETH text in place: the model gets the column it asked for.\n */\nexport function addBalanceText(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(row)) {\n out[key] = key.endsWith('_eth') && isWeiText(value) ? ethText(value) : value\n if (BALANCE_KEY.test(key) && isWeiText(value) && !(`${key}_eth` in row)) out[`${key}_eth`] = ethText(value)\n }\n return out\n}\n\n/**\n * The `_eth` twin is added by the proxy; it is no chain property. A query\n * that names `a.balance_eth` is rewritten to `a.balance`, so the row comes\n * back and the twin is written beside it. An alias after AS stays as written.\n */\nexport function stripEthProperties(query: string): string {\n return query.replace(/\\.((?:[A-Za-z_][A-Za-z0-9_]*_)?balance)_eth\\b/g, '.$1')\n}\n","/**\n * A UTC text twin beside every timestamp column of a graph answer.\n *\n * The graph serves integer milliseconds. A model that converts them by head\n * gets the time of day wrong about one time in three (loop 3, 2026-10-10:\n * four of twelve answers about one block named 00:15, 09:35, 06:35 or 09:15\n * for 23:55:43 UTC). Hosts without a shell (Claude Desktop) have no other\n * way to convert, so the proxy writes `<column>_utc` next to each one.\n */\n\nimport { addBalanceText, stripEthProperties } from './balance-text.js'\n\nconst TIMESTAMP_KEY = /(_timestamp|_time|_at|_seen|_active)$/\nconst MIN_MS = 1_000_000_000_000 // 2001-09-09\nconst MAX_MS = 4_000_000_000_000 // 2096-10-02\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** `2026-08-05 23:55:43 UTC` for epoch milliseconds. */\nexport function utcText(ms: number): string {\n return `${new Date(ms).toISOString().slice(0, 19).replace('T', ' ')} UTC`\n}\n\nfunction isEpochMs(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= MIN_MS && value <= MAX_MS\n}\n\n/**\n * One row with a `<key>_utc` string after every epoch-millisecond timestamp\n * column. A column the model itself named `*_utc` (its query asked for the\n * twin as a property, see stripUtcProperties) arrives as milliseconds and is\n * rendered as UTC text in place: the model gets the column it asked for.\n */\nexport function addTimestampText(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(row)) {\n out[key] = key.endsWith('_utc') && isEpochMs(value) ? utcText(value) : value\n if (TIMESTAMP_KEY.test(key) && isEpochMs(value) && !(`${key}_utc` in row)) out[`${key}_utc`] = utcText(value)\n }\n return out\n}\n\n/**\n * The `_utc` twin is added to a reply by the proxy; it is no graph property.\n * Thirteen of 48 loop-5 rounds wrote `f.first_seen_timestamp_utc` into a\n * query anyway: topology answers null, a SWAP read is refused. The property\n * reference is rewritten to its millisecond column, `x.<column>_utc` to\n * `x.<column>`; an alias after AS is not a property and stays as written, so\n * `f.first_seen_timestamp_utc AS first_seen_utc` yields a `first_seen_utc`\n * column that addTimestampText renders as text.\n */\nexport function stripUtcProperties(query: string): string {\n return stripEthProperties(query.replace(/\\.([A-Za-z_][A-Za-z0-9_]*?)_utc\\b/g, '.$1'))\n}\n\n/** graph_query and graph_query_batch arguments with stripUtcProperties applied to every query. */\nexport function withoutUtcProperties(toolName: string, args: Record<string, unknown>): Record<string, unknown> {\n if (toolName === 'graph_query' && typeof args.query === 'string') return { ...args, query: stripUtcProperties(args.query) }\n if (toolName === 'graph_query_batch' && Array.isArray(args.queries)) {\n return {\n ...args,\n queries: args.queries.map((entry) =>\n isRecord(entry) && typeof entry.query === 'string' ? { ...entry, query: stripUtcProperties(entry.query) } : entry\n ),\n }\n }\n return args\n}\n\nfunction mapResults(holder: unknown): unknown {\n if (!isRecord(holder) || !Array.isArray(holder.results)) return holder\n return {\n ...holder,\n results: holder.results.map((row) => (isRecord(row) ? addBalanceText(addTimestampText(row)) : row)),\n }\n}\n\n/**\n * The structured content of graph_query (facts.query.results) or\n * graph_query_batch (facts.queries[].results) with the UTC twins and the\n * ETH twins (balance-text.ts) added.\n * Anything else comes back as it was.\n */\nexport function withTimestampText(structuredContent: unknown): unknown {\n if (!isRecord(structuredContent) || !isRecord(structuredContent.facts)) return structuredContent\n const facts = structuredContent.facts\n if (!isRecord(facts.query) && !Array.isArray(facts.queries)) return structuredContent\n const next: Record<string, unknown> = { ...facts }\n if (isRecord(facts.query)) next.query = mapResults(facts.query)\n if (Array.isArray(facts.queries)) next.queries = facts.queries.map(mapResults)\n return { ...structuredContent, facts: next }\n}\n\ntype TextBlock = { type: string; text?: string }\n\n/**\n * A tool result whose structured content and single JSON text block both carry\n * the UTC twins. A text block that is not the JSON of the structured content\n * is left alone.\n */\nexport function withTimestampTextResult<T extends { content?: TextBlock[]; structuredContent?: unknown }>(result: T): T {\n const structured = withTimestampText(result.structuredContent)\n if (structured === result.structuredContent) return result\n const content = result.content ?? []\n const only = content.length === 1 && content[0]?.type === 'text' ? content[0] : null\n let parsed: unknown = undefined\n try {\n parsed = only?.text ? JSON.parse(only.text) : undefined\n } catch {\n parsed = undefined\n }\n const textIsStructured =\n parsed !== undefined && JSON.stringify(parsed) === JSON.stringify(result.structuredContent)\n return {\n ...result,\n structuredContent: structured,\n content: textIsStructured ? [{ ...only, text: JSON.stringify(structured) }] : content,\n }\n}\n","import { withTimestampTextResult } from './timestamp-text.js'\n\nexport interface McpTextResult {\n content?: Array<{ type: string; text?: string }>\n structuredContent?: unknown\n isError?: boolean\n}\n\n/**\n * The refusal envelope of the graph server, as the reply carries it in\n * `structuredContent.error_detail`. `class` is one of `refused`, `killed`,\n * `capacity` or `failed`, and it decides the caller's next move.\n */\nexport interface McpErrorDetail {\n code?: string\n rule?: string\n class?: string\n fix?: string\n example?: string\n}\n\nconst ERROR_DETAIL_FIELDS = ['code', 'rule', 'class', 'fix', 'example'] as const\n\n/**\n * A tool reply flagged `isError`: the server answered, and its answer is an\n * error. It is never a failure of the connection, so no caller may report it\n * as an unreachable endpoint. `message` is the server's own text and\n * `errorDetail` is the envelope of the reply, absent when the reply has none.\n */\nexport class McpToolError extends Error {\n readonly errorDetail?: McpErrorDetail\n\n constructor(message: string, errorDetail?: McpErrorDetail) {\n super(message)\n this.name = 'McpToolError'\n if (errorDetail) this.errorDetail = errorDetail\n }\n}\n\nexport interface McpPrintOptions {\n tool?: string\n json?: boolean\n}\n\n// What a switched-off or lagging chain layer means for the caller, in the line\n// under the server's text. The endpoint answered, so the endpoint is up, and\n// the other two layers do not depend on the chain node.\nconst CHAIN_UNAVAILABLE_MEANING =\n 'the chain layer is off or behind. The endpoint is up. USE topology and USE facts still work.'\n\n/**\n * How `cia mcp call` shows an McpToolError: the server's own text, then one\n * line built from its error_detail, `code <code> · class <class> · <fix>`. A\n * reply with no error_detail is the server text alone. For `chain_unavailable`\n * the line also says what the code means for the caller.\n */\nexport function formatMcpToolError(err: McpToolError): string {\n const detail = err.errorDetail\n if (!detail) return err.message\n const parts: string[] = []\n if (detail.code) parts.push(`code ${detail.code}`)\n if (detail.class) parts.push(`class ${detail.class}`)\n if (detail.fix) parts.push(detail.fix)\n if (detail.code === 'chain_unavailable') parts.push(CHAIN_UNAVAILABLE_MEANING)\n if (parts.length === 0) return err.message\n return `${err.message}\\n ${parts.join(' · ')}`\n}\n\ntype JsonRecord = Record<string, unknown>\n\nfunction isRecord(value: unknown): value is JsonRecord {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\n/** The text fields of `structuredContent.error_detail`, or undefined when it has none. */\nfunction errorDetailOf(structuredContent: unknown): McpErrorDetail | undefined {\n if (!isRecord(structuredContent) || !isRecord(structuredContent.error_detail)) return undefined\n const detail: McpErrorDetail = {}\n for (const field of ERROR_DETAIL_FIELDS) {\n const value = structuredContent.error_detail[field]\n if (typeof value === 'string') detail[field] = value\n }\n return Object.keys(detail).length > 0 ? detail : undefined\n}\n\nfunction displayValue(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n}\n\nfunction formatTable(rows: Array<JsonRecord>): string[] {\n if (rows.length === 0) return []\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))]\n const values = rows.map((row) => columns.map((column) => displayValue(row[column])))\n const widths = columns.map((column, index) =>\n Math.max(column.length, ...values.map((row) => row[index]?.length ?? 0))\n )\n const line = (row: string[]): string =>\n row\n .map((value, index) => (index === row.length - 1 ? value : value.padEnd(widths[index] ?? 0)))\n .join(' | ')\n return [line(columns), widths.map((width) => '-'.repeat(width)).join('-+-'), ...values.map(line)]\n}\n\nfunction formatGraphResult(value: JsonRecord, fallbackTool?: string): string | null {\n const facts = isRecord(value.facts) ? value.facts : undefined\n const query = facts && isRecord(facts.query) ? facts.query : undefined\n const queries = facts && Array.isArray(facts.queries) ? facts.queries : undefined\n if (!query && !queries) return null\n\n const lines = [`Tool: ${displayValue(value.tool) || fallbackTool || 'MCP result'}`]\n const subject = isRecord(value.subject)\n ? value.subject\n : facts && isRecord(facts.subject)\n ? facts.subject\n : undefined\n if (subject?.network !== undefined) lines.push(`Network: ${displayValue(subject.network)}`)\n\n if (query) {\n const results = Array.isArray(query.results) ? query.results.filter(isRecord) : []\n if (query.count !== undefined) lines.push(`Rows: ${displayValue(query.count)}`)\n else lines.push(`Rows: ${results.length}`)\n if (query.billable_units !== undefined) {\n lines.push(`Billed units: ${displayValue(query.billable_units)}`)\n }\n if (query.elapsed_ms !== undefined) lines.push(`Elapsed: ${displayValue(query.elapsed_ms)} ms`)\n if (query.truncated !== undefined) lines.push(`Truncated: ${displayValue(query.truncated)}`)\n if (query.error !== undefined) lines.push(`Error: ${displayValue(query.error)}`)\n if (results.length > 0) lines.push('', 'Results:', ...formatTable(results))\n else lines.push('', 'Results: none')\n }\n\n if (queries) {\n lines.push(`Queries: ${queries.length}`)\n for (const entry of queries) {\n if (!isRecord(entry)) continue\n const id = displayValue(entry.id) || 'query'\n const results = Array.isArray(entry.results) ? entry.results.filter(isRecord) : []\n lines.push('', `[${id}]`)\n if (entry.error !== undefined) lines.push(`Error: ${displayValue(entry.error)}`)\n lines.push(`Rows: ${displayValue(entry.count ?? results.length)}`)\n if (results.length > 0) lines.push(...formatTable(results))\n else lines.push('Results: none')\n }\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Formats one MCP text block for terminal output. JSON is indented when the\n * caller requests `--json`; graph query envelopes are rendered as a compact\n * result summary and table by default.\n */\nexport function formatMcpTextContent(\n text: string,\n tool?: string,\n options: Pick<McpPrintOptions, 'json'> = {}\n): string {\n let parsed: unknown\n try {\n parsed = JSON.parse(text) as unknown\n } catch {\n return text\n }\n\n if (options.json) {\n try {\n return JSON.stringify(parsed, null, 2)\n } catch {\n return text\n }\n }\n\n if (isRecord(parsed)) {\n return formatGraphResult(parsed, tool) ?? JSON.stringify(parsed, null, 2)\n }\n return displayValue(parsed)\n}\n\n/**\n * Prints the text blocks of an MCP tool result to stdout. When the result is\n * flagged `isError`, throws an McpToolError with the tool's error text instead\n * — MCP `callTool` returns tool errors as ordinary results (it does not\n * reject), so callers must surface them as failures (non-zero exit) rather than\n * printing to stdout and exiting 0. The error keeps the `error_detail` of the\n * reply, so a caller can tell a refused query from a dead endpoint.\n */\n/** The tools whose rows carry the `_utc` and `_eth` twins, the same two the proxy adds (src/mcp/proxy.ts). */\nfunction isGraphReadTool(tool?: string): boolean {\n return tool === 'graph_query' || tool === 'graph_query_batch'\n}\n\n/**\n * A graph reply with the twins the proxy writes for a host: `<column>_utc`\n * beside every timestamp and `<column>_eth` beside every wei balance. The\n * shell path printed the server row as it arrived (chain-insights #509), so a\n * person reading `cia mcp call` got the raw milliseconds and the raw wei.\n * The text block the server sends is the JSON of its structured content, so\n * both are rewritten together; a result with no structured content is read\n * from its text block first.\n */\nexport function withGraphTwins(result: McpTextResult, tool?: string): McpTextResult {\n if (!isGraphReadTool(tool) || result.isError) return result\n const only = (result.content ?? []).length === 1 && result.content?.[0]?.type === 'text' ? result.content[0] : null\n let structured = result.structuredContent\n if (structured === undefined && only?.text) {\n try {\n structured = JSON.parse(only.text) as unknown\n } catch {\n return result\n }\n }\n const twinned = withTimestampTextResult({ ...result, structuredContent: structured })\n return twinned.structuredContent === structured && result.structuredContent !== undefined ? result : twinned\n}\n\nexport function printMcpTextContent(result: McpTextResult, options: McpPrintOptions = {}): void {\n result = withGraphTwins(result, options.tool)\n const texts = (result.content ?? [])\n .filter((item) => item.type === 'text')\n .map((item) => item.text ?? '')\n\n if (result.isError) {\n throw new McpToolError(\n texts.join('\\n').trim() || 'MCP tool returned an error',\n errorDetailOf(result.structuredContent)\n )\n }\n\n for (const text of texts) console.log(formatMcpTextContent(text, options.tool, options))\n}\n"],"mappings":";;;AAEA,MAAa,eAAe,KAAK,MAC/B,aAAa,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,MAAM,CAClE;AAEA,MAAa,kBAAkB,aAAa;;;;;;;;;;;;ACI5C,MAAM,cAAc;AACpB,MAAM,WAAW;AACjB,MAAM,cAAc,OAAO;;AAG3B,SAAgB,QAAQ,KAAqB;CAC3C,MAAM,QAAQ,OAAO,GAAG;CACxB,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,QAAQ,YAAA,CAAa,SAAS,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;CACjF,OAAO,SAAS,KAAK,MAAM,SAAS,IAAI,GAAG,MAAM,GAAG;AACtD;AAEA,SAAS,UAAU,OAAiC;CAClD,OAAO,OAAO,UAAU,YAAY,SAAS,KAAK,KAAK;AACzD;;;;;;;AAQA,SAAgB,eAAe,KAAuD;CACpF,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAAG;EAC9C,IAAI,OAAO,IAAI,SAAS,MAAM,KAAK,UAAU,KAAK,IAAI,QAAQ,KAAK,IAAI;EACvE,IAAI,YAAY,KAAK,GAAG,KAAK,UAAU,KAAK,KAAK,EAAE,GAAG,IAAI,SAAS,MAAM,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;CAC5G;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,mBAAmB,OAAuB;CACxD,OAAO,MAAM,QAAQ,kDAAkD,KAAK;AAC9E;;;;;;;;;;;;;;;;;;;;ACpCA,MAAM,gBAAgB;AACtB,MAAM,SAAS;AACf,MAAM,SAAS;AAEf,SAASA,WAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;AAGA,SAAgB,QAAQ,IAAoB;CAC1C,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,QAAQ,KAAK,GAAG,EAAE;AACtE;AAEA,SAAS,UAAU,OAAiC;CAClD,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,UAAU,SAAS;AAC7F;;;;;;;AAQA,SAAgB,iBAAiB,KAAuD;CACtF,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAAG;EAC9C,IAAI,OAAO,IAAI,SAAS,MAAM,KAAK,UAAU,KAAK,IAAI,QAAQ,KAAK,IAAI;EACvE,IAAI,cAAc,KAAK,GAAG,KAAK,UAAU,KAAK,KAAK,EAAE,GAAG,IAAI,SAAS,MAAM,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;CAC9G;CACA,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,mBAAmB,OAAuB;CACxD,OAAO,mBAAmB,MAAM,QAAQ,sCAAsC,KAAK,CAAC;AACtF;;AAGA,SAAgB,qBAAqB,UAAkB,MAAwD;CAC7G,IAAI,aAAa,iBAAiB,OAAO,KAAK,UAAU,UAAU,OAAO;EAAE,GAAG;EAAM,OAAO,mBAAmB,KAAK,KAAK;CAAE;CAC1H,IAAI,aAAa,uBAAuB,MAAM,QAAQ,KAAK,OAAO,GAChE,OAAO;EACL,GAAG;EACH,SAAS,KAAK,QAAQ,KAAK,UACzBA,WAAS,KAAK,KAAK,OAAO,MAAM,UAAU,WAAW;GAAE,GAAG;GAAO,OAAO,mBAAmB,MAAM,KAAK;EAAE,IAAI,KAC9G;CACF;CAEF,OAAO;AACT;AAEA,SAAS,WAAW,QAA0B;CAC5C,IAAI,CAACA,WAAS,MAAM,KAAK,CAAC,MAAM,QAAQ,OAAO,OAAO,GAAG,OAAO;CAChE,OAAO;EACL,GAAG;EACH,SAAS,OAAO,QAAQ,KAAK,QAASA,WAAS,GAAG,IAAI,eAAe,iBAAiB,GAAG,CAAC,IAAI,GAAI;CACpG;AACF;;;;;;;AAQA,SAAgB,kBAAkB,mBAAqC;CACrE,IAAI,CAACA,WAAS,iBAAiB,KAAK,CAACA,WAAS,kBAAkB,KAAK,GAAG,OAAO;CAC/E,MAAM,QAAQ,kBAAkB;CAChC,IAAI,CAACA,WAAS,MAAM,KAAK,KAAK,CAAC,MAAM,QAAQ,MAAM,OAAO,GAAG,OAAO;CACpE,MAAM,OAAgC,EAAE,GAAG,MAAM;CACjD,IAAIA,WAAS,MAAM,KAAK,GAAG,KAAK,QAAQ,WAAW,MAAM,KAAK;CAC9D,IAAI,MAAM,QAAQ,MAAM,OAAO,GAAG,KAAK,UAAU,MAAM,QAAQ,IAAI,UAAU;CAC7E,OAAO;EAAE,GAAG;EAAmB,OAAO;CAAK;AAC7C;;;;;;AASA,SAAgB,wBAA0F,QAAc;CACtH,MAAM,aAAa,kBAAkB,OAAO,iBAAiB;CAC7D,IAAI,eAAe,OAAO,mBAAmB,OAAO;CACpD,MAAM,UAAU,OAAO,WAAW,CAAC;CACnC,MAAM,OAAO,QAAQ,WAAW,KAAK,QAAQ,EAAE,EAAE,SAAS,SAAS,QAAQ,KAAK;CAChF,IAAI,SAAkB,KAAA;CACtB,IAAI;EACF,SAAS,MAAM,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,KAAA;CAChD,QAAQ;EACN,SAAS,KAAA;CACX;CACA,MAAM,mBACJ,WAAW,KAAA,KAAa,KAAK,UAAU,MAAM,MAAM,KAAK,UAAU,OAAO,iBAAiB;CAC5F,OAAO;EACL,GAAG;EACH,mBAAmB;EACnB,SAAS,mBAAmB,CAAC;GAAE,GAAG;GAAM,MAAM,KAAK,UAAU,UAAU;EAAE,CAAC,IAAI;CAChF;AACF;;;ACnGA,MAAM,sBAAsB;CAAC;CAAQ;CAAQ;CAAS;CAAO;AAAS;;;;;;;AAQtE,IAAa,eAAb,cAAkC,MAAM;CACtC;CAEA,YAAY,SAAiB,aAA8B;EACzD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,IAAI,aAAa,KAAK,cAAc;CACtC;AACF;AAUA,MAAM,4BACJ;;;;;;;AAQF,SAAgB,mBAAmB,KAA2B;CAC5D,MAAM,SAAS,IAAI;CACnB,IAAI,CAAC,QAAQ,OAAO,IAAI;CACxB,MAAM,QAAkB,CAAC;CACzB,IAAI,OAAO,MAAM,MAAM,KAAK,QAAQ,OAAO,MAAM;CACjD,IAAI,OAAO,OAAO,MAAM,KAAK,SAAS,OAAO,OAAO;CACpD,IAAI,OAAO,KAAK,MAAM,KAAK,OAAO,GAAG;CACrC,IAAI,OAAO,SAAS,qBAAqB,MAAM,KAAK,yBAAyB;CAC7E,IAAI,MAAM,WAAW,GAAG,OAAO,IAAI;CACnC,OAAO,GAAG,IAAI,QAAQ,MAAM,MAAM,KAAK,KAAK;AAC9C;AAIA,SAAS,SAAS,OAAqC;CACrD,OAAO,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AACrE;;AAGA,SAAS,cAAc,mBAAwD;CAC7E,IAAI,CAAC,SAAS,iBAAiB,KAAK,CAAC,SAAS,kBAAkB,YAAY,GAAG,OAAO,KAAA;CACtF,MAAM,SAAyB,CAAC;CAChC,KAAK,MAAM,SAAS,qBAAqB;EACvC,MAAM,QAAQ,kBAAkB,aAAa;EAC7C,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS;CACjD;CACA,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,SAAS,KAAA;AACnD;AAEA,SAAS,aAAa,OAAwB;CAC5C,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW,OAAO,OAAO,KAAK;CAChF,IAAI;EACF,OAAO,KAAK,UAAU,KAAK;CAC7B,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;AAEA,SAAS,YAAY,MAAmC;CACtD,IAAI,KAAK,WAAW,GAAG,OAAO,CAAC;CAC/B,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,KAAK,SAAS,QAAQ,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC;CACpE,MAAM,SAAS,KAAK,KAAK,QAAQ,QAAQ,KAAK,WAAW,aAAa,IAAI,OAAO,CAAC,CAAC;CACnF,MAAM,SAAS,QAAQ,KAAK,QAAQ,UAClC,KAAK,IAAI,OAAO,QAAQ,GAAG,OAAO,KAAK,QAAQ,IAAI,MAAM,EAAE,UAAU,CAAC,CAAC,CACzE;CACA,MAAM,QAAQ,QACZ,IACG,KAAK,OAAO,UAAW,UAAU,IAAI,SAAS,IAAI,QAAQ,MAAM,OAAO,OAAO,UAAU,CAAC,CAAE,CAAC,CAC5F,KAAK,KAAK;CACf,OAAO;EAAC,KAAK,OAAO;EAAG,OAAO,KAAK,UAAU,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;EAAG,GAAG,OAAO,IAAI,IAAI;CAAC;AAClG;AAEA,SAAS,kBAAkB,OAAmB,cAAsC;CAClF,MAAM,QAAQ,SAAS,MAAM,KAAK,IAAI,MAAM,QAAQ,KAAA;CACpD,MAAM,QAAQ,SAAS,SAAS,MAAM,KAAK,IAAI,MAAM,QAAQ,KAAA;CAC7D,MAAM,UAAU,SAAS,MAAM,QAAQ,MAAM,OAAO,IAAI,MAAM,UAAU,KAAA;CACxE,IAAI,CAAC,SAAS,CAAC,SAAS,OAAO;CAE/B,MAAM,QAAQ,CAAC,SAAS,aAAa,MAAM,IAAI,KAAK,gBAAgB,cAAc;CAClF,MAAM,UAAU,SAAS,MAAM,OAAO,IAClC,MAAM,UACN,SAAS,SAAS,MAAM,OAAO,IAC7B,MAAM,UACN,KAAA;CACN,IAAI,SAAS,YAAY,KAAA,GAAW,MAAM,KAAK,YAAY,aAAa,QAAQ,OAAO,GAAG;CAE1F,IAAI,OAAO;EACT,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IAAI,MAAM,QAAQ,OAAO,QAAQ,IAAI,CAAC;EACjF,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,KAAK,SAAS,aAAa,MAAM,KAAK,GAAG;OACzE,MAAM,KAAK,SAAS,QAAQ,QAAQ;EACzC,IAAI,MAAM,mBAAmB,KAAA,GAC3B,MAAM,KAAK,iBAAiB,aAAa,MAAM,cAAc,GAAG;EAElE,IAAI,MAAM,eAAe,KAAA,GAAW,MAAM,KAAK,YAAY,aAAa,MAAM,UAAU,EAAE,IAAI;EAC9F,IAAI,MAAM,cAAc,KAAA,GAAW,MAAM,KAAK,cAAc,aAAa,MAAM,SAAS,GAAG;EAC3F,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,KAAK,UAAU,aAAa,MAAM,KAAK,GAAG;EAC/E,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAK,IAAI,YAAY,GAAG,YAAY,OAAO,CAAC;OACrE,MAAM,KAAK,IAAI,eAAe;CACrC;CAEA,IAAI,SAAS;EACX,MAAM,KAAK,YAAY,QAAQ,QAAQ;EACvC,KAAK,MAAM,SAAS,SAAS;GAC3B,IAAI,CAAC,SAAS,KAAK,GAAG;GACtB,MAAM,KAAK,aAAa,MAAM,EAAE,KAAK;GACrC,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IAAI,MAAM,QAAQ,OAAO,QAAQ,IAAI,CAAC;GACjF,MAAM,KAAK,IAAI,IAAI,GAAG,EAAE;GACxB,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,KAAK,UAAU,aAAa,MAAM,KAAK,GAAG;GAC/E,MAAM,KAAK,SAAS,aAAa,MAAM,SAAS,QAAQ,MAAM,GAAG;GACjE,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAK,GAAG,YAAY,OAAO,CAAC;QACrD,MAAM,KAAK,eAAe;EACjC;CACF;CAEA,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;AAOA,SAAgB,qBACd,MACA,MACA,UAAyC,CAAC,GAClC;CACR,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,IAAI;CAC1B,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,QAAQ,MACV,IAAI;EACF,OAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;CACvC,QAAQ;EACN,OAAO;CACT;CAGF,IAAI,SAAS,MAAM,GACjB,OAAO,kBAAkB,QAAQ,IAAI,KAAK,KAAK,UAAU,QAAQ,MAAM,CAAC;CAE1E,OAAO,aAAa,MAAM;AAC5B;;;;;;;;;;AAWA,SAAS,gBAAgB,MAAwB;CAC/C,OAAO,SAAS,iBAAiB,SAAS;AAC5C;;;;;;;;;;AAWA,SAAgB,eAAe,QAAuB,MAA8B;CAClF,IAAI,CAAC,gBAAgB,IAAI,KAAK,OAAO,SAAS,OAAO;CACrD,MAAM,QAAQ,OAAO,WAAW,CAAC,EAAA,CAAG,WAAW,KAAK,OAAO,UAAU,EAAE,EAAE,SAAS,SAAS,OAAO,QAAQ,KAAK;CAC/G,IAAI,aAAa,OAAO;CACxB,IAAI,eAAe,KAAA,KAAa,MAAM,MACpC,IAAI;EACF,aAAa,KAAK,MAAM,KAAK,IAAI;CACnC,QAAQ;EACN,OAAO;CACT;CAEF,MAAM,UAAU,wBAAwB;EAAE,GAAG;EAAQ,mBAAmB;CAAW,CAAC;CACpF,OAAO,QAAQ,sBAAsB,cAAc,OAAO,sBAAsB,KAAA,IAAY,SAAS;AACvG;AAEA,SAAgB,oBAAoB,QAAuB,UAA2B,CAAC,GAAS;CAC9F,SAAS,eAAe,QAAQ,QAAQ,IAAI;CAC5C,MAAM,SAAS,OAAO,WAAW,CAAC,EAAA,CAC/B,QAAQ,SAAS,KAAK,SAAS,MAAM,CAAC,CACtC,KAAK,SAAS,KAAK,QAAQ,EAAE;CAEhC,IAAI,OAAO,SACT,MAAM,IAAI,aACR,MAAM,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,8BAC3B,cAAc,OAAO,iBAAiB,CACxC;CAGF,KAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI,qBAAqB,MAAM,QAAQ,MAAM,OAAO,CAAC;AACzF"}
|