chain-insights 0.2.32 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +40 -14
  2. package/dist/cases-Cp9DUbEV.mjs +6 -0
  3. package/dist/{cases-c0iV-XLI.cjs → cases-sTY5aXav.cjs} +3 -3
  4. package/dist/cli.cjs +122 -66
  5. package/dist/cli.mjs +122 -66
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/{viz-Da9YWN_I.cjs → data-extractor-Cavd7wHk.cjs} +11 -34
  8. package/dist/{viz-DkJyqlUu.mjs → data-extractor-DZUJu1Bz.mjs} +3 -32
  9. package/dist/data-extractor-DZUJu1Bz.mjs.map +1 -0
  10. package/dist/{dossier-Br62hCG7.cjs → dossier-BXy57V4-.cjs} +13 -1
  11. package/dist/{dossier-Bl0NkJKC.mjs → dossier-Bjpcbcxa.mjs} +4 -2
  12. package/dist/{dossier-Bl0NkJKC.mjs.map → dossier-Bjpcbcxa.mjs.map} +1 -1
  13. package/dist/export-BqTCO9lP.mjs +591 -0
  14. package/dist/export-BqTCO9lP.mjs.map +1 -0
  15. package/dist/export-DsXgtCwO.cjs +592 -0
  16. package/dist/index.cjs +1 -1
  17. package/dist/index.mjs +1 -1
  18. package/dist/{init-DBC9Ml33.mjs → init-DLBL_nVG.mjs} +27 -1
  19. package/dist/{init-DBC9Ml33.mjs.map → init-DLBL_nVG.mjs.map} +1 -1
  20. package/dist/{init-CFaUWgjK.cjs → init-zqbd7i-_.cjs} +26 -0
  21. package/dist/mcp-proxy.cjs +215 -77
  22. package/dist/mcp-proxy.d.cts.map +1 -1
  23. package/dist/mcp-proxy.d.mts.map +1 -1
  24. package/dist/mcp-proxy.mjs +215 -77
  25. package/dist/mcp-proxy.mjs.map +1 -1
  26. package/dist/{public-tools-BwguvIsf.cjs → public-tools-BvMb3H2P.cjs} +701 -1479
  27. package/dist/{public-tools-DoRNhMn9.mjs → public-tools-wJoAFDFa.mjs} +700 -1479
  28. package/dist/public-tools-wJoAFDFa.mjs.map +1 -0
  29. package/dist/{resolver-D7VBb0uB.mjs → resolver-2jXNtWQO.mjs} +12 -29
  30. package/dist/resolver-2jXNtWQO.mjs.map +1 -0
  31. package/dist/{resolver-BUU7ZgW-.cjs → resolver-CZdQwKvh.cjs} +11 -28
  32. package/dist/{runner-BCDeBYsR.cjs → runner-BhZ4lnF1.cjs} +2 -2
  33. package/dist/{runner-CTFK0Qcg.mjs → runner-DIJSbkjc.mjs} +3 -3
  34. package/dist/{runner-CTFK0Qcg.mjs.map → runner-DIJSbkjc.mjs.map} +1 -1
  35. package/dist/{selector-CTUiQrzI.mjs → selector-CF2o5gxN.mjs} +2 -2
  36. package/dist/{selector-CTUiQrzI.mjs.map → selector-CF2o5gxN.mjs.map} +1 -1
  37. package/dist/{selector-DBS2jYH4.cjs → selector-DfAMZEC9.cjs} +1 -1
  38. package/dist/{session-DwyikazY.cjs → session-BT7VpbAd.cjs} +13 -1
  39. package/dist/{session-Bha3zFrx.mjs → session-DROyhebe.mjs} +4 -2
  40. package/dist/{session-Bha3zFrx.mjs.map → session-DROyhebe.mjs.map} +1 -1
  41. package/dist/{store-BT2SCcQr.mjs → store-CTtqQtaE.mjs} +10 -4
  42. package/dist/{store-BT2SCcQr.mjs.map → store-CTtqQtaE.mjs.map} +1 -1
  43. package/dist/{store-DogLawSj.cjs → store-CqPfs47P.cjs} +37 -7
  44. package/dist/{tool-visibility-BHRFLXuU.mjs → tool-visibility-BpyZHRBi.mjs} +4 -2
  45. package/dist/tool-visibility-BpyZHRBi.mjs.map +1 -0
  46. package/dist/{tool-visibility-iAVQV3t0.cjs → tool-visibility-Buq7YdUZ.cjs} +3 -1
  47. package/dist/viz-5y24S5X1.mjs +35 -0
  48. package/dist/viz-5y24S5X1.mjs.map +1 -0
  49. package/dist/viz-Dqp3C5kb.cjs +44 -0
  50. package/docs/contributing.md +3 -2
  51. package/docs/graph-tools.md +125 -117
  52. package/docs/investigation-workspaces.md +14 -0
  53. package/docs/mcp-proxy.md +15 -2
  54. package/package.json +1 -1
  55. package/skills/chain-insights-cypher/SKILL.md +6 -0
  56. package/skills/chain-insights-developer-experience/SKILL.md +26 -6
  57. package/skills/chain-insights-investigation/SKILL.md +64 -48
  58. package/skills/chain-insights-trace-funds/SKILL.md +80 -197
  59. package/skills/test-chain-insights-graphrag-mcp/SKILL.md +1 -1
  60. package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +4 -4
  61. package/dist/cases-qjPtbnUd.mjs +0 -6
  62. package/dist/public-tools-DoRNhMn9.mjs.map +0 -1
  63. package/dist/resolver-D7VBb0uB.mjs.map +0 -1
  64. package/dist/tool-visibility-BHRFLXuU.mjs.map +0 -1
  65. package/dist/viz-DkJyqlUu.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { n as PACKAGE_VERSION } from "./version-BA3J8hu4.mjs";
