chain-insights 0.2.16
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/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +10 -0
- package/bin/install.cjs +252 -0
- package/bin/mcp-proxy.cjs +10 -0
- package/dist/active-BSrxLKwn.mjs +50 -0
- package/dist/active-BSrxLKwn.mjs.map +1 -0
- package/dist/active-Dv7Tu-O4.cjs +68 -0
- package/dist/app-BjjuQM0B.mjs +155 -0
- package/dist/app-BjjuQM0B.mjs.map +1 -0
- package/dist/app-Dq1TdB6p.cjs +161 -0
- package/dist/artifact-server-DoxJ7fCx.cjs +47 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs +48 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs.map +1 -0
- package/dist/assets/bg-pattern.png +0 -0
- package/dist/assets/logo.png +0 -0
- package/dist/call-args-DQA2QcRA.cjs +27 -0
- package/dist/call-args-Lk_wOJxd.mjs +29 -0
- package/dist/call-args-Lk_wOJxd.mjs.map +1 -0
- package/dist/capabilities-CB97WMA5.cjs +83 -0
- package/dist/capabilities-DliMBim-.mjs +84 -0
- package/dist/capabilities-DliMBim-.mjs.map +1 -0
- package/dist/cases-By7INiOa.mjs +6 -0
- package/dist/cases-CDcNU91B.cjs +9 -0
- package/dist/chunk-CZWwpsFl.cjs +43 -0
- package/dist/cli.cjs +752 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +753 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-D4Bq0rp9.mjs +111 -0
- package/dist/client-D4Bq0rp9.mjs.map +1 -0
- package/dist/client-D4fZgIaO.cjs +132 -0
- package/dist/config-Bmdl5hdk.cjs +67 -0
- package/dist/config-BwrBYmiC.mjs +44 -0
- package/dist/config-BwrBYmiC.mjs.map +1 -0
- package/dist/data-extractor-BNGj7ECT.cjs +347 -0
- package/dist/data-extractor-DFzsa5CS.mjs +336 -0
- package/dist/data-extractor-DFzsa5CS.mjs.map +1 -0
- package/dist/dossier-BsroDgD3.mjs +76 -0
- package/dist/dossier-BsroDgD3.mjs.map +1 -0
- package/dist/dossier-DtxREpPm.cjs +76 -0
- package/dist/evidence-BGcdKxuV.cjs +200 -0
- package/dist/evidence-BhvFW-y_.mjs +195 -0
- package/dist/evidence-BhvFW-y_.mjs.map +1 -0
- package/dist/format-Ce1RObVl.mjs +22 -0
- package/dist/format-Ce1RObVl.mjs.map +1 -0
- package/dist/format-DOrPvXEr.cjs +20 -0
- package/dist/frontmatter-D8wWCeOa.mjs +26 -0
- package/dist/frontmatter-D8wWCeOa.mjs.map +1 -0
- package/dist/frontmatter-DgAuai7E.cjs +35 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs +130 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs.map +1 -0
- package/dist/graph-normalizer-DeIj6Ses.cjs +133 -0
- package/dist/graph-reports-C4TBjCkM.mjs +63 -0
- package/dist/graph-reports-C4TBjCkM.mjs.map +1 -0
- package/dist/graph-reports-DU05YCei.cjs +64 -0
- package/dist/html-generator-CAv81IWH.cjs +85 -0
- package/dist/html-generator-V6Bp0uRb.mjs +68 -0
- package/dist/html-generator-V6Bp0uRb.mjs.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +9 -0
- package/dist/init-BjuFt54X.cjs +232 -0
- package/dist/init-CaOsHTIo.mjs +232 -0
- package/dist/init-CaOsHTIo.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +1257 -0
- package/dist/mcp-proxy.d.cts +12 -0
- package/dist/mcp-proxy.d.cts.map +1 -0
- package/dist/mcp-proxy.d.mts +12 -0
- package/dist/mcp-proxy.d.mts.map +1 -0
- package/dist/mcp-proxy.mjs +1255 -0
- package/dist/mcp-proxy.mjs.map +1 -0
- package/dist/output-root-CFYms3ad.cjs +43 -0
- package/dist/output-root-CmWM7aV2.mjs +33 -0
- package/dist/output-root-CmWM7aV2.mjs.map +1 -0
- package/dist/parser-BUIWW1OH.cjs +182 -0
- package/dist/parser-DO0_SssG.mjs +182 -0
- package/dist/parser-DO0_SssG.mjs.map +1 -0
- package/dist/public-tools-D4UI-Zb0.mjs +2554 -0
- package/dist/public-tools-D4UI-Zb0.mjs.map +1 -0
- package/dist/public-tools-XSpkz2ky.cjs +2556 -0
- package/dist/resolver-C2ZS7oC8.mjs +201 -0
- package/dist/resolver-C2ZS7oC8.mjs.map +1 -0
- package/dist/resolver-zYbu4wDV.cjs +203 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/runner-1Eq55OYb.cjs +148 -0
- package/dist/runner-BhUHbiHG.mjs +149 -0
- package/dist/runner-BhUHbiHG.mjs.map +1 -0
- package/dist/schema-4XpzDFQM.cjs +55 -0
- package/dist/schema-8d0rVIdZ.mjs +37 -0
- package/dist/schema-8d0rVIdZ.mjs.map +1 -0
- package/dist/schema-cache-9CksD7tX.mjs +34 -0
- package/dist/schema-cache-9CksD7tX.mjs.map +1 -0
- package/dist/schema-cache-CgWRCN2N.cjs +36 -0
- package/dist/selector-CkFcTXzz.cjs +10 -0
- package/dist/selector-xjm6NTHI.mjs +12 -0
- package/dist/selector-xjm6NTHI.mjs.map +1 -0
- package/dist/server-BkM5xrXb.mjs +45 -0
- package/dist/server-BkM5xrXb.mjs.map +1 -0
- package/dist/server-DXowbpfi.cjs +54 -0
- package/dist/session-BpNylyuJ.cjs +115 -0
- package/dist/session-CcTgYxsj.mjs +115 -0
- package/dist/session-CcTgYxsj.mjs.map +1 -0
- package/dist/setup-DOpKPrlx.cjs +81 -0
- package/dist/setup-DyrWHuwQ.mjs +80 -0
- package/dist/setup-DyrWHuwQ.mjs.map +1 -0
- package/dist/store-BiUhQOIf.cjs +230 -0
- package/dist/store-BoWE-Gtl.mjs +225 -0
- package/dist/store-BoWE-Gtl.mjs.map +1 -0
- package/dist/templates/graph.html +1406 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs +28 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs.map +1 -0
- package/dist/tool-visibility-CwgY205r.cjs +36 -0
- package/dist/tools-Cp2jAAAb.mjs +100 -0
- package/dist/tools-Cp2jAAAb.mjs.map +1 -0
- package/dist/tools-f_vJUZAF.cjs +139 -0
- package/dist/topup-server-BZuQifvh.cjs +940 -0
- package/dist/topup-server-DUjyFftI.mjs +919 -0
- package/dist/topup-server-DUjyFftI.mjs.map +1 -0
- package/dist/version-1gP19Lhi.mjs +8 -0
- package/dist/version-1gP19Lhi.mjs.map +1 -0
- package/dist/version-BNGtdpmH.cjs +18 -0
- package/dist/viz-BlCJe6Tk.mjs +35 -0
- package/dist/viz-BlCJe6Tk.mjs.map +1 -0
- package/dist/viz-ClezVXrJ.cjs +44 -0
- package/dist/wallet-BMelXBYP.mjs +104 -0
- package/dist/wallet-BMelXBYP.mjs.map +1 -0
- package/dist/wallet-RnvvSpV2.cjs +146 -0
- package/docs/architecture.md +145 -0
- package/docs/contributing.md +68 -0
- package/docs/debugging.md +68 -0
- package/docs/development.md +44 -0
- package/docs/graph-tools.md +251 -0
- package/docs/images/graph-mcp-iframe.png +0 -0
- package/docs/images/graph-visualization.png +0 -0
- package/docs/images/topup-page.png +0 -0
- package/docs/investigation-workspaces.md +151 -0
- package/docs/mcp-proxy.md +180 -0
- package/package.json +59 -0
- package/skills/chain-insights-developer-experience/SKILL.md +101 -0
- package/skills/chain-insights-investigation/SKILL.md +285 -0
- package/skills/chain-insights-investigation/agents/openai.yaml +4 -0
- package/skills/chain-insights-investigation/scripts/run-target-uat.sh +197 -0
- package/skills/chain-insights-trace-funds/SKILL.md +249 -0
- package/skills/ci-case/SKILL.md +43 -0
- package/skills/ci-status/SKILL.md +45 -0
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +75 -0
- package/skills/test-chain-insights-graphrag-mcp/agents/openai.yaml +4 -0
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +414 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-Ce1RObVl.mjs","names":[],"sources":["../src/mcp/format.ts"],"sourcesContent":["import type { McpTool } from './schema-cache.js'\n\nconst NAME_WIDTH = 30\nconst DESC_MAX = 60\n\n/**\n * Formats an array of MCP tools as a plain text table string.\n * Returns \"No tools available.\" for an empty array.\n * Caller controls output — use console.log(formatToolsTable(tools)).\n */\nexport function formatToolsTable(tools: McpTool[]): string {\n if (tools.length === 0) return 'No tools available.'\n const header = `${'Tool'.padEnd(NAME_WIDTH)} Description`\n const divider = '-'.repeat(NAME_WIDTH) + ' ' + '-'.repeat(DESC_MAX)\n const rows = tools.map((t) => {\n const name = t.name.padEnd(NAME_WIDTH)\n const desc = (t.description ?? '').slice(0, DESC_MAX)\n return `${name} ${desc}`\n })\n return [header, divider, ...rows].join('\\n')\n}\n"],"mappings":";AAEA,MAAM,aAAa;AACnB,MAAM,WAAW;;;;;;AAOjB,SAAgB,iBAAiB,OAA0B;AACzD,KAAI,MAAM,WAAW,EAAG,QAAO;AAQ/B,QAAO;EAAC,GAPU,OAAO,OAAO,WAAW,CAAC;EAC5B,IAAI,OAAO,WAAW,GAAG,OAAO,IAAI,OAAO,SAAS;EAM3C,GALZ,MAAM,KAAK,MAAM;AAG5B,UAAO,GAFM,EAAE,KAAK,OAAO,WAEb,CAAC,KADD,EAAE,eAAe,IAAI,MAAM,GAAG,SACrB;IAEO;EAAC,CAAC,KAAK,KAAK"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/mcp/format.ts
|
|
2
|
+
const NAME_WIDTH = 30;
|
|
3
|
+
const DESC_MAX = 60;
|
|
4
|
+
/**
|
|
5
|
+
* Formats an array of MCP tools as a plain text table string.
|
|
6
|
+
* Returns "No tools available." for an empty array.
|
|
7
|
+
* Caller controls output — use console.log(formatToolsTable(tools)).
|
|
8
|
+
*/
|
|
9
|
+
function formatToolsTable(tools) {
|
|
10
|
+
if (tools.length === 0) return "No tools available.";
|
|
11
|
+
return [
|
|
12
|
+
`${"Tool".padEnd(NAME_WIDTH)} Description`,
|
|
13
|
+
"-".repeat(NAME_WIDTH) + " " + "-".repeat(DESC_MAX),
|
|
14
|
+
...tools.map((t) => {
|
|
15
|
+
return `${t.name.padEnd(NAME_WIDTH)} ${(t.description ?? "").slice(0, DESC_MAX)}`;
|
|
16
|
+
})
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
exports.formatToolsTable = formatToolsTable;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/cases/frontmatter.ts
|
|
2
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
3
|
+
function parseFrontmatter(content) {
|
|
4
|
+
const m = content.match(FRONTMATTER_RE);
|
|
5
|
+
if (!m) return {
|
|
6
|
+
frontmatter: {},
|
|
7
|
+
body: content
|
|
8
|
+
};
|
|
9
|
+
const fm = {};
|
|
10
|
+
for (const line of m[1].split("\n")) {
|
|
11
|
+
const colon = line.indexOf(":");
|
|
12
|
+
if (colon < 0) continue;
|
|
13
|
+
fm[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
frontmatter: fm,
|
|
17
|
+
body: m[2]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function serializeFrontmatter(fm, body) {
|
|
21
|
+
return `---\n${Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join("\n")}\n---\n${body}`;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { serializeFrontmatter as n, parseFrontmatter as t };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=frontmatter-D8wWCeOa.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter-D8wWCeOa.mjs","names":[],"sources":["../src/cases/frontmatter.ts"],"sourcesContent":["// Hand-rolled YAML frontmatter parser — GSD pattern (no gray-matter dependency).\n// Only supports flat key: value pairs. Arrays must be stored as comma-separated strings.\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/\n\nexport function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {\n const m = content.match(FRONTMATTER_RE)\n if (!m) return { frontmatter: {}, body: content }\n const fm: Record<string, string> = {}\n for (const line of m[1].split('\\n')) {\n const colon = line.indexOf(':')\n if (colon < 0) continue\n fm[line.slice(0, colon).trim()] = line.slice(colon + 1).trim()\n }\n return { frontmatter: fm, body: m[2] }\n}\n\nexport function serializeFrontmatter(fm: Record<string, string>, body: string): string {\n const lines = Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join('\\n')\n return `---\\n${lines}\\n---\\n${body}`\n}\n"],"mappings":";AAEA,MAAM,iBAAiB;AAEvB,SAAgB,iBAAiB,SAAwE;CACvG,MAAM,IAAI,QAAQ,MAAM,eAAe;AACvC,KAAI,CAAC,EAAG,QAAO;EAAE,aAAa,EAAE;EAAE,MAAM;EAAS;CACjD,MAAM,KAA6B,EAAE;AACrC,MAAK,MAAM,QAAQ,EAAE,GAAG,MAAM,KAAK,EAAE;EACnC,MAAM,QAAQ,KAAK,QAAQ,IAAI;AAC/B,MAAI,QAAQ,EAAG;AACf,KAAG,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,KAAK,MAAM,QAAQ,EAAE,CAAC,MAAM;;AAEhE,QAAO;EAAE,aAAa;EAAI,MAAM,EAAE;EAAI;;AAGxC,SAAgB,qBAAqB,IAA4B,MAAsB;AAErF,QAAO,QADO,OAAO,QAAQ,GAAG,CAAC,KAAK,CAAC,GAAG,OAAO,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,KAChD,CAAC,SAAS"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/cases/frontmatter.ts
|
|
2
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
3
|
+
function parseFrontmatter(content) {
|
|
4
|
+
const m = content.match(FRONTMATTER_RE);
|
|
5
|
+
if (!m) return {
|
|
6
|
+
frontmatter: {},
|
|
7
|
+
body: content
|
|
8
|
+
};
|
|
9
|
+
const fm = {};
|
|
10
|
+
for (const line of m[1].split("\n")) {
|
|
11
|
+
const colon = line.indexOf(":");
|
|
12
|
+
if (colon < 0) continue;
|
|
13
|
+
fm[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
frontmatter: fm,
|
|
17
|
+
body: m[2]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function serializeFrontmatter(fm, body) {
|
|
21
|
+
return `---\n${Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join("\n")}\n---\n${body}`;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
Object.defineProperty(exports, "parseFrontmatter", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function() {
|
|
27
|
+
return parseFrontmatter;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "serializeFrontmatter", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function() {
|
|
33
|
+
return serializeFrontmatter;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
//#region src/viz/graph-normalizer.ts
|
|
2
|
+
const GRAPH_TYPE_LABELS = new Set([
|
|
3
|
+
"Address",
|
|
4
|
+
"Exchange",
|
|
5
|
+
"Miner",
|
|
6
|
+
"Validator",
|
|
7
|
+
"Hotkey",
|
|
8
|
+
"Subnet",
|
|
9
|
+
"IPAddress"
|
|
10
|
+
]);
|
|
11
|
+
const SOURCE_ADDRESS_TYPES = new Set(["substrate", "evm"]);
|
|
12
|
+
const FLOW_EDGE_TYPES = new Set([
|
|
13
|
+
"flows_to",
|
|
14
|
+
"transfer",
|
|
15
|
+
"transfer_to",
|
|
16
|
+
"transfers_to"
|
|
17
|
+
]);
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function stringArray(value) {
|
|
22
|
+
return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
|
|
23
|
+
}
|
|
24
|
+
function unique(values) {
|
|
25
|
+
return [...new Set(values)];
|
|
26
|
+
}
|
|
27
|
+
function lowerSnake(value) {
|
|
28
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
function isGraphTypeLabel(label) {
|
|
31
|
+
return GRAPH_TYPE_LABELS.has(label);
|
|
32
|
+
}
|
|
33
|
+
function displayLabels(node) {
|
|
34
|
+
return unique(stringArray(node["labels"])).filter((label) => !isGraphTypeLabel(label));
|
|
35
|
+
}
|
|
36
|
+
function systemLabels(node) {
|
|
37
|
+
return unique([
|
|
38
|
+
...stringArray(node["system_labels"]),
|
|
39
|
+
...stringArray(node["graph_labels"]),
|
|
40
|
+
...stringArray(node["raw_labels"]),
|
|
41
|
+
...stringArray(node["labels"]).filter(isGraphTypeLabel)
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
function normalizeRoles(node) {
|
|
45
|
+
const roles = stringArray(node["roles"]);
|
|
46
|
+
const role = typeof node["role"] === "string" ? node["role"] : "";
|
|
47
|
+
if (role === "source_exchange") roles.push("exchange");
|
|
48
|
+
else if (role) roles.push(role);
|
|
49
|
+
const display = stringArray(node["labels"]).map(lowerSnake);
|
|
50
|
+
if (systemLabels(node).some((label) => lowerSnake(label) === "exchange") || display.includes("exchange")) roles.push("exchange");
|
|
51
|
+
return unique(roles.map(lowerSnake).filter(Boolean));
|
|
52
|
+
}
|
|
53
|
+
function normalizeNodeType(node) {
|
|
54
|
+
if (typeof node["node_type"] === "string" && node["node_type"].trim()) return lowerSnake(node["node_type"]);
|
|
55
|
+
if (typeof node["address"] === "string" || typeof node["id"] === "string") return "address";
|
|
56
|
+
const labels = systemLabels(node);
|
|
57
|
+
if (labels.length > 0) return lowerSnake(labels[0]);
|
|
58
|
+
return "unknown";
|
|
59
|
+
}
|
|
60
|
+
function normalizeNode(node) {
|
|
61
|
+
if (!isRecord(node)) return {};
|
|
62
|
+
const normalized = {};
|
|
63
|
+
for (const [key, value] of Object.entries(node)) {
|
|
64
|
+
if ([
|
|
65
|
+
"address_type",
|
|
66
|
+
"address_subtypes",
|
|
67
|
+
"entity_kind",
|
|
68
|
+
"graph_labels",
|
|
69
|
+
"labels",
|
|
70
|
+
"node_type",
|
|
71
|
+
"pattern_flags",
|
|
72
|
+
"raw_labels",
|
|
73
|
+
"role",
|
|
74
|
+
"roles",
|
|
75
|
+
"system_labels",
|
|
76
|
+
"type"
|
|
77
|
+
].includes(key)) continue;
|
|
78
|
+
normalized[key] = value;
|
|
79
|
+
}
|
|
80
|
+
const id = typeof node["id"] === "string" ? node["id"] : typeof node["address"] === "string" ? node["address"] : void 0;
|
|
81
|
+
if (id) normalized["id"] = id;
|
|
82
|
+
const address = typeof node["address"] === "string" ? node["address"] : id;
|
|
83
|
+
if (address) normalized["address"] = address;
|
|
84
|
+
normalized["node_type"] = normalizeNodeType(node);
|
|
85
|
+
normalized["labels"] = displayLabels(node);
|
|
86
|
+
if (typeof node["address_type"] === "string" && SOURCE_ADDRESS_TYPES.has(node["address_type"])) normalized["address_type"] = node["address_type"];
|
|
87
|
+
const addressSubtypes = unique(stringArray(node["address_subtypes"]));
|
|
88
|
+
if (addressSubtypes.length > 0) normalized["address_subtypes"] = addressSubtypes;
|
|
89
|
+
const roles = normalizeRoles(node);
|
|
90
|
+
if (roles.length > 0) normalized["roles"] = roles;
|
|
91
|
+
if (!Array.isArray(node["flags"]) && Array.isArray(node["pattern_flags"]) && node["pattern_flags"].length > 0) normalized["flags"] = node["pattern_flags"].map(String);
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
function normalizeEdgeType(edge) {
|
|
95
|
+
const edgeType = lowerSnake(typeof edge["edge_type"] === "string" && edge["edge_type"].trim() ? edge["edge_type"] : typeof edge["type"] === "string" && edge["type"].trim() ? edge["type"] : typeof edge["relationship_type"] === "string" && edge["relationship_type"].trim() ? edge["relationship_type"] : "related_to");
|
|
96
|
+
return FLOW_EDGE_TYPES.has(edgeType) ? "flows_to" : edgeType;
|
|
97
|
+
}
|
|
98
|
+
function normalizeEdge(edge) {
|
|
99
|
+
if (!isRecord(edge)) return {};
|
|
100
|
+
const normalized = {};
|
|
101
|
+
for (const [key, value] of Object.entries(edge)) {
|
|
102
|
+
if ([
|
|
103
|
+
"edge_type",
|
|
104
|
+
"from_address",
|
|
105
|
+
"relationship_type",
|
|
106
|
+
"to_address",
|
|
107
|
+
"type"
|
|
108
|
+
].includes(key)) continue;
|
|
109
|
+
normalized[key] = value;
|
|
110
|
+
}
|
|
111
|
+
if (typeof normalized["source"] !== "string" && typeof edge["from_address"] === "string") normalized["source"] = edge["from_address"];
|
|
112
|
+
if (typeof normalized["target"] !== "string" && typeof edge["to_address"] === "string") normalized["target"] = edge["to_address"];
|
|
113
|
+
normalized["edge_type"] = normalizeEdgeType(edge);
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
function normalizeGraphPayload(payload) {
|
|
117
|
+
if (!isRecord(payload) || payload["schema"] !== "chain-insights.graph.v1") throw new Error("Unsupported graph payload schema");
|
|
118
|
+
return {
|
|
119
|
+
...payload,
|
|
120
|
+
schema: "chain-insights.graph.v1",
|
|
121
|
+
nodes: Array.isArray(payload["nodes"]) ? payload["nodes"].map(normalizeNode) : [],
|
|
122
|
+
edges: Array.isArray(payload["edges"]) ? payload["edges"].map(normalizeEdge) : [],
|
|
123
|
+
flows: Array.isArray(payload["flows"]) ? payload["flows"] : [],
|
|
124
|
+
edge_anchors: Array.isArray(payload["edge_anchors"]) ? payload["edge_anchors"] : []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { normalizeGraphPayload as t };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=graph-normalizer-Cv9yK9Pg.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-normalizer-Cv9yK9Pg.mjs","names":[],"sources":["../src/viz/graph-normalizer.ts"],"sourcesContent":["const GRAPH_TYPE_LABELS = new Set(['Address', 'Exchange', 'Miner', 'Validator', 'Hotkey', 'Subnet', 'IPAddress'])\nconst SOURCE_ADDRESS_TYPES = new Set(['substrate', 'evm'])\nconst FLOW_EDGE_TYPES = new Set(['flows_to', 'transfer', 'transfer_to', 'transfers_to'])\n\ntype GraphRecord = Record<string, unknown>\n\nexport type NormalizedGraphPayload = {\n schema: 'chain-insights.graph.v1'\n nodes: GraphRecord[]\n edges: GraphRecord[]\n flows: unknown[]\n edge_anchors: unknown[]\n [key: string]: unknown\n}\n\nfunction isRecord(value: unknown): value is GraphRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction stringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.map(String).filter(Boolean) : []\n}\n\nfunction unique(values: string[]): string[] {\n return [...new Set(values)]\n}\n\nfunction lowerSnake(value: string): string {\n return value\n .trim()\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase()\n}\n\nfunction isGraphTypeLabel(label: string): boolean {\n return GRAPH_TYPE_LABELS.has(label)\n}\n\nfunction displayLabels(node: GraphRecord): string[] {\n return unique(stringArray(node['labels'])).filter((label) => !isGraphTypeLabel(label))\n}\n\nfunction systemLabels(node: GraphRecord): string[] {\n return unique([\n ...stringArray(node['system_labels']),\n ...stringArray(node['graph_labels']),\n ...stringArray(node['raw_labels']),\n ...stringArray(node['labels']).filter(isGraphTypeLabel),\n ])\n}\n\nfunction normalizeRoles(node: GraphRecord): string[] {\n const roles = stringArray(node['roles'])\n const role = typeof node['role'] === 'string' ? node['role'] : ''\n if (role === 'source_exchange') roles.push('exchange')\n else if (role) roles.push(role)\n\n const display = stringArray(node['labels']).map(lowerSnake)\n if (systemLabels(node).some((label) => lowerSnake(label) === 'exchange') || display.includes('exchange')) {\n roles.push('exchange')\n }\n\n return unique(roles.map(lowerSnake).filter(Boolean))\n}\n\nfunction normalizeNodeType(node: GraphRecord): string {\n if (typeof node['node_type'] === 'string' && node['node_type'].trim()) {\n return lowerSnake(node['node_type'])\n }\n if (typeof node['address'] === 'string' || typeof node['id'] === 'string') return 'address'\n const labels = systemLabels(node)\n if (labels.length > 0) return lowerSnake(labels[0]!)\n return 'unknown'\n}\n\nfunction normalizeNode(node: unknown): GraphRecord {\n if (!isRecord(node)) return {}\n\n const normalized: GraphRecord = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n [\n 'address_type',\n 'address_subtypes',\n 'entity_kind',\n 'graph_labels',\n 'labels',\n 'node_type',\n 'pattern_flags',\n 'raw_labels',\n 'role',\n 'roles',\n 'system_labels',\n 'type',\n ].includes(key)\n ) {\n continue\n }\n normalized[key] = value\n }\n\n const id = typeof node['id'] === 'string' ? node['id'] : typeof node['address'] === 'string' ? node['address'] : undefined\n if (id) normalized['id'] = id\n\n const address = typeof node['address'] === 'string' ? node['address'] : id\n if (address) normalized['address'] = address\n\n normalized['node_type'] = normalizeNodeType(node)\n normalized['labels'] = displayLabels(node)\n\n if (typeof node['address_type'] === 'string' && SOURCE_ADDRESS_TYPES.has(node['address_type'])) {\n normalized['address_type'] = node['address_type']\n }\n\n const addressSubtypes = unique(stringArray(node['address_subtypes']))\n if (addressSubtypes.length > 0) normalized['address_subtypes'] = addressSubtypes\n\n const roles = normalizeRoles(node)\n if (roles.length > 0) normalized['roles'] = roles\n\n if (!Array.isArray(node['flags']) && Array.isArray(node['pattern_flags']) && node['pattern_flags'].length > 0) {\n normalized['flags'] = node['pattern_flags'].map(String)\n }\n\n return normalized\n}\n\nfunction normalizeEdgeType(edge: GraphRecord): string {\n const rawType =\n typeof edge['edge_type'] === 'string' && edge['edge_type'].trim()\n ? edge['edge_type']\n : typeof edge['type'] === 'string' && edge['type'].trim()\n ? edge['type']\n : typeof edge['relationship_type'] === 'string' && edge['relationship_type'].trim()\n ? edge['relationship_type']\n : 'related_to'\n const edgeType = lowerSnake(rawType)\n return FLOW_EDGE_TYPES.has(edgeType) ? 'flows_to' : edgeType\n}\n\nfunction normalizeEdge(edge: unknown): GraphRecord {\n if (!isRecord(edge)) return {}\n\n const normalized: GraphRecord = {}\n for (const [key, value] of Object.entries(edge)) {\n if (['edge_type', 'from_address', 'relationship_type', 'to_address', 'type'].includes(key)) continue\n normalized[key] = value\n }\n\n if (typeof normalized['source'] !== 'string' && typeof edge['from_address'] === 'string') {\n normalized['source'] = edge['from_address']\n }\n if (typeof normalized['target'] !== 'string' && typeof edge['to_address'] === 'string') {\n normalized['target'] = edge['to_address']\n }\n normalized['edge_type'] = normalizeEdgeType(edge)\n\n return normalized\n}\n\nexport function normalizeGraphPayload(payload: unknown): NormalizedGraphPayload {\n if (!isRecord(payload) || payload['schema'] !== 'chain-insights.graph.v1') {\n throw new Error('Unsupported graph payload schema')\n }\n\n return {\n ...payload,\n schema: 'chain-insights.graph.v1',\n nodes: Array.isArray(payload['nodes']) ? payload['nodes'].map(normalizeNode) : [],\n edges: Array.isArray(payload['edges']) ? payload['edges'].map(normalizeEdge) : [],\n flows: Array.isArray(payload['flows']) ? payload['flows'] : [],\n edge_anchors: Array.isArray(payload['edge_anchors']) ? payload['edge_anchors'] : [],\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//#region src/viz/graph-normalizer.ts
|
|
2
|
+
const GRAPH_TYPE_LABELS = new Set([
|
|
3
|
+
"Address",
|
|
4
|
+
"Exchange",
|
|
5
|
+
"Miner",
|
|
6
|
+
"Validator",
|
|
7
|
+
"Hotkey",
|
|
8
|
+
"Subnet",
|
|
9
|
+
"IPAddress"
|
|
10
|
+
]);
|
|
11
|
+
const SOURCE_ADDRESS_TYPES = new Set(["substrate", "evm"]);
|
|
12
|
+
const FLOW_EDGE_TYPES = new Set([
|
|
13
|
+
"flows_to",
|
|
14
|
+
"transfer",
|
|
15
|
+
"transfer_to",
|
|
16
|
+
"transfers_to"
|
|
17
|
+
]);
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function stringArray(value) {
|
|
22
|
+
return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
|
|
23
|
+
}
|
|
24
|
+
function unique(values) {
|
|
25
|
+
return [...new Set(values)];
|
|
26
|
+
}
|
|
27
|
+
function lowerSnake(value) {
|
|
28
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
function isGraphTypeLabel(label) {
|
|
31
|
+
return GRAPH_TYPE_LABELS.has(label);
|
|
32
|
+
}
|
|
33
|
+
function displayLabels(node) {
|
|
34
|
+
return unique(stringArray(node["labels"])).filter((label) => !isGraphTypeLabel(label));
|
|
35
|
+
}
|
|
36
|
+
function systemLabels(node) {
|
|
37
|
+
return unique([
|
|
38
|
+
...stringArray(node["system_labels"]),
|
|
39
|
+
...stringArray(node["graph_labels"]),
|
|
40
|
+
...stringArray(node["raw_labels"]),
|
|
41
|
+
...stringArray(node["labels"]).filter(isGraphTypeLabel)
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
function normalizeRoles(node) {
|
|
45
|
+
const roles = stringArray(node["roles"]);
|
|
46
|
+
const role = typeof node["role"] === "string" ? node["role"] : "";
|
|
47
|
+
if (role === "source_exchange") roles.push("exchange");
|
|
48
|
+
else if (role) roles.push(role);
|
|
49
|
+
const display = stringArray(node["labels"]).map(lowerSnake);
|
|
50
|
+
if (systemLabels(node).some((label) => lowerSnake(label) === "exchange") || display.includes("exchange")) roles.push("exchange");
|
|
51
|
+
return unique(roles.map(lowerSnake).filter(Boolean));
|
|
52
|
+
}
|
|
53
|
+
function normalizeNodeType(node) {
|
|
54
|
+
if (typeof node["node_type"] === "string" && node["node_type"].trim()) return lowerSnake(node["node_type"]);
|
|
55
|
+
if (typeof node["address"] === "string" || typeof node["id"] === "string") return "address";
|
|
56
|
+
const labels = systemLabels(node);
|
|
57
|
+
if (labels.length > 0) return lowerSnake(labels[0]);
|
|
58
|
+
return "unknown";
|
|
59
|
+
}
|
|
60
|
+
function normalizeNode(node) {
|
|
61
|
+
if (!isRecord(node)) return {};
|
|
62
|
+
const normalized = {};
|
|
63
|
+
for (const [key, value] of Object.entries(node)) {
|
|
64
|
+
if ([
|
|
65
|
+
"address_type",
|
|
66
|
+
"address_subtypes",
|
|
67
|
+
"entity_kind",
|
|
68
|
+
"graph_labels",
|
|
69
|
+
"labels",
|
|
70
|
+
"node_type",
|
|
71
|
+
"pattern_flags",
|
|
72
|
+
"raw_labels",
|
|
73
|
+
"role",
|
|
74
|
+
"roles",
|
|
75
|
+
"system_labels",
|
|
76
|
+
"type"
|
|
77
|
+
].includes(key)) continue;
|
|
78
|
+
normalized[key] = value;
|
|
79
|
+
}
|
|
80
|
+
const id = typeof node["id"] === "string" ? node["id"] : typeof node["address"] === "string" ? node["address"] : void 0;
|
|
81
|
+
if (id) normalized["id"] = id;
|
|
82
|
+
const address = typeof node["address"] === "string" ? node["address"] : id;
|
|
83
|
+
if (address) normalized["address"] = address;
|
|
84
|
+
normalized["node_type"] = normalizeNodeType(node);
|
|
85
|
+
normalized["labels"] = displayLabels(node);
|
|
86
|
+
if (typeof node["address_type"] === "string" && SOURCE_ADDRESS_TYPES.has(node["address_type"])) normalized["address_type"] = node["address_type"];
|
|
87
|
+
const addressSubtypes = unique(stringArray(node["address_subtypes"]));
|
|
88
|
+
if (addressSubtypes.length > 0) normalized["address_subtypes"] = addressSubtypes;
|
|
89
|
+
const roles = normalizeRoles(node);
|
|
90
|
+
if (roles.length > 0) normalized["roles"] = roles;
|
|
91
|
+
if (!Array.isArray(node["flags"]) && Array.isArray(node["pattern_flags"]) && node["pattern_flags"].length > 0) normalized["flags"] = node["pattern_flags"].map(String);
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
function normalizeEdgeType(edge) {
|
|
95
|
+
const edgeType = lowerSnake(typeof edge["edge_type"] === "string" && edge["edge_type"].trim() ? edge["edge_type"] : typeof edge["type"] === "string" && edge["type"].trim() ? edge["type"] : typeof edge["relationship_type"] === "string" && edge["relationship_type"].trim() ? edge["relationship_type"] : "related_to");
|
|
96
|
+
return FLOW_EDGE_TYPES.has(edgeType) ? "flows_to" : edgeType;
|
|
97
|
+
}
|
|
98
|
+
function normalizeEdge(edge) {
|
|
99
|
+
if (!isRecord(edge)) return {};
|
|
100
|
+
const normalized = {};
|
|
101
|
+
for (const [key, value] of Object.entries(edge)) {
|
|
102
|
+
if ([
|
|
103
|
+
"edge_type",
|
|
104
|
+
"from_address",
|
|
105
|
+
"relationship_type",
|
|
106
|
+
"to_address",
|
|
107
|
+
"type"
|
|
108
|
+
].includes(key)) continue;
|
|
109
|
+
normalized[key] = value;
|
|
110
|
+
}
|
|
111
|
+
if (typeof normalized["source"] !== "string" && typeof edge["from_address"] === "string") normalized["source"] = edge["from_address"];
|
|
112
|
+
if (typeof normalized["target"] !== "string" && typeof edge["to_address"] === "string") normalized["target"] = edge["to_address"];
|
|
113
|
+
normalized["edge_type"] = normalizeEdgeType(edge);
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
function normalizeGraphPayload(payload) {
|
|
117
|
+
if (!isRecord(payload) || payload["schema"] !== "chain-insights.graph.v1") throw new Error("Unsupported graph payload schema");
|
|
118
|
+
return {
|
|
119
|
+
...payload,
|
|
120
|
+
schema: "chain-insights.graph.v1",
|
|
121
|
+
nodes: Array.isArray(payload["nodes"]) ? payload["nodes"].map(normalizeNode) : [],
|
|
122
|
+
edges: Array.isArray(payload["edges"]) ? payload["edges"].map(normalizeEdge) : [],
|
|
123
|
+
flows: Array.isArray(payload["flows"]) ? payload["flows"] : [],
|
|
124
|
+
edge_anchors: Array.isArray(payload["edge_anchors"]) ? payload["edge_anchors"] : []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
Object.defineProperty(exports, "normalizeGraphPayload", {
|
|
129
|
+
enumerable: true,
|
|
130
|
+
get: function() {
|
|
131
|
+
return normalizeGraphPayload;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { n as workspaceOutputPaths } from "./output-root-CmWM7aV2.mjs";
|
|
2
|
+
import { t as normalizeGraphPayload } from "./graph-normalizer-Cv9yK9Pg.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
//#region src/mcp/graph-reports.ts
|
|
8
|
+
const GraphReportInputSchema = z.object({
|
|
9
|
+
schema: z.literal("chain-insights.graph.v1"),
|
|
10
|
+
nodes: z.array(z.unknown()),
|
|
11
|
+
edges: z.array(z.unknown()),
|
|
12
|
+
flows: z.array(z.unknown()).optional(),
|
|
13
|
+
edge_anchors: z.array(z.unknown()).optional()
|
|
14
|
+
}).passthrough();
|
|
15
|
+
function graphPayloadSchema(graphData) {
|
|
16
|
+
return typeof graphData === "object" && graphData !== null && "schema" in graphData ? String(graphData.schema) : "unknown";
|
|
17
|
+
}
|
|
18
|
+
function sanitizeSlug(slug) {
|
|
19
|
+
return slug.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "") || "graph";
|
|
20
|
+
}
|
|
21
|
+
function timestampSegment(date = /* @__PURE__ */ new Date()) {
|
|
22
|
+
return date.toISOString().replace(/[-:.]/g, "");
|
|
23
|
+
}
|
|
24
|
+
function uniqueFilename(slug) {
|
|
25
|
+
const suffix = randomUUID().replace(/-/g, "").slice(0, 12);
|
|
26
|
+
return `${timestampSegment()}-${sanitizeSlug(slug)}-${suffix}.graph.json`;
|
|
27
|
+
}
|
|
28
|
+
async function ensurePrivateDirectory(dir) {
|
|
29
|
+
await mkdir(dir, {
|
|
30
|
+
recursive: true,
|
|
31
|
+
mode: 448
|
|
32
|
+
});
|
|
33
|
+
await chmod(dir, 448);
|
|
34
|
+
}
|
|
35
|
+
async function writeGraphReport(graphData, options) {
|
|
36
|
+
const parsed = GraphReportInputSchema.safeParse(graphData);
|
|
37
|
+
if (!parsed.success) {
|
|
38
|
+
const schema = graphPayloadSchema(graphData);
|
|
39
|
+
if (schema !== "chain-insights.graph.v1") throw new Error(`Unsupported graph payload schema: ${schema}`);
|
|
40
|
+
throw new Error("Invalid graph payload: nodes and edges must be arrays; flows and edge_anchors must be arrays when present");
|
|
41
|
+
}
|
|
42
|
+
const normalized = normalizeGraphPayload({
|
|
43
|
+
...parsed.data,
|
|
44
|
+
flows: parsed.data.flows ?? [],
|
|
45
|
+
edge_anchors: parsed.data.edge_anchors ?? []
|
|
46
|
+
});
|
|
47
|
+
const paths = workspaceOutputPaths();
|
|
48
|
+
const filename = uniqueFilename(options.slug);
|
|
49
|
+
const filePath = path.join(paths.reportGraphsRoot, filename);
|
|
50
|
+
await ensurePrivateDirectory(paths.reportsRoot);
|
|
51
|
+
await ensurePrivateDirectory(paths.reportGraphsRoot);
|
|
52
|
+
await writeFile(filePath, JSON.stringify(normalized, null, 2) + "\n", { mode: 384 });
|
|
53
|
+
return {
|
|
54
|
+
schema: normalized.schema,
|
|
55
|
+
filename,
|
|
56
|
+
path: filePath,
|
|
57
|
+
url: `http://127.0.0.1:${options.serverPort}/graph-reports/${filename}`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { writeGraphReport };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=graph-reports-C4TBjCkM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-reports-C4TBjCkM.mjs","names":[],"sources":["../src/mcp/graph-reports.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { chmod, mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport * as z from 'zod'\nimport { normalizeGraphPayload } from '../viz/graph-normalizer.js'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\n\nconst GraphReportInputSchema = z.object({\n schema: z.literal('chain-insights.graph.v1'),\n nodes: z.array(z.unknown()),\n edges: z.array(z.unknown()),\n flows: z.array(z.unknown()).optional(),\n edge_anchors: z.array(z.unknown()).optional(),\n}).passthrough()\n\nexport type GraphReportRef = {\n schema: 'chain-insights.graph.v1'\n filename: string\n url: string\n path: string\n}\n\nexport type WriteGraphReportOptions = {\n serverPort: number\n slug: string\n}\n\nfunction graphPayloadSchema(graphData: unknown): string {\n return typeof graphData === 'object' && graphData !== null && 'schema' in graphData\n ? String(graphData.schema)\n : 'unknown'\n}\n\nfunction sanitizeSlug(slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^[._-]+|[._-]+$/g, '')\n return sanitized || 'graph'\n}\n\nfunction timestampSegment(date = new Date()): string {\n return date.toISOString().replace(/[-:.]/g, '')\n}\n\nfunction uniqueFilename(slug: string): string {\n const suffix = randomUUID().replace(/-/g, '').slice(0, 12)\n return `${timestampSegment()}-${sanitizeSlug(slug)}-${suffix}.graph.json`\n}\n\nasync function ensurePrivateDirectory(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true, mode: 0o700 })\n await chmod(dir, 0o700)\n}\n\nexport async function writeGraphReport(\n graphData: unknown,\n options: WriteGraphReportOptions,\n): Promise<GraphReportRef> {\n const parsed = GraphReportInputSchema.safeParse(graphData)\n if (!parsed.success) {\n const schema = graphPayloadSchema(graphData)\n if (schema !== 'chain-insights.graph.v1') {\n throw new Error(`Unsupported graph payload schema: ${schema}`)\n }\n\n throw new Error('Invalid graph payload: nodes and edges must be arrays; flows and edge_anchors must be arrays when present')\n }\n\n const normalized = normalizeGraphPayload({\n ...parsed.data,\n flows: parsed.data.flows ?? [],\n edge_anchors: parsed.data.edge_anchors ?? [],\n })\n const paths = workspaceOutputPaths()\n const filename = uniqueFilename(options.slug)\n const filePath = path.join(paths.reportGraphsRoot, filename)\n\n await ensurePrivateDirectory(paths.reportsRoot)\n await ensurePrivateDirectory(paths.reportGraphsRoot)\n await writeFile(filePath, JSON.stringify(normalized, null, 2) + '\\n', { mode: 0o600 })\n\n return {\n schema: normalized.schema,\n filename,\n path: filePath,\n url: `http://127.0.0.1:${options.serverPort}/graph-reports/${filename}`,\n }\n}\n"],"mappings":";;;;;;;AAOA,MAAM,yBAAyB,EAAE,OAAO;CACtC,QAAQ,EAAE,QAAQ,0BAA0B;CAC5C,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC;CAC3B,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC;CAC3B,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,UAAU;CACtC,cAAc,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,UAAU;CAC9C,CAAC,CAAC,aAAa;AAchB,SAAS,mBAAmB,WAA4B;AACtD,QAAO,OAAO,cAAc,YAAY,cAAc,QAAQ,YAAY,YACtE,OAAO,UAAU,OAAO,GACxB;;AAGN,SAAS,aAAa,MAAsB;AAM1C,QALkB,KACf,aAAa,CACb,QAAQ,kBAAkB,IAAI,CAC9B,QAAQ,OAAO,IAAI,CACnB,QAAQ,oBAAoB,GACf,IAAI;;AAGtB,SAAS,iBAAiB,uBAAO,IAAI,MAAM,EAAU;AACnD,QAAO,KAAK,aAAa,CAAC,QAAQ,UAAU,GAAG;;AAGjD,SAAS,eAAe,MAAsB;CAC5C,MAAM,SAAS,YAAY,CAAC,QAAQ,MAAM,GAAG,CAAC,MAAM,GAAG,GAAG;AAC1D,QAAO,GAAG,kBAAkB,CAAC,GAAG,aAAa,KAAK,CAAC,GAAG,OAAO;;AAG/D,eAAe,uBAAuB,KAA4B;AAChE,OAAM,MAAM,KAAK;EAAE,WAAW;EAAM,MAAM;EAAO,CAAC;AAClD,OAAM,MAAM,KAAK,IAAM;;AAGzB,eAAsB,iBACpB,WACA,SACyB;CACzB,MAAM,SAAS,uBAAuB,UAAU,UAAU;AAC1D,KAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,mBAAmB,UAAU;AAC5C,MAAI,WAAW,0BACb,OAAM,IAAI,MAAM,qCAAqC,SAAS;AAGhE,QAAM,IAAI,MAAM,4GAA4G;;CAG9H,MAAM,aAAa,sBAAsB;EACvC,GAAG,OAAO;EACV,OAAO,OAAO,KAAK,SAAS,EAAE;EAC9B,cAAc,OAAO,KAAK,gBAAgB,EAAE;EAC7C,CAAC;CACF,MAAM,QAAQ,sBAAsB;CACpC,MAAM,WAAW,eAAe,QAAQ,KAAK;CAC7C,MAAM,WAAW,KAAK,KAAK,MAAM,kBAAkB,SAAS;AAE5D,OAAM,uBAAuB,MAAM,YAAY;AAC/C,OAAM,uBAAuB,MAAM,iBAAiB;AACpD,OAAM,UAAU,UAAU,KAAK,UAAU,YAAY,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,KAAO,CAAC;AAEtF,QAAO;EACL,QAAQ,WAAW;EACnB;EACA,MAAM;EACN,KAAK,oBAAoB,QAAQ,WAAW,iBAAiB;EAC9D"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
const require_output_root = require("./output-root-CFYms3ad.cjs");
|
|
3
|
+
const require_graph_normalizer = require("./graph-normalizer-DeIj6Ses.cjs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let zod = require("zod");
|
|
8
|
+
zod = require_chunk.__toESM(zod, 1);
|
|
9
|
+
let node_crypto = require("node:crypto");
|
|
10
|
+
//#region src/mcp/graph-reports.ts
|
|
11
|
+
const GraphReportInputSchema = zod.object({
|
|
12
|
+
schema: zod.literal("chain-insights.graph.v1"),
|
|
13
|
+
nodes: zod.array(zod.unknown()),
|
|
14
|
+
edges: zod.array(zod.unknown()),
|
|
15
|
+
flows: zod.array(zod.unknown()).optional(),
|
|
16
|
+
edge_anchors: zod.array(zod.unknown()).optional()
|
|
17
|
+
}).passthrough();
|
|
18
|
+
function graphPayloadSchema(graphData) {
|
|
19
|
+
return typeof graphData === "object" && graphData !== null && "schema" in graphData ? String(graphData.schema) : "unknown";
|
|
20
|
+
}
|
|
21
|
+
function sanitizeSlug(slug) {
|
|
22
|
+
return slug.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "") || "graph";
|
|
23
|
+
}
|
|
24
|
+
function timestampSegment(date = /* @__PURE__ */ new Date()) {
|
|
25
|
+
return date.toISOString().replace(/[-:.]/g, "");
|
|
26
|
+
}
|
|
27
|
+
function uniqueFilename(slug) {
|
|
28
|
+
const suffix = (0, node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12);
|
|
29
|
+
return `${timestampSegment()}-${sanitizeSlug(slug)}-${suffix}.graph.json`;
|
|
30
|
+
}
|
|
31
|
+
async function ensurePrivateDirectory(dir) {
|
|
32
|
+
await (0, node_fs_promises.mkdir)(dir, {
|
|
33
|
+
recursive: true,
|
|
34
|
+
mode: 448
|
|
35
|
+
});
|
|
36
|
+
await (0, node_fs_promises.chmod)(dir, 448);
|
|
37
|
+
}
|
|
38
|
+
async function writeGraphReport(graphData, options) {
|
|
39
|
+
const parsed = GraphReportInputSchema.safeParse(graphData);
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
const schema = graphPayloadSchema(graphData);
|
|
42
|
+
if (schema !== "chain-insights.graph.v1") throw new Error(`Unsupported graph payload schema: ${schema}`);
|
|
43
|
+
throw new Error("Invalid graph payload: nodes and edges must be arrays; flows and edge_anchors must be arrays when present");
|
|
44
|
+
}
|
|
45
|
+
const normalized = require_graph_normalizer.normalizeGraphPayload({
|
|
46
|
+
...parsed.data,
|
|
47
|
+
flows: parsed.data.flows ?? [],
|
|
48
|
+
edge_anchors: parsed.data.edge_anchors ?? []
|
|
49
|
+
});
|
|
50
|
+
const paths = require_output_root.workspaceOutputPaths();
|
|
51
|
+
const filename = uniqueFilename(options.slug);
|
|
52
|
+
const filePath = node_path.default.join(paths.reportGraphsRoot, filename);
|
|
53
|
+
await ensurePrivateDirectory(paths.reportsRoot);
|
|
54
|
+
await ensurePrivateDirectory(paths.reportGraphsRoot);
|
|
55
|
+
await (0, node_fs_promises.writeFile)(filePath, JSON.stringify(normalized, null, 2) + "\n", { mode: 384 });
|
|
56
|
+
return {
|
|
57
|
+
schema: normalized.schema,
|
|
58
|
+
filename,
|
|
59
|
+
path: filePath,
|
|
60
|
+
url: `http://127.0.0.1:${options.serverPort}/graph-reports/${filename}`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
exports.writeGraphReport = writeGraphReport;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
let node_url = require("node:url");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
5
|
+
let node_fs = require("node:fs");
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let node_os = require("node:os");
|
|
8
|
+
node_os = require_chunk.__toESM(node_os, 1);
|
|
9
|
+
//#region src/viz/html-generator.ts
|
|
10
|
+
var html_generator_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
|
11
|
+
generateHtml: () => generateHtml,
|
|
12
|
+
generateInlineGraphHtml: () => generateInlineGraphHtml,
|
|
13
|
+
transformToGraphHtml: () => transformToGraphHtml,
|
|
14
|
+
writeVizHtml: () => writeVizHtml
|
|
15
|
+
});
|
|
16
|
+
const __dirname$1 = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
17
|
+
const template = (0, node_fs.readFileSync)(node_path.default.resolve(__dirname$1, "templates", "graph.html"), "utf-8");
|
|
18
|
+
const ENTITY_TO_ROLE = {
|
|
19
|
+
eoa: "search",
|
|
20
|
+
contract: "intermediary",
|
|
21
|
+
exchange: "exchange",
|
|
22
|
+
mixer: "intermediary",
|
|
23
|
+
unknown: null
|
|
24
|
+
};
|
|
25
|
+
function transformToGraphHtml(data) {
|
|
26
|
+
return {
|
|
27
|
+
nodes: data.nodes.map((n) => ({
|
|
28
|
+
address: n.id,
|
|
29
|
+
address_type: n.entityType === "exchange" ? "exchange" : "wallet",
|
|
30
|
+
labels: n.label ? [n.label] : [],
|
|
31
|
+
flow_in_usd: n.totalIn,
|
|
32
|
+
flow_out_usd: n.totalOut,
|
|
33
|
+
role: ENTITY_TO_ROLE[n.entityType] ?? null,
|
|
34
|
+
risk_level: n.riskLevel === "unknown" ? null : n.riskLevel,
|
|
35
|
+
pattern_flags: []
|
|
36
|
+
})),
|
|
37
|
+
edges: data.edges.map((e) => ({
|
|
38
|
+
source: e.source,
|
|
39
|
+
target: e.target,
|
|
40
|
+
usd_amount: e.value,
|
|
41
|
+
tx_count: 1,
|
|
42
|
+
edge_type: "flows_to"
|
|
43
|
+
})),
|
|
44
|
+
metadata: { title: data.metadata.title }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function generateHtml(data, _title) {
|
|
48
|
+
return generateInlineGraphHtml(transformToGraphHtml(data));
|
|
49
|
+
}
|
|
50
|
+
function generateInlineGraphHtml(data) {
|
|
51
|
+
const inlineScript = `<script>var INLINE_DATA = ${JSON.stringify(data).replaceAll("<\/script>", "<\\/script>")};<\/script>`;
|
|
52
|
+
return template.replace("</body>", `${inlineScript}\n</body>`);
|
|
53
|
+
}
|
|
54
|
+
function sanitizePathSegment(segment) {
|
|
55
|
+
if (/[/\\]|^\.\.?$/.test(segment)) throw new Error(`Invalid path segment: ${segment}`);
|
|
56
|
+
return segment;
|
|
57
|
+
}
|
|
58
|
+
async function writeVizHtml(vizId, html, caseId) {
|
|
59
|
+
let vizDir;
|
|
60
|
+
if (caseId) vizDir = node_path.default.join(node_os.default.homedir(), ".chain-insights", "cases", sanitizePathSegment(caseId), "viz");
|
|
61
|
+
else vizDir = node_path.default.join(node_os.default.homedir(), ".chain-insights", "viz");
|
|
62
|
+
await (0, node_fs_promises.mkdir)(vizDir, { recursive: true });
|
|
63
|
+
const filePath = node_path.default.join(vizDir, `${vizId}.html`);
|
|
64
|
+
await (0, node_fs_promises.writeFile)(filePath, html, { mode: 384 });
|
|
65
|
+
return filePath;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
Object.defineProperty(exports, "generateHtml", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
get: function() {
|
|
71
|
+
return generateHtml;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(exports, "html_generator_exports", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
get: function() {
|
|
77
|
+
return html_generator_exports;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(exports, "writeVizHtml", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
get: function() {
|
|
83
|
+
return writeVizHtml;
|
|
84
|
+
}
|
|
85
|
+
});
|