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,201 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const BUILTIN_PLAYBOOKS = {
|
|
5
|
+
"trace-funds": `---
|
|
6
|
+
name: trace-funds
|
|
7
|
+
description: Trace stolen funds from a victim address to exchange deposits using Chain Insights GraphRAG
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
params:
|
|
10
|
+
- name: address
|
|
11
|
+
type: string
|
|
12
|
+
required: true
|
|
13
|
+
- name: network
|
|
14
|
+
type: string
|
|
15
|
+
required: false
|
|
16
|
+
default: bittensor
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Step 1: Screen Victim Address
|
|
20
|
+
|
|
21
|
+
\`\`\`tool
|
|
22
|
+
address_risk
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
\`\`\`params
|
|
26
|
+
address: {{address}}
|
|
27
|
+
network: {{network}}
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
## Step 2: Trace Funds To Exchanges
|
|
31
|
+
|
|
32
|
+
\`\`\`tool
|
|
33
|
+
track_funds
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
\`\`\`params
|
|
37
|
+
trusted_addresses: {{address}}
|
|
38
|
+
network: {{network}}
|
|
39
|
+
\`\`\`
|
|
40
|
+
`,
|
|
41
|
+
"scam-topology": `---
|
|
42
|
+
name: scam-topology
|
|
43
|
+
description: Build laundering topology and scam labels from a victim incident
|
|
44
|
+
version: 1.0.0
|
|
45
|
+
params:
|
|
46
|
+
- name: address
|
|
47
|
+
type: string
|
|
48
|
+
required: true
|
|
49
|
+
- name: incident_timestamp_ms
|
|
50
|
+
type: string
|
|
51
|
+
required: true
|
|
52
|
+
- name: network
|
|
53
|
+
type: string
|
|
54
|
+
required: false
|
|
55
|
+
default: bittensor
|
|
56
|
+
- name: activity_policy
|
|
57
|
+
type: string
|
|
58
|
+
required: false
|
|
59
|
+
default: node_relative_only
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Step 1: Screen Known Scam Address
|
|
63
|
+
|
|
64
|
+
\`\`\`tool
|
|
65
|
+
address_risk
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
\`\`\`params
|
|
69
|
+
address: {{address}}
|
|
70
|
+
network: {{network}}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
## Step 2: Build Scam Topology
|
|
74
|
+
|
|
75
|
+
The victim-only traversal is outward from victim/source funds. The
|
|
76
|
+
primary traversal is a node-relative novelty wave: each new node expands only
|
|
77
|
+
once, repeated targets remain as non-expanding convergence context, and
|
|
78
|
+
downstream edges must be active at or after the current node's wave-arrival
|
|
79
|
+
timestamp. Set activity_policy to global_incident_only to filter every wave
|
|
80
|
+
against incident_timestamp_ms instead. Exchange terminal safety stops exchange
|
|
81
|
+
endpoints, and label candidates are reviewable, not automatic writes.
|
|
82
|
+
Contract summary: victim-only traversal is outward from victim/source funds;
|
|
83
|
+
node-relative novelty wave by default; global_incident_only is available;
|
|
84
|
+
exchange terminal safety; scam_labels are ML-ready flags.
|
|
85
|
+
|
|
86
|
+
\`\`\`tool
|
|
87
|
+
scam_topology
|
|
88
|
+
\`\`\`
|
|
89
|
+
|
|
90
|
+
\`\`\`params
|
|
91
|
+
victim_address: {{address}}
|
|
92
|
+
incident_timestamp_ms: {{incident_timestamp_ms}}
|
|
93
|
+
network: {{network}}
|
|
94
|
+
activity_policy: {{activity_policy}}
|
|
95
|
+
\`\`\`
|
|
96
|
+
`,
|
|
97
|
+
"risk-check": `---
|
|
98
|
+
name: risk-check
|
|
99
|
+
description: Screen an address for Chain Insights risk, behavior, counterparties, exchange connections, and AML patterns
|
|
100
|
+
version: 1.0.0
|
|
101
|
+
params:
|
|
102
|
+
- name: address
|
|
103
|
+
type: string
|
|
104
|
+
required: true
|
|
105
|
+
- name: network
|
|
106
|
+
type: string
|
|
107
|
+
required: false
|
|
108
|
+
default: bittensor
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Step 1: Address Risk
|
|
112
|
+
|
|
113
|
+
\`\`\`tool
|
|
114
|
+
address_risk
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
\`\`\`params
|
|
118
|
+
address: {{address}}
|
|
119
|
+
network: {{network}}
|
|
120
|
+
\`\`\`
|
|
121
|
+
`,
|
|
122
|
+
"entity-profile": `---
|
|
123
|
+
name: entity-profile
|
|
124
|
+
description: Build an entity profile from Chain Insights address identity, metrics, risk, counterparties, and labels
|
|
125
|
+
version: 1.0.0
|
|
126
|
+
params:
|
|
127
|
+
- name: address
|
|
128
|
+
type: string
|
|
129
|
+
required: true
|
|
130
|
+
- name: network
|
|
131
|
+
type: string
|
|
132
|
+
required: false
|
|
133
|
+
default: bittensor
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Step 1: Entity Profile
|
|
137
|
+
|
|
138
|
+
\`\`\`tool
|
|
139
|
+
address_risk
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
\`\`\`params
|
|
143
|
+
address: {{address}}
|
|
144
|
+
network: {{network}}
|
|
145
|
+
\`\`\`
|
|
146
|
+
`
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/playbooks/resolver.ts
|
|
150
|
+
function userDir() {
|
|
151
|
+
return path.join(os.homedir(), ".chain-insights", "playbooks");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolve a playbook name to its markdown content.
|
|
155
|
+
* Checks user directory (~/.chain-insights/playbooks/<name>.md) first,
|
|
156
|
+
* then falls back to the built-in BUILTIN_PLAYBOOKS map.
|
|
157
|
+
* Security: sanitizes name to prevent path traversal (T-05-01).
|
|
158
|
+
*/
|
|
159
|
+
async function resolvePlaybookContent(name) {
|
|
160
|
+
const safeName = name.replace(/[^a-z0-9_-]/gi, "");
|
|
161
|
+
if (!safeName) throw new Error(`Invalid playbook name: ${name}`);
|
|
162
|
+
const userPath = path.join(userDir(), `${safeName}.md`);
|
|
163
|
+
try {
|
|
164
|
+
return await readFile(userPath, "utf8");
|
|
165
|
+
} catch {}
|
|
166
|
+
const builtin = BUILTIN_PLAYBOOKS[safeName];
|
|
167
|
+
if (builtin !== void 0) return builtin;
|
|
168
|
+
throw new Error(`Playbook not found: "${safeName}". Run \`chain-insights playbook list\` to see available playbooks.`);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* List all available playbooks — user dir first (overrides), then built-ins.
|
|
172
|
+
* Returns array of { name, source } objects.
|
|
173
|
+
*/
|
|
174
|
+
async function listPlaybooks() {
|
|
175
|
+
const result = [];
|
|
176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
177
|
+
try {
|
|
178
|
+
const userFiles = await readdir(userDir());
|
|
179
|
+
for (const file of userFiles) {
|
|
180
|
+
if (!file.endsWith(".md")) continue;
|
|
181
|
+
const name = file.slice(0, -3);
|
|
182
|
+
seen.add(name);
|
|
183
|
+
result.push({
|
|
184
|
+
name,
|
|
185
|
+
source: "user"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
for (const name of Object.keys(BUILTIN_PLAYBOOKS)) {
|
|
190
|
+
if (seen.has(name)) continue;
|
|
191
|
+
result.push({
|
|
192
|
+
name,
|
|
193
|
+
source: "builtin"
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { listPlaybooks, resolvePlaybookContent };
|
|
200
|
+
|
|
201
|
+
//# sourceMappingURL=resolver-C2ZS7oC8.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver-C2ZS7oC8.mjs","names":[],"sources":["../src/playbooks/builtins.ts","../src/playbooks/resolver.ts"],"sourcesContent":["// Built-in playbook definitions tied to the current GraphRAG public MCP tools.\n// Source of truth inspected in rbmk/repos/ml/graphrag/src/mcp_server/tools.\n\nexport const KNOWN_GRAPHRAG_PUBLIC_TOOLS = [\n 'address_risk',\n 'scam_topology',\n 'track_funds',\n 'graph_query',\n 'graph_query_batch',\n] as const\n\nexport const TRACE_FUNDS_PLAYBOOK = `---\nname: trace-funds\ndescription: Trace stolen funds from a victim address to exchange deposits using Chain Insights GraphRAG\nversion: 1.0.0\nparams:\n - name: address\n type: string\n required: true\n - name: network\n type: string\n required: false\n default: bittensor\n---\n\n## Step 1: Screen Victim Address\n\n\\`\\`\\`tool\naddress_risk\n\\`\\`\\`\n\n\\`\\`\\`params\naddress: {{address}}\nnetwork: {{network}}\n\\`\\`\\`\n\n## Step 2: Trace Funds To Exchanges\n\n\\`\\`\\`tool\ntrack_funds\n\\`\\`\\`\n\n\\`\\`\\`params\ntrusted_addresses: {{address}}\nnetwork: {{network}}\n\\`\\`\\`\n`\n\nexport const RISK_CHECK_PLAYBOOK = `---\nname: risk-check\ndescription: Screen an address for Chain Insights risk, behavior, counterparties, exchange connections, and AML patterns\nversion: 1.0.0\nparams:\n - name: address\n type: string\n required: true\n - name: network\n type: string\n required: false\n default: bittensor\n---\n\n## Step 1: Address Risk\n\n\\`\\`\\`tool\naddress_risk\n\\`\\`\\`\n\n\\`\\`\\`params\naddress: {{address}}\nnetwork: {{network}}\n\\`\\`\\`\n`\n\nexport const ENTITY_PROFILE_PLAYBOOK = `---\nname: entity-profile\ndescription: Build an entity profile from Chain Insights address identity, metrics, risk, counterparties, and labels\nversion: 1.0.0\nparams:\n - name: address\n type: string\n required: true\n - name: network\n type: string\n required: false\n default: bittensor\n---\n\n## Step 1: Entity Profile\n\n\\`\\`\\`tool\naddress_risk\n\\`\\`\\`\n\n\\`\\`\\`params\naddress: {{address}}\nnetwork: {{network}}\n\\`\\`\\`\n`\n\nexport const SCAM_TOPOLOGY_PLAYBOOK = `---\nname: scam-topology\ndescription: Build laundering topology and scam labels from a victim incident\nversion: 1.0.0\nparams:\n - name: address\n type: string\n required: true\n - name: incident_timestamp_ms\n type: string\n required: true\n - name: network\n type: string\n required: false\n default: bittensor\n - name: activity_policy\n type: string\n required: false\n default: node_relative_only\n---\n\n## Step 1: Screen Known Scam Address\n\n\\`\\`\\`tool\naddress_risk\n\\`\\`\\`\n\n\\`\\`\\`params\naddress: {{address}}\nnetwork: {{network}}\n\\`\\`\\`\n\n## Step 2: Build Scam Topology\n\nThe victim-only traversal is outward from victim/source funds. The\nprimary traversal is a node-relative novelty wave: each new node expands only\nonce, repeated targets remain as non-expanding convergence context, and\ndownstream edges must be active at or after the current node's wave-arrival\ntimestamp. Set activity_policy to global_incident_only to filter every wave\nagainst incident_timestamp_ms instead. Exchange terminal safety stops exchange\nendpoints, and label candidates are reviewable, not automatic writes.\nContract summary: victim-only traversal is outward from victim/source funds;\nnode-relative novelty wave by default; global_incident_only is available;\nexchange terminal safety; scam_labels are ML-ready flags.\n\n\\`\\`\\`tool\nscam_topology\n\\`\\`\\`\n\n\\`\\`\\`params\nvictim_address: {{address}}\nincident_timestamp_ms: {{incident_timestamp_ms}}\nnetwork: {{network}}\nactivity_policy: {{activity_policy}}\n\\`\\`\\`\n`\n\nexport const BUILTIN_PLAYBOOKS: Record<string, string> = {\n 'trace-funds': TRACE_FUNDS_PLAYBOOK,\n 'scam-topology': SCAM_TOPOLOGY_PLAYBOOK,\n 'risk-check': RISK_CHECK_PLAYBOOK,\n 'entity-profile': ENTITY_PROFILE_PLAYBOOK,\n}\n","import path from 'node:path'\nimport { access, readFile, readdir } from 'node:fs/promises'\nimport os from 'node:os'\nimport { BUILTIN_PLAYBOOKS } from './builtins.js'\n\nfunction userDir(): string {\n return path.join(os.homedir(), '.chain-insights', 'playbooks')\n}\n\n/**\n * Resolve a playbook name to its absolute file path.\n * Checks user directory (~/.chain-insights/playbooks/) first, then built-in directory.\n * Security: sanitizes name to prevent path traversal (T-05-01).\n *\n * @deprecated Prefer resolvePlaybookContent() which returns markdown directly.\n */\nexport async function resolvePlaybook(name: string): Promise<string> {\n // Security: sanitize name to prevent path traversal (per security threat T-05-01)\n const safeName = name.replace(/[^a-z0-9_-]/gi, '')\n if (!safeName) throw new Error(`Invalid playbook name: ${name}`)\n\n const userPath = path.join(userDir(), `${safeName}.md`)\n\n try {\n await access(userPath)\n return userPath\n } catch {\n // Fall through to built-in check\n }\n\n // Check built-in playbooks map (T-05-01: no filesystem path exposure for built-ins)\n if (safeName in BUILTIN_PLAYBOOKS) {\n // Return a sentinel path for legacy callers — content comes from BUILTIN_PLAYBOOKS\n return `builtin:${safeName}`\n }\n\n throw new Error(\n `Playbook not found: \"${safeName}\". Run \\`chain-insights playbook list\\` to see available playbooks.`\n )\n}\n\n/**\n * Resolve a playbook name to its markdown content.\n * Checks user directory (~/.chain-insights/playbooks/<name>.md) first,\n * then falls back to the built-in BUILTIN_PLAYBOOKS map.\n * Security: sanitizes name to prevent path traversal (T-05-01).\n */\nexport async function resolvePlaybookContent(name: string): Promise<string> {\n // Security: sanitize name to prevent path traversal (per security threat T-05-01)\n const safeName = name.replace(/[^a-z0-9_-]/gi, '')\n if (!safeName) throw new Error(`Invalid playbook name: ${name}`)\n\n // 1. Check user directory first (user playbooks override built-ins)\n const userPath = path.join(userDir(), `${safeName}.md`)\n try {\n return await readFile(userPath, 'utf8')\n } catch {\n // Not in user dir — fall through\n }\n\n // 2. Fall back to built-in map\n const builtin = BUILTIN_PLAYBOOKS[safeName]\n if (builtin !== undefined) return builtin\n\n throw new Error(\n `Playbook not found: \"${safeName}\". Run \\`chain-insights playbook list\\` to see available playbooks.`\n )\n}\n\n/**\n * List all available playbooks — user dir first (overrides), then built-ins.\n * Returns array of { name, source } objects.\n */\nexport async function listPlaybooks(): Promise<Array<{ name: string; source: 'builtin' | 'user' }>> {\n const result: Array<{ name: string; source: 'builtin' | 'user' }> = []\n const seen = new Set<string>()\n\n // User playbooks first\n try {\n const userFiles = await readdir(userDir())\n for (const file of userFiles) {\n if (!file.endsWith('.md')) continue\n const name = file.slice(0, -3) // remove .md\n seen.add(name)\n result.push({ name, source: 'user' })\n }\n } catch {\n // User dir doesn't exist — that's fine\n }\n\n // Built-in playbooks from the embedded map (not filesystem)\n for (const name of Object.keys(BUILTIN_PLAYBOOKS)) {\n if (seen.has(name)) continue // user override takes precedence\n result.push({ name, source: 'builtin' })\n }\n\n return result\n}\n"],"mappings":";;;AA6JA,MAAa,oBAA4C;CACvD,eAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAClB,iBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAClB,cAAkB;;;;;;;;;;;;;;;;;;;;;;;;;CAClB,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;CACnB;;;AC7JD,SAAS,UAAkB;AACzB,QAAO,KAAK,KAAK,GAAG,SAAS,EAAE,mBAAmB,YAAY;;;;;;;;AAyChE,eAAsB,uBAAuB,MAA+B;CAE1E,MAAM,WAAW,KAAK,QAAQ,iBAAiB,GAAG;AAClD,KAAI,CAAC,SAAU,OAAM,IAAI,MAAM,0BAA0B,OAAO;CAGhE,MAAM,WAAW,KAAK,KAAK,SAAS,EAAE,GAAG,SAAS,KAAK;AACvD,KAAI;AACF,SAAO,MAAM,SAAS,UAAU,OAAO;SACjC;CAKR,MAAM,UAAU,kBAAkB;AAClC,KAAI,YAAY,KAAA,EAAW,QAAO;AAElC,OAAM,IAAI,MACR,wBAAwB,SAAS,qEAClC;;;;;;AAOH,eAAsB,gBAA8E;CAClG,MAAM,SAA8D,EAAE;CACtE,MAAM,uBAAO,IAAI,KAAa;AAG9B,KAAI;EACF,MAAM,YAAY,MAAM,QAAQ,SAAS,CAAC;AAC1C,OAAK,MAAM,QAAQ,WAAW;AAC5B,OAAI,CAAC,KAAK,SAAS,MAAM,CAAE;GAC3B,MAAM,OAAO,KAAK,MAAM,GAAG,GAAG;AAC9B,QAAK,IAAI,KAAK;AACd,UAAO,KAAK;IAAE;IAAM,QAAQ;IAAQ,CAAC;;SAEjC;AAKR,MAAK,MAAM,QAAQ,OAAO,KAAK,kBAAkB,EAAE;AACjD,MAAI,KAAK,IAAI,KAAK,CAAE;AACpB,SAAO,KAAK;GAAE;GAAM,QAAQ;GAAW,CAAC;;AAG1C,QAAO"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
let node_path = require("node:path");
|
|
3
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
4
|
+
let node_fs_promises = require("node:fs/promises");
|
|
5
|
+
let node_os = require("node:os");
|
|
6
|
+
node_os = require_chunk.__toESM(node_os, 1);
|
|
7
|
+
const BUILTIN_PLAYBOOKS = {
|
|
8
|
+
"trace-funds": `---
|
|
9
|
+
name: trace-funds
|
|
10
|
+
description: Trace stolen funds from a victim address to exchange deposits using Chain Insights GraphRAG
|
|
11
|
+
version: 1.0.0
|
|
12
|
+
params:
|
|
13
|
+
- name: address
|
|
14
|
+
type: string
|
|
15
|
+
required: true
|
|
16
|
+
- name: network
|
|
17
|
+
type: string
|
|
18
|
+
required: false
|
|
19
|
+
default: bittensor
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Step 1: Screen Victim Address
|
|
23
|
+
|
|
24
|
+
\`\`\`tool
|
|
25
|
+
address_risk
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
\`\`\`params
|
|
29
|
+
address: {{address}}
|
|
30
|
+
network: {{network}}
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
## Step 2: Trace Funds To Exchanges
|
|
34
|
+
|
|
35
|
+
\`\`\`tool
|
|
36
|
+
track_funds
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
\`\`\`params
|
|
40
|
+
trusted_addresses: {{address}}
|
|
41
|
+
network: {{network}}
|
|
42
|
+
\`\`\`
|
|
43
|
+
`,
|
|
44
|
+
"scam-topology": `---
|
|
45
|
+
name: scam-topology
|
|
46
|
+
description: Build laundering topology and scam labels from a victim incident
|
|
47
|
+
version: 1.0.0
|
|
48
|
+
params:
|
|
49
|
+
- name: address
|
|
50
|
+
type: string
|
|
51
|
+
required: true
|
|
52
|
+
- name: incident_timestamp_ms
|
|
53
|
+
type: string
|
|
54
|
+
required: true
|
|
55
|
+
- name: network
|
|
56
|
+
type: string
|
|
57
|
+
required: false
|
|
58
|
+
default: bittensor
|
|
59
|
+
- name: activity_policy
|
|
60
|
+
type: string
|
|
61
|
+
required: false
|
|
62
|
+
default: node_relative_only
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Step 1: Screen Known Scam Address
|
|
66
|
+
|
|
67
|
+
\`\`\`tool
|
|
68
|
+
address_risk
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
\`\`\`params
|
|
72
|
+
address: {{address}}
|
|
73
|
+
network: {{network}}
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
## Step 2: Build Scam Topology
|
|
77
|
+
|
|
78
|
+
The victim-only traversal is outward from victim/source funds. The
|
|
79
|
+
primary traversal is a node-relative novelty wave: each new node expands only
|
|
80
|
+
once, repeated targets remain as non-expanding convergence context, and
|
|
81
|
+
downstream edges must be active at or after the current node's wave-arrival
|
|
82
|
+
timestamp. Set activity_policy to global_incident_only to filter every wave
|
|
83
|
+
against incident_timestamp_ms instead. Exchange terminal safety stops exchange
|
|
84
|
+
endpoints, and label candidates are reviewable, not automatic writes.
|
|
85
|
+
Contract summary: victim-only traversal is outward from victim/source funds;
|
|
86
|
+
node-relative novelty wave by default; global_incident_only is available;
|
|
87
|
+
exchange terminal safety; scam_labels are ML-ready flags.
|
|
88
|
+
|
|
89
|
+
\`\`\`tool
|
|
90
|
+
scam_topology
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
\`\`\`params
|
|
94
|
+
victim_address: {{address}}
|
|
95
|
+
incident_timestamp_ms: {{incident_timestamp_ms}}
|
|
96
|
+
network: {{network}}
|
|
97
|
+
activity_policy: {{activity_policy}}
|
|
98
|
+
\`\`\`
|
|
99
|
+
`,
|
|
100
|
+
"risk-check": `---
|
|
101
|
+
name: risk-check
|
|
102
|
+
description: Screen an address for Chain Insights risk, behavior, counterparties, exchange connections, and AML patterns
|
|
103
|
+
version: 1.0.0
|
|
104
|
+
params:
|
|
105
|
+
- name: address
|
|
106
|
+
type: string
|
|
107
|
+
required: true
|
|
108
|
+
- name: network
|
|
109
|
+
type: string
|
|
110
|
+
required: false
|
|
111
|
+
default: bittensor
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Step 1: Address Risk
|
|
115
|
+
|
|
116
|
+
\`\`\`tool
|
|
117
|
+
address_risk
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
\`\`\`params
|
|
121
|
+
address: {{address}}
|
|
122
|
+
network: {{network}}
|
|
123
|
+
\`\`\`
|
|
124
|
+
`,
|
|
125
|
+
"entity-profile": `---
|
|
126
|
+
name: entity-profile
|
|
127
|
+
description: Build an entity profile from Chain Insights address identity, metrics, risk, counterparties, and labels
|
|
128
|
+
version: 1.0.0
|
|
129
|
+
params:
|
|
130
|
+
- name: address
|
|
131
|
+
type: string
|
|
132
|
+
required: true
|
|
133
|
+
- name: network
|
|
134
|
+
type: string
|
|
135
|
+
required: false
|
|
136
|
+
default: bittensor
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Step 1: Entity Profile
|
|
140
|
+
|
|
141
|
+
\`\`\`tool
|
|
142
|
+
address_risk
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
\`\`\`params
|
|
146
|
+
address: {{address}}
|
|
147
|
+
network: {{network}}
|
|
148
|
+
\`\`\`
|
|
149
|
+
`
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/playbooks/resolver.ts
|
|
153
|
+
function userDir() {
|
|
154
|
+
return node_path.default.join(node_os.default.homedir(), ".chain-insights", "playbooks");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve a playbook name to its markdown content.
|
|
158
|
+
* Checks user directory (~/.chain-insights/playbooks/<name>.md) first,
|
|
159
|
+
* then falls back to the built-in BUILTIN_PLAYBOOKS map.
|
|
160
|
+
* Security: sanitizes name to prevent path traversal (T-05-01).
|
|
161
|
+
*/
|
|
162
|
+
async function resolvePlaybookContent(name) {
|
|
163
|
+
const safeName = name.replace(/[^a-z0-9_-]/gi, "");
|
|
164
|
+
if (!safeName) throw new Error(`Invalid playbook name: ${name}`);
|
|
165
|
+
const userPath = node_path.default.join(userDir(), `${safeName}.md`);
|
|
166
|
+
try {
|
|
167
|
+
return await (0, node_fs_promises.readFile)(userPath, "utf8");
|
|
168
|
+
} catch {}
|
|
169
|
+
const builtin = BUILTIN_PLAYBOOKS[safeName];
|
|
170
|
+
if (builtin !== void 0) return builtin;
|
|
171
|
+
throw new Error(`Playbook not found: "${safeName}". Run \`chain-insights playbook list\` to see available playbooks.`);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* List all available playbooks — user dir first (overrides), then built-ins.
|
|
175
|
+
* Returns array of { name, source } objects.
|
|
176
|
+
*/
|
|
177
|
+
async function listPlaybooks() {
|
|
178
|
+
const result = [];
|
|
179
|
+
const seen = /* @__PURE__ */ new Set();
|
|
180
|
+
try {
|
|
181
|
+
const userFiles = await (0, node_fs_promises.readdir)(userDir());
|
|
182
|
+
for (const file of userFiles) {
|
|
183
|
+
if (!file.endsWith(".md")) continue;
|
|
184
|
+
const name = file.slice(0, -3);
|
|
185
|
+
seen.add(name);
|
|
186
|
+
result.push({
|
|
187
|
+
name,
|
|
188
|
+
source: "user"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
for (const name of Object.keys(BUILTIN_PLAYBOOKS)) {
|
|
193
|
+
if (seen.has(name)) continue;
|
|
194
|
+
result.push({
|
|
195
|
+
name,
|
|
196
|
+
source: "builtin"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
exports.listPlaybooks = listPlaybooks;
|
|
203
|
+
exports.resolvePlaybookContent = resolvePlaybookContent;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
const require_version = require("./version-BNGtdpmH.cjs");
|
|
3
|
+
const require_config = require("./config-Bmdl5hdk.cjs");
|
|
4
|
+
const require_client = require("./client-D4fZgIaO.cjs");
|
|
5
|
+
const require_viz = require("./viz-ClezVXrJ.cjs");
|
|
6
|
+
const require_store = require("./store-BiUhQOIf.cjs");
|
|
7
|
+
const require_evidence = require("./evidence-BGcdKxuV.cjs");
|
|
8
|
+
let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
9
|
+
let _modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
10
|
+
//#region src/playbooks/runner.ts
|
|
11
|
+
/** Sleep for ms milliseconds. */
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
/** Check if an error is a timeout/abort error. */
|
|
16
|
+
function isTimeoutError(err) {
|
|
17
|
+
if (!(err instanceof Error)) return false;
|
|
18
|
+
return err.name === "AbortError" || err.code === "ECONNRESET";
|
|
19
|
+
}
|
|
20
|
+
/** Check if an error is a payment failure. */
|
|
21
|
+
function isPaymentError(err) {
|
|
22
|
+
if (!(err instanceof Error)) return false;
|
|
23
|
+
const msg = err.message.toLowerCase();
|
|
24
|
+
return msg.includes("http 402") || msg.includes("status 402") || msg.includes("payment required") || msg.includes("x402");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Call an MCP tool with retry logic on timeout (up to 3 total attempts).
|
|
28
|
+
* Returns the text result or throws on non-retryable error.
|
|
29
|
+
*/
|
|
30
|
+
async function callWithRetry(client, toolName, params) {
|
|
31
|
+
const MAX_ATTEMPTS = 3;
|
|
32
|
+
let lastErr;
|
|
33
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) try {
|
|
34
|
+
return (await client.callTool({
|
|
35
|
+
name: toolName,
|
|
36
|
+
arguments: params
|
|
37
|
+
})).content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (isTimeoutError(err) && attempt < MAX_ATTEMPTS) {
|
|
40
|
+
lastErr = err;
|
|
41
|
+
await sleep(1e3);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
throw lastErr;
|
|
47
|
+
}
|
|
48
|
+
async function validateStepTools(client, steps) {
|
|
49
|
+
const result = await client.listTools();
|
|
50
|
+
const available = new Set(result.tools.map((tool) => tool.name));
|
|
51
|
+
const missing = [...new Set(steps.map((step) => step.tool).filter((tool) => !available.has(tool)))];
|
|
52
|
+
if (missing.length === 0) return;
|
|
53
|
+
const availableList = [...available].sort().join(", ") || "none";
|
|
54
|
+
throw new Error(`Unknown MCP tool(s) in playbook: ${missing.join(", ")}. Available tools: ${availableList}. Run \`chain-insights mcp tools --refresh\` to inspect the live MCP schema.`);
|
|
55
|
+
}
|
|
56
|
+
const PlaybookRunner = {
|
|
57
|
+
/**
|
|
58
|
+
* Execute a playbook definition step-by-step against the live MCP.
|
|
59
|
+
*
|
|
60
|
+
* @param playbook - Parsed and validated PlaybookDefinition
|
|
61
|
+
* @param opts - Runner options (caseId, from, dryRun, params)
|
|
62
|
+
*/
|
|
63
|
+
async run(playbook, opts) {
|
|
64
|
+
const startIndex = (opts.from ?? 1) - 1;
|
|
65
|
+
const stepsToRun = playbook.steps.slice(startIndex);
|
|
66
|
+
const totalSteps = playbook.steps.length;
|
|
67
|
+
if (opts.dryRun) {
|
|
68
|
+
console.log(`Playbook: ${playbook.name} (dry run — no MCP calls)`);
|
|
69
|
+
console.log(`Steps: ${totalSteps} total, starting from ${startIndex + 1}`);
|
|
70
|
+
console.log("");
|
|
71
|
+
for (const step of stepsToRun) console.log(`Step ${step.index}/${totalSteps}: ${step.tool} (params: ${JSON.stringify(step.params)})`);
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log("Cost: unknown (MCP pricing not available without live connection)");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const config = await require_config.loadConfig();
|
|
77
|
+
const mcpFetch = await require_client.createConfiguredMcpFetch(config);
|
|
78
|
+
let caseId;
|
|
79
|
+
if (opts.caseId) caseId = (await require_store.CaseStore.get(opts.caseId)).id;
|
|
80
|
+
else {
|
|
81
|
+
caseId = (await require_store.CaseStore.create({
|
|
82
|
+
name: `quick-${playbook.name}-${Date.now()}`,
|
|
83
|
+
tags: [
|
|
84
|
+
"quick",
|
|
85
|
+
"playbook",
|
|
86
|
+
playbook.name
|
|
87
|
+
],
|
|
88
|
+
description: `Auto-created for one-off playbook run: ${playbook.name}`
|
|
89
|
+
})).id;
|
|
90
|
+
console.log(`Created quick case: ${caseId}`);
|
|
91
|
+
}
|
|
92
|
+
const client = new _modelcontextprotocol_sdk_client_index_js.Client({
|
|
93
|
+
name: "chain-insights-playbook",
|
|
94
|
+
version: require_version.PACKAGE_VERSION
|
|
95
|
+
});
|
|
96
|
+
await client.connect(new _modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(config.mcpEndpoint), { fetch: mcpFetch }));
|
|
97
|
+
let evidenceCount = 0;
|
|
98
|
+
try {
|
|
99
|
+
await validateStepTools(client, stepsToRun);
|
|
100
|
+
for (const step of stepsToRun) {
|
|
101
|
+
console.log(`Step ${step.index}/${totalSteps}: ${step.label}...`);
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await callWithRetry(client, step.tool, step.params);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (isPaymentError(err)) if (process.stdin.isTTY) {
|
|
107
|
+
const { createInterface } = await import("node:readline");
|
|
108
|
+
const rl = createInterface({
|
|
109
|
+
input: process.stdin,
|
|
110
|
+
output: process.stdout
|
|
111
|
+
});
|
|
112
|
+
const answer = await new Promise((resolve) => {
|
|
113
|
+
rl.question(`Payment required for step ${step.index}. (retry/skip/abort): `, resolve);
|
|
114
|
+
});
|
|
115
|
+
rl.close();
|
|
116
|
+
if (answer.trim().toLowerCase() === "retry") result = await callWithRetry(client, step.tool, step.params);
|
|
117
|
+
else if (answer.trim().toLowerCase() === "skip") {
|
|
118
|
+
console.log(`Step ${step.index} skipped.`);
|
|
119
|
+
continue;
|
|
120
|
+
} else throw new Error(`Aborted at step ${step.index} due to payment failure.`);
|
|
121
|
+
} else throw new Error(`Payment required for step ${step.index} but no interactive terminal available. Configure wallet with \`chain-insights config set walletPrivateKey <key>\`. Aborting.`);
|
|
122
|
+
else {
|
|
123
|
+
const completedMsg = step.index - 1 - startIndex > 0 ? `Completed: steps ${startIndex + 1}..${step.index - 1}.` : "No steps completed before failure.";
|
|
124
|
+
console.error(`Step ${step.index} failed: ${err.message}. ${completedMsg} Run with --from ${step.index} to resume.`);
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await require_evidence.EvidenceStore.append(caseId, {
|
|
129
|
+
source: step.tool,
|
|
130
|
+
content: result,
|
|
131
|
+
queryParams: JSON.stringify(step.params)
|
|
132
|
+
});
|
|
133
|
+
evidenceCount++;
|
|
134
|
+
console.log(` (${result.length} chars stored)`);
|
|
135
|
+
}
|
|
136
|
+
if (playbook.name === "trace-funds") try {
|
|
137
|
+
const viz = await require_viz.generateVisualization({ caseId });
|
|
138
|
+
console.log(`Visualization generated: ${viz.htmlPath}`);
|
|
139
|
+
} catch {
|
|
140
|
+
console.log("No transaction data to visualize.");
|
|
141
|
+
}
|
|
142
|
+
console.log(`Playbook complete. Case: ${caseId}. Evidence: ${evidenceCount} entries.`);
|
|
143
|
+
} finally {
|
|
144
|
+
await client.close();
|
|
145
|
+
}
|
|
146
|
+
} };
|
|
147
|
+
//#endregion
|
|
148
|
+
exports.PlaybookRunner = PlaybookRunner;
|