chain-insights 0.2.32 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -14
- package/dist/cases-Cp9DUbEV.mjs +6 -0
- package/dist/{cases-c0iV-XLI.cjs → cases-sTY5aXav.cjs} +3 -3
- package/dist/cli.cjs +122 -66
- package/dist/cli.mjs +122 -66
- package/dist/cli.mjs.map +1 -1
- package/dist/{viz-Da9YWN_I.cjs → data-extractor-Cavd7wHk.cjs} +11 -34
- package/dist/{viz-DkJyqlUu.mjs → data-extractor-DZUJu1Bz.mjs} +3 -32
- package/dist/data-extractor-DZUJu1Bz.mjs.map +1 -0
- package/dist/{dossier-Br62hCG7.cjs → dossier-BXy57V4-.cjs} +13 -1
- package/dist/{dossier-Bl0NkJKC.mjs → dossier-Bjpcbcxa.mjs} +4 -2
- package/dist/{dossier-Bl0NkJKC.mjs.map → dossier-Bjpcbcxa.mjs.map} +1 -1
- package/dist/export-BqTCO9lP.mjs +591 -0
- package/dist/export-BqTCO9lP.mjs.map +1 -0
- package/dist/export-DsXgtCwO.cjs +592 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{init-DBC9Ml33.mjs → init-DLBL_nVG.mjs} +27 -1
- package/dist/{init-DBC9Ml33.mjs.map → init-DLBL_nVG.mjs.map} +1 -1
- package/dist/{init-CFaUWgjK.cjs → init-zqbd7i-_.cjs} +26 -0
- package/dist/mcp-proxy.cjs +215 -77
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +215 -77
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-BwguvIsf.cjs → public-tools-BvMb3H2P.cjs} +701 -1479
- package/dist/{public-tools-DoRNhMn9.mjs → public-tools-wJoAFDFa.mjs} +700 -1479
- package/dist/public-tools-wJoAFDFa.mjs.map +1 -0
- package/dist/{resolver-D7VBb0uB.mjs → resolver-2jXNtWQO.mjs} +12 -29
- package/dist/resolver-2jXNtWQO.mjs.map +1 -0
- package/dist/{resolver-BUU7ZgW-.cjs → resolver-CZdQwKvh.cjs} +11 -28
- package/dist/{runner-BCDeBYsR.cjs → runner-BhZ4lnF1.cjs} +2 -2
- package/dist/{runner-CTFK0Qcg.mjs → runner-DIJSbkjc.mjs} +3 -3
- package/dist/{runner-CTFK0Qcg.mjs.map → runner-DIJSbkjc.mjs.map} +1 -1
- package/dist/{selector-CTUiQrzI.mjs → selector-CF2o5gxN.mjs} +2 -2
- package/dist/{selector-CTUiQrzI.mjs.map → selector-CF2o5gxN.mjs.map} +1 -1
- package/dist/{selector-DBS2jYH4.cjs → selector-DfAMZEC9.cjs} +1 -1
- package/dist/{session-DwyikazY.cjs → session-BT7VpbAd.cjs} +13 -1
- package/dist/{session-Bha3zFrx.mjs → session-DROyhebe.mjs} +4 -2
- package/dist/{session-Bha3zFrx.mjs.map → session-DROyhebe.mjs.map} +1 -1
- package/dist/{store-BT2SCcQr.mjs → store-CTtqQtaE.mjs} +10 -4
- package/dist/{store-BT2SCcQr.mjs.map → store-CTtqQtaE.mjs.map} +1 -1
- package/dist/{store-DogLawSj.cjs → store-CqPfs47P.cjs} +37 -7
- package/dist/{tool-visibility-BHRFLXuU.mjs → tool-visibility-BpyZHRBi.mjs} +4 -2
- package/dist/tool-visibility-BpyZHRBi.mjs.map +1 -0
- package/dist/{tool-visibility-iAVQV3t0.cjs → tool-visibility-Buq7YdUZ.cjs} +3 -1
- package/dist/viz-5y24S5X1.mjs +35 -0
- package/dist/viz-5y24S5X1.mjs.map +1 -0
- package/dist/viz-Dqp3C5kb.cjs +44 -0
- package/docs/contributing.md +3 -2
- package/docs/graph-tools.md +125 -117
- package/docs/investigation-workspaces.md +14 -0
- package/docs/mcp-proxy.md +15 -2
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +6 -0
- package/skills/chain-insights-developer-experience/SKILL.md +26 -6
- package/skills/chain-insights-investigation/SKILL.md +64 -48
- package/skills/chain-insights-trace-funds/SKILL.md +80 -197
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +1 -1
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +4 -4
- package/dist/cases-qjPtbnUd.mjs +0 -6
- package/dist/public-tools-DoRNhMn9.mjs.map +0 -1
- package/dist/resolver-D7VBb0uB.mjs.map +0 -1
- package/dist/tool-visibility-BHRFLXuU.mjs.map +0 -1
- package/dist/viz-DkJyqlUu.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-BqTCO9lP.mjs","names":[],"sources":["../src/export/paths.ts","../src/export/schema.ts","../src/export/canvas.ts","../src/export/graph.ts","../src/export/markdown.ts","../src/export/redaction.ts","../src/export/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { lstat, mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport function safeSlug(value: string): string {\n const slug = value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80)\n return slug || 'case-export'\n}\n\nexport function safeFilename(value: string): string {\n const parsed = path.parse(value)\n const name = safeSlug(parsed.name)\n const ext = parsed.ext.toLowerCase().replace(/[^.a-z0-9]/g, '')\n return `${name}${ext || '.md'}`\n}\n\nexport function assertInsideDirectory(root: string, candidate: string): void {\n const resolvedRoot = path.resolve(root)\n const resolvedCandidate = path.resolve(candidate)\n const relative = path.relative(resolvedRoot, resolvedCandidate)\n if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) return\n throw new Error(`Refusing to write outside export directory: ${candidate}`)\n}\n\nexport async function assertNoSymlink(filePath: string): Promise<void> {\n try {\n const stat = await lstat(filePath)\n if (stat.isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${filePath}`)\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return\n throw err\n }\n}\n\nexport async function writePrivateFile(\n root: string,\n relativePath: string,\n content: string,\n): Promise<{ path: string; sha256: string; bytes: number }> {\n const filePath = path.join(root, relativePath)\n assertInsideDirectory(root, filePath)\n await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 })\n await assertNoSymlink(filePath)\n await writeFile(filePath, content, { mode: 0o600 })\n const bytes = Buffer.byteLength(content, 'utf8')\n const sha256 = createHash('sha256').update(content).digest('hex')\n return { path: relativePath, sha256, bytes }\n}\n","import * as z from 'zod'\n\nexport const CaseExportTargetSchema = z.enum(['obsidian-llmwiki'])\nexport type CaseExportTarget = z.infer<typeof CaseExportTargetSchema>\n\nexport const CaseExportModeSchema = z.enum(['private', 'partner', 'public'])\nexport type CaseExportMode = z.infer<typeof CaseExportModeSchema>\n\nconst caseIdRegex = /^\\d{8}_\\d{3}_[a-z0-9][a-z0-9-]*$/\n\nexport const CaseExportOptionsSchema = z.object({\n caseId: z.string().regex(caseIdRegex),\n target: CaseExportTargetSchema.default('obsidian-llmwiki'),\n mode: CaseExportModeSchema.default('private'),\n outputDir: z.string().optional(),\n})\nexport type CaseExportOptions = z.infer<typeof CaseExportOptionsSchema>\n\nexport const ExportedFileSchema = z.object({\n path: z.string().min(1),\n sha256: z.string().regex(/^[a-f0-9]{64}$/),\n bytes: z.number().int().nonnegative(),\n})\nexport type ExportedFile = z.infer<typeof ExportedFileSchema>\n\nexport const CaseExportManifestSchema = z.object({\n schema: z.literal('chain-insights.case_export.v1'),\n case_id: z.string().regex(caseIdRegex),\n case_name: z.string().min(1),\n exported_at: z.string().datetime(),\n mode: CaseExportModeSchema,\n target: CaseExportTargetSchema,\n source_workspace: z.string().min(1),\n verification: z.object({\n evidence_manifest_verified: z.boolean(),\n verified_at: z.string().datetime(),\n evidence_count: z.number().int().nonnegative(),\n }),\n files: z.array(ExportedFileSchema),\n redactions: z.array(z.string()),\n warnings: z.array(z.string()),\n})\nexport type CaseExportManifest = z.infer<typeof CaseExportManifestSchema>\n\nexport const JsonCanvasNodeSchema = z.object({\n id: z.string().min(1),\n type: z.enum(['text', 'file', 'link', 'group']),\n x: z.number(),\n y: z.number(),\n width: z.number().positive(),\n height: z.number().positive(),\n text: z.string().optional(),\n file: z.string().optional(),\n url: z.string().optional(),\n label: z.string().optional(),\n color: z.string().optional(),\n})\n\nexport const JsonCanvasEdgeSchema = z.object({\n id: z.string().min(1),\n fromNode: z.string().min(1),\n toNode: z.string().min(1),\n fromSide: z.enum(['top', 'right', 'bottom', 'left']).optional(),\n toSide: z.enum(['top', 'right', 'bottom', 'left']).optional(),\n toEnd: z.enum(['none', 'arrow']).optional(),\n label: z.string().optional(),\n color: z.string().optional(),\n})\n\nexport const JsonCanvasSchema = z.object({\n nodes: z.array(JsonCanvasNodeSchema),\n edges: z.array(JsonCanvasEdgeSchema),\n})\nexport type JsonCanvas = z.infer<typeof JsonCanvasSchema>\n\nexport type CaseExportResult = {\n manifestPath: string\n outputDir: string\n fileCount: number\n warnings: string[]\n nextFile: string\n}\n","import { safeFilename } from './paths.js'\nimport { JsonCanvasSchema, type JsonCanvas } from './schema.js'\n\nfunction roleColor(roles: string[]): string {\n if (roles.includes('victim')) return '1'\n if (roles.includes('suspect') || roles.includes('scam_candidate')) return '2'\n if (roles.includes('deposit')) return '3'\n if (roles.includes('exchange')) return '5'\n if (roles.includes('service')) return '6'\n return '#808080'\n}\n\nfunction nodeRoles(node: Record<string, unknown>): string[] {\n return Array.isArray(node['roles']) ? node['roles'].map(String) : []\n}\n\nfunction nodeLabel(node: Record<string, unknown>): string {\n return String(node['address'] ?? node['id'] ?? 'unknown')\n}\n\nexport function graphNodeId(node: Record<string, unknown>, index: number): string {\n return String(node['id'] ?? node['address'] ?? `node-${index + 1}`)\n}\n\nexport function entityNotePath(entityId: string): string {\n return `Entities/${safeFilename(entityId)}`\n}\n\nexport function graphToCanvas(graph: { nodes: Record<string, unknown>[]; edges: Record<string, unknown>[] }): JsonCanvas {\n const nodes = [\n {\n id: 'case',\n type: 'file' as const,\n file: 'Case.md',\n x: 0,\n y: 0,\n width: 360,\n height: 120,\n color: '4',\n },\n ]\n\n const nodeIdMap = new Map<string, string>()\n graph.nodes.forEach((node, index) => {\n const rawId = graphNodeId(node, index)\n const canvasId = `entity-${index + 1}`\n nodeIdMap.set(rawId, canvasId)\n nodes.push({\n id: canvasId,\n type: 'file' as const,\n file: entityNotePath(rawId),\n x: 420 + (index % 4) * 340,\n y: Math.floor(index / 4) * 220,\n width: 300,\n height: 120,\n color: roleColor(nodeRoles(node)),\n })\n })\n\n const edges = graph.edges.flatMap((edge, index) => {\n const from = nodeIdMap.get(String(edge['source'] ?? ''))\n const to = nodeIdMap.get(String(edge['target'] ?? ''))\n if (!from || !to) return []\n return [{\n id: `edge-${index + 1}`,\n fromNode: from,\n toNode: to,\n fromSide: 'right' as const,\n toSide: 'left' as const,\n toEnd: 'arrow' as const,\n label: String(edge['edge_type'] ?? 'related_to'),\n }]\n })\n\n for (const [index, node] of graph.nodes.entries()) {\n edges.push({\n id: `case-link-${index + 1}`,\n fromNode: 'case',\n toNode: `entity-${index + 1}`,\n fromSide: 'right' as const,\n toSide: 'left' as const,\n toEnd: 'arrow' as const,\n label: nodeLabel(node),\n })\n }\n\n return JsonCanvasSchema.parse({ nodes, edges })\n}\n","import { readFile, readdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { normalizeGraphPayload, type NormalizedGraphPayload } from '../viz/graph-normalizer.js'\nimport { extractGraphFromCase } from '../viz/data-extractor.js'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction nodeId(node: Record<string, unknown>): string {\n return String(node['id'] ?? node['address'] ?? '')\n}\n\nfunction edgeKey(edge: Record<string, unknown>): string {\n return `${String(edge['source'] ?? '')}->${String(edge['target'] ?? '')}:${String(edge['edge_type'] ?? 'related_to')}`\n}\n\nfunction mergeGraphs(graphs: NormalizedGraphPayload[]): NormalizedGraphPayload {\n const nodes = new Map<string, Record<string, unknown>>()\n const edges = new Map<string, Record<string, unknown>>()\n for (const graph of graphs) {\n for (const rawNode of graph.nodes) {\n const id = nodeId(rawNode)\n if (id) nodes.set(id, { ...(nodes.get(id) ?? {}), ...rawNode, id })\n }\n for (const rawEdge of graph.edges) {\n if (typeof rawEdge['source'] !== 'string' || typeof rawEdge['target'] !== 'string') continue\n edges.set(edgeKey(rawEdge), { ...(edges.get(edgeKey(rawEdge)) ?? {}), ...rawEdge })\n }\n }\n return {\n schema: 'chain-insights.graph.v1',\n nodes: [...nodes.values()],\n edges: [...edges.values()],\n flows: graphs.flatMap(graph => graph.flows),\n edge_anchors: graphs.flatMap(graph => graph.edge_anchors),\n metadata: {\n source: 'case-export',\n graph_count: graphs.length,\n generated_at: new Date().toISOString(),\n },\n }\n}\n\nexport async function loadCaseExportGraph(caseId: string): Promise<NormalizedGraphPayload> {\n const paths = workspaceOutputPaths()\n const files = await readdir(paths.reportGraphsRoot).catch(() => [])\n const graphs: NormalizedGraphPayload[] = []\n for (const file of files.filter(name => name.endsWith('.graph.json')).sort()) {\n const parsed = JSON.parse(await readFile(path.join(paths.reportGraphsRoot, file), 'utf8')) as unknown\n if (isRecord(parsed) && parsed['schema'] === 'chain-insights.graph.v1') {\n graphs.push(normalizeGraphPayload(parsed))\n }\n }\n if (graphs.length > 0) return mergeGraphs(graphs)\n\n const fallback = await extractGraphFromCase(caseId)\n return normalizeGraphPayload({\n schema: 'chain-insights.graph.v1',\n nodes: fallback.nodes,\n edges: fallback.edges,\n flows: [],\n edge_anchors: [],\n metadata: fallback.metadata,\n })\n}\n","import type { CaseExportMode } from './schema.js'\n\nexport type MarkdownCase = {\n id: string\n name: string\n status: string\n tags: string[]\n description?: string\n}\n\nexport function frontmatter(values: Record<string, unknown>): string {\n const lines = ['---']\n for (const [key, value] of Object.entries(values)) {\n if (Array.isArray(value)) {\n lines.push(`${key}:`)\n for (const item of value) lines.push(` - ${JSON.stringify(String(item))}`)\n } else {\n lines.push(`${key}: ${JSON.stringify(value)}`)\n }\n }\n lines.push('---', '')\n return lines.join('\\n')\n}\n\nexport function renderReadme(caseName: string): string {\n return [\n `# ${caseName} Export`,\n '',\n 'Open this directory as an Obsidian vault or give it to an LLMWiki-style knowledge workflow.',\n '',\n 'Start with:',\n '',\n '- `Case.md`',\n '- `Agent Console.md`',\n '- `LLMWIKI.md`',\n '- `graph.chain-insights.json` when present',\n '',\n ].join('\\n')\n}\n\nexport function renderCaseMarkdown(input: {\n caseInfo: MarkdownCase\n mode: CaseExportMode\n evidenceVerified: boolean\n evidenceCount: number\n entityCount: number\n}): string {\n return frontmatter({\n type: 'chain-insights-case',\n case_id: input.caseInfo.id,\n status: input.caseInfo.status,\n tags: input.caseInfo.tags,\n contains_sensitive_data: input.mode !== 'public',\n }) + [\n `# ${input.caseInfo.name}`,\n '',\n `Case ID: \\`${input.caseInfo.id}\\``,\n `Status: ${input.caseInfo.status}`,\n `Evidence manifest: ${input.evidenceVerified ? 'verified' : 'failed'}`,\n `Evidence files: ${input.evidenceCount}`,\n `Entities: ${input.entityCount}`,\n '',\n '## Summary',\n '',\n input.caseInfo.description || 'No description recorded.',\n '',\n '## Start Here',\n '',\n '- [[Agent Console]]',\n '- [[LLMWIKI]]',\n '- [[Sources/evidence-manifest]]',\n '',\n ].join('\\n')\n}\n\nexport function renderAgentConsole(caseName: string): string {\n return [\n '# Agent Console',\n '',\n `Case: [[Case|${caseName}]]`,\n '',\n '## Reading Order',\n '',\n '1. [[Case]]',\n '2. [[LLMWIKI]]',\n '3. `graph.chain-insights.json`',\n '4. [[Sources/evidence-manifest]]',\n '5. Entity and evidence notes linked from the case.',\n '',\n '## Agent Prompts',\n '',\n '- [[Prompts/Codex]]',\n '- [[Prompts/Claude-Code]]',\n '- [[Prompts/ChatGPT]]',\n '',\n '## Rules',\n '',\n '- Treat Chain Insights case evidence and manifests as canonical.',\n '- Use Chain Insights tools for fresh graph facts.',\n '- Preserve full blockchain addresses exactly unless this is a public redacted export.',\n '',\n ].join('\\n')\n}\n\nexport function renderLlmWiki(): string {\n return [\n '# LLMWiki Entry',\n '',\n 'This directory is a Chain Insights case export.',\n '',\n 'Canonical machine files:',\n '',\n '- `manifest.chain-insights.json`',\n '- `graph.chain-insights.json`',\n '- `Graph.canvas`',\n '',\n 'Human and agent notes:',\n '',\n '- `Case.md`',\n '- `Agent Console.md`',\n '- `Entities/`',\n '- `Evidence/`',\n '- `Prompts/`',\n '',\n ].join('\\n')\n}\n\nexport function renderLlmsTxt(): string {\n return [\n '# Chain Insights Case Export',\n '',\n 'Read these files first:',\n '- Case.md',\n '- Agent Console.md',\n '- graph.chain-insights.json',\n '- Entities/',\n '- Evidence/',\n '',\n 'Source of truth:',\n '- manifest.chain-insights.json',\n '- Sources/evidence-manifest.md',\n '',\n ].join('\\n')\n}\n\nexport function renderPrompt(agentName: string): string {\n return [\n `# ${agentName} Case Prompt`,\n '',\n 'You are reading a Chain Insights case export.',\n '',\n 'Treat `manifest.chain-insights.json`, `Sources/evidence-manifest.md`, and original case evidence as canonical.',\n 'Use generated prose for orientation, not as a replacement for evidence.',\n 'Use Chain Insights MCP tools for fresh graph facts when available.',\n '',\n ].join('\\n')\n}\n","import type { CaseExportMode } from './schema.js'\n\nconst EVM_ADDRESS_RE = /\\b0x[a-fA-F0-9]{40}\\b/g\nconst SUBSTRATE_ADDRESS_RE = /\\b5[1-9A-HJ-NP-Za-km-z]{20,64}\\b/g\nconst SECRET_PATTERNS = [\n /\\bci_test_[A-Za-z0-9_-]+\\b/g,\n /\\b(?:privateKey|walletPrivateKey|secret|token|authorization)\\s*[:=]\\s*[\"']?[^\"'\\s]+/gi,\n /\\b0x[a-fA-F0-9]{64}\\b/g,\n]\n\nexport type Redactor = {\n text(input: string): string\n value<T>(input: T): T\n aliasFor(address: string): string\n redactions(): string[]\n}\n\nexport function createRedactor(mode: CaseExportMode): Redactor {\n const aliases = new Map<string, string>()\n const redactions = new Set<string>()\n\n function aliasFor(address: string): string {\n const existing = aliases.get(address)\n if (existing) return existing\n const alias = `addr_${String(aliases.size + 1).padStart(3, '0')}`\n aliases.set(address, alias)\n redactions.add(`aliased:${alias}`)\n return alias\n }\n\n function redactSecrets(input: string): string {\n let output = input\n for (const pattern of SECRET_PATTERNS) {\n output = output.replace(pattern, () => {\n redactions.add('secret')\n return '[redacted-secret]'\n })\n }\n return output\n }\n\n function text(input: string): string {\n let output = redactSecrets(input)\n if (mode === 'public') {\n output = output.replace(SUBSTRATE_ADDRESS_RE, match => aliasFor(match))\n output = output.replace(EVM_ADDRESS_RE, match => aliasFor(match))\n }\n return output\n }\n\n function value<T>(input: T): T {\n if (typeof input === 'string') return text(input) as T\n if (Array.isArray(input)) return input.map(item => value(item)) as T\n if (input && typeof input === 'object') {\n return Object.fromEntries(\n Object.entries(input).map(([key, entry]) => [key, value(entry)]),\n ) as T\n }\n return input\n }\n\n return {\n text,\n value,\n aliasFor,\n redactions: () => [...redactions].sort(),\n }\n}\n","import { mkdir, readFile, readdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { CaseStore, DossierStore, EvidenceStore, parseFrontmatter } from '../cases/index.js'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { entityNotePath, graphNodeId, graphToCanvas } from './canvas.js'\nimport { loadCaseExportGraph } from './graph.js'\nimport { renderAgentConsole, renderCaseMarkdown, renderLlmsTxt, renderLlmWiki, renderPrompt, renderReadme } from './markdown.js'\nimport { safeFilename, safeSlug, writePrivateFile } from './paths.js'\nimport { createRedactor } from './redaction.js'\nimport { CaseExportManifestSchema, CaseExportOptionsSchema, type CaseExportOptions, type CaseExportResult, type ExportedFile } from './schema.js'\n\ntype EvidenceDoc = {\n id: string\n filename: string\n source: string\n timestamp: string\n body: string\n}\n\nasync function readEvidence(caseId: string): Promise<EvidenceDoc[]> {\n const paths = workspaceOutputPaths()\n const dir = path.join(paths.casesRoot, caseId, 'evidence')\n const files = await readdir(dir).catch(() => [])\n const docs: EvidenceDoc[] = []\n for (const filename of files.filter(file => file.endsWith('.md')).sort()) {\n const raw = await readFile(path.join(dir, filename), 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n docs.push({\n id: frontmatter['id'] || filename.replace(/\\.md$/, ''),\n filename,\n source: frontmatter['source'] || 'unknown',\n timestamp: frontmatter['timestamp'] || '',\n body,\n })\n }\n return docs\n}\n\nasync function writeFiles(root: string, entries: Array<[string, string]>): Promise<ExportedFile[]> {\n const written: ExportedFile[] = []\n for (const [relativePath, content] of entries) {\n written.push(await writePrivateFile(root, relativePath, content))\n }\n return written\n}\n\nexport async function exportCase(rawOptions: CaseExportOptions): Promise<CaseExportResult> {\n const options = CaseExportOptionsSchema.parse(rawOptions)\n const workspace = workspaceOutputPaths()\n const caseInfo = await CaseStore.get(options.caseId)\n const redactor = createRedactor(options.mode)\n const evidenceVerification = await EvidenceStore.verifyManifest(options.caseId)\n const evidenceDocs = await readEvidence(options.caseId)\n const dossiers = await DossierStore.listSummaries(options.caseId)\n const graph = redactor.value(await loadCaseExportGraph(options.caseId))\n const canvas = graphToCanvas(graph)\n const outputRoot = path.resolve(options.outputDir ?? path.join(workspace.root, 'published', safeSlug(caseInfo.name)))\n await mkdir(outputRoot, { recursive: true, mode: 0o700 })\n\n const entries: Array<[string, string]> = [\n ['README.md', renderReadme(redactor.text(caseInfo.name))],\n ['Case.md', renderCaseMarkdown({\n caseInfo: {\n id: caseInfo.id,\n name: redactor.text(caseInfo.name),\n status: caseInfo.status,\n tags: caseInfo.tags,\n description: redactor.text(caseInfo.description),\n },\n mode: options.mode,\n evidenceVerified: evidenceVerification.ok,\n evidenceCount: evidenceVerification.count,\n entityCount: dossiers.length,\n })],\n ['LLMWIKI.md', renderLlmWiki()],\n ['llms.txt', renderLlmsTxt()],\n ['Agent Console.md', renderAgentConsole(redactor.text(caseInfo.name))],\n ['Prompts/Codex.md', renderPrompt('Codex')],\n ['Prompts/Claude-Code.md', renderPrompt('Claude Code')],\n ['Prompts/ChatGPT.md', renderPrompt('ChatGPT')],\n ['Sources/evidence-manifest.md', `# Evidence Manifest\\n\\nVerified: ${evidenceVerification.ok ? 'yes' : 'no'}\\nEvidence files: ${evidenceVerification.count}\\n`],\n ['Sources/reports-index.md', '# Reports Index\\n\\nGraph and report artifacts are exported when present.\\n'],\n ['graph.chain-insights.json', JSON.stringify(graph, null, 2) + '\\n'],\n ['Graph.canvas', JSON.stringify(canvas, null, 2) + '\\n'],\n ]\n\n for (const evidence of evidenceDocs) {\n entries.push([\n path.join('Evidence', safeFilename(evidence.id)),\n redactor.text([\n `# Evidence: ${evidence.source}`,\n '',\n `Source file: \\`${evidence.filename}\\``,\n `Captured: ${evidence.timestamp || 'unknown'}`,\n '',\n evidence.body,\n '',\n ].join('\\n')),\n ])\n }\n\n const entityPaths = new Set<string>()\n for (const dossier of dossiers) {\n const entityId = options.mode === 'public' ? redactor.aliasFor(dossier.address) : dossier.address\n const entityPath = path.join('Entities', safeFilename(entityId))\n entityPaths.add(entityPath)\n entries.push([\n entityPath,\n redactor.text([\n `# Entity: ${entityId}`,\n '',\n `Type: ${dossier.type}`,\n `First seen: ${dossier.firstSeen || 'unknown'}`,\n `Last seen: ${dossier.lastSeen || 'unknown'}`,\n `Risk tags: ${dossier.riskTags || 'none'}`,\n '',\n ].join('\\n')),\n ])\n }\n\n for (const [index, node] of graph.nodes.entries()) {\n const entityId = graphNodeId(node, index)\n const entityPath = entityNotePath(entityId)\n if (entityPaths.has(entityPath)) continue\n entityPaths.add(entityPath)\n entries.push([\n entityPath,\n [\n `# Entity: ${entityId}`,\n '',\n `Address: ${String(node['address'] ?? entityId)}`,\n `Roles: ${Array.isArray(node['roles']) ? node['roles'].map(String).join(', ') || 'none' : 'none'}`,\n `Node type: ${String(node['node_type'] ?? 'unknown')}`,\n '',\n '## Graph Links',\n '',\n '- [[Graph.canvas]]',\n '',\n ].join('\\n'),\n ])\n }\n\n const files = await writeFiles(outputRoot, entries)\n const exportedAt = new Date().toISOString()\n const manifest = CaseExportManifestSchema.parse({\n schema: 'chain-insights.case_export.v1',\n case_id: caseInfo.id,\n case_name: redactor.text(caseInfo.name),\n exported_at: exportedAt,\n mode: options.mode,\n target: options.target,\n source_workspace: workspace.root,\n verification: {\n evidence_manifest_verified: evidenceVerification.ok,\n verified_at: exportedAt,\n evidence_count: evidenceVerification.count,\n },\n files,\n redactions: redactor.redactions(),\n warnings: evidenceVerification.ok ? [] : [`Evidence manifest failed: ${(evidenceVerification.tampered ?? []).join(', ')}`],\n })\n const manifestFile = await writePrivateFile(outputRoot, 'manifest.chain-insights.json', JSON.stringify(manifest, null, 2) + '\\n')\n\n return {\n manifestPath: path.join(outputRoot, manifestFile.path),\n outputDir: outputRoot,\n fileCount: files.length + 1,\n warnings: manifest.warnings,\n nextFile: 'Agent Console.md',\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-DakpK96I.cjs");
|
|
2
|
+
const require_data_extractor = require("./data-extractor-Cavd7wHk.cjs");
|
|
3
|
+
const require_frontmatter = require("./frontmatter-Dvqa5HX6.cjs");
|
|
4
|
+
const require_output_root = require("./output-root-YIbl6PwF.cjs");
|
|
5
|
+
const require_dossier = require("./dossier-BXy57V4-.cjs");
|
|
6
|
+
const require_store = require("./store-CqPfs47P.cjs");
|
|
7
|
+
const require_evidence = require("./evidence-CvEesemA.cjs");
|
|
8
|
+
require("./cases-sTY5aXav.cjs");
|
|
9
|
+
const require_graph_normalizer = require("./graph-normalizer-DbjlbMpz.cjs");
|
|
10
|
+
let node_path = require("node:path");
|
|
11
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
12
|
+
let node_fs_promises = require("node:fs/promises");
|
|
13
|
+
let zod = require("zod");
|
|
14
|
+
zod = require_chunk.__toESM(zod, 1);
|
|
15
|
+
let node_crypto = require("node:crypto");
|
|
16
|
+
//#region src/export/paths.ts
|
|
17
|
+
function safeSlug(value) {
|
|
18
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "case-export";
|
|
19
|
+
}
|
|
20
|
+
function safeFilename(value) {
|
|
21
|
+
const parsed = node_path.default.parse(value);
|
|
22
|
+
return `${safeSlug(parsed.name)}${parsed.ext.toLowerCase().replace(/[^.a-z0-9]/g, "") || ".md"}`;
|
|
23
|
+
}
|
|
24
|
+
function assertInsideDirectory(root, candidate) {
|
|
25
|
+
const resolvedRoot = node_path.default.resolve(root);
|
|
26
|
+
const resolvedCandidate = node_path.default.resolve(candidate);
|
|
27
|
+
const relative = node_path.default.relative(resolvedRoot, resolvedCandidate);
|
|
28
|
+
if (relative === "" || !relative.startsWith("..") && !node_path.default.isAbsolute(relative)) return;
|
|
29
|
+
throw new Error(`Refusing to write outside export directory: ${candidate}`);
|
|
30
|
+
}
|
|
31
|
+
async function assertNoSymlink(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
if ((await (0, node_fs_promises.lstat)(filePath)).isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${filePath}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.code === "ENOENT") return;
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function writePrivateFile(root, relativePath, content) {
|
|
40
|
+
const filePath = node_path.default.join(root, relativePath);
|
|
41
|
+
assertInsideDirectory(root, filePath);
|
|
42
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
mode: 448
|
|
45
|
+
});
|
|
46
|
+
await assertNoSymlink(filePath);
|
|
47
|
+
await (0, node_fs_promises.writeFile)(filePath, content, { mode: 384 });
|
|
48
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
49
|
+
return {
|
|
50
|
+
path: relativePath,
|
|
51
|
+
sha256: (0, node_crypto.createHash)("sha256").update(content).digest("hex"),
|
|
52
|
+
bytes
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/export/schema.ts
|
|
57
|
+
const CaseExportTargetSchema = zod.enum(["obsidian-llmwiki"]);
|
|
58
|
+
const CaseExportModeSchema = zod.enum([
|
|
59
|
+
"private",
|
|
60
|
+
"partner",
|
|
61
|
+
"public"
|
|
62
|
+
]);
|
|
63
|
+
const caseIdRegex = /^\d{8}_\d{3}_[a-z0-9][a-z0-9-]*$/;
|
|
64
|
+
const CaseExportOptionsSchema = zod.object({
|
|
65
|
+
caseId: zod.string().regex(caseIdRegex),
|
|
66
|
+
target: CaseExportTargetSchema.default("obsidian-llmwiki"),
|
|
67
|
+
mode: CaseExportModeSchema.default("private"),
|
|
68
|
+
outputDir: zod.string().optional()
|
|
69
|
+
});
|
|
70
|
+
const ExportedFileSchema = zod.object({
|
|
71
|
+
path: zod.string().min(1),
|
|
72
|
+
sha256: zod.string().regex(/^[a-f0-9]{64}$/),
|
|
73
|
+
bytes: zod.number().int().nonnegative()
|
|
74
|
+
});
|
|
75
|
+
const CaseExportManifestSchema = zod.object({
|
|
76
|
+
schema: zod.literal("chain-insights.case_export.v1"),
|
|
77
|
+
case_id: zod.string().regex(caseIdRegex),
|
|
78
|
+
case_name: zod.string().min(1),
|
|
79
|
+
exported_at: zod.string().datetime(),
|
|
80
|
+
mode: CaseExportModeSchema,
|
|
81
|
+
target: CaseExportTargetSchema,
|
|
82
|
+
source_workspace: zod.string().min(1),
|
|
83
|
+
verification: zod.object({
|
|
84
|
+
evidence_manifest_verified: zod.boolean(),
|
|
85
|
+
verified_at: zod.string().datetime(),
|
|
86
|
+
evidence_count: zod.number().int().nonnegative()
|
|
87
|
+
}),
|
|
88
|
+
files: zod.array(ExportedFileSchema),
|
|
89
|
+
redactions: zod.array(zod.string()),
|
|
90
|
+
warnings: zod.array(zod.string())
|
|
91
|
+
});
|
|
92
|
+
const JsonCanvasNodeSchema = zod.object({
|
|
93
|
+
id: zod.string().min(1),
|
|
94
|
+
type: zod.enum([
|
|
95
|
+
"text",
|
|
96
|
+
"file",
|
|
97
|
+
"link",
|
|
98
|
+
"group"
|
|
99
|
+
]),
|
|
100
|
+
x: zod.number(),
|
|
101
|
+
y: zod.number(),
|
|
102
|
+
width: zod.number().positive(),
|
|
103
|
+
height: zod.number().positive(),
|
|
104
|
+
text: zod.string().optional(),
|
|
105
|
+
file: zod.string().optional(),
|
|
106
|
+
url: zod.string().optional(),
|
|
107
|
+
label: zod.string().optional(),
|
|
108
|
+
color: zod.string().optional()
|
|
109
|
+
});
|
|
110
|
+
const JsonCanvasEdgeSchema = zod.object({
|
|
111
|
+
id: zod.string().min(1),
|
|
112
|
+
fromNode: zod.string().min(1),
|
|
113
|
+
toNode: zod.string().min(1),
|
|
114
|
+
fromSide: zod.enum([
|
|
115
|
+
"top",
|
|
116
|
+
"right",
|
|
117
|
+
"bottom",
|
|
118
|
+
"left"
|
|
119
|
+
]).optional(),
|
|
120
|
+
toSide: zod.enum([
|
|
121
|
+
"top",
|
|
122
|
+
"right",
|
|
123
|
+
"bottom",
|
|
124
|
+
"left"
|
|
125
|
+
]).optional(),
|
|
126
|
+
toEnd: zod.enum(["none", "arrow"]).optional(),
|
|
127
|
+
label: zod.string().optional(),
|
|
128
|
+
color: zod.string().optional()
|
|
129
|
+
});
|
|
130
|
+
const JsonCanvasSchema = zod.object({
|
|
131
|
+
nodes: zod.array(JsonCanvasNodeSchema),
|
|
132
|
+
edges: zod.array(JsonCanvasEdgeSchema)
|
|
133
|
+
});
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/export/canvas.ts
|
|
136
|
+
function roleColor(roles) {
|
|
137
|
+
if (roles.includes("victim")) return "1";
|
|
138
|
+
if (roles.includes("suspect") || roles.includes("scam_candidate")) return "2";
|
|
139
|
+
if (roles.includes("deposit")) return "3";
|
|
140
|
+
if (roles.includes("exchange")) return "5";
|
|
141
|
+
if (roles.includes("service")) return "6";
|
|
142
|
+
return "#808080";
|
|
143
|
+
}
|
|
144
|
+
function nodeRoles(node) {
|
|
145
|
+
return Array.isArray(node["roles"]) ? node["roles"].map(String) : [];
|
|
146
|
+
}
|
|
147
|
+
function nodeLabel(node) {
|
|
148
|
+
return String(node["address"] ?? node["id"] ?? "unknown");
|
|
149
|
+
}
|
|
150
|
+
function graphNodeId(node, index) {
|
|
151
|
+
return String(node["id"] ?? node["address"] ?? `node-${index + 1}`);
|
|
152
|
+
}
|
|
153
|
+
function entityNotePath(entityId) {
|
|
154
|
+
return `Entities/${safeFilename(entityId)}`;
|
|
155
|
+
}
|
|
156
|
+
function graphToCanvas(graph) {
|
|
157
|
+
const nodes = [{
|
|
158
|
+
id: "case",
|
|
159
|
+
type: "file",
|
|
160
|
+
file: "Case.md",
|
|
161
|
+
x: 0,
|
|
162
|
+
y: 0,
|
|
163
|
+
width: 360,
|
|
164
|
+
height: 120,
|
|
165
|
+
color: "4"
|
|
166
|
+
}];
|
|
167
|
+
const nodeIdMap = /* @__PURE__ */ new Map();
|
|
168
|
+
graph.nodes.forEach((node, index) => {
|
|
169
|
+
const rawId = graphNodeId(node, index);
|
|
170
|
+
const canvasId = `entity-${index + 1}`;
|
|
171
|
+
nodeIdMap.set(rawId, canvasId);
|
|
172
|
+
nodes.push({
|
|
173
|
+
id: canvasId,
|
|
174
|
+
type: "file",
|
|
175
|
+
file: entityNotePath(rawId),
|
|
176
|
+
x: 420 + index % 4 * 340,
|
|
177
|
+
y: Math.floor(index / 4) * 220,
|
|
178
|
+
width: 300,
|
|
179
|
+
height: 120,
|
|
180
|
+
color: roleColor(nodeRoles(node))
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
const edges = graph.edges.flatMap((edge, index) => {
|
|
184
|
+
const from = nodeIdMap.get(String(edge["source"] ?? ""));
|
|
185
|
+
const to = nodeIdMap.get(String(edge["target"] ?? ""));
|
|
186
|
+
if (!from || !to) return [];
|
|
187
|
+
return [{
|
|
188
|
+
id: `edge-${index + 1}`,
|
|
189
|
+
fromNode: from,
|
|
190
|
+
toNode: to,
|
|
191
|
+
fromSide: "right",
|
|
192
|
+
toSide: "left",
|
|
193
|
+
toEnd: "arrow",
|
|
194
|
+
label: String(edge["edge_type"] ?? "related_to")
|
|
195
|
+
}];
|
|
196
|
+
});
|
|
197
|
+
for (const [index, node] of graph.nodes.entries()) edges.push({
|
|
198
|
+
id: `case-link-${index + 1}`,
|
|
199
|
+
fromNode: "case",
|
|
200
|
+
toNode: `entity-${index + 1}`,
|
|
201
|
+
fromSide: "right",
|
|
202
|
+
toSide: "left",
|
|
203
|
+
toEnd: "arrow",
|
|
204
|
+
label: nodeLabel(node)
|
|
205
|
+
});
|
|
206
|
+
return JsonCanvasSchema.parse({
|
|
207
|
+
nodes,
|
|
208
|
+
edges
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/export/graph.ts
|
|
213
|
+
function isRecord(value) {
|
|
214
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
function nodeId(node) {
|
|
217
|
+
return String(node["id"] ?? node["address"] ?? "");
|
|
218
|
+
}
|
|
219
|
+
function edgeKey(edge) {
|
|
220
|
+
return `${String(edge["source"] ?? "")}->${String(edge["target"] ?? "")}:${String(edge["edge_type"] ?? "related_to")}`;
|
|
221
|
+
}
|
|
222
|
+
function mergeGraphs(graphs) {
|
|
223
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
224
|
+
const edges = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const graph of graphs) {
|
|
226
|
+
for (const rawNode of graph.nodes) {
|
|
227
|
+
const id = nodeId(rawNode);
|
|
228
|
+
if (id) nodes.set(id, {
|
|
229
|
+
...nodes.get(id) ?? {},
|
|
230
|
+
...rawNode,
|
|
231
|
+
id
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
for (const rawEdge of graph.edges) {
|
|
235
|
+
if (typeof rawEdge["source"] !== "string" || typeof rawEdge["target"] !== "string") continue;
|
|
236
|
+
edges.set(edgeKey(rawEdge), {
|
|
237
|
+
...edges.get(edgeKey(rawEdge)) ?? {},
|
|
238
|
+
...rawEdge
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
schema: "chain-insights.graph.v1",
|
|
244
|
+
nodes: [...nodes.values()],
|
|
245
|
+
edges: [...edges.values()],
|
|
246
|
+
flows: graphs.flatMap((graph) => graph.flows),
|
|
247
|
+
edge_anchors: graphs.flatMap((graph) => graph.edge_anchors),
|
|
248
|
+
metadata: {
|
|
249
|
+
source: "case-export",
|
|
250
|
+
graph_count: graphs.length,
|
|
251
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function loadCaseExportGraph(caseId) {
|
|
256
|
+
const paths = require_output_root.workspaceOutputPaths();
|
|
257
|
+
const files = await (0, node_fs_promises.readdir)(paths.reportGraphsRoot).catch(() => []);
|
|
258
|
+
const graphs = [];
|
|
259
|
+
for (const file of files.filter((name) => name.endsWith(".graph.json")).sort()) {
|
|
260
|
+
const parsed = JSON.parse(await (0, node_fs_promises.readFile)(node_path.default.join(paths.reportGraphsRoot, file), "utf8"));
|
|
261
|
+
if (isRecord(parsed) && parsed["schema"] === "chain-insights.graph.v1") graphs.push(require_graph_normalizer.normalizeGraphPayload(parsed));
|
|
262
|
+
}
|
|
263
|
+
if (graphs.length > 0) return mergeGraphs(graphs);
|
|
264
|
+
const fallback = await require_data_extractor.extractGraphFromCase(caseId);
|
|
265
|
+
return require_graph_normalizer.normalizeGraphPayload({
|
|
266
|
+
schema: "chain-insights.graph.v1",
|
|
267
|
+
nodes: fallback.nodes,
|
|
268
|
+
edges: fallback.edges,
|
|
269
|
+
flows: [],
|
|
270
|
+
edge_anchors: [],
|
|
271
|
+
metadata: fallback.metadata
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/export/markdown.ts
|
|
276
|
+
function frontmatter(values) {
|
|
277
|
+
const lines = ["---"];
|
|
278
|
+
for (const [key, value] of Object.entries(values)) if (Array.isArray(value)) {
|
|
279
|
+
lines.push(`${key}:`);
|
|
280
|
+
for (const item of value) lines.push(` - ${JSON.stringify(String(item))}`);
|
|
281
|
+
} else lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
282
|
+
lines.push("---", "");
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
function renderReadme(caseName) {
|
|
286
|
+
return [
|
|
287
|
+
`# ${caseName} Export`,
|
|
288
|
+
"",
|
|
289
|
+
"Open this directory as an Obsidian vault or give it to an LLMWiki-style knowledge workflow.",
|
|
290
|
+
"",
|
|
291
|
+
"Start with:",
|
|
292
|
+
"",
|
|
293
|
+
"- `Case.md`",
|
|
294
|
+
"- `Agent Console.md`",
|
|
295
|
+
"- `LLMWIKI.md`",
|
|
296
|
+
"- `graph.chain-insights.json` when present",
|
|
297
|
+
""
|
|
298
|
+
].join("\n");
|
|
299
|
+
}
|
|
300
|
+
function renderCaseMarkdown(input) {
|
|
301
|
+
return frontmatter({
|
|
302
|
+
type: "chain-insights-case",
|
|
303
|
+
case_id: input.caseInfo.id,
|
|
304
|
+
status: input.caseInfo.status,
|
|
305
|
+
tags: input.caseInfo.tags,
|
|
306
|
+
contains_sensitive_data: input.mode !== "public"
|
|
307
|
+
}) + [
|
|
308
|
+
`# ${input.caseInfo.name}`,
|
|
309
|
+
"",
|
|
310
|
+
`Case ID: \`${input.caseInfo.id}\``,
|
|
311
|
+
`Status: ${input.caseInfo.status}`,
|
|
312
|
+
`Evidence manifest: ${input.evidenceVerified ? "verified" : "failed"}`,
|
|
313
|
+
`Evidence files: ${input.evidenceCount}`,
|
|
314
|
+
`Entities: ${input.entityCount}`,
|
|
315
|
+
"",
|
|
316
|
+
"## Summary",
|
|
317
|
+
"",
|
|
318
|
+
input.caseInfo.description || "No description recorded.",
|
|
319
|
+
"",
|
|
320
|
+
"## Start Here",
|
|
321
|
+
"",
|
|
322
|
+
"- [[Agent Console]]",
|
|
323
|
+
"- [[LLMWIKI]]",
|
|
324
|
+
"- [[Sources/evidence-manifest]]",
|
|
325
|
+
""
|
|
326
|
+
].join("\n");
|
|
327
|
+
}
|
|
328
|
+
function renderAgentConsole(caseName) {
|
|
329
|
+
return [
|
|
330
|
+
"# Agent Console",
|
|
331
|
+
"",
|
|
332
|
+
`Case: [[Case|${caseName}]]`,
|
|
333
|
+
"",
|
|
334
|
+
"## Reading Order",
|
|
335
|
+
"",
|
|
336
|
+
"1. [[Case]]",
|
|
337
|
+
"2. [[LLMWIKI]]",
|
|
338
|
+
"3. `graph.chain-insights.json`",
|
|
339
|
+
"4. [[Sources/evidence-manifest]]",
|
|
340
|
+
"5. Entity and evidence notes linked from the case.",
|
|
341
|
+
"",
|
|
342
|
+
"## Agent Prompts",
|
|
343
|
+
"",
|
|
344
|
+
"- [[Prompts/Codex]]",
|
|
345
|
+
"- [[Prompts/Claude-Code]]",
|
|
346
|
+
"- [[Prompts/ChatGPT]]",
|
|
347
|
+
"",
|
|
348
|
+
"## Rules",
|
|
349
|
+
"",
|
|
350
|
+
"- Treat Chain Insights case evidence and manifests as canonical.",
|
|
351
|
+
"- Use Chain Insights tools for fresh graph facts.",
|
|
352
|
+
"- Preserve full blockchain addresses exactly unless this is a public redacted export.",
|
|
353
|
+
""
|
|
354
|
+
].join("\n");
|
|
355
|
+
}
|
|
356
|
+
function renderLlmWiki() {
|
|
357
|
+
return [
|
|
358
|
+
"# LLMWiki Entry",
|
|
359
|
+
"",
|
|
360
|
+
"This directory is a Chain Insights case export.",
|
|
361
|
+
"",
|
|
362
|
+
"Canonical machine files:",
|
|
363
|
+
"",
|
|
364
|
+
"- `manifest.chain-insights.json`",
|
|
365
|
+
"- `graph.chain-insights.json`",
|
|
366
|
+
"- `Graph.canvas`",
|
|
367
|
+
"",
|
|
368
|
+
"Human and agent notes:",
|
|
369
|
+
"",
|
|
370
|
+
"- `Case.md`",
|
|
371
|
+
"- `Agent Console.md`",
|
|
372
|
+
"- `Entities/`",
|
|
373
|
+
"- `Evidence/`",
|
|
374
|
+
"- `Prompts/`",
|
|
375
|
+
""
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|
|
378
|
+
function renderLlmsTxt() {
|
|
379
|
+
return [
|
|
380
|
+
"# Chain Insights Case Export",
|
|
381
|
+
"",
|
|
382
|
+
"Read these files first:",
|
|
383
|
+
"- Case.md",
|
|
384
|
+
"- Agent Console.md",
|
|
385
|
+
"- graph.chain-insights.json",
|
|
386
|
+
"- Entities/",
|
|
387
|
+
"- Evidence/",
|
|
388
|
+
"",
|
|
389
|
+
"Source of truth:",
|
|
390
|
+
"- manifest.chain-insights.json",
|
|
391
|
+
"- Sources/evidence-manifest.md",
|
|
392
|
+
""
|
|
393
|
+
].join("\n");
|
|
394
|
+
}
|
|
395
|
+
function renderPrompt(agentName) {
|
|
396
|
+
return [
|
|
397
|
+
`# ${agentName} Case Prompt`,
|
|
398
|
+
"",
|
|
399
|
+
"You are reading a Chain Insights case export.",
|
|
400
|
+
"",
|
|
401
|
+
"Treat `manifest.chain-insights.json`, `Sources/evidence-manifest.md`, and original case evidence as canonical.",
|
|
402
|
+
"Use generated prose for orientation, not as a replacement for evidence.",
|
|
403
|
+
"Use Chain Insights MCP tools for fresh graph facts when available.",
|
|
404
|
+
""
|
|
405
|
+
].join("\n");
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/export/redaction.ts
|
|
409
|
+
const EVM_ADDRESS_RE = /\b0x[a-fA-F0-9]{40}\b/g;
|
|
410
|
+
const SUBSTRATE_ADDRESS_RE = /\b5[1-9A-HJ-NP-Za-km-z]{20,64}\b/g;
|
|
411
|
+
const SECRET_PATTERNS = [
|
|
412
|
+
/\bci_test_[A-Za-z0-9_-]+\b/g,
|
|
413
|
+
/\b(?:privateKey|walletPrivateKey|secret|token|authorization)\s*[:=]\s*["']?[^"'\s]+/gi,
|
|
414
|
+
/\b0x[a-fA-F0-9]{64}\b/g
|
|
415
|
+
];
|
|
416
|
+
function createRedactor(mode) {
|
|
417
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
418
|
+
const redactions = /* @__PURE__ */ new Set();
|
|
419
|
+
function aliasFor(address) {
|
|
420
|
+
const existing = aliases.get(address);
|
|
421
|
+
if (existing) return existing;
|
|
422
|
+
const alias = `addr_${String(aliases.size + 1).padStart(3, "0")}`;
|
|
423
|
+
aliases.set(address, alias);
|
|
424
|
+
redactions.add(`aliased:${alias}`);
|
|
425
|
+
return alias;
|
|
426
|
+
}
|
|
427
|
+
function redactSecrets(input) {
|
|
428
|
+
let output = input;
|
|
429
|
+
for (const pattern of SECRET_PATTERNS) output = output.replace(pattern, () => {
|
|
430
|
+
redactions.add("secret");
|
|
431
|
+
return "[redacted-secret]";
|
|
432
|
+
});
|
|
433
|
+
return output;
|
|
434
|
+
}
|
|
435
|
+
function text(input) {
|
|
436
|
+
let output = redactSecrets(input);
|
|
437
|
+
if (mode === "public") {
|
|
438
|
+
output = output.replace(SUBSTRATE_ADDRESS_RE, (match) => aliasFor(match));
|
|
439
|
+
output = output.replace(EVM_ADDRESS_RE, (match) => aliasFor(match));
|
|
440
|
+
}
|
|
441
|
+
return output;
|
|
442
|
+
}
|
|
443
|
+
function value(input) {
|
|
444
|
+
if (typeof input === "string") return text(input);
|
|
445
|
+
if (Array.isArray(input)) return input.map((item) => value(item));
|
|
446
|
+
if (input && typeof input === "object") return Object.fromEntries(Object.entries(input).map(([key, entry]) => [key, value(entry)]));
|
|
447
|
+
return input;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
text,
|
|
451
|
+
value,
|
|
452
|
+
aliasFor,
|
|
453
|
+
redactions: () => [...redactions].sort()
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/export/index.ts
|
|
458
|
+
async function readEvidence(caseId) {
|
|
459
|
+
const paths = require_output_root.workspaceOutputPaths();
|
|
460
|
+
const dir = node_path.default.join(paths.casesRoot, caseId, "evidence");
|
|
461
|
+
const files = await (0, node_fs_promises.readdir)(dir).catch(() => []);
|
|
462
|
+
const docs = [];
|
|
463
|
+
for (const filename of files.filter((file) => file.endsWith(".md")).sort()) {
|
|
464
|
+
const { frontmatter, body } = require_frontmatter.parseFrontmatter(await (0, node_fs_promises.readFile)(node_path.default.join(dir, filename), "utf8"));
|
|
465
|
+
docs.push({
|
|
466
|
+
id: frontmatter["id"] || filename.replace(/\.md$/, ""),
|
|
467
|
+
filename,
|
|
468
|
+
source: frontmatter["source"] || "unknown",
|
|
469
|
+
timestamp: frontmatter["timestamp"] || "",
|
|
470
|
+
body
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return docs;
|
|
474
|
+
}
|
|
475
|
+
async function writeFiles(root, entries) {
|
|
476
|
+
const written = [];
|
|
477
|
+
for (const [relativePath, content] of entries) written.push(await writePrivateFile(root, relativePath, content));
|
|
478
|
+
return written;
|
|
479
|
+
}
|
|
480
|
+
async function exportCase(rawOptions) {
|
|
481
|
+
const options = CaseExportOptionsSchema.parse(rawOptions);
|
|
482
|
+
const workspace = require_output_root.workspaceOutputPaths();
|
|
483
|
+
const caseInfo = await require_store.CaseStore.get(options.caseId);
|
|
484
|
+
const redactor = createRedactor(options.mode);
|
|
485
|
+
const evidenceVerification = await require_evidence.EvidenceStore.verifyManifest(options.caseId);
|
|
486
|
+
const evidenceDocs = await readEvidence(options.caseId);
|
|
487
|
+
const dossiers = await require_dossier.DossierStore.listSummaries(options.caseId);
|
|
488
|
+
const graph = redactor.value(await loadCaseExportGraph(options.caseId));
|
|
489
|
+
const canvas = graphToCanvas(graph);
|
|
490
|
+
const outputRoot = node_path.default.resolve(options.outputDir ?? node_path.default.join(workspace.root, "published", safeSlug(caseInfo.name)));
|
|
491
|
+
await (0, node_fs_promises.mkdir)(outputRoot, {
|
|
492
|
+
recursive: true,
|
|
493
|
+
mode: 448
|
|
494
|
+
});
|
|
495
|
+
const entries = [
|
|
496
|
+
["README.md", renderReadme(redactor.text(caseInfo.name))],
|
|
497
|
+
["Case.md", renderCaseMarkdown({
|
|
498
|
+
caseInfo: {
|
|
499
|
+
id: caseInfo.id,
|
|
500
|
+
name: redactor.text(caseInfo.name),
|
|
501
|
+
status: caseInfo.status,
|
|
502
|
+
tags: caseInfo.tags,
|
|
503
|
+
description: redactor.text(caseInfo.description)
|
|
504
|
+
},
|
|
505
|
+
mode: options.mode,
|
|
506
|
+
evidenceVerified: evidenceVerification.ok,
|
|
507
|
+
evidenceCount: evidenceVerification.count,
|
|
508
|
+
entityCount: dossiers.length
|
|
509
|
+
})],
|
|
510
|
+
["LLMWIKI.md", renderLlmWiki()],
|
|
511
|
+
["llms.txt", renderLlmsTxt()],
|
|
512
|
+
["Agent Console.md", renderAgentConsole(redactor.text(caseInfo.name))],
|
|
513
|
+
["Prompts/Codex.md", renderPrompt("Codex")],
|
|
514
|
+
["Prompts/Claude-Code.md", renderPrompt("Claude Code")],
|
|
515
|
+
["Prompts/ChatGPT.md", renderPrompt("ChatGPT")],
|
|
516
|
+
["Sources/evidence-manifest.md", `# Evidence Manifest\n\nVerified: ${evidenceVerification.ok ? "yes" : "no"}\nEvidence files: ${evidenceVerification.count}\n`],
|
|
517
|
+
["Sources/reports-index.md", "# Reports Index\n\nGraph and report artifacts are exported when present.\n"],
|
|
518
|
+
["graph.chain-insights.json", JSON.stringify(graph, null, 2) + "\n"],
|
|
519
|
+
["Graph.canvas", JSON.stringify(canvas, null, 2) + "\n"]
|
|
520
|
+
];
|
|
521
|
+
for (const evidence of evidenceDocs) entries.push([node_path.default.join("Evidence", safeFilename(evidence.id)), redactor.text([
|
|
522
|
+
`# Evidence: ${evidence.source}`,
|
|
523
|
+
"",
|
|
524
|
+
`Source file: \`${evidence.filename}\``,
|
|
525
|
+
`Captured: ${evidence.timestamp || "unknown"}`,
|
|
526
|
+
"",
|
|
527
|
+
evidence.body,
|
|
528
|
+
""
|
|
529
|
+
].join("\n"))]);
|
|
530
|
+
const entityPaths = /* @__PURE__ */ new Set();
|
|
531
|
+
for (const dossier of dossiers) {
|
|
532
|
+
const entityId = options.mode === "public" ? redactor.aliasFor(dossier.address) : dossier.address;
|
|
533
|
+
const entityPath = node_path.default.join("Entities", safeFilename(entityId));
|
|
534
|
+
entityPaths.add(entityPath);
|
|
535
|
+
entries.push([entityPath, redactor.text([
|
|
536
|
+
`# Entity: ${entityId}`,
|
|
537
|
+
"",
|
|
538
|
+
`Type: ${dossier.type}`,
|
|
539
|
+
`First seen: ${dossier.firstSeen || "unknown"}`,
|
|
540
|
+
`Last seen: ${dossier.lastSeen || "unknown"}`,
|
|
541
|
+
`Risk tags: ${dossier.riskTags || "none"}`,
|
|
542
|
+
""
|
|
543
|
+
].join("\n"))]);
|
|
544
|
+
}
|
|
545
|
+
for (const [index, node] of graph.nodes.entries()) {
|
|
546
|
+
const entityId = graphNodeId(node, index);
|
|
547
|
+
const entityPath = entityNotePath(entityId);
|
|
548
|
+
if (entityPaths.has(entityPath)) continue;
|
|
549
|
+
entityPaths.add(entityPath);
|
|
550
|
+
entries.push([entityPath, [
|
|
551
|
+
`# Entity: ${entityId}`,
|
|
552
|
+
"",
|
|
553
|
+
`Address: ${String(node["address"] ?? entityId)}`,
|
|
554
|
+
`Roles: ${Array.isArray(node["roles"]) ? node["roles"].map(String).join(", ") || "none" : "none"}`,
|
|
555
|
+
`Node type: ${String(node["node_type"] ?? "unknown")}`,
|
|
556
|
+
"",
|
|
557
|
+
"## Graph Links",
|
|
558
|
+
"",
|
|
559
|
+
"- [[Graph.canvas]]",
|
|
560
|
+
""
|
|
561
|
+
].join("\n")]);
|
|
562
|
+
}
|
|
563
|
+
const files = await writeFiles(outputRoot, entries);
|
|
564
|
+
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
565
|
+
const manifest = CaseExportManifestSchema.parse({
|
|
566
|
+
schema: "chain-insights.case_export.v1",
|
|
567
|
+
case_id: caseInfo.id,
|
|
568
|
+
case_name: redactor.text(caseInfo.name),
|
|
569
|
+
exported_at: exportedAt,
|
|
570
|
+
mode: options.mode,
|
|
571
|
+
target: options.target,
|
|
572
|
+
source_workspace: workspace.root,
|
|
573
|
+
verification: {
|
|
574
|
+
evidence_manifest_verified: evidenceVerification.ok,
|
|
575
|
+
verified_at: exportedAt,
|
|
576
|
+
evidence_count: evidenceVerification.count
|
|
577
|
+
},
|
|
578
|
+
files,
|
|
579
|
+
redactions: redactor.redactions(),
|
|
580
|
+
warnings: evidenceVerification.ok ? [] : [`Evidence manifest failed: ${(evidenceVerification.tampered ?? []).join(", ")}`]
|
|
581
|
+
});
|
|
582
|
+
const manifestFile = await writePrivateFile(outputRoot, "manifest.chain-insights.json", JSON.stringify(manifest, null, 2) + "\n");
|
|
583
|
+
return {
|
|
584
|
+
manifestPath: node_path.default.join(outputRoot, manifestFile.path),
|
|
585
|
+
outputDir: outputRoot,
|
|
586
|
+
fileCount: files.length + 1,
|
|
587
|
+
warnings: manifest.warnings,
|
|
588
|
+
nextFile: "Agent Console.md"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
exports.exportCase = exportCase;
|