2
2
  import { t as PaymentRequiredError } from "./client-D4JE7fFF.mjs";
3
- import { t as HIDDEN_REMOTE_TOOL_NAMES } from "./tool-visibility-BHRFLXuU.mjs";
3
+ import { t as HIDDEN_REMOTE_TOOL_NAMES } from "./tool-visibility-BpyZHRBi.mjs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import path from "node:path";
6
6
  import { readFileSync } from "node:fs";
@@ -20,17 +20,19 @@ const LOCAL_TOOL_NAMES = new Set([
20
20
  "case_resume",
21
21
  "case_add_evidence",
22
22
  "case_verify_evidence",
23
+ "case_export",
23
24
  "case_update_dossier",
24
25
  "case_start_session",
25
26
  "case_end_session"
26
27
  ]);
27
- const PUBLIC_GRAPHRAG_PROMPT_NAMES = new Set(["address-risk", "track-funds"]);
28
+ const PUBLIC_GRAPHRAG_PROMPT_NAMES = new Set(["address-risk", "trace-tools"]);
28
29
  const GRAPH_RESOURCE_URI = "ui://chain-insights/graph";
29
30
  const GRAPH_APP_TOOL_NAMES = new Set([
30
31
  "address_risk",
31
- "scam_topology",
32
32
  "stake_insights",
33
- "track_funds"
33
+ "trace_victim_funds",
34
+ "trace_suspect_funds",
35
+ "trace_deposit_sources"
34
36
  ]);
35
37
  const GRAPH_ARRAY_KEYS = [
36
38
  "nodes",
@@ -39,25 +41,28 @@ const GRAPH_ARRAY_KEYS = [
39
41
  "edge_anchors"
40
42
  ];
41
43
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
- const COMMA_SEPARATED_ADDRESS_FIELDS = new Set(["trusted_addresses", "untrusted_addresses"]);
44
+ const COMMA_SEPARATED_ADDRESS_FIELDS = new Set([
45
+ "victim_addresses",
46
+ "known_suspect_addresses",
47
+ "suspect_addresses",
48
+ "deposit_addresses"
49
+ ]);
43
50
  const KNOWN_PUBLIC_TOOL_REQUIRED_ARGS = {
44
51
  address_risk: ["address", "network"],
45
- scam_topology: [
46
- "victim_address",
47
- "incident_timestamp_ms",
48
- "network"
49
- ],
50
52
  stake_insights: ["network"],
51
- track_funds: ["trusted_addresses", "network"],
53
+ trace_victim_funds: ["victim_addresses", "network"],
54
+ trace_suspect_funds: ["suspect_addresses", "network"],
55
+ trace_deposit_sources: ["deposit_addresses", "network"],
52
56
  graph_query: ["query", "network"],
53
57
  graph_query_batch: ["network", "queries"]
54
58
  };
55
59
  const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
56
60
  network_capabilities: "Return supported Chain Insights networks, capability layers, tool availability, data retention windows, and freshness. Use this before choosing network-specific tools.",
57
61
  address_risk: "Screen one full blockchain address for AML risk, behavior patterns, neighborhood context, exchange exposure, and optional comparison with compare_address. This includes the exchange-behavior analysis formerly covered by money_flows_between_exchanges. Use this as the first tool for a single-address investigation. The tool returns an investigator-ready summary; preserve full addresses exactly.",
58
- scam_topology: "Build victim-incident laundering topology from one victim/source address and the earliest known incident timestamp. Traversal uses one explicit activity policy: node_relative_only by default, or global_incident_only when requested. Repeated targets are kept as non-expanding convergence edges. Returns ML-ready scam_labels plus review context and a track_funds-compatible graph report: primary flows, deposits, reverse_leads. Victims, exchange endpoints, and generic labeled context nodes are not automatic scam labels; preserve full addresses exactly.",
59
62
  stake_insights: "Explain Bittensor staking behavior around one full address, coldkey, or hotkey. Requires network plus exactly one of address, coldkey, or hotkey. Returns net staked/unstaked amounts, active coldkey-hotkey-netuid relationships, aggregate stake movement amounts, top counterparties, first/last activity, source backend, query evidence, and optional graph report metadata.",
60
- track_funds: "Trace funds from trusted victim/source addresses through intermediaries to exchange deposit addresses. Use this when the user has a victim/source address or known untrusted/scammer addresses. The tool returns an investigator-ready fund-flow report and recommended next actions.",
63
+ trace_victim_funds: "Trace victim/source funds forward through intermediaries to exchange deposit candidates. Use only when the input addresses are victims or trusted stolen-source addresses; do not use for suspected deposit addresses because traceback belongs to trace_deposit_sources. Exchange hot wallets are terminal only, never candidate deposits. Returns chain-insights.trace.v1 and preserves full addresses exactly.",
64
+ trace_suspect_funds: "Trace suspected scammer, mule, operator, or laundering-ring funds forward to cashout topology. Use when the input addresses are suspect-controlled seeds; incident_timestamp_ms is optional. Do not use for victim/source addresses or suspected deposit endpoints. Exchange hot wallets are terminal only, never candidate suspects or intermediates. Returns chain-insights.trace.v1 and preserves full addresses exactly.",
65
+ trace_deposit_sources: "Trace backward from suspected deposit/cashout addresses to upstream sources, shared funders, and convergence. Use only when the input addresses are suspected non-exchange deposit endpoints; do not treat these seeds as scammers and do not continue forward from discovered suspects here. Exchange hot wallets are excluded as seeds and upstream sources. Returns chain-insights.trace.v1 and preserves full addresses exactly.",
61
66
  graph_query: "Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. Use USE live_topology for recent topology, USE archive_topology for historical topology, and USE facts for labels, features, risk scores, assets, and enrichment. Cross-layer correlated joins may be limited by the active graph endpoint; preserve full addresses exactly.",
62
67
  graph_query_batch: "Run multiple read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch. Prefer this for related topology/facts reads."
63
68
  };
