chain-insights 0.2.31 → 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 (67) 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 +129 -118
  52. package/docs/investigation-workspaces.md +14 -0
  53. package/docs/mcp-proxy.md +20 -2
  54. package/package.json +1 -1
  55. package/skills/chain-insights-bittensor-cypher/SKILL.md +24 -0
  56. package/skills/chain-insights-cypher/SKILL.md +16 -1
  57. package/skills/chain-insights-cypher/references/memgraph-examples.md +193 -0
  58. package/skills/chain-insights-developer-experience/SKILL.md +26 -6
  59. package/skills/chain-insights-investigation/SKILL.md +64 -48
  60. package/skills/chain-insights-trace-funds/SKILL.md +80 -197
  61. package/skills/test-chain-insights-graphrag-mcp/SKILL.md +1 -1
  62. package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +4 -4
  63. package/dist/cases-qjPtbnUd.mjs +0 -6
  64. package/dist/public-tools-DoRNhMn9.mjs.map +0 -1
  65. package/dist/resolver-D7VBb0uB.mjs.map +0 -1
  66. package/dist/tool-visibility-BHRFLXuU.mjs.map +0 -1
  67. package/dist/viz-DkJyqlUu.mjs.map +0 -1
@@ -33,36 +33,29 @@ network: {{network}}
33
33
  ## Step 2: Trace Funds To Exchanges
34
34
 
35
35
  \`\`\`tool
36
- track_funds
36
+ trace_victim_funds
37
37
  \`\`\`
38
38
 
39
39
  \`\`\`params
40
- trusted_addresses: {{address}}
40
+ victim_addresses: {{address}}
41
41
  network: {{network}}
42
42
  \`\`\`
43
43
  `,
44
- "scam-topology": `---
45
- name: scam-topology
46
- description: Build laundering topology and scam labels from a victim incident
44
+ "trace-suspect-funds": `---
45
+ name: trace-suspect-funds
46
+ description: Trace suspect-controlled funds from a suspected scammer, mule, operator, or laundering-ring address
47
47
  version: 1.0.0
48
48
  params:
49
49
  - name: address
50
50
  type: string
51
51
  required: true
52
- - name: incident_timestamp_ms
53
- type: string
54
- required: true
55
52
  - name: network
56
53
  type: string
57
54
  required: false
58
55
  default: bittensor
59
- - name: activity_policy
60
- type: string
61
- required: false
62
- default: node_relative_only
63
56
  ---
64
57
 
65
- ## Step 1: Screen Known Scam Address
58
+ ## Step 1: Screen Suspect Address
66
59
 
67
60
  \`\`\`tool
68
61
  address_risk
@@ -73,28 +66,18 @@ address: {{address}}
73
66
  network: {{network}}
74
67
  \`\`\`
75
68
 
76
- ## Step 2: Build Scam Topology
69
+ ## Step 2: Trace Suspect Funds
77
70
 
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.
71
+ Use this when the provided address is suspect-controlled. It traces forward to
72
+ cashout topology and returns reviewable candidate labels, not automatic writes.
88
73
 
89
74
  \`\`\`tool
90
- scam_topology
75
+ trace_suspect_funds
91
76
  \`\`\`
92
77
 
93
78
  \`\`\`params
94
- victim_address: {{address}}
95
- incident_timestamp_ms: {{incident_timestamp_ms}}
79
+ suspect_addresses: {{address}}
96
80
  network: {{network}}
97
- activity_policy: {{activity_policy}}
98
81
  \`\`\`
99
82
  `,