@@ -72,9 +77,10 @@ const CHAIN_INSIGHTS_WORKFLOW = [
72
77
  "Workflow:",
73
78
  "1. If the user is starting or continuing an investigation, use case_open or case_list/case_resume first.",
74
79
  "2. Do not call investigation tools until required arguments are known. Network is required; use network_capabilities to check supported networks, data layers, retention, and freshness, or ask the user if missing.",
75
- "3. Use address_risk first for a single address when facts and topology are available. Use stake_insights for Bittensor coldkey/hotkey staking behavior. Use track_funds for victim/source fund tracing when topology is available. Use scam_topology when known victim incident ground truth should become ML-ready scam labels. Use graph_query(_batch) for the universal graph-language path over topology and facts.",
80
+ "3. Use address_risk for single-address enrichment. Use trace_victim_funds for victim/source forward tracing, trace_deposit_sources for reverse traceback from suspected deposit endpoints, and trace_suspect_funds for suspect-controlled outbound laundering/cashout topology. Use stake_insights for Bittensor staking behavior. Use graph_query(_batch) only when the high-level trace tools do not answer the exact question.",
76
81
  "4. After a material result, preserve it with case_add_evidence when a case is active or ask whether to create/select a case.",
77
- "5. Use case_update_dossier for durable address/entity findings and case_start_session/case_end_session for session notes."
82
+ "5. Use case_update_dossier for durable address/entity findings and case_start_session/case_end_session for session notes.",
83
+ "6. When a case reaches a useful checkpoint, use case_verify_evidence and case_export to produce Obsidian, LLMWiki, Codex, Claude Code, and ChatGPT-ready files."
78
84
  ].join("\n");