100
83
  "risk-check": `---
@@ -1,8 +1,8 @@
1
1
  const require_version = require("./version-CO9Or_YV.cjs");
2
2
  const require_config = require("./config-BwVx19Og.cjs");
3
3
  const require_client = require("./client-Db6IV1tv.cjs");
4
- const require_viz = require("./viz-Da9YWN_I.cjs");
5
- const require_store = require("./store-DogLawSj.cjs");
4
+ const require_viz = require("./viz-Dqp3C5kb.cjs");
5
+ const require_store = require("./store-CqPfs47P.cjs");
6
6
  const require_evidence = require("./evidence-CvEesemA.cjs");
7
7
  let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
8
8
  let _modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
@@ -1,8 +1,8 @@
1
1
  import { n as PACKAGE_VERSION } from "./version-BA3J8hu4.mjs";
2
2
  import { n as loadConfig } from "./config-Drgc2HuF.mjs";
3
3
  import { r as createConfiguredMcpFetch } from "./client-D4JE7fFF.mjs";
4
- import { t as generateVisualization } from "./viz-DkJyqlUu.mjs";
5
- import { CaseStore } from "./store-BT2SCcQr.mjs";
4
+ import { t as generateVisualization } from "./viz-5y24S5X1.mjs";
5
+ import { t as CaseStore } from "./store-CTtqQtaE.mjs";
6
6
  import { t as EvidenceStore } from "./evidence-D96PTzOQ.mjs";
7
7
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
8
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -146,4 +146,4 @@ async run(playbook, opts) {
146
146
  //#endregion
147
147
  export { PlaybookRunner };
148
148
 
149
- //# sourceMappingURL=runner-CTFK0Qcg.mjs.map
149
+ //# sourceMappingURL=runner-DIJSbkjc.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner-CTFK0Qcg.mjs","names":[],"sources":["../src/playbooks/runner.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { CaseStore } from '../cases/store.js'\nimport { EvidenceStore } from '../cases/evidence.js'\nimport { loadConfig } from '../config/index.js'\nimport { createConfiguredMcpFetch } from '../mcp/client.js'\nimport { generateVisualization } from '../viz/index.js'\nimport { PACKAGE_VERSION } from '../version.js'\nimport type { PlaybookDefinition } from './schema.js'\n\nexport interface RunnerOptions {\n caseId?: string // attach to existing case; omit for quick-case auto-creation\n from?: number // 1-based step to resume from (default: 1)\n dryRun?: boolean // print steps, no MCP calls\n params?: Record<string, string>\n}\n\n/** Sleep for ms milliseconds. */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/** Check if an error is a timeout/abort error. */\nfunction isTimeoutError(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n return err.name === 'AbortError' || (err as NodeJS.ErrnoException).code === 'ECONNRESET'\n}\n\n/** Check if an error is a payment failure. */\nfunction isPaymentError(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n const msg = err.message.toLowerCase()\n // Match HTTP 402 status more precisely, or x402-specific error signals\n return msg.includes('http 402') ||\n msg.includes('status 402') ||\n msg.includes('payment required') ||\n msg.includes('x402')\n}\n\n/**\n * Call an MCP tool with retry logic on timeout (up to 3 total attempts).\n * Returns the text result or throws on non-retryable error.\n */\nasync function callWithRetry(\n client: Client,\n toolName: string,\n params: Record<string, string>\n): Promise<string> {\n const MAX_ATTEMPTS = 3\n let lastErr: unknown\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n const result = await client.callTool({ name: toolName, arguments: params })\n const content = result.content as Array<{ type: string; text?: string }>\n return content.filter(c => c.type === 'text').map(c => c.text ?? '').join('\\n')\n } catch (err) {\n if (isTimeoutError(err) && attempt < MAX_ATTEMPTS) {\n lastErr = err\n await sleep(1000)\n continue\n }\n throw err\n }\n }\n\n throw lastErr\n}\n\nasync function validateStepTools(client: Client, steps: PlaybookDefinition['steps']): Promise<void> {\n const result = await client.listTools()\n const available = new Set(result.tools.map(tool => tool.name))\n const missing = [...new Set(steps.map(step => step.tool).filter(tool => !available.has(tool)))]\n if (missing.length === 0) return\n\n const availableList = [...available].sort().join(', ') || 'none'\n throw new Error(\n `Unknown MCP tool(s) in playbook: ${missing.join(', ')}. ` +\n `Available tools: ${availableList}. Run \\`chain-insights mcp tools --refresh\\` to inspect the live MCP schema.`\n )\n}\n\nexport const PlaybookRunner = {\n /**\n * Execute a playbook definition step-by-step against the live MCP.\n *\n * @param playbook - Parsed and validated PlaybookDefinition\n * @param opts - Runner options (caseId, from, dryRun, params)\n */\n async run(playbook: PlaybookDefinition, opts: RunnerOptions): Promise<void> {\n const startIndex = (opts.from ?? 1) - 1 // convert 1-based to 0-based\n const stepsToRun = playbook.steps.slice(startIndex)\n const totalSteps = playbook.steps.length\n\n // --- DRY RUN ---\n if (opts.dryRun) {\n console.log(`Playbook: ${playbook.name} (dry run — no MCP calls)`)\n console.log(`Steps: ${totalSteps} total, starting from ${startIndex + 1}`)\n console.log('')\n for (const step of stepsToRun) {\n console.log(`Step ${step.index}/${totalSteps}: ${step.tool} (params: ${JSON.stringify(step.params)})`)\n }\n console.log('')\n console.log('Cost: unknown (MCP pricing not available without live connection)')\n return\n }\n\n // --- MCP AUTH CHECK (before case creation to avoid orphan cases) ---\n const config = await loadConfig()\n const mcpFetch = await createConfiguredMcpFetch(config)\n\n // --- CASE RESOLUTION ---\n let caseId: string\n if (opts.caseId) {\n const existingCase = await CaseStore.get(opts.caseId)\n caseId = existingCase.id\n } else {\n const newCase = await CaseStore.create({\n name: `quick-${playbook.name}-${Date.now()}`,\n tags: ['quick', 'playbook', playbook.name],\n description: `Auto-created for one-off playbook run: ${playbook.name}`,\n })\n caseId = newCase.id\n console.log(`Created quick case: ${caseId}`)\n }\n\n // --- MCP CONNECTION ---\n const client = new Client({ name: 'chain-insights-playbook', version: PACKAGE_VERSION })\n await client.connect(\n new StreamableHTTPClientTransport(new URL(config.mcpEndpoint), { fetch: mcpFetch })\n )\n\n let evidenceCount = 0\n\n try {\n await validateStepTools(client, stepsToRun)\n\n // --- STEP LOOP ---\n for (const step of stepsToRun) {\n console.log(`Step ${step.index}/${totalSteps}: ${step.label}...`)\n\n let result: string\n try {\n result = await callWithRetry(client, step.tool, step.params)\n } catch (err) {\n if (isPaymentError(err)) {\n if (process.stdin.isTTY) {\n // Interactive: prompt user\n const { createInterface } = await import('node:readline')\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n const answer = await new Promise<string>(resolve => {\n rl.question(`Payment required for step ${step.index}. (retry/skip/abort): `, resolve)\n })\n rl.close()\n\n if (answer.trim().toLowerCase() === 'retry') {\n result = await callWithRetry(client, step.tool, step.params)\n } else if (answer.trim().toLowerCase() === 'skip') {\n console.log(`Step ${step.index} skipped.`)\n continue\n } else {\n throw new Error(`Aborted at step ${step.index} due to payment failure.`)\n }\n } else {\n // Non-TTY: abort\n throw new Error(\n `Payment required for step ${step.index} but no interactive terminal available. ` +\n `Configure wallet with \\`chain-insights wallet import <private-key>\\`, then run \\`chain-insights wallet ready\\`. Aborting.`\n )\n }\n } else {\n // Non-payment, non-timeout MCP error — stop and report\n const completedSteps = step.index - 1 - startIndex\n const completedMsg = completedSteps > 0\n ? `Completed: steps ${startIndex + 1}..${step.index - 1}.`\n : 'No steps completed before failure.'\n console.error(\n `Step ${step.index} failed: ${(err as Error).message}. ` +\n `${completedMsg} Run with --from ${step.index} to resume.`\n )\n throw err\n }\n }\n\n // --- STORE EVIDENCE ---\n await EvidenceStore.append(caseId, {\n source: step.tool,\n content: result,\n queryParams: JSON.stringify(step.params),\n })\n evidenceCount++\n console.log(` (${result.length} chars stored)`)\n }\n\n // --- AUTO-VIZ for trace-funds ---\n if (playbook.name === 'trace-funds') {\n try {\n const viz = await generateVisualization({ caseId })\n console.log(`Visualization generated: ${viz.htmlPath}`)\n } catch {\n console.log('No transaction data to visualize.')\n }\n }\n\n // --- FINAL SUMMARY ---\n console.log(`Playbook complete. Case: ${caseId}. Evidence: ${evidenceCount} entries.`)\n } finally {\n await client.close()\n }\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runner-DIJSbkjc.mjs","names":[],"sources":["../src/playbooks/runner.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { CaseStore } from '../cases/store.js'\nimport { EvidenceStore } from '../cases/evidence.js'\nimport { loadConfig } from '../config/index.js'\nimport { createConfiguredMcpFetch } from '../mcp/client.js'\nimport { generateVisualization } from '../viz/index.js'\nimport { PACKAGE_VERSION } from '../version.js'\nimport type { PlaybookDefinition } from './schema.js'\n\nexport interface RunnerOptions {\n caseId?: string // attach to existing case; omit for quick-case auto-creation\n from?: number // 1-based step to resume from (default: 1)\n dryRun?: boolean // print steps, no MCP calls\n params?: Record<string, string>\n}\n\n/** Sleep for ms milliseconds. */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/** Check if an error is a timeout/abort error. */\nfunction isTimeoutError(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n return err.name === 'AbortError' || (err as NodeJS.ErrnoException).code === 'ECONNRESET'\n}\n\n/** Check if an error is a payment failure. */\nfunction isPaymentError(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n const msg = err.message.toLowerCase()\n // Match HTTP 402 status more precisely, or x402-specific error signals\n return msg.includes('http 402') ||\n msg.includes('status 402') ||\n msg.includes('payment required') ||\n msg.includes('x402')\n}\n\n/**\n * Call an MCP tool with retry logic on timeout (up to 3 total attempts).\n * Returns the text result or throws on non-retryable error.\n */\nasync function callWithRetry(\n client: Client,\n toolName: string,\n params: Record<string, string>\n): Promise<string> {\n const MAX_ATTEMPTS = 3\n let lastErr: unknown\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n const result = await client.callTool({ name: toolName, arguments: params })\n const content = result.content as Array<{ type: string; text?: string }>\n return content.filter(c => c.type === 'text').map(c => c.text ?? '').join('\\n')\n } catch (err) {\n if (isTimeoutError(err) && attempt < MAX_ATTEMPTS) {\n lastErr = err\n await sleep(1000)\n continue\n }\n throw err\n }\n }\n\n throw lastErr\n}\n\nasync function validateStepTools(client: Client, steps: PlaybookDefinition['steps']): Promise<void> {\n const result = await client.listTools()\n const available = new Set(result.tools.map(tool => tool.name))\n const missing = [...new Set(steps.map(step => step.tool).filter(tool => !available.has(tool)))]\n if (missing.length === 0) return\n\n const availableList = [...available].sort().join(', ') || 'none'\n throw new Error(\n `Unknown MCP tool(s) in playbook: ${missing.join(', ')}. ` +\n `Available tools: ${availableList}. Run \\`chain-insights mcp tools --refresh\\` to inspect the live MCP schema.`\n )\n}\n\nexport const PlaybookRunner = {\n /**\n * Execute a playbook definition step-by-step against the live MCP.\n *\n * @param playbook - Parsed and validated PlaybookDefinition\n * @param opts - Runner options (caseId, from, dryRun, params)\n */\n async run(playbook: PlaybookDefinition, opts: RunnerOptions): Promise<void> {\n const startIndex = (opts.from ?? 1) - 1 // convert 1-based to 0-based\n const stepsToRun = playbook.steps.slice(startIndex)\n const totalSteps = playbook.steps.length\n\n // --- DRY RUN ---\n if (opts.dryRun) {\n console.log(`Playbook: ${playbook.name} (dry run — no MCP calls)`)\n console.log(`Steps: ${totalSteps} total, starting from ${startIndex + 1}`)\n console.log('')\n for (const step of stepsToRun) {\n console.log(`Step ${step.index}/${totalSteps}: ${step.tool} (params: ${JSON.stringify(step.params)})`)\n }\n console.log('')\n console.log('Cost: unknown (MCP pricing not available without live connection)')\n return\n }\n\n // --- MCP AUTH CHECK (before case creation to avoid orphan cases) ---\n const config = await loadConfig()\n const mcpFetch = await createConfiguredMcpFetch(config)\n\n // --- CASE RESOLUTION ---\n let caseId: string\n if (opts.caseId) {\n const existingCase = await CaseStore.get(opts.caseId)\n caseId = existingCase.id\n } else {\n const newCase = await CaseStore.create({\n name: `quick-${playbook.name}-${Date.now()}`,\n tags: ['quick', 'playbook', playbook.name],\n description: `Auto-created for one-off playbook run: ${playbook.name}`,\n })\n caseId = newCase.id\n console.log(`Created quick case: ${caseId}`)\n }\n\n // --- MCP CONNECTION ---\n const client = new Client({ name: 'chain-insights-playbook', version: PACKAGE_VERSION })\n await client.connect(\n new StreamableHTTPClientTransport(new URL(config.mcpEndpoint), { fetch: mcpFetch })\n )\n\n let evidenceCount = 0\n\n try {\n await validateStepTools(client, stepsToRun)\n\n // --- STEP LOOP ---\n for (const step of stepsToRun) {\n console.log(`Step ${step.index}/${totalSteps}: ${step.label}...`)\n\n let result: string\n try {\n result = await callWithRetry(client, step.tool, step.params)\n } catch (err) {\n if (isPaymentError(err)) {\n if (process.stdin.isTTY) {\n // Interactive: prompt user\n const { createInterface } = await import('node:readline')\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n const answer = await new Promise<string>(resolve => {\n rl.question(`Payment required for step ${step.index}. (retry/skip/abort): `, resolve)\n })\n rl.close()\n\n if (answer.trim().toLowerCase() === 'retry') {\n result = await callWithRetry(client, step.tool, step.params)\n } else if (answer.trim().toLowerCase() === 'skip') {\n console.log(`Step ${step.index} skipped.`)\n continue\n } else {\n throw new Error(`Aborted at step ${step.index} due to payment failure.`)\n }\n } else {\n // Non-TTY: abort\n throw new Error(\n `Payment required for step ${step.index} but no interactive terminal available. ` +\n `Configure wallet with \\`chain-insights wallet import <private-key>\\`, then run \\`chain-insights wallet ready\\`. Aborting.`\n )\n }\n } else {\n // Non-payment, non-timeout MCP error — stop and report\n const completedSteps = step.index - 1 - startIndex\n const completedMsg = completedSteps > 0\n ? `Completed: steps ${startIndex + 1}..${step.index - 1}.`\n : 'No steps completed before failure.'\n console.error(\n `Step ${step.index} failed: ${(err as Error).message}. ` +\n `${completedMsg} Run with --from ${step.index} to resume.`\n )\n throw err\n }\n }\n\n // --- STORE EVIDENCE ---\n await EvidenceStore.append(caseId, {\n source: step.tool,\n content: result,\n queryParams: JSON.stringify(step.params),\n })\n evidenceCount++\n console.log(` (${result.length} chars stored)`)\n }\n\n // --- AUTO-VIZ for trace-funds ---\n if (playbook.name === 'trace-funds') {\n try {\n const viz = await generateVisualization({ caseId })\n console.log(`Visualization generated: ${viz.htmlPath}`)\n } catch {\n console.log('No transaction data to visualize.')\n }\n }\n\n // --- FINAL SUMMARY ---\n console.log(`Playbook complete. Case: ${caseId}. Evidence: ${evidenceCount} entries.`)\n } finally {\n await client.close()\n }\n },\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { CaseStore } from "./store-BT2SCcQr.mjs";
1
+ import { t as CaseStore } from "./store-CTtqQtaE.mjs";
2
2
  //#region src/cases/selector.ts
3
3
  async function resolveCaseSelector(input) {
4
4
  if (!/^[1-9]\d*$/.test(input)) return input;
@@ -9,4 +9,4 @@ async function resolveCaseSelector(input) {
9
9
  //#endregion
10
10
  export { resolveCaseSelector };
11
11
 
12
- //# sourceMappingURL=selector-CTUiQrzI.mjs.map
12
+ //# sourceMappingURL=selector-CF2o5gxN.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"selector-CTUiQrzI.mjs","names":[],"sources":["../src/cases/selector.ts"],"sourcesContent":["import { CaseStore } from './store.js'\n\nexport async function resolveCaseSelector(input: string): Promise<string> {\n if (!/^[1-9]\\d*$/.test(input)) return input\n\n const cases = await CaseStore.list()\n const index = Number(input) - 1\n const selected = cases[index]\n if (!selected) {\n throw new Error(`No case numbered ${input}. Run \\`cia case list\\` to see available cases.`)\n }\n return selected.id\n}\n"],"mappings":";;AAEA,eAAsB,oBAAoB,OAAgC;CACxE,IAAI,CAAC,aAAa,KAAK,KAAK,GAAG,OAAO;CAItC,MAAM,YAAW,MAFG,UAAU,KAAK,GACrB,OAAO,KAAK,IAAI;CAE9B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,oBAAoB,MAAM,gDAAgD;CAE5F,OAAO,SAAS;AAClB"}
1
+ {"version":3,"file":"selector-CF2o5gxN.mjs","names":[],"sources":["../src/cases/selector.ts"],"sourcesContent":["import { CaseStore } from './store.js'\n\nexport async function resolveCaseSelector(input: string): Promise<string> {\n if (!/^[1-9]\\d*$/.test(input)) return input\n\n const cases = await CaseStore.list()\n const index = Number(input) - 1\n const selected = cases[index]\n if (!selected) {\n throw new Error(`No case numbered ${input}. Run \\`cia case list\\` to see available cases.`)\n }\n return selected.id\n}\n"],"mappings":";;AAEA,eAAsB,oBAAoB,OAAgC;CACxE,IAAI,CAAC,aAAa,KAAK,KAAK,GAAG,OAAO;CAItC,MAAM,YAAW,MAFG,UAAU,KAAK,GACrB,OAAO,KAAK,IAAI;CAE9B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,oBAAoB,MAAM,gDAAgD;CAE5F,OAAO,SAAS;AAClB"}
@@ -1,4 +1,4 @@
1
- const require_store = require("./store-DogLawSj.cjs");
1
+ const require_store = require("./store-CqPfs47P.cjs");
2
2
  //#region src/cases/selector.ts
3
3
  async function resolveCaseSelector(input) {
4
4
  if (!/^[1-9]\d*$/.test(input)) return input;
@@ -5,6 +5,7 @@ let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path, 1);
6
6
  let node_fs_promises = require("node:fs/promises");
7
7
  //#region src/cases/session.ts
8
+ var session_exports = /* @__PURE__ */ require_chunk.__exportAll({ SessionStore: () => SessionStore });
8
9
  function caseDir(caseId) {
9
10
  return node_path.default.join(require_output_root.workspaceOutputPaths().casesRoot, caseId);
10
11
  }
@@ -112,4 +113,15 @@ const SessionStore = {
112
113
  }
113
114
  };
114
115
  //#endregion
115
- exports.SessionStore = SessionStore;
116
+ Object.defineProperty(exports, "SessionStore", {
117
+ enumerable: true,
118
+ get: function() {
119
+ return SessionStore;
120
+ }
121
+ });
122
+ Object.defineProperty(exports, "session_exports", {
123
+ enumerable: true,
124
+ get: function() {
125
+ return session_exports;
126
+ }
127
+ });
@@ -1,8 +1,10 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
1
2
  import { n as serializeFrontmatter, t as parseFrontmatter } from "./frontmatter-D0ccQnUM.mjs";
2
3
  import { n as workspaceOutputPaths } from "./output-root-BRhzhhXZ.mjs";
3
4
  import path from "node:path";
4
5
  import { readFile, readdir, rm, writeFile } from "node:fs/promises";
5
6
  //#region src/cases/session.ts
7
+ var session_exports = /* @__PURE__ */ __exportAll({ SessionStore: () => SessionStore });
6
8
  function caseDir(caseId) {
7
9
  return path.join(workspaceOutputPaths().casesRoot, caseId);
8
10
  }
@@ -110,6 +112,6 @@ const SessionStore = {
110
112
  }
111
113
  };
112
114
  //#endregion
113
- export { SessionStore };
115
+ export { session_exports as n, SessionStore as t };
114
116
 
115
- //# sourceMappingURL=session-Bha3zFrx.mjs.map
117
+ //# sourceMappingURL=session-DROyhebe.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-Bha3zFrx.mjs","names":[],"sources":["../src/cases/session.ts"],"sourcesContent":["import { readFile, writeFile, readdir, rm } from 'node:fs/promises'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\nimport { type Session } from './schema.js'\n\nfunction caseDir(caseId: string): string {\n return path.join(workspaceOutputPaths().casesRoot, caseId)\n}\n\nconst MAX_SESSIONS = 5\n\nfunction sessionNumber(filename: string): number {\n return parseInt(filename.replace('session_', '').replace('.md', ''), 10)\n}\n\nfunction sessionFromFrontmatter(frontmatter: Record<string, string>): Session {\n return {\n sessionId: frontmatter['sessionId'] ?? '',\n caseId: frontmatter['caseId'] ?? '',\n startTime: frontmatter['startTime'] ?? new Date().toISOString(),\n endTime: frontmatter['endTime'] || undefined,\n status: frontmatter['status'] === 'ended' ? 'ended' : 'active',\n }\n}\n\nasync function listSessionFiles(dir: string): Promise<string[]> {\n const files = await readdir(dir)\n return files\n .filter(f => f.match(/^session_\\d+\\.md$/))\n .sort((a, b) => sessionNumber(b) - sessionNumber(a))\n}\n\nexport const SessionStore = {\n async start(caseId: string, input: { title?: string } = {}): Promise<Session> {\n const dir = caseDir(caseId)\n let sessionFiles: string[] = []\n try {\n sessionFiles = await listSessionFiles(dir)\n } catch {\n sessionFiles = []\n }\n\n for (const filename of sessionFiles) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n if (frontmatter['status'] !== 'ended') {\n return sessionFromFrontmatter(frontmatter)\n }\n }\n\n const seq = sessionFiles.length + 1\n const seqStr = String(seq).padStart(3, '0')\n const filename = `session_${seqStr}.md`\n const sessionId = `${caseId}_s${seqStr}`\n const now = new Date().toISOString()\n const title = input.title?.trim()\n\n const fm: Record<string, string> = {\n sessionId,\n caseId,\n startTime: now,\n endTime: '',\n status: 'active',\n }\n if (title) fm['title'] = title\n const heading = title || now.slice(0, 10)\n const body = `# Session ${seq}: ${heading}\\n\\n## Investigation Log\\n\\n## Key Findings\\n\\n## Next Steps\\n\\n`\n await writeFile(path.join(dir, filename), serializeFrontmatter(fm, body), { mode: 0o600 })\n\n return { sessionId, caseId, startTime: now, status: 'active' }\n },\n\n async end(\n caseId: string,\n input: { findings: string; nextSteps: string }\n ): Promise<void> {\n const dir = caseDir(caseId)\n const sessionFiles = await listSessionFiles(dir)\n if (sessionFiles.length === 0) throw new Error(`No active session for case ${caseId}`)\n let activeFile: string | null = null\n let activeFrontmatter: Record<string, string> | null = null\n let activeBody = ''\n for (const filename of sessionFiles) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n if (frontmatter['status'] !== 'ended') {\n activeFile = filename\n activeFrontmatter = frontmatter\n activeBody = body\n break\n }\n }\n if (!activeFile || !activeFrontmatter) throw new Error(`No active session for case ${caseId}`)\n const now = new Date().toISOString()\n activeFrontmatter['endTime'] = now\n activeFrontmatter['status'] = 'ended'\n\n // Append findings and next steps to body\n const updatedBody = activeBody\n .replace('## Key Findings\\n', `## Key Findings\\n\\n${input.findings}\\n`)\n .replace('## Next Steps\\n', `## Next Steps\\n\\n${input.nextSteps}\\n`)\n\n await writeFile(path.join(dir, activeFile), serializeFrontmatter(activeFrontmatter, updatedBody), { mode: 0o600 })\n },\n\n async getLatest(caseId: string): Promise<{ frontmatter: Record<string, string>; body: string } | null> {\n const dir = caseDir(caseId)\n try {\n const sessionFiles = await listSessionFiles(dir)\n if (sessionFiles.length === 0) return null\n const raw = await readFile(path.join(dir, sessionFiles[0]!), 'utf8')\n return parseFrontmatter(raw)\n } catch {\n return null\n }\n },\n\n async archiveOldSessions(caseId: string): Promise<void> {\n const dir = caseDir(caseId)\n const sessionFiles = (await listSessionFiles(dir)).reverse()\n\n if (sessionFiles.length <= MAX_SESSIONS) return\n\n // Files to archive = everything beyond the 5 most recent (which are at the end)\n const toArchive = sessionFiles.slice(0, sessionFiles.length - MAX_SESSIONS)\n const historyPath = path.join(dir, 'history.md')\n\n // Pitfall 5 mitigation: ENOENT-safe read of history.md\n const existingHistory = await readFile(historyPath, 'utf8').catch(() => '')\n\n const summaries: string[] = []\n for (const filename of toArchive) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n // Extract a one-paragraph summary: sessionId + key findings section\n const findingsMatch = body.match(/## Key Findings\\n+([\\s\\S]*?)(?:\\n## |$)/)\n const findings = findingsMatch ? findingsMatch[1]!.trim() : '(no findings recorded)'\n summaries.push(`### ${frontmatter['sessionId'] ?? filename} (${frontmatter['startTime'] ?? ''})\\n\\n${findings}\\n`)\n }\n\n const newHistory = existingHistory + '\\n' + summaries.join('\\n') + '\\n'\n await writeFile(historyPath, newHistory, { mode: 0o600 })\n\n // Delete archived session files\n for (const filename of toArchive) {\n await rm(path.join(dir, filename))\n }\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-DROyhebe.mjs","names":[],"sources":["../src/cases/session.ts"],"sourcesContent":["import { readFile, writeFile, readdir, rm } from 'node:fs/promises'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\nimport { type Session } from './schema.js'\n\nfunction caseDir(caseId: string): string {\n return path.join(workspaceOutputPaths().casesRoot, caseId)\n}\n\nconst MAX_SESSIONS = 5\n\nfunction sessionNumber(filename: string): number {\n return parseInt(filename.replace('session_', '').replace('.md', ''), 10)\n}\n\nfunction sessionFromFrontmatter(frontmatter: Record<string, string>): Session {\n return {\n sessionId: frontmatter['sessionId'] ?? '',\n caseId: frontmatter['caseId'] ?? '',\n startTime: frontmatter['startTime'] ?? new Date().toISOString(),\n endTime: frontmatter['endTime'] || undefined,\n status: frontmatter['status'] === 'ended' ? 'ended' : 'active',\n }\n}\n\nasync function listSessionFiles(dir: string): Promise<string[]> {\n const files = await readdir(dir)\n return files\n .filter(f => f.match(/^session_\\d+\\.md$/))\n .sort((a, b) => sessionNumber(b) - sessionNumber(a))\n}\n\nexport const SessionStore = {\n async start(caseId: string, input: { title?: string } = {}): Promise<Session> {\n const dir = caseDir(caseId)\n let sessionFiles: string[] = []\n try {\n sessionFiles = await listSessionFiles(dir)\n } catch {\n sessionFiles = []\n }\n\n for (const filename of sessionFiles) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n if (frontmatter['status'] !== 'ended') {\n return sessionFromFrontmatter(frontmatter)\n }\n }\n\n const seq = sessionFiles.length + 1\n const seqStr = String(seq).padStart(3, '0')\n const filename = `session_${seqStr}.md`\n const sessionId = `${caseId}_s${seqStr}`\n const now = new Date().toISOString()\n const title = input.title?.trim()\n\n const fm: Record<string, string> = {\n sessionId,\n caseId,\n startTime: now,\n endTime: '',\n status: 'active',\n }\n if (title) fm['title'] = title\n const heading = title || now.slice(0, 10)\n const body = `# Session ${seq}: ${heading}\\n\\n## Investigation Log\\n\\n## Key Findings\\n\\n## Next Steps\\n\\n`\n await writeFile(path.join(dir, filename), serializeFrontmatter(fm, body), { mode: 0o600 })\n\n return { sessionId, caseId, startTime: now, status: 'active' }\n },\n\n async end(\n caseId: string,\n input: { findings: string; nextSteps: string }\n ): Promise<void> {\n const dir = caseDir(caseId)\n const sessionFiles = await listSessionFiles(dir)\n if (sessionFiles.length === 0) throw new Error(`No active session for case ${caseId}`)\n let activeFile: string | null = null\n let activeFrontmatter: Record<string, string> | null = null\n let activeBody = ''\n for (const filename of sessionFiles) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n if (frontmatter['status'] !== 'ended') {\n activeFile = filename\n activeFrontmatter = frontmatter\n activeBody = body\n break\n }\n }\n if (!activeFile || !activeFrontmatter) throw new Error(`No active session for case ${caseId}`)\n const now = new Date().toISOString()\n activeFrontmatter['endTime'] = now\n activeFrontmatter['status'] = 'ended'\n\n // Append findings and next steps to body\n const updatedBody = activeBody\n .replace('## Key Findings\\n', `## Key Findings\\n\\n${input.findings}\\n`)\n .replace('## Next Steps\\n', `## Next Steps\\n\\n${input.nextSteps}\\n`)\n\n await writeFile(path.join(dir, activeFile), serializeFrontmatter(activeFrontmatter, updatedBody), { mode: 0o600 })\n },\n\n async getLatest(caseId: string): Promise<{ frontmatter: Record<string, string>; body: string } | null> {\n const dir = caseDir(caseId)\n try {\n const sessionFiles = await listSessionFiles(dir)\n if (sessionFiles.length === 0) return null\n const raw = await readFile(path.join(dir, sessionFiles[0]!), 'utf8')\n return parseFrontmatter(raw)\n } catch {\n return null\n }\n },\n\n async archiveOldSessions(caseId: string): Promise<void> {\n const dir = caseDir(caseId)\n const sessionFiles = (await listSessionFiles(dir)).reverse()\n\n if (sessionFiles.length <= MAX_SESSIONS) return\n\n // Files to archive = everything beyond the 5 most recent (which are at the end)\n const toArchive = sessionFiles.slice(0, sessionFiles.length - MAX_SESSIONS)\n const historyPath = path.join(dir, 'history.md')\n\n // Pitfall 5 mitigation: ENOENT-safe read of history.md\n const existingHistory = await readFile(historyPath, 'utf8').catch(() => '')\n\n const summaries: string[] = []\n for (const filename of toArchive) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n // Extract a one-paragraph summary: sessionId + key findings section\n const findingsMatch = body.match(/## Key Findings\\n+([\\s\\S]*?)(?:\\n## |$)/)\n const findings = findingsMatch ? findingsMatch[1]!.trim() : '(no findings recorded)'\n summaries.push(`### ${frontmatter['sessionId'] ?? filename} (${frontmatter['startTime'] ?? ''})\\n\\n${findings}\\n`)\n }\n\n const newHistory = existingHistory + '\\n' + summaries.join('\\n') + '\\n'\n await writeFile(historyPath, newHistory, { mode: 0o600 })\n\n // Delete archived session files\n for (const filename of toArchive) {\n await rm(path.join(dir, filename))\n }\n },\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
1
2
  import { n as serializeFrontmatter, t as parseFrontmatter } from "./frontmatter-D0ccQnUM.mjs";