79
85
  const GRAPH_SCHEMA_HINTS = [
80
86
  "Graph query hints for network=bittensor:",
@@ -85,6 +91,7 @@ const GRAPH_SCHEMA_HINTS = [
85
91
  "- Risk and ML properties may appear as live hints, but source-of-truth risk rows are RiskScore facts.",
86
92
  "- Common relationships include FLOWS_TO, OPERATED_FROM, SERVED_FROM, REGISTERED_NEURON, BELONGS_TO, SYBIL_CLUSTER, LAYERING_HOP, BURST_ACTIVITY, CYCLE_PARTICIPANT, SMURFING_CLUSTER.",
87
93
  "- FLOWS_TO properties are scoped to the selected topology graph and commonly carry amount_sum, amount_usd_sum, tx_count, first_seen_timestamp, last_seen_timestamp, first_tx_id, last_tx_id. Confirm available fields through runtime schema before relying on them.",
94
+ "- Traversal rule: for BFS, fixed-hop fallback, shortest-path, or manual FLOWS_TO traversal, exchange hot wallets are terminal endpoints only. Do not expand from, through, or classify exchange nodes as deposit, suspect, or intermediate candidates; filter every non-terminal node with is_exchange IS NULL.",
88
95
  "- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) WHERE n.address IS NOT NULL RETURN n.labels AS labels, n.address AS address LIMIT 20",
89
96
  "- Relationship discovery: MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.amount_sum AS amount_sum, r.amount_usd_sum AS amount_usd_sum LIMIT 20",
90
97
  "- graph_query uses the active Chain Insights graph endpoint. Use USE live_topology for recent topology, USE archive_topology for historical topology, and USE facts for labels, features, risk scores, assets, and enrichment.",
@@ -148,20 +155,27 @@ function knownPublicToolInputSchema(toolName) {
148
155
  compare_address: z.string().optional().describe("Optional second full address for comparison"),
149
156
  include_attachments: z.boolean().optional().describe("Include graph app report metadata")
150
157
  };
151
- case "track_funds": return {
152
- trusted_addresses: z.string().min(1).describe("Comma-separated full trusted victim addresses. Min 1, max 5."),
158
+ case "trace_victim_funds": return {
159
+ victim_addresses: z.string().min(1).describe("Comma-separated full victim/source addresses. Min 1, max 5."),
153
160
  network: z.string().min(1).describe(NETWORK_DESCRIPTION),
154
- untrusted_addresses: z.string().optional().describe("Comma-separated full untrusted/scammer addresses. Max 5."),
161
+ known_suspect_addresses: z.string().optional().describe("Optional known suspect addresses for context only. They are not reverse-traced by this tool. Max 5."),
162
+ incident_timestamp_ms: z.number().min(0).optional().describe("Optional incident timestamp in milliseconds."),
155
163
  include_attachments: z.boolean().optional().describe("Include graph app report metadata")
156
164
  };
157
- case "scam_topology": return {
165
+ case "trace_suspect_funds": return {
158
166
  network: z.string().min(1).describe(NETWORK_DESCRIPTION),
159
- victim_address: z.string().min(1).describe("Full victim/source address that anchors the scam incident. Victims are not risky labels."),
160
- incident_timestamp_ms: z.number().min(0).describe("Earliest known incident transfer timestamp in milliseconds. Primary traversal uses node-relative wave-arrival filtering."),
161
- max_hops: z.number().int().min(1).max(64).optional().describe("Maximum forward expansion depth. Default 16."),
162
- activity_policy: z.enum(["node_relative_only", "global_incident_only"]).optional().describe("Traversal activity policy. Default node_relative_only."),
167
+ suspect_addresses: z.string().min(1).describe("Comma-separated full suspected scammer, mule, operator, or laundering-ring addresses. Min 1, max 5."),
168
+ incident_timestamp_ms: z.number().min(0).optional().describe("Optional incident timestamp in milliseconds. This tool also works without a timestamp."),
169
+ max_hops: z.number().int().min(1).max(5).optional().describe("Maximum forward trace hops. Default 3."),
163
170
  case_id: z.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest.")
164
171
  };
172
+ case "trace_deposit_sources": return {
173
+ network: z.string().min(1).describe(NETWORK_DESCRIPTION),
174
+ deposit_addresses: z.string().min(1).describe("Comma-separated full suspected deposit/cashout addresses. Min 1, max 5."),
175
+ max_hops: z.number().int().min(1).max(5).optional().describe("Maximum reverse traceback hops. Default 2."),
176
+ case_id: z.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest."),
177
+ include_attachments: z.boolean().optional().describe("Include graph app report metadata")
178
+ };
165
179
  case "stake_insights": return {
166
180
  network: z.string().min(1).describe(NETWORK_DESCRIPTION),
167
181
  address: z.string().optional().describe("Full Bittensor address to inspect as either coldkey or hotkey. Provide exactly one of address, coldkey, or hotkey."),
@@ -417,24 +431,27 @@ function registerLocalPrompts(server, remotePromptNames) {
417
431
  "",
418
432
  "Present the summary as-is. Do not add analysis, verdicts, or risk assessments; the tool output already contains the risk assessment."
419
433
  ].join("\n"), "Address risk screening"));
420
- if (!remotePromptNames.has("track-funds")) server.registerPrompt("track-funds", {
421
- title: "Track Funds",
422
- description: "Trace stolen funds from victim addresses through intermediaries to exchange deposit addresses.",
434
+ if (!remotePromptNames.has("trace-tools")) server.registerPrompt("trace-tools", {
435
+ title: "Trace Tools",
436
+ description: "Choose trace_victim_funds, trace_deposit_sources, or trace_suspect_funds based on the evidence role.",
423
437
  argsSchema: {
424
- trusted_addresses: z.string().describe("Victim/trusted addresses, comma-separated full addresses"),
425
- untrusted_addresses: z.string().optional().describe("Known scammer/untrusted addresses, comma-separated full addresses"),
438
+ addresses: z.string().describe("Input addresses, comma-separated full addresses"),
439
+ role: z.enum([
440
+ "victim",
441
+ "suspect",
442
+ "deposit"
443
+ ]).describe("Role of the supplied addresses"),
426
444
  network: z.string().describe(NETWORK_DESCRIPTION)
427
445
  }
428
- }, async ({ trusted_addresses, untrusted_addresses, network }) => {
429
- const untrusted = untrusted_addresses?.trim() ? `\nKnown untrusted addresses:\n${untrusted_addresses}\n` : "";
446
+ }, async ({ addresses, role, network }) => {
430
447
  return promptResult([
431
- `Use Chain Insights track_funds on ${network}.`,
448
+ `Use Chain Insights ${role === "deposit" ? "trace_deposit_sources" : `trace_${role}_funds`} on ${network}.`,
449
+ "",
450
+ "Full addresses:",
451
+ addresses,
432
452
  "",
433
- "Trusted victim addresses:",
434
- trusted_addresses,
435
- untrusted,
436
- "Present the summary as-is and include recommended next actions exactly as returned."
437
- ].join("\n"), "Trace stolen funds");
453
+ role === "deposit" ? "For deposit role, use trace_deposit_sources rather than trace_deposit_funds." : "Present the summary as-is and use continuation.recommended_next_tools for follow-up."
454
+ ].join("\n"), "Trace role-specific funds");
438
455
  });
439
456
  server.registerPrompt("graph-query", {
440
457
  title: "Federated Graph Query",
@@ -754,13 +771,13 @@ async function createProxy() {
754
771
  }
755
772
  }, async ({ name, tags, description }) => {
756
773
  try {
757
- const { CaseStore } = await import("./cases-qjPtbnUd.mjs");
774
+ const { CaseStore } = await import("./cases-Cp9DUbEV.mjs");
758
775
  const created = await CaseStore.create({
759
776
  name,
760
777
  tags: parseTags(tags),
761
778
  description: description ?? ""
762
779
  });
763
- const { casesRoot } = await import("./store-BT2SCcQr.mjs");
780
+ const { casesRoot } = await import("./store-CTtqQtaE.mjs").then((n) => n.r);
764
781
  return {
765
782
  content: [{
766
783
  type: "text",
@@ -794,7 +811,7 @@ async function createProxy() {
794
811
  }
795
812
  }, async ({ status }) => {
796
813
  try {
797
- const { CaseStore } = await import("./cases-qjPtbnUd.mjs");
814
+ const { CaseStore } = await import("./cases-Cp9DUbEV.mjs");
798
815
  const cases = await CaseStore.list();
799
816
  const filtered = status ? cases.filter((entry) => entry.status === status) : cases;
800
817
  return {
@@ -819,7 +836,7 @@ async function createProxy() {
819
836
  }
820
837
  }, async ({ case_id }) => {
821
838
  try {
822
- const { CaseStore } = await import("./cases-qjPtbnUd.mjs");
839
+ const { CaseStore } = await import("./cases-Cp9DUbEV.mjs");
823
840
  const context = await CaseStore.loadContext(case_id);
824
841
  return {
825
842
  content: [{
@@ -833,7 +850,7 @@ async function createProxy() {
833
850
  }
834
851
  });
835
852
  server.registerTool("case_add_evidence", {
836
- description: "Append a tool result or analyst note to a local case evidence manifest. Use after address_risk, track_funds, graph_query, or manual findings that should be preserved.",
853
+ description: "Append a tool result or analyst note to a local case evidence manifest. Use after address_risk, trace_victim_funds, trace_suspect_funds, trace_deposit_sources, graph_query, or manual findings that should be preserved.",
837
854
  inputSchema: {
838
855
  case_id: z.string().min(1).describe("Chain Insights case ID"),
839
856
  source: z.string().min(1).describe("Source tool or evidence origin"),
@@ -848,7 +865,7 @@ async function createProxy() {
848
865
  }
849
866
  }, async ({ case_id, source, content, query_params }) => {
850
867
  try {
851
- const { EvidenceStore } = await import("./cases-qjPtbnUd.mjs");
868
+ const { EvidenceStore } = await import("./cases-Cp9DUbEV.mjs");
852
869
  const saved = await EvidenceStore.append(case_id, {
853
870
  source,
854
871
  content,
@@ -876,7 +893,7 @@ async function createProxy() {
876
893
  }
877
894
  }, async ({ case_id }) => {
878
895
  try {
879
- const { EvidenceStore } = await import("./cases-qjPtbnUd.mjs");
896
+ const { EvidenceStore } = await import("./cases-Cp9DUbEV.mjs");
880
897
  const result = await EvidenceStore.verifyManifest(case_id);
881
898
  return {
882
899
  content: [{
@@ -889,6 +906,51 @@ async function createProxy() {
889
906
  return caseToolError("Evidence verify", err);
890
907
  }
891
908
  });
909
+ server.registerTool("case_export", {
910
+ description: "Export a Chain Insights case to an Obsidian, LLMWiki, Codex, Claude Code, and ChatGPT-friendly local bundle.",
911
+ inputSchema: {
912
+ case_id: z.string().min(1).describe("Chain Insights case ID to export"),
913
+ target: z.enum(["obsidian-llmwiki"]).optional().describe("Export target. Default obsidian-llmwiki."),
914
+ mode: z.enum([
915
+ "private",
916
+ "partner",
917
+ "public"
918
+ ]).optional().describe("Redaction mode. Default private."),
919
+ output_dir: z.string().optional().describe("Optional output directory. Defaults to published/<case-slug>.")
920
+ },
921
+ annotations: {
922
+ readOnlyHint: false,
923
+ destructiveHint: false,
924
+ idempotentHint: false,
925
+ openWorldHint: false
926
+ }
927
+ }, async ({ case_id, target, mode, output_dir }) => {
928
+ try {
929
+ const { exportCase } = await import("./export-BqTCO9lP.mjs");
930
+ const result = await exportCase({
931
+ caseId: case_id,
932
+ target: target ?? "obsidian-llmwiki",
933
+ mode: mode ?? "private",
934
+ outputDir: output_dir
935
+ });
936
+ return {
937
+ content: [{
938
+ type: "text",
939
+ text: [
940
+ `Case exported: ${result.outputDir}`,
941
+ `Manifest: ${result.manifestPath}`,
942
+ `Files: ${result.fileCount}`,
943
+ `Open first: ${result.nextFile}`,
944
+ ...result.warnings.map((warning) => `Warning: ${warning}`)
945
+ ].join("\n")
946
+ }],
947
+ structuredContent: result,
948
+ isError: false
949
+ };
950
+ } catch (err) {
951
+ return caseToolError("Case export", err);
952
+ }
953
+ });
892
954
  server.registerTool("case_update_dossier", {
893
955
  description: "Append a finding to an address/entity dossier inside a local Chain Insights case.",
894
956
  inputSchema: {
@@ -911,7 +973,7 @@ async function createProxy() {
911
973
  }
912
974
  }, async ({ case_id, address, finding, entity_type }) => {
913
975
  try {
914
- const { DossierStore } = await import("./cases-qjPtbnUd.mjs");
976
+ const { DossierStore } = await import("./cases-Cp9DUbEV.mjs");
915
977
  await DossierStore.appendFinding(case_id, address, finding, entity_type ?? "unknown");
916
978
  return {
917
979
  content: [{
@@ -939,7 +1001,7 @@ async function createProxy() {
939
1001
  }
940
1002
  }, async ({ case_id }) => {
941
1003
  try {
942
- const { SessionStore } = await import("./cases-qjPtbnUd.mjs");
1004
+ const { SessionStore } = await import("./cases-Cp9DUbEV.mjs");
943
1005
  const session = await SessionStore.start(case_id);
944
1006
  return {
945
1007
  content: [{
@@ -967,7 +1029,7 @@ async function createProxy() {
967
1029
  }
968
1030
  }, async ({ case_id, findings, next_steps }) => {
969
1031
  try {
970
- const { SessionStore } = await import("./cases-qjPtbnUd.mjs");
1032
+ const { SessionStore } = await import("./cases-Cp9DUbEV.mjs");
971
1033
  await SessionStore.end(case_id, {
972
1034
  findings: findings ?? "",
973
1035
  nextSteps: next_steps ?? ""
@@ -1012,7 +1074,7 @@ async function createProxy() {
1012
1074
  }],
1013
1075
  isError: true
1014
1076
  };
1015
- const { addressRisk } = await import("./public-tools-DoRNhMn9.mjs");
1077
+ const { addressRisk } = await import("./public-tools-wJoAFDFa.mjs");
1016
1078
  const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
1017
1079
  const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
1018
1080
  const result = await addressRisk(remoteClient, {
@@ -1054,15 +1116,16 @@ async function createProxy() {
1054
1116
  };
1055
1117
  }
1056
1118
  });
1057
- if (!remoteToolNames.has("track_funds")) registerAppTool(server, "track_funds", {
1058
- title: "Track Funds",
1059
- description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.track_funds,
1119
+ if (!remoteToolNames.has("trace_victim_funds")) registerAppTool(server, "trace_victim_funds", {
1120
+ title: "Trace Victim Funds",
1121
+ description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.trace_victim_funds,
1060
1122
  inputSchema: {
1061
- trusted_addresses: z.union([z.string().min(1), z.array(z.string().min(1))]).describe("Comma-separated full trusted victim addresses, or an array. Min 1, max 5."),
1123
+ victim_addresses: z.union([z.string().min(1), z.array(z.string().min(1))]).describe("Comma-separated full victim/source addresses, or an array. Min 1, max 5."),
1062
1124
  network: z.string().min(1).describe(NETWORK_DESCRIPTION),
1063
- untrusted_addresses: z.union([z.string(), z.array(z.string())]).optional().describe("Known scammer/untrusted addresses. Max 5."),
1125
+ known_suspect_addresses: z.union([z.string(), z.array(z.string())]).optional().describe("Known suspect addresses for context only. This tool does not reverse-trace them. Max 5."),
1064
1126
  include_attachments: z.boolean().optional().describe("Include graph app report metadata"),
1065
1127
  case_id: z.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest."),
1128
+ incident_timestamp_ms: z.number().min(0).optional().describe("Optional incident timestamp in milliseconds."),
1066
1129
  max_hops: z.number().int().min(1).max(5).optional(),
1067
1130
  per_address_limit: z.number().int().min(1).max(10).optional(),
1068
1131
  min_amount_sum: z.number().min(0).optional()
@@ -1074,7 +1137,7 @@ async function createProxy() {
1074
1137
  idempotentHint: false,
1075
1138
  openWorldHint: true
1076
1139
  }
1077
- }, async ({ trusted_addresses, untrusted_addresses, network, case_id, max_hops, per_address_limit, min_amount_sum }) => {
1140
+ }, async ({ victim_addresses, known_suspect_addresses, network, case_id, incident_timestamp_ms, max_hops, per_address_limit, min_amount_sum }) => {
1078
1141
  try {
1079
1142
  if (!remoteConnected) return {
1080
1143
  content: [{
@@ -1083,21 +1146,22 @@ async function createProxy() {
1083
1146
  }],
1084
1147
  isError: true
1085
1148
  };
1086
- const { trackFunds } = await import("./public-tools-DoRNhMn9.mjs");
1149
+ const { traceVictimFunds } = await import("./public-tools-wJoAFDFa.mjs");
1087
1150
  const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
1088
1151
  const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
1089
- const result = await trackFunds(remoteClient, config, {
1090
- trustedAddresses: trusted_addresses,
1091
- untrustedAddresses: untrusted_addresses,
1152
+ const result = await traceVictimFunds(remoteClient, config, {
1153
+ victimAddresses: victim_addresses,
1154
+ knownSuspectAddresses: known_suspect_addresses,
1092
1155
  network,
1093
1156
  caseId: case_id,
1157
+ incidentTimestampMs: incident_timestamp_ms,
1094
1158
  maxHops: max_hops,
1095
1159
  perAddressLimit: per_address_limit,
1096
1160
  minAmountSum: min_amount_sum
1097
1161
  });
1098
1162
  const report = await writeGraphReport(result.graphData, {
1099
1163
  serverPort: config.serverPort,
1100
- slug: `track-funds-${network}`
1164
+ slug: `trace-victim-funds-${network}`
1101
1165
  });
1102
1166
  await ensureArtifactServer(config.serverPort);
1103
1167
  return {
@@ -1123,22 +1187,24 @@ async function createProxy() {
1123
1187
  return {
1124
1188
  content: [{
1125
1189
  type: "text",
1126
- text: `Track funds failed: ${err.message}`
1190
+ text: `Trace victim funds failed: ${err.message}`
1127
1191
  }],
1128
1192
  isError: true
1129
1193
  };
1130
1194
  }
1131
1195
  });
1132
- if (!remoteToolNames.has("scam_topology")) registerAppTool(server, "scam_topology", {
1133
- title: "Scam Topology",
1134
- description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.scam_topology,
1196
+ if (!remoteToolNames.has("trace_suspect_funds")) registerAppTool(server, "trace_suspect_funds", {
1197
+ title: "Trace Suspect Funds",
1198
+ description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.trace_suspect_funds,
1135
1199
  inputSchema: {
1136
1200
  network: z.string().min(1).describe(NETWORK_DESCRIPTION),
1137
- victim_address: z.string().min(1).describe("Full victim/source address that anchors the scam incident. Victims are not risky labels."),
1138
- incident_timestamp_ms: z.number().min(0).describe("Earliest known incident transfer timestamp in milliseconds. Primary traversal uses node-relative wave-arrival filtering."),
1139
- max_hops: z.number().int().min(1).max(64).optional().describe("Maximum forward expansion depth. Default 16."),
1140
- activity_policy: z.enum(["node_relative_only", "global_incident_only"]).optional().describe("Traversal activity policy. Default node_relative_only."),
1141
- case_id: z.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest.")
1201
+ suspect_addresses: z.union([z.string().min(1), z.array(z.string().min(1))]).describe("Comma-separated full suspect-controlled addresses, or an array. Min 1, max 5."),
1202
+ incident_timestamp_ms: z.number().min(0).optional().describe("Optional incident timestamp in milliseconds. This tool works without it."),
1203
+ max_hops: z.number().int().min(1).max(5).optional().describe("Maximum forward trace hops. Default 3."),
1204
+ per_address_limit: z.number().int().min(1).max(10).optional(),
1205
+ min_amount_sum: z.number().min(0).optional(),
1206
+ case_id: z.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest."),
1207
+ include_attachments: z.boolean().optional().describe("Include graph app report metadata")
1142
1208
  },
1143
1209
  _meta: { ui: { resourceUri: GRAPH_RESOURCE_URI } },
1144
1210
  annotations: {
@@ -1147,7 +1213,7 @@ async function createProxy() {
1147
1213
  idempotentHint: false,
1148
1214
  openWorldHint: true
1149
1215
  }
1150
- }, async ({ victim_address, incident_timestamp_ms, network, max_hops, activity_policy, case_id }) => {
1216
+ }, async ({ suspect_addresses, incident_timestamp_ms, network, max_hops, per_address_limit, min_amount_sum, case_id }) => {
1151
1217
  try {
1152
1218
  if (!remoteConnected) return {
1153
1219
  content: [{
@@ -1156,20 +1222,90 @@ async function createProxy() {
1156
1222
  }],
1157
1223
  isError: true
1158
1224
  };
1159
- const { scamTopology } = await import("./public-tools-DoRNhMn9.mjs");
1225
+ const { traceSuspectFunds } = await import("./public-tools-wJoAFDFa.mjs");
1160
1226
  const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
1161
1227
  const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
1162
- const result = await scamTopology(remoteClient, config, {
1163
- victimAddress: victim_address,
1228
+ const result = await traceSuspectFunds(remoteClient, config, {
1229
+ suspectAddresses: suspect_addresses,
1164
1230
  network,
1165
1231
  maxHops: max_hops,
1232
+ perAddressLimit: per_address_limit,
1233
+ minAmountSum: min_amount_sum,
1166
1234
  incidentTimestampMs: incident_timestamp_ms,
1167
- activityPolicyMode: activity_policy,
1168
1235
  caseId: case_id
1169
1236
  });
1170
1237
  const report = await writeGraphReport(result.graphData, {
1171
1238
  serverPort: config.serverPort,
1172
- slug: `scam-topology-${network}`
1239
+ slug: `trace-suspect-funds-${network}`
1240
+ });
1241
+ await ensureArtifactServer(config.serverPort);
1242
+ return {
1243
+ content: [{
1244
+ type: "text",
1245
+ text: result.summaryText
1246
+ }],
1247
+ structuredContent: result.structuredContent,
1248
+ _meta: { chainInsights: { graph: {
1249
+ schema: report.schema,
1250
+ url: report.url
1251
+ } } },
1252
+ isError: false
1253
+ };
1254
+ } catch (err) {
1255
+ if (err instanceof PaymentRequiredError) return {
1256
+ content: [{
1257
+ type: "text",
1258
+ text: err.message
1259
+ }],
1260
+ isError: true
1261
+ };
1262
+ return {
1263
+ content: [{
1264
+ type: "text",
1265
+ text: `Trace suspect funds failed: ${err.message}`
1266
+ }],
1267
+ isError: true
1268
+ };
1269
+ }
1270
+ });
1271
+ if (!remoteToolNames.has("trace_deposit_sources")) registerAppTool(server, "trace_deposit_sources", {
1272
+ title: "Trace Deposit Sources",
1273
+ description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.trace_deposit_sources,
1274
+ inputSchema: {
1275
+ network: z.string().min(1).describe(NETWORK_DESCRIPTION),
1276
+ deposit_addresses: z.union([z.string().min(1), z.array(z.string().min(1))]).describe("Comma-separated full suspected deposit/cashout addresses, or an array. Min 1, max 5."),
1277
+ max_hops: z.number().int().min(1).max(5).optional().describe("Maximum reverse traceback hops. Default 2."),
1278
+ case_id: z.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest."),
1279
+ include_attachments: z.boolean().optional().describe("Include graph app report metadata")
1280
+ },
1281
+ _meta: { ui: { resourceUri: GRAPH_RESOURCE_URI } },
1282
+ annotations: {
1283
+ readOnlyHint: false,
1284
+ destructiveHint: false,
1285
+ idempotentHint: false,
1286
+ openWorldHint: true
1287
+ }
1288
+ }, async ({ deposit_addresses, network, max_hops, case_id }) => {
1289
+ try {
1290
+ if (!remoteConnected) return {
1291
+ content: [{
1292
+ type: "text",
1293
+ text: `${remoteUnavailableMessage ?? `Graph MCP is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
1294
+ }],
1295
+ isError: true
1296
+ };
1297
+ const { traceDepositSources } = await import("./public-tools-wJoAFDFa.mjs");
1298
+ const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
1299
+ const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
1300
+ const result = await traceDepositSources(remoteClient, config, {
1301
+ depositAddresses: deposit_addresses,
1302
+ network,
1303
+ maxHops: max_hops,
1304
+ caseId: case_id
1305
+ });
1306
+ const report = await writeGraphReport(result.graphData, {
1307
+ serverPort: config.serverPort,
1308
+ slug: `trace-deposit-sources-${network}`
1173
1309
  });
1174
1310
  await ensureArtifactServer(config.serverPort);
1175
1311
  return {
@@ -1195,7 +1331,7 @@ async function createProxy() {
1195
1331
  return {
1196
1332
  content: [{
1197
1333
  type: "text",
1198
- text: `Scam topology failed: ${err.message}`
1334
+ text: `Trace deposit sources failed: ${err.message}`
1199
1335
  }],
1200
1336
  isError: true
1201
1337
  };
@@ -1233,7 +1369,7 @@ async function createProxy() {
1233
1369
  }],
1234
1370
  isError: true
1235
1371
  };
1236
- const { stakeInsights } = await import("./public-tools-DoRNhMn9.mjs");
1372
+ const { stakeInsights } = await import("./public-tools-wJoAFDFa.mjs");
1237
1373
  const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
1238
1374
  const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
1239
1375
  const result = await stakeInsights(remoteClient, {
@@ -1298,8 +1434,9 @@ async function createProxy() {
1298
1434
  "- network_capabilities: inspect supported networks, data layers, tool availability, retention windows, and freshness.",
1299
1435
  "- address_risk: screen a full address for AML risk, behavior, neighborhood, exchange exposure, and optional compare_address connection checks.",
1300
1436
  "- stake_insights: explain Bittensor staking around one address, coldkey, or hotkey with net stake, movement amounts, counterparties, backend, and query evidence.",
1301
- "- track_funds: trace up to five trusted/victim addresses plus up to five known untrusted/scammer addresses through intermediaries to exchange deposit addresses.",
1302
- "- scam_topology: derive ML-ready scam_labels from one victim incident address and incident_timestamp_ms.",
1437
+ "- trace_victim_funds: trace up to five victim/source addresses forward to exchange deposit candidates.",
1438
+ "- trace_deposit_sources: trace backward from suspected deposit/cashout addresses to upstream funders and shared-source convergence.",
1439
+ "- trace_suspect_funds: trace up to five suspected scammer, mule, operator, or laundering-ring addresses forward to cashout topology.",
1303
1440
  "- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE live_topology, USE archive_topology, or USE facts.",
1304
1441
  "- graph_query_batch: run related read-only graph-language queries through one paid graph call.",
1305
1442
  "",
@@ -1309,6 +1446,7 @@ async function createProxy() {
1309
1446
  "- case_resume: load case context, evidence count, dossiers, and latest session.",
1310
1447
  "- case_add_evidence: append a report or note to the case evidence manifest.",
1311
1448
  "- case_verify_evidence: verify saved evidence integrity.",
1449
+ "- case_export: export a case for Obsidian, LLMWiki, Codex, Claude Code, and ChatGPT.",
1312
1450
  "- case_update_dossier: add a finding to an address/entity dossier.",
1313
1451
  "- case_start_session and case_end_session: record session notes.",
1314
1452
  "",