2
3
  import { n as workspaceOutputPaths } from "./output-root-BRhzhhXZ.mjs";
3
4
  import path from "node:path";
@@ -50,6 +51,11 @@ z.object({
50
51
  });
51
52
  //#endregion
52
53
  //#region src/cases/store.ts
54
+ var store_exports = /* @__PURE__ */ __exportAll({
55
+ CaseStore: () => CaseStore,
56
+ casesRoot: () => casesRoot,
57
+ generateCaseId: () => generateCaseId
58
+ });
53
59
  const casesRoot = () => workspaceOutputPaths().casesRoot;
54
60
  function caseDir(id) {
55
61
  return path.join(casesRoot(), id);
@@ -190,8 +196,8 @@ const CaseStore = {
190
196
  const dir = caseDir(id);
191
197
  const { frontmatter } = parseFrontmatter(await readFile(path.join(dir, "case.md"), "utf8"));
192
198
  const tags = (frontmatter["tags"] ?? "").split(",").filter(Boolean);
193
- const { SessionStore } = await import("./session-Bha3zFrx.mjs");
194
- const { DossierStore } = await import("./dossier-Bl0NkJKC.mjs");
199
+ const { SessionStore } = await import("./session-DROyhebe.mjs").then((n) => n.n);
200
+ const { DossierStore } = await import("./dossier-Bjpcbcxa.mjs").then((n) => n.n);
195
201
  const [latestSession, dossierSummaries, manifest] = await Promise.all([
196
202
  SessionStore.getLatest(id),
197
203
  DossierStore.listSummaries(id),
@@ -220,6 +226,6 @@ const CaseStore = {
220
226
  }
221
227
  };
222
228
  //#endregion
223
- export { CaseStore, casesRoot, generateCaseId, CaseStatusEnum as n, CaseSchema as t };
229
+ export { CaseStatusEnum as a, CaseSchema as i, generateCaseId as n, store_exports as r, CaseStore as t };
224
230
 
225
- //# sourceMappingURL=store-BT2SCcQr.mjs.map
231
+ //# sourceMappingURL=store-CTtqQtaE.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"store-BT2SCcQr.mjs","names":["nodeErr"],"sources":["../src/cases/schema.ts","../src/cases/store.ts"],"sourcesContent":["import * as z from 'zod'\n\n// Case ID format: YYYYMMDD_NNN_slug (e.g. 20260511_001_tornado-mixer)\n// Regex rejects path traversal chars (../, shell chars) per T-03-01 threat model.\nconst caseIdRegex = /^\\d{8}_\\d{3}_[a-z0-9][a-z0-9-]*$/\n\nexport const CaseStatusEnum = z.enum(['open', 'active', 'suspended', 'closed'])\nexport type CaseStatus = z.infer<typeof CaseStatusEnum>\n\nexport const CaseSchema = z.object({\n id: z.string().regex(caseIdRegex, 'Invalid case ID format'),\n name: z.string().min(1).max(200),\n status: CaseStatusEnum.default('open'),\n created: z.string().datetime(),\n updated: z.string().datetime(),\n tags: z.array(z.string()).default([]),\n description: z.string().default(''),\n slug: z.string().optional(),\n})\nexport type Case = z.infer<typeof CaseSchema>\n\nexport const EvidenceSchema = z.object({\n id: z.string().min(1),\n caseId: z.string().regex(caseIdRegex),\n source: z.string().min(1),\n timestamp: z.string().datetime(),\n queryParams: z.string().default(''),\n})\nexport type Evidence = z.infer<typeof EvidenceSchema>\n\nexport const DossierSchema = z.object({\n address: z.string().min(1).max(100),\n type: z.enum(['eoa', 'contract', 'exchange', 'mixer', 'unknown']).default('unknown'),\n firstSeen: z.string().datetime(),\n lastSeen: z.string().datetime(),\n riskTags: z.string().default(''),\n})\nexport type Dossier = z.infer<typeof DossierSchema>\n\nexport const SessionSchema = z.object({\n sessionId: z.string().min(1),\n caseId: z.string().regex(caseIdRegex),\n startTime: z.string().datetime(),\n endTime: z.string().optional(),\n status: z.enum(['active', 'ended']).default('active'),\n})\nexport type Session = z.infer<typeof SessionSchema>\n","import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\nimport { CaseSchema, type Case, type CaseStatus } from './schema.js'\n\nexport const casesRoot = () => workspaceOutputPaths().casesRoot\n\nfunction caseDir(id: string): string {\n return path.join(casesRoot(), id)\n}\n\nexport function generateCaseId(name: string, existingIds: string[]): string {\n const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')\n const slug = name.toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n .slice(0, 40)\n const todayNums = existingIds\n .filter(id => id.startsWith(date + '_'))\n .map(id => parseInt(id.split('_')[1] ?? '0', 10))\n .filter(n => !isNaN(n))\n const next = todayNums.length > 0 ? Math.max(...todayNums) + 1 : 1\n return `${date}_${String(next).padStart(3, '0')}_${slug}`\n}\n\nexport const CaseStore = {\n async create(input: { name: string; tags: string[]; description: string }): Promise<Case> {\n const root = casesRoot()\n await mkdir(root, { recursive: true })\n const existingIds = await readdir(root).catch(() => [])\n const id = generateCaseId(input.name, existingIds)\n const slug = id.split('_').slice(2).join('_')\n const now = new Date().toISOString()\n const tags = input.tags\n\n const dir = caseDir(id)\n await mkdir(path.join(dir, 'evidence'), { recursive: true })\n await mkdir(path.join(dir, 'dossiers'), { recursive: true })\n\n const fm: Record<string, string> = {\n id,\n name: input.name,\n status: 'open',\n created: now,\n updated: now,\n tags: tags.join(','),\n description: input.description,\n slug,\n }\n const body = [\n `# ${input.name}`,\n '',\n `Opened: ${now}`,\n `Status: open`,\n '',\n '## Question',\n '',\n input.description || 'TBD',\n '',\n '## Current Assessment',\n '',\n 'TBD',\n '',\n '## Top Findings',\n '',\n '| Finding | Confidence | Evidence |',\n '|---|---:|---|',\n '',\n '## Next Actions',\n '',\n '- TBD',\n '',\n '## Reports',\n '',\n ].join('\\n')\n await writeFile(path.join(dir, 'case.md'), serializeFrontmatter(fm, body), { mode: 0o600 })\n\n const manifest = JSON.stringify({ caseId: id, entries: [] }, null, 2) + '\\n'\n await writeFile(path.join(dir, 'manifest.json'), manifest, { mode: 0o600 })\n\n return CaseSchema.parse({ id, name: input.name, status: 'open', created: now, updated: now, tags, description: input.description, slug })\n },\n\n async setStatus(id: string, status: CaseStatus): Promise<Case> {\n const dir = caseDir(id)\n const filePath = path.join(dir, 'case.md')\n const raw = await readFile(filePath, 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n const now = new Date().toISOString()\n frontmatter['status'] = status\n frontmatter['updated'] = now\n await writeFile(filePath, serializeFrontmatter(frontmatter, body), { mode: 0o600 })\n\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n return CaseSchema.parse({\n id,\n name: frontmatter['name'] ?? '',\n status,\n created: frontmatter['created'] ?? now,\n updated: now,\n tags,\n description: frontmatter['description'] ?? '',\n })\n },\n\n async list(): Promise<Array<{ id: string; name: string; status: string }>> {\n const root = casesRoot()\n try {\n const ids = await readdir(root)\n const cases: Array<{ id: string; name: string; status: string; created: string }> = []\n for (const id of ids) {\n try {\n const raw = await readFile(path.join(caseDir(id), 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n cases.push({\n id,\n name: frontmatter['name'] ?? id,\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? '',\n })\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code !== 'ENOENT' && nodeErr.code !== 'ENOTDIR') throw err\n }\n }\n return cases\n .sort((a, b) => b.created.localeCompare(a.created) || b.id.localeCompare(a.id))\n .map(({ id, name, status }) => ({ id, name, status }))\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code === 'ENOENT') return []\n throw err\n }\n },\n\n async get(id: string): Promise<Case> {\n const dir = caseDir(id)\n const raw = await readFile(path.join(dir, 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n return CaseSchema.parse({\n id,\n name: frontmatter['name'] ?? '',\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? new Date().toISOString(),\n updated: frontmatter['updated'] ?? new Date().toISOString(),\n tags,\n description: frontmatter['description'] ?? '',\n })\n },\n\n async loadContext(id: string): Promise<{\n case: { id: string; name: string; status: string; created: string; updated: string; tags: string[] };\n lastSession: { sessionId: string; startTime: string; endTime?: string; body: string } | null;\n dossierSummaries: Array<{ address: string; type: string; riskTags: string; firstSeen: string; lastSeen: string }>;\n evidenceCount: number;\n }> {\n const dir = caseDir(id)\n\n // Read case.md\n const raw = await readFile(path.join(dir, 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n\n // Lazy imports to avoid circular deps\n const { SessionStore } = await import('./session.js')\n const { DossierStore } = await import('./dossier.js')\n\n const [latestSession, dossierSummaries, manifest] = await Promise.all([\n SessionStore.getLatest(id),\n DossierStore.listSummaries(id),\n readFile(path.join(dir, 'manifest.json'), 'utf8').catch(() => '{\"entries\":[]}'),\n ])\n\n const manifestData = JSON.parse(manifest) as { entries: unknown[] }\n const evidenceCount = manifestData.entries.length\n\n const lastSession = latestSession\n ? {\n sessionId: latestSession.frontmatter['sessionId'] ?? '',\n startTime: latestSession.frontmatter['startTime'] ?? '',\n endTime: latestSession.frontmatter['endTime'] || undefined,\n body: latestSession.body,\n }\n : null\n\n return {\n case: {\n id,\n name: frontmatter['name'] ?? '',\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? '',\n updated: frontmatter['updated'] ?? '',\n tags,\n },\n lastSession,\n dossierSummaries,\n evidenceCount,\n }\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"store-CTtqQtaE.mjs","names":["nodeErr"],"sources":["../src/cases/schema.ts","../src/cases/store.ts"],"sourcesContent":["import * as z from 'zod'\n\n// Case ID format: YYYYMMDD_NNN_slug (e.g. 20260511_001_tornado-mixer)\n// Regex rejects path traversal chars (../, shell chars) per T-03-01 threat model.\nconst caseIdRegex = /^\\d{8}_\\d{3}_[a-z0-9][a-z0-9-]*$/\n\nexport const CaseStatusEnum = z.enum(['open', 'active', 'suspended', 'closed'])\nexport type CaseStatus = z.infer<typeof CaseStatusEnum>\n\nexport const CaseSchema = z.object({\n id: z.string().regex(caseIdRegex, 'Invalid case ID format'),\n name: z.string().min(1).max(200),\n status: CaseStatusEnum.default('open'),\n created: z.string().datetime(),\n updated: z.string().datetime(),\n tags: z.array(z.string()).default([]),\n description: z.string().default(''),\n slug: z.string().optional(),\n})\nexport type Case = z.infer<typeof CaseSchema>\n\nexport const EvidenceSchema = z.object({\n id: z.string().min(1),\n caseId: z.string().regex(caseIdRegex),\n source: z.string().min(1),\n timestamp: z.string().datetime(),\n queryParams: z.string().default(''),\n})\nexport type Evidence = z.infer<typeof EvidenceSchema>\n\nexport const DossierSchema = z.object({\n address: z.string().min(1).max(100),\n type: z.enum(['eoa', 'contract', 'exchange', 'mixer', 'unknown']).default('unknown'),\n firstSeen: z.string().datetime(),\n lastSeen: z.string().datetime(),\n riskTags: z.string().default(''),\n})\nexport type Dossier = z.infer<typeof DossierSchema>\n\nexport const SessionSchema = z.object({\n sessionId: z.string().min(1),\n caseId: z.string().regex(caseIdRegex),\n startTime: z.string().datetime(),\n endTime: z.string().optional(),\n status: z.enum(['active', 'ended']).default('active'),\n})\nexport type Session = z.infer<typeof SessionSchema>\n","import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\nimport { CaseSchema, type Case, type CaseStatus } from './schema.js'\n\nexport const casesRoot = () => workspaceOutputPaths().casesRoot\n\nfunction caseDir(id: string): string {\n return path.join(casesRoot(), id)\n}\n\nexport function generateCaseId(name: string, existingIds: string[]): string {\n const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')\n const slug = name.toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n .slice(0, 40)\n const todayNums = existingIds\n .filter(id => id.startsWith(date + '_'))\n .map(id => parseInt(id.split('_')[1] ?? '0', 10))\n .filter(n => !isNaN(n))\n const next = todayNums.length > 0 ? Math.max(...todayNums) + 1 : 1\n return `${date}_${String(next).padStart(3, '0')}_${slug}`\n}\n\nexport const CaseStore = {\n async create(input: { name: string; tags: string[]; description: string }): Promise<Case> {\n const root = casesRoot()\n await mkdir(root, { recursive: true })\n const existingIds = await readdir(root).catch(() => [])\n const id = generateCaseId(input.name, existingIds)\n const slug = id.split('_').slice(2).join('_')\n const now = new Date().toISOString()\n const tags = input.tags\n\n const dir = caseDir(id)\n await mkdir(path.join(dir, 'evidence'), { recursive: true })\n await mkdir(path.join(dir, 'dossiers'), { recursive: true })\n\n const fm: Record<string, string> = {\n id,\n name: input.name,\n status: 'open',\n created: now,\n updated: now,\n tags: tags.join(','),\n description: input.description,\n slug,\n }\n const body = [\n `# ${input.name}`,\n '',\n `Opened: ${now}`,\n `Status: open`,\n '',\n '## Question',\n '',\n input.description || 'TBD',\n '',\n '## Current Assessment',\n '',\n 'TBD',\n '',\n '## Top Findings',\n '',\n '| Finding | Confidence | Evidence |',\n '|---|---:|---|',\n '',\n '## Next Actions',\n '',\n '- TBD',\n '',\n '## Reports',\n '',\n ].join('\\n')\n await writeFile(path.join(dir, 'case.md'), serializeFrontmatter(fm, body), { mode: 0o600 })\n\n const manifest = JSON.stringify({ caseId: id, entries: [] }, null, 2) + '\\n'\n await writeFile(path.join(dir, 'manifest.json'), manifest, { mode: 0o600 })\n\n return CaseSchema.parse({ id, name: input.name, status: 'open', created: now, updated: now, tags, description: input.description, slug })\n },\n\n async setStatus(id: string, status: CaseStatus): Promise<Case> {\n const dir = caseDir(id)\n const filePath = path.join(dir, 'case.md')\n const raw = await readFile(filePath, 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n const now = new Date().toISOString()\n frontmatter['status'] = status\n frontmatter['updated'] = now\n await writeFile(filePath, serializeFrontmatter(frontmatter, body), { mode: 0o600 })\n\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n return CaseSchema.parse({\n id,\n name: frontmatter['name'] ?? '',\n status,\n created: frontmatter['created'] ?? now,\n updated: now,\n tags,\n description: frontmatter['description'] ?? '',\n })\n },\n\n async list(): Promise<Array<{ id: string; name: string; status: string }>> {\n const root = casesRoot()\n try {\n const ids = await readdir(root)\n const cases: Array<{ id: string; name: string; status: string; created: string }> = []\n for (const id of ids) {\n try {\n const raw = await readFile(path.join(caseDir(id), 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n cases.push({\n id,\n name: frontmatter['name'] ?? id,\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? '',\n })\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code !== 'ENOENT' && nodeErr.code !== 'ENOTDIR') throw err\n }\n }\n return cases\n .sort((a, b) => b.created.localeCompare(a.created) || b.id.localeCompare(a.id))\n .map(({ id, name, status }) => ({ id, name, status }))\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code === 'ENOENT') return []\n throw err\n }\n },\n\n async get(id: string): Promise<Case> {\n const dir = caseDir(id)\n const raw = await readFile(path.join(dir, 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n return CaseSchema.parse({\n id,\n name: frontmatter['name'] ?? '',\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? new Date().toISOString(),\n updated: frontmatter['updated'] ?? new Date().toISOString(),\n tags,\n description: frontmatter['description'] ?? '',\n })\n },\n\n async loadContext(id: string): Promise<{\n case: { id: string; name: string; status: string; created: string; updated: string; tags: string[] };\n lastSession: { sessionId: string; startTime: string; endTime?: string; body: string } | null;\n dossierSummaries: Array<{ address: string; type: string; riskTags: string; firstSeen: string; lastSeen: string }>;\n evidenceCount: number;\n }> {\n const dir = caseDir(id)\n\n // Read case.md\n const raw = await readFile(path.join(dir, 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n\n // Lazy imports to avoid circular deps\n const { SessionStore } = await import('./session.js')\n const { DossierStore } = await import('./dossier.js')\n\n const [latestSession, dossierSummaries, manifest] = await Promise.all([\n SessionStore.getLatest(id),\n DossierStore.listSummaries(id),\n readFile(path.join(dir, 'manifest.json'), 'utf8').catch(() => '{\"entries\":[]}'),\n ])\n\n const manifestData = JSON.parse(manifest) as { entries: unknown[] }\n const evidenceCount = manifestData.entries.length\n\n const lastSession = latestSession\n ? {\n sessionId: latestSession.frontmatter['sessionId'] ?? '',\n startTime: latestSession.frontmatter['startTime'] ?? '',\n endTime: latestSession.frontmatter['endTime'] || undefined,\n body: latestSession.body,\n }\n : null\n\n return {\n case: {\n id,\n name: frontmatter['name'] ?? '',\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? '',\n updated: frontmatter['updated'] ?? '',\n tags,\n },\n lastSession,\n dossierSummaries,\n evidenceCount,\n }\n },\n}\n"],"mappings":"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"}
@@ -53,6 +53,11 @@ zod.object({
53
53
  });
54
54
  //#endregion
55
55
  //#region src/cases/store.ts
56
+ var store_exports = /* @__PURE__ */ require_chunk.__exportAll({
57
+ CaseStore: () => CaseStore,
58
+ casesRoot: () => casesRoot,
59
+ generateCaseId: () => generateCaseId
60
+ });
56
61
  const casesRoot = () => require_output_root.workspaceOutputPaths().casesRoot;
57
62
  function caseDir(id) {
58
63
  return node_path.default.join(casesRoot(), id);
@@ -193,8 +198,8 @@ const CaseStore = {
193
198
  const dir = caseDir(id);
194
199
  const { frontmatter } = require_frontmatter.parseFrontmatter(await (0, node_fs_promises.readFile)(node_path.default.join(dir, "case.md"), "utf8"));
195
200
  const tags = (frontmatter["tags"] ?? "").split(",").filter(Boolean);
196
- const { SessionStore } = await Promise.resolve().then(() => require("./session-DwyikazY.cjs"));
197
- const { DossierStore } = await Promise.resolve().then(() => require("./dossier-Br62hCG7.cjs"));
201
+ const { SessionStore } = await Promise.resolve().then(() => require("./session-BT7VpbAd.cjs")).then((n) => n.session_exports);
202
+ const { DossierStore } = await Promise.resolve().then(() => require("./dossier-BXy57V4-.cjs")).then((n) => n.dossier_exports);
198
203
  const [latestSession, dossierSummaries, manifest] = await Promise.all([
199
204
  SessionStore.getLatest(id),
200
205
  DossierStore.listSummaries(id),
@@ -223,8 +228,33 @@ const CaseStore = {
223
228
  }
224
229
  };
225
230
  //#endregion
226
- exports.CaseSchema = CaseSchema;
227
- exports.CaseStatusEnum = CaseStatusEnum;
228
- exports.CaseStore = CaseStore;
229
- exports.casesRoot = casesRoot;
230
- exports.generateCaseId = generateCaseId;
231
+ Object.defineProperty(exports, "CaseSchema", {
232
+ enumerable: true,
233
+ get: function() {
234
+ return CaseSchema;
235
+ }
236
+ });
237
+ Object.defineProperty(exports, "CaseStatusEnum", {
238
+ enumerable: true,
239
+ get: function() {
240
+ return CaseStatusEnum;
241
+ }
242
+ });
243
+ Object.defineProperty(exports, "CaseStore", {
244
+ enumerable: true,
245
+ get: function() {
246
+ return CaseStore;
247
+ }
248
+ });
249
+ Object.defineProperty(exports, "generateCaseId", {
250
+ enumerable: true,
251
+ get: function() {
252
+ return generateCaseId;
253
+ }
254
+ });
255
+ Object.defineProperty(exports, "store_exports", {
256
+ enumerable: true,
257
+ get: function() {
258
+ return store_exports;
259
+ }
260
+ });
@@ -9,6 +9,8 @@ var tool_visibility_exports = /* @__PURE__ */ __exportAll({
9
9
  const HIDDEN_REMOTE_TOOL_NAMES = new Set([
10
10
  "topup",
11
11
  "trace_funds",
12
+ "track_funds",
13
+ "scam_topology",
12
14
  "money_flows_between_exchanges",
13
15
  "address_connection_risk"
14
16
  ]);
@@ -20,9 +22,9 @@ function visibleRemoteTools(tools) {
20
22
  }
21
23
  function assertPublicMcpToolName(name) {
22
24
  if (!isHiddenRemoteToolName(name)) return;
23
- throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${name === "trace_funds" ? " Use track_funds instead." : ""}`);
25
+ throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${name === "trace_funds" ? " Use trace_victim_funds, trace_suspect_funds, or trace_deposit_sources instead." : name === "track_funds" ? " Use trace_victim_funds instead." : name === "scam_topology" ? " Use trace_suspect_funds instead." : ""}`);
24
26
  }
25
27
  //#endregion
26
28
  export { tool_visibility_exports as n, HIDDEN_REMOTE_TOOL_NAMES as t };
27
29
 
28
- //# sourceMappingURL=tool-visibility-BHRFLXuU.mjs.map
30
+ //# sourceMappingURL=tool-visibility-BpyZHRBi.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-visibility-BpyZHRBi.mjs","names":[],"sources":["../src/mcp/tool-visibility.ts"],"sourcesContent":["export const HIDDEN_REMOTE_TOOL_NAMES = new Set([\n 'topup',\n 'trace_funds',\n 'track_funds',\n 'scam_topology',\n 'money_flows_between_exchanges',\n 'address_connection_risk',\n])\n\nexport function isHiddenRemoteToolName(name: string): boolean {\n return HIDDEN_REMOTE_TOOL_NAMES.has(name)\n}\n\nexport function visibleRemoteTools<T extends { name: string }>(tools: T[]): T[] {\n return tools.filter((tool) => !isHiddenRemoteToolName(tool.name))\n}\n\nexport function assertPublicMcpToolName(name: string): void {\n if (!isHiddenRemoteToolName(name)) return\n const replacement = name === 'trace_funds'\n ? ' Use trace_victim_funds, trace_suspect_funds, or trace_deposit_sources instead.'\n : name === 'track_funds'\n ? ' Use trace_victim_funds instead.'\n : name === 'scam_topology'\n ? ' Use trace_suspect_funds instead.'\n : ''\n throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${replacement}`)\n}\n"],"mappings":";;;;;;;;AAAA,MAAa,2BAA2B,IAAI,IAAI;CAC9C;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAgB,uBAAuB,MAAuB;CAC5D,OAAO,yBAAyB,IAAI,IAAI;AAC1C;AAEA,SAAgB,mBAA+C,OAAiB;CAC9E,OAAO,MAAM,QAAQ,SAAS,CAAC,uBAAuB,KAAK,IAAI,CAAC;AAClE;AAEA,SAAgB,wBAAwB,MAAoB;CAC1D,IAAI,CAAC,uBAAuB,IAAI,GAAG;CAQnC,MAAM,IAAI,MAAM,aAAa,KAAK,qCAPd,SAAS,gBACzB,oFACA,SAAS,gBACP,qCACA,SAAS,kBACP,sCACA,IAC4E;AACtF"}
@@ -8,6 +8,8 @@ var tool_visibility_exports = /* @__PURE__ */ require("./chunk-DakpK96I.cjs").__
8
8
  const HIDDEN_REMOTE_TOOL_NAMES = new Set([
9
9
  "topup",
10
10
  "trace_funds",
11
+ "track_funds",
12
+ "scam_topology",
11
13
  "money_flows_between_exchanges",
12
14
  "address_connection_risk"
13
15
  ]);
@@ -19,7 +21,7 @@ function visibleRemoteTools(tools) {
19
21
  }
20
22
  function assertPublicMcpToolName(name) {
21
23
  if (!isHiddenRemoteToolName(name)) return;
22
- throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${name === "trace_funds" ? " Use track_funds instead." : ""}`);
24
+ throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${name === "trace_funds" ? " Use trace_victim_funds, trace_suspect_funds, or trace_deposit_sources instead." : name === "track_funds" ? " Use trace_victim_funds instead." : name === "scam_topology" ? " Use trace_suspect_funds instead." : ""}`);
23
25
  }
24
26
  //#endregion
25
27
  Object.defineProperty(exports, "HIDDEN_REMOTE_TOOL_NAMES", {
@@ -0,0 +1,35 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { r as truncateGraph } from "./data-extractor-DZUJu1Bz.mjs";
3
+ import { r as writeVizHtml, t as generateHtml } from "./html-generator-AowOmzyi.mjs";
4
+ import { readFile } from "node:fs/promises";
5
+ //#region src/viz/index.ts
6
+ var viz_exports = /* @__PURE__ */ __exportAll({ generateVisualization: () => generateVisualization });
7
+ async function generateVisualization(opts) {
8
+ let rawData;
9
+ if (opts.dataFile) {
10
+ const content = await readFile(opts.dataFile, "utf-8");
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(content);
14
+ } catch {
15
+ throw new Error("Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.");
16
+ }
17
+ const { extractGraphFromJson } = await import("./data-extractor-DZUJu1Bz.mjs").then((n) => n.t);
18
+ rawData = extractGraphFromJson(parsed);
19
+ } else if (opts.caseId) {
20
+ const { extractGraphFromCase } = await import("./data-extractor-DZUJu1Bz.mjs").then((n) => n.t);
21
+ const extracted = await extractGraphFromCase(opts.caseId);
22
+ if (extracted.nodes.length === 0) throw new Error("No Transaction Data. This case has no evidence with transaction data. Add evidence using `chain-insights evidence add` or provide a JSON file with `chain-insights viz --data <file.json>`.");
23
+ rawData = extracted;
24
+ } else throw new Error("Provide either a case ID or --data <file.json>");
25
+ const data = truncateGraph(rawData);
26
+ const vizId = opts.caseId ? `${opts.caseId}_${Date.now()}` : `adhoc_${Date.now()}`;
27
+ return {
28
+ vizId,
29
+ htmlPath: await writeVizHtml(vizId, generateHtml(data, data.metadata.caseId ? `${data.metadata.caseId} - Money Flow` : "Ad-hoc Visualization"), opts.caseId)
30
+ };
31
+ }
32
+ //#endregion
33
+ export { viz_exports as n, generateVisualization as t };
34
+
35
+ //# sourceMappingURL=viz-5y24S5X1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viz-5y24S5X1.mjs","names":[],"sources":["../src/viz/index.ts"],"sourcesContent":["export { GraphData, GraphNode, GraphEdge, EntityType, RiskLevel, truncateGraph } from './graph-model.js'\nexport type { GraphData as GraphDataType, GraphNode as GraphNodeType, GraphEdge as GraphEdgeType } from './graph-model.js'\nexport { generateHtml, writeVizHtml, transformToGraphHtml } from './html-generator.js'\nexport { DataExtractor, extractGraphFromCase, extractGraphFromJson } from './data-extractor.js'\n\nimport { readFile } from 'node:fs/promises'\nimport { truncateGraph } from './graph-model.js'\nimport { generateHtml, writeVizHtml } from './html-generator.js'\n\nexport async function generateVisualization(opts: {\n caseId?: string\n dataFile?: string\n}): Promise<{ vizId: string; htmlPath: string }> {\n let rawData: unknown\n\n if (opts.dataFile) {\n const content = await readFile(opts.dataFile, 'utf-8')\n let parsed: unknown\n try {\n parsed = JSON.parse(content)\n } catch {\n throw new Error('Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.')\n }\n const { extractGraphFromJson } = await import('./data-extractor.js')\n rawData = extractGraphFromJson(parsed)\n } else if (opts.caseId) {\n const { extractGraphFromCase } = await import('./data-extractor.js')\n const extracted = await extractGraphFromCase(opts.caseId)\n if (extracted.nodes.length === 0) {\n throw new Error('No Transaction Data. This case has no evidence with transaction data. Add evidence using `chain-insights evidence add` or provide a JSON file with `chain-insights viz --data <file.json>`.')\n }\n rawData = extracted\n } else {\n throw new Error('Provide either a case ID or --data <file.json>')\n }\n\n const data = truncateGraph(rawData as Parameters<typeof truncateGraph>[0])\n\n const vizId = opts.caseId\n ? `${opts.caseId}_${Date.now()}`\n : `adhoc_${Date.now()}`\n\n const title = data.metadata.caseId\n ? `${data.metadata.caseId} - Money Flow`\n : 'Ad-hoc Visualization'\n\n const html = generateHtml(data, title)\n const htmlPath = await writeVizHtml(vizId, html, opts.caseId)\n\n return { vizId, htmlPath }\n}\n"],"mappings":";;;;;;AASA,eAAsB,sBAAsB,MAGK;CAC/C,IAAI;CAEJ,IAAI,KAAK,UAAU;EACjB,MAAM,UAAU,MAAM,SAAS,KAAK,UAAU,OAAO;EACrD,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,OAAO;EAC7B,QAAQ;GACN,MAAM,IAAI,MAAM,kIAAkI;EACpJ;EACA,MAAM,EAAE,yBAAyB,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,CAAA;EAC9C,UAAU,qBAAqB,MAAM;CACvC,OAAO,IAAI,KAAK,QAAQ;EACtB,MAAM,EAAE,yBAAyB,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,CAAA;EAC9C,MAAM,YAAY,MAAM,qBAAqB,KAAK,MAAM;EACxD,IAAI,UAAU,MAAM,WAAW,GAC7B,MAAM,IAAI,MAAM,6LAA6L;EAE/M,UAAU;CACZ,OACE,MAAM,IAAI,MAAM,gDAAgD;CAGlE,MAAM,OAAO,cAAc,OAA8C;CAEzE,MAAM,QAAQ,KAAK,SACf,GAAG,KAAK,OAAO,GAAG,KAAK,IAAI,MAC3B,SAAS,KAAK,IAAI;CAStB,OAAO;EAAE;EAAO,UAAA,MAFO,aAAa,OADvB,aAAa,MAJZ,KAAK,SAAS,SACxB,GAAG,KAAK,SAAS,OAAO,iBACxB,sBAG0C,GAAG,KAAK,MAAM;CAEnC;AAC3B"}
@@ -0,0 +1,44 @@
1
+ const require_chunk = require("./chunk-DakpK96I.cjs");
2
+ const require_data_extractor = require("./data-extractor-Cavd7wHk.cjs");
3
+ const require_html_generator = require("./html-generator-Bx3UcLTB.cjs");
4
+ let node_fs_promises = require("node:fs/promises");
5
+ //#region src/viz/index.ts
6
+ var viz_exports = /* @__PURE__ */ require_chunk.__exportAll({ generateVisualization: () => generateVisualization });
7
+ async function generateVisualization(opts) {
8
+ let rawData;
9
+ if (opts.dataFile) {
10
+ const content = await (0, node_fs_promises.readFile)(opts.dataFile, "utf-8");
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(content);
14
+ } catch {
15
+ throw new Error("Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.");
16
+ }
17
+ const { extractGraphFromJson } = await Promise.resolve().then(() => require("./data-extractor-Cavd7wHk.cjs")).then((n) => n.data_extractor_exports);
18
+ rawData = extractGraphFromJson(parsed);
19
+ } else if (opts.caseId) {
20
+ const { extractGraphFromCase } = await Promise.resolve().then(() => require("./data-extractor-Cavd7wHk.cjs")).then((n) => n.data_extractor_exports);
21
+ const extracted = await extractGraphFromCase(opts.caseId);
22
+ if (extracted.nodes.length === 0) throw new Error("No Transaction Data. This case has no evidence with transaction data. Add evidence using `chain-insights evidence add` or provide a JSON file with `chain-insights viz --data <file.json>`.");
23
+ rawData = extracted;
24
+ } else throw new Error("Provide either a case ID or --data <file.json>");
25
+ const data = require_data_extractor.truncateGraph(rawData);
26
+ const vizId = opts.caseId ? `${opts.caseId}_${Date.now()}` : `adhoc_${Date.now()}`;
27
+ return {
28
+ vizId,
29
+ htmlPath: await require_html_generator.writeVizHtml(vizId, require_html_generator.generateHtml(data, data.metadata.caseId ? `${data.metadata.caseId} - Money Flow` : "Ad-hoc Visualization"), opts.caseId)
30
+ };
31
+ }
32
+ //#endregion
33
+ Object.defineProperty(exports, "generateVisualization", {
34
+ enumerable: true,
35
+ get: function() {
36
+ return generateVisualization;
37
+ }
38
+ });
39
+ Object.defineProperty(exports, "viz_exports", {
40
+ enumerable: true,
41
+ get: function() {
42
+ return viz_exports;
43
+ }
44
+ });
@@ -18,8 +18,9 @@ Current AML tools live in the Chain Insights layer:
18
18
 
19
19
  - `address_risk`
20
20
  - `stake_insights`
21
- - `track_funds`
22
- - `scam_topology`
21
+ - `trace_victim_funds`
22
+ - `trace_deposit_sources`
23
+ - `trace_suspect_funds`
23
24
 
24
25
  When adding a tool, document:
25
26