chain-insights 0.2.32 → 0.3.4

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 (66) hide show
  1. package/README.md +43 -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 +126 -117
  52. package/docs/investigation-workspaces.md +17 -0
  53. package/docs/knowledge-exports.md +200 -0
  54. package/docs/mcp-proxy.md +16 -2
  55. package/package.json +1 -1
  56. package/skills/chain-insights-cypher/SKILL.md +6 -0
  57. package/skills/chain-insights-developer-experience/SKILL.md +26 -6
  58. package/skills/chain-insights-investigation/SKILL.md +64 -48
  59. package/skills/chain-insights-trace-funds/SKILL.md +80 -197
  60. package/skills/test-chain-insights-graphrag-mcp/SKILL.md +1 -1
  61. package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +4 -4
  62. package/dist/cases-qjPtbnUd.mjs +0 -6
  63. package/dist/public-tools-DoRNhMn9.mjs.map +0 -1
  64. package/dist/resolver-D7VBb0uB.mjs.map +0 -1
  65. package/dist/tool-visibility-BHRFLXuU.mjs.map +0 -1
  66. package/dist/viz-DkJyqlUu.mjs.map +0 -1
@@ -6,6 +6,7 @@ node_path = require_chunk.__toESM(node_path, 1);
6
6
  let node_fs_promises = require("node:fs/promises");
7
7
  require("node:crypto");
8
8
  //#region src/cases/dossier.ts
9
+ var dossier_exports = /* @__PURE__ */ require_chunk.__exportAll({ DossierStore: () => DossierStore });
9
10
  function caseDir(caseId) {
10
11
  return node_path.default.join(require_output_root.workspaceOutputPaths().casesRoot, caseId);
11
12
  }
@@ -73,4 +74,15 @@ const DossierStore = {
73
74
  }
74
75
  };
75
76
  //#endregion
76
- exports.DossierStore = DossierStore;
77
+ Object.defineProperty(exports, "DossierStore", {
78
+ enumerable: true,
79
+ get: function() {
80
+ return DossierStore;
81
+ }
82
+ });
83
+ Object.defineProperty(exports, "dossier_exports", {
84
+ enumerable: true,
85
+ get: function() {
86
+ return dossier_exports;
87
+ }
88
+ });
@@ -1,9 +1,11 @@
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 { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
6
  import "node:crypto";
6
7
  //#region src/cases/dossier.ts
8
+ var dossier_exports = /* @__PURE__ */ __exportAll({ DossierStore: () => DossierStore });
7
9
  function caseDir(caseId) {
8
10
  return path.join(workspaceOutputPaths().casesRoot, caseId);
9
11
  }
@@ -71,6 +73,6 @@ const DossierStore = {
71
73
  }
72
74
  };
73
75
  //#endregion
74
- export { DossierStore };
76
+ export { dossier_exports as n, DossierStore as t };
75
77
 
76
- //# sourceMappingURL=dossier-Bl0NkJKC.mjs.map
78
+ //# sourceMappingURL=dossier-Bjpcbcxa.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dossier-Bl0NkJKC.mjs","names":["nodeErr"],"sources":["../src/cases/dossier.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport { createHash } from 'node:crypto'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\n\nfunction caseDir(caseId: string): string {\n return path.join(workspaceOutputPaths().casesRoot, caseId)\n}\n\nfunction sanitizeAddress(address: string): string {\n // Security T-03-06: prevent path traversal by stripping all non-alphanumeric chars\n return address.replace(/[^a-zA-Z0-9]/g, '').slice(0, 66)\n}\n\nfunction contentHash(text: string): string {\n return createHash('sha256').update(text).digest('hex')\n}\n\nexport const DossierStore = {\n async appendFinding(\n caseId: string,\n address: string,\n finding: string,\n entityType: 'eoa' | 'contract' | 'exchange' | 'mixer' | 'unknown' = 'unknown'\n ): Promise<void> {\n const safeAddr = sanitizeAddress(address)\n const dossierDir = path.join(caseDir(caseId), 'dossiers')\n await mkdir(dossierDir, { recursive: true })\n const filePath = path.join(dossierDir, `${safeAddr}.md`)\n const now = new Date().toISOString()\n\n let raw: string\n let isNew = false\n try {\n raw = await readFile(filePath, 'utf8')\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code !== 'ENOENT') throw err\n // New dossier — create template\n const fm: Record<string, string> = {\n address,\n type: entityType,\n firstSeen: now,\n lastSeen: now,\n riskTags: '',\n }\n const summary = entityType === 'exchange'\n ? 'Exchange-labeled entity observed in this case.'\n : entityType === 'contract'\n ? 'Contract entity observed in this case.'\n : entityType === 'mixer'\n ? 'Mixer-labeled entity observed in this case.'\n : 'Address/entity observed in this case.'\n const body = `# Entity: ${address}\\n\\n## Summary\\n\\n${summary}\\n\\n## Findings\\n\\n`\n raw = serializeFrontmatter(fm, body)\n isNew = true\n }\n\n // Content-hash deduplication — skip if finding already present (text presence check)\n if (!isNew && raw.includes(finding)) {\n return\n }\n\n // Update frontmatter lastSeen\n const { frontmatter, body } = parseFrontmatter(raw)\n frontmatter['lastSeen'] = now\n if (!isNew) {\n frontmatter['type'] = entityType\n }\n\n // Append finding to ## Findings section\n const findingEntry = `- [${now}] ${finding}\\n`\n const updatedBody = body.replace('## Findings\\n', `## Findings\\n\\n${findingEntry}`)\n\n await writeFile(filePath, serializeFrontmatter(frontmatter, updatedBody), { mode: 0o600 })\n },\n\n async get(caseId: string, address: string): Promise<{ frontmatter: Record<string, string>; body: string } | null> {\n const safeAddr = sanitizeAddress(address)\n const filePath = path.join(caseDir(caseId), 'dossiers', `${safeAddr}.md`)\n try {\n const raw = await readFile(filePath, 'utf8')\n return parseFrontmatter(raw)\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code === 'ENOENT') return null\n throw err\n }\n },\n\n async listSummaries(caseId: string): Promise<Array<{ address: string; type: string; riskTags: string; firstSeen: string; lastSeen: string }>> {\n const dossierDir = path.join(caseDir(caseId), 'dossiers')\n try {\n const files = await readdir(dossierDir)\n const summaries = []\n for (const file of files.filter(f => f.endsWith('.md'))) {\n const raw = await readFile(path.join(dossierDir, file), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n summaries.push({\n address: frontmatter['address'] ?? file.replace('.md', ''),\n type: frontmatter['type'] ?? 'unknown',\n riskTags: frontmatter['riskTags'] ?? '',\n firstSeen: frontmatter['firstSeen'] ?? '',\n lastSeen: frontmatter['lastSeen'] ?? '',\n })\n }\n return summaries\n } catch {\n return []\n }\n },\n}\n"],"mappings":";;;;;;AAMA,SAAS,QAAQ,QAAwB;CACvC,OAAO,KAAK,KAAK,qBAAqB,EAAE,WAAW,MAAM;AAC3D;AAEA,SAAS,gBAAgB,SAAyB;CAEhD,OAAO,QAAQ,QAAQ,iBAAiB,EAAE,EAAE,MAAM,GAAG,EAAE;AACzD;AAMA,MAAa,eAAe;CAC1B,MAAM,cACJ,QACA,SACA,SACA,aAAoE,WACrD;EACf,MAAM,WAAW,gBAAgB,OAAO;EACxC,MAAM,aAAa,KAAK,KAAK,QAAQ,MAAM,GAAG,UAAU;EACxD,MAAM,MAAM,YAAY,EAAE,WAAW,KAAK,CAAC;EAC3C,MAAM,WAAW,KAAK,KAAK,YAAY,GAAG,SAAS,IAAI;EACvD,MAAM,uBAAM,IAAI,KAAK,GAAE,YAAY;EAEnC,IAAI;EACJ,IAAI,QAAQ;EACZ,IAAI;GACF,MAAM,MAAM,SAAS,UAAU,MAAM;EACvC,SAAS,KAAc;GAErB,IAAIA,IAAQ,SAAS,UAAU,MAAM;GAiBrC,MAAM,qBAAqB;IAdzB;IACA,MAAM;IACN,WAAW;IACX,UAAU;IACV,UAAU;GAUgB,GAAG,aADL,QAAQ,oBAPlB,eAAe,aAC3B,mDACA,eAAe,aACb,2CACA,eAAe,UACb,gDACA,wCACsD,oBAC3B;GACnC,QAAQ;EACV;EAGA,IAAI,CAAC,SAAS,IAAI,SAAS,OAAO,GAChC;EAIF,MAAM,EAAE,aAAa,SAAS,iBAAiB,GAAG;EAClD,YAAY,cAAc;EAC1B,IAAI,CAAC,OACH,YAAY,UAAU;EAIxB,MAAM,eAAe,MAAM,IAAI,IAAI,QAAQ;EAG3C,MAAM,UAAU,UAAU,qBAAqB,aAF3B,KAAK,QAAQ,iBAAiB,kBAAkB,cAEE,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;CAC3F;CAEA,MAAM,IAAI,QAAgB,SAAwF;EAChH,MAAM,WAAW,gBAAgB,OAAO;EACxC,MAAM,WAAW,KAAK,KAAK,QAAQ,MAAM,GAAG,YAAY,GAAG,SAAS,IAAI;EACxE,IAAI;GAEF,OAAO,iBAAiB,MADN,SAAS,UAAU,MAAM,CAChB;EAC7B,SAAS,KAAc;GAErB,IAAIA,IAAQ,SAAS,UAAU,OAAO;GACtC,MAAM;EACR;CACF;CAEA,MAAM,cAAc,QAA0H;EAC5I,MAAM,aAAa,KAAK,KAAK,QAAQ,MAAM,GAAG,UAAU;EACxD,IAAI;GACF,MAAM,QAAQ,MAAM,QAAQ,UAAU;GACtC,MAAM,YAAY,CAAC;GACnB,KAAK,MAAM,QAAQ,MAAM,QAAO,MAAK,EAAE,SAAS,KAAK,CAAC,GAAG;IAEvD,MAAM,EAAE,gBAAgB,iBAAiB,MADvB,SAAS,KAAK,KAAK,YAAY,IAAI,GAAG,MAAM,CAClB;IAC5C,UAAU,KAAK;KACb,SAAS,YAAY,cAAc,KAAK,QAAQ,OAAO,EAAE;KACzD,MAAM,YAAY,WAAW;KAC7B,UAAU,YAAY,eAAe;KACrC,WAAW,YAAY,gBAAgB;KACvC,UAAU,YAAY,eAAe;IACvC,CAAC;GACH;GACA,OAAO;EACT,QAAQ;GACN,OAAO,CAAC;EACV;CACF;AACF"}
1
+ {"version":3,"file":"dossier-Bjpcbcxa.mjs","names":["nodeErr"],"sources":["../src/cases/dossier.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport { createHash } from 'node:crypto'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\n\nfunction caseDir(caseId: string): string {\n return path.join(workspaceOutputPaths().casesRoot, caseId)\n}\n\nfunction sanitizeAddress(address: string): string {\n // Security T-03-06: prevent path traversal by stripping all non-alphanumeric chars\n return address.replace(/[^a-zA-Z0-9]/g, '').slice(0, 66)\n}\n\nfunction contentHash(text: string): string {\n return createHash('sha256').update(text).digest('hex')\n}\n\nexport const DossierStore = {\n async appendFinding(\n caseId: string,\n address: string,\n finding: string,\n entityType: 'eoa' | 'contract' | 'exchange' | 'mixer' | 'unknown' = 'unknown'\n ): Promise<void> {\n const safeAddr = sanitizeAddress(address)\n const dossierDir = path.join(caseDir(caseId), 'dossiers')\n await mkdir(dossierDir, { recursive: true })\n const filePath = path.join(dossierDir, `${safeAddr}.md`)\n const now = new Date().toISOString()\n\n let raw: string\n let isNew = false\n try {\n raw = await readFile(filePath, 'utf8')\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code !== 'ENOENT') throw err\n // New dossier — create template\n const fm: Record<string, string> = {\n address,\n type: entityType,\n firstSeen: now,\n lastSeen: now,\n riskTags: '',\n }\n const summary = entityType === 'exchange'\n ? 'Exchange-labeled entity observed in this case.'\n : entityType === 'contract'\n ? 'Contract entity observed in this case.'\n : entityType === 'mixer'\n ? 'Mixer-labeled entity observed in this case.'\n : 'Address/entity observed in this case.'\n const body = `# Entity: ${address}\\n\\n## Summary\\n\\n${summary}\\n\\n## Findings\\n\\n`\n raw = serializeFrontmatter(fm, body)\n isNew = true\n }\n\n // Content-hash deduplication — skip if finding already present (text presence check)\n if (!isNew && raw.includes(finding)) {\n return\n }\n\n // Update frontmatter lastSeen\n const { frontmatter, body } = parseFrontmatter(raw)\n frontmatter['lastSeen'] = now\n if (!isNew) {\n frontmatter['type'] = entityType\n }\n\n // Append finding to ## Findings section\n const findingEntry = `- [${now}] ${finding}\\n`\n const updatedBody = body.replace('## Findings\\n', `## Findings\\n\\n${findingEntry}`)\n\n await writeFile(filePath, serializeFrontmatter(frontmatter, updatedBody), { mode: 0o600 })\n },\n\n async get(caseId: string, address: string): Promise<{ frontmatter: Record<string, string>; body: string } | null> {\n const safeAddr = sanitizeAddress(address)\n const filePath = path.join(caseDir(caseId), 'dossiers', `${safeAddr}.md`)\n try {\n const raw = await readFile(filePath, 'utf8')\n return parseFrontmatter(raw)\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code === 'ENOENT') return null\n throw err\n }\n },\n\n async listSummaries(caseId: string): Promise<Array<{ address: string; type: string; riskTags: string; firstSeen: string; lastSeen: string }>> {\n const dossierDir = path.join(caseDir(caseId), 'dossiers')\n try {\n const files = await readdir(dossierDir)\n const summaries = []\n for (const file of files.filter(f => f.endsWith('.md'))) {\n const raw = await readFile(path.join(dossierDir, file), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n summaries.push({\n address: frontmatter['address'] ?? file.replace('.md', ''),\n type: frontmatter['type'] ?? 'unknown',\n riskTags: frontmatter['riskTags'] ?? '',\n firstSeen: frontmatter['firstSeen'] ?? '',\n lastSeen: frontmatter['lastSeen'] ?? '',\n })\n }\n return summaries\n } catch {\n return []\n }\n },\n}\n"],"mappings":";;;;;;;;AAMA,SAAS,QAAQ,QAAwB;CACvC,OAAO,KAAK,KAAK,qBAAqB,EAAE,WAAW,MAAM;AAC3D;AAEA,SAAS,gBAAgB,SAAyB;CAEhD,OAAO,QAAQ,QAAQ,iBAAiB,EAAE,EAAE,MAAM,GAAG,EAAE;AACzD;AAMA,MAAa,eAAe;CAC1B,MAAM,cACJ,QACA,SACA,SACA,aAAoE,WACrD;EACf,MAAM,WAAW,gBAAgB,OAAO;EACxC,MAAM,aAAa,KAAK,KAAK,QAAQ,MAAM,GAAG,UAAU;EACxD,MAAM,MAAM,YAAY,EAAE,WAAW,KAAK,CAAC;EAC3C,MAAM,WAAW,KAAK,KAAK,YAAY,GAAG,SAAS,IAAI;EACvD,MAAM,uBAAM,IAAI,KAAK,GAAE,YAAY;EAEnC,IAAI;EACJ,IAAI,QAAQ;EACZ,IAAI;GACF,MAAM,MAAM,SAAS,UAAU,MAAM;EACvC,SAAS,KAAc;GAErB,IAAIA,IAAQ,SAAS,UAAU,MAAM;GAiBrC,MAAM,qBAAqB;IAdzB;IACA,MAAM;IACN,WAAW;IACX,UAAU;IACV,UAAU;GAUgB,GAAG,aADL,QAAQ,oBAPlB,eAAe,aAC3B,mDACA,eAAe,aACb,2CACA,eAAe,UACb,gDACA,wCACsD,oBAC3B;GACnC,QAAQ;EACV;EAGA,IAAI,CAAC,SAAS,IAAI,SAAS,OAAO,GAChC;EAIF,MAAM,EAAE,aAAa,SAAS,iBAAiB,GAAG;EAClD,YAAY,cAAc;EAC1B,IAAI,CAAC,OACH,YAAY,UAAU;EAIxB,MAAM,eAAe,MAAM,IAAI,IAAI,QAAQ;EAG3C,MAAM,UAAU,UAAU,qBAAqB,aAF3B,KAAK,QAAQ,iBAAiB,kBAAkB,cAEE,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;CAC3F;CAEA,MAAM,IAAI,QAAgB,SAAwF;EAChH,MAAM,WAAW,gBAAgB,OAAO;EACxC,MAAM,WAAW,KAAK,KAAK,QAAQ,MAAM,GAAG,YAAY,GAAG,SAAS,IAAI;EACxE,IAAI;GAEF,OAAO,iBAAiB,MADN,SAAS,UAAU,MAAM,CAChB;EAC7B,SAAS,KAAc;GAErB,IAAIA,IAAQ,SAAS,UAAU,OAAO;GACtC,MAAM;EACR;CACF;CAEA,MAAM,cAAc,QAA0H;EAC5I,MAAM,aAAa,KAAK,KAAK,QAAQ,MAAM,GAAG,UAAU;EACxD,IAAI;GACF,MAAM,QAAQ,MAAM,QAAQ,UAAU;GACtC,MAAM,YAAY,CAAC;GACnB,KAAK,MAAM,QAAQ,MAAM,QAAO,MAAK,EAAE,SAAS,KAAK,CAAC,GAAG;IAEvD,MAAM,EAAE,gBAAgB,iBAAiB,MADvB,SAAS,KAAK,KAAK,YAAY,IAAI,GAAG,MAAM,CAClB;IAC5C,UAAU,KAAK;KACb,SAAS,YAAY,cAAc,KAAK,QAAQ,OAAO,EAAE;KACzD,MAAM,YAAY,WAAW;KAC7B,UAAU,YAAY,eAAe;KACrC,WAAW,YAAY,gBAAgB;KACvC,UAAU,YAAY,eAAe;IACvC,CAAC;GACH;GACA,OAAO;EACT,QAAQ;GACN,OAAO,CAAC;EACV;CACF;AACF"}
@@ -0,0 +1,591 @@
1
+ import { n as extractGraphFromCase } from "./data-extractor-DZUJu1Bz.mjs";
2
+ import { t as parseFrontmatter } from "./frontmatter-D0ccQnUM.mjs";
3
+ import { n as workspaceOutputPaths } from "./output-root-BRhzhhXZ.mjs";
4
+ import { t as DossierStore } from "./dossier-Bjpcbcxa.mjs";
5
+ import { t as CaseStore } from "./store-CTtqQtaE.mjs";
6
+ import { t as EvidenceStore } from "./evidence-D96PTzOQ.mjs";
7
+ import "./cases-Cp9DUbEV.mjs";
8
+ import { t as normalizeGraphPayload } from "./graph-normalizer-CXP06jKh.mjs";
9
+ import path from "node:path";
10
+ import { lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
11
+ import * as z from "zod";
12
+ import { createHash } from "node:crypto";
13
+ //#region src/export/paths.ts
14
+ function safeSlug(value) {
15
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "case-export";
16
+ }
17
+ function safeFilename(value) {
18
+ const parsed = path.parse(value);
19
+ return `${safeSlug(parsed.name)}${parsed.ext.toLowerCase().replace(/[^.a-z0-9]/g, "") || ".md"}`;
20
+ }
21
+ function assertInsideDirectory(root, candidate) {
22
+ const resolvedRoot = path.resolve(root);
23
+ const resolvedCandidate = path.resolve(candidate);
24
+ const relative = path.relative(resolvedRoot, resolvedCandidate);
25
+ if (relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative)) return;
26
+ throw new Error(`Refusing to write outside export directory: ${candidate}`);
27
+ }
28
+ async function assertNoSymlink(filePath) {
29
+ try {
30
+ if ((await lstat(filePath)).isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${filePath}`);
31
+ } catch (err) {
32
+ if (err.code === "ENOENT") return;
33
+ throw err;
34
+ }
35
+ }
36
+ async function writePrivateFile(root, relativePath, content) {
37
+ const filePath = path.join(root, relativePath);
38
+ assertInsideDirectory(root, filePath);
39
+ await mkdir(path.dirname(filePath), {
40
+ recursive: true,
41
+ mode: 448
42
+ });
43
+ await assertNoSymlink(filePath);
44
+ await writeFile(filePath, content, { mode: 384 });
45
+ const bytes = Buffer.byteLength(content, "utf8");
46
+ return {
47
+ path: relativePath,
48
+ sha256: createHash("sha256").update(content).digest("hex"),
49
+ bytes
50
+ };
51
+ }
52
+ //#endregion
53
+ //#region src/export/schema.ts
54
+ const CaseExportTargetSchema = z.enum(["obsidian-llmwiki"]);
55
+ const CaseExportModeSchema = z.enum([
56
+ "private",
57
+ "partner",
58
+ "public"
59
+ ]);
60
+ const caseIdRegex = /^\d{8}_\d{3}_[a-z0-9][a-z0-9-]*$/;
61
+ const CaseExportOptionsSchema = z.object({
62
+ caseId: z.string().regex(caseIdRegex),
63
+ target: CaseExportTargetSchema.default("obsidian-llmwiki"),
64
+ mode: CaseExportModeSchema.default("private"),
65
+ outputDir: z.string().optional()
66
+ });
67
+ const ExportedFileSchema = z.object({
68
+ path: z.string().min(1),
69
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
70
+ bytes: z.number().int().nonnegative()
71
+ });
72
+ const CaseExportManifestSchema = z.object({
73
+ schema: z.literal("chain-insights.case_export.v1"),
74
+ case_id: z.string().regex(caseIdRegex),
75
+ case_name: z.string().min(1),
76
+ exported_at: z.string().datetime(),
77
+ mode: CaseExportModeSchema,
78
+ target: CaseExportTargetSchema,
79
+ source_workspace: z.string().min(1),
80
+ verification: z.object({
81
+ evidence_manifest_verified: z.boolean(),
82
+ verified_at: z.string().datetime(),
83
+ evidence_count: z.number().int().nonnegative()
84
+ }),
85
+ files: z.array(ExportedFileSchema),
86
+ redactions: z.array(z.string()),
87
+ warnings: z.array(z.string())
88
+ });
89
+ const JsonCanvasNodeSchema = z.object({
90
+ id: z.string().min(1),
91
+ type: z.enum([
92
+ "text",
93
+ "file",
94
+ "link",
95
+ "group"
96
+ ]),
97
+ x: z.number(),
98
+ y: z.number(),
99
+ width: z.number().positive(),
100
+ height: z.number().positive(),
101
+ text: z.string().optional(),
102
+ file: z.string().optional(),
103
+ url: z.string().optional(),
104
+ label: z.string().optional(),
105
+ color: z.string().optional()
106
+ });
107
+ const JsonCanvasEdgeSchema = z.object({
108
+ id: z.string().min(1),
109
+ fromNode: z.string().min(1),
110
+ toNode: z.string().min(1),
111
+ fromSide: z.enum([
112
+ "top",
113
+ "right",
114
+ "bottom",
115
+ "left"
116
+ ]).optional(),
117
+ toSide: z.enum([
118
+ "top",
119
+ "right",
120
+ "bottom",
121
+ "left"
122
+ ]).optional(),
123
+ toEnd: z.enum(["none", "arrow"]).optional(),
124
+ label: z.string().optional(),
125
+ color: z.string().optional()
126
+ });
127
+ const JsonCanvasSchema = z.object({
128
+ nodes: z.array(JsonCanvasNodeSchema),
129
+ edges: z.array(JsonCanvasEdgeSchema)
130
+ });
131
+ //#endregion
132
+ //#region src/export/canvas.ts
133
+ function roleColor(roles) {
134
+ if (roles.includes("victim")) return "1";
135
+ if (roles.includes("suspect") || roles.includes("scam_candidate")) return "2";
136
+ if (roles.includes("deposit")) return "3";
137
+ if (roles.includes("exchange")) return "5";
138
+ if (roles.includes("service")) return "6";
139
+ return "#808080";
140
+ }
141
+ function nodeRoles(node) {
142
+ return Array.isArray(node["roles"]) ? node["roles"].map(String) : [];
143
+ }
144
+ function nodeLabel(node) {
145
+ return String(node["address"] ?? node["id"] ?? "unknown");
146
+ }
147
+ function graphNodeId(node, index) {
148
+ return String(node["id"] ?? node["address"] ?? `node-${index + 1}`);
149
+ }
150
+ function entityNotePath(entityId) {
151
+ return `Entities/${safeFilename(entityId)}`;
152
+ }
153
+ function graphToCanvas(graph) {
154
+ const nodes = [{
155
+ id: "case",
156
+ type: "file",
157
+ file: "Case.md",
158
+ x: 0,
159
+ y: 0,
160
+ width: 360,
161
+ height: 120,
162
+ color: "4"
163
+ }];
164
+ const nodeIdMap = /* @__PURE__ */ new Map();
165
+ graph.nodes.forEach((node, index) => {
166
+ const rawId = graphNodeId(node, index);
167
+ const canvasId = `entity-${index + 1}`;
168
+ nodeIdMap.set(rawId, canvasId);
169
+ nodes.push({
170
+ id: canvasId,
171
+ type: "file",
172
+ file: entityNotePath(rawId),
173
+ x: 420 + index % 4 * 340,
174
+ y: Math.floor(index / 4) * 220,
175
+ width: 300,
176
+ height: 120,
177
+ color: roleColor(nodeRoles(node))
178
+ });
179
+ });
180
+ const edges = graph.edges.flatMap((edge, index) => {
181
+ const from = nodeIdMap.get(String(edge["source"] ?? ""));
182
+ const to = nodeIdMap.get(String(edge["target"] ?? ""));
183
+ if (!from || !to) return [];
184
+ return [{
185
+ id: `edge-${index + 1}`,
186
+ fromNode: from,
187
+ toNode: to,
188
+ fromSide: "right",
189
+ toSide: "left",
190
+ toEnd: "arrow",
191
+ label: String(edge["edge_type"] ?? "related_to")
192
+ }];
193
+ });
194
+ for (const [index, node] of graph.nodes.entries()) edges.push({
195
+ id: `case-link-${index + 1}`,
196
+ fromNode: "case",
197
+ toNode: `entity-${index + 1}`,
198
+ fromSide: "right",
199
+ toSide: "left",
200
+ toEnd: "arrow",
201
+ label: nodeLabel(node)
202
+ });
203
+ return JsonCanvasSchema.parse({
204
+ nodes,
205
+ edges
206
+ });
207
+ }
208
+ //#endregion
209
+ //#region src/export/graph.ts
210
+ function isRecord(value) {
211
+ return !!value && typeof value === "object" && !Array.isArray(value);
212
+ }
213
+ function nodeId(node) {
214
+ return String(node["id"] ?? node["address"] ?? "");
215
+ }
216
+ function edgeKey(edge) {
217
+ return `${String(edge["source"] ?? "")}->${String(edge["target"] ?? "")}:${String(edge["edge_type"] ?? "related_to")}`;
218
+ }
219
+ function mergeGraphs(graphs) {
220
+ const nodes = /* @__PURE__ */ new Map();
221
+ const edges = /* @__PURE__ */ new Map();
222
+ for (const graph of graphs) {
223
+ for (const rawNode of graph.nodes) {
224
+ const id = nodeId(rawNode);
225
+ if (id) nodes.set(id, {
226
+ ...nodes.get(id) ?? {},
227
+ ...rawNode,
228
+ id
229
+ });
230
+ }
231
+ for (const rawEdge of graph.edges) {
232
+ if (typeof rawEdge["source"] !== "string" || typeof rawEdge["target"] !== "string") continue;
233
+ edges.set(edgeKey(rawEdge), {
234
+ ...edges.get(edgeKey(rawEdge)) ?? {},
235
+ ...rawEdge
236
+ });
237
+ }
238
+ }
239
+ return {
240
+ schema: "chain-insights.graph.v1",
241
+ nodes: [...nodes.values()],
242
+ edges: [...edges.values()],
243
+ flows: graphs.flatMap((graph) => graph.flows),
244
+ edge_anchors: graphs.flatMap((graph) => graph.edge_anchors),
245
+ metadata: {
246
+ source: "case-export",
247
+ graph_count: graphs.length,
248
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
249
+ }
250
+ };
251
+ }
252
+ async function loadCaseExportGraph(caseId) {
253
+ const paths = workspaceOutputPaths();
254
+ const files = await readdir(paths.reportGraphsRoot).catch(() => []);
255
+ const graphs = [];
256
+ for (const file of files.filter((name) => name.endsWith(".graph.json")).sort()) {
257
+ const parsed = JSON.parse(await readFile(path.join(paths.reportGraphsRoot, file), "utf8"));
258
+ if (isRecord(parsed) && parsed["schema"] === "chain-insights.graph.v1") graphs.push(normalizeGraphPayload(parsed));
259
+ }
260
+ if (graphs.length > 0) return mergeGraphs(graphs);
261
+ const fallback = await extractGraphFromCase(caseId);
262
+ return normalizeGraphPayload({
263
+ schema: "chain-insights.graph.v1",
264
+ nodes: fallback.nodes,
265
+ edges: fallback.edges,
266
+ flows: [],
267
+ edge_anchors: [],
268
+ metadata: fallback.metadata
269
+ });
270
+ }
271
+ //#endregion
272
+ //#region src/export/markdown.ts
273
+ function frontmatter(values) {
274
+ const lines = ["---"];
275
+ for (const [key, value] of Object.entries(values)) if (Array.isArray(value)) {
276
+ lines.push(`${key}:`);
277
+ for (const item of value) lines.push(` - ${JSON.stringify(String(item))}`);
278
+ } else lines.push(`${key}: ${JSON.stringify(value)}`);
279
+ lines.push("---", "");
280
+ return lines.join("\n");
281
+ }
282
+ function renderReadme(caseName) {
283
+ return [
284
+ `# ${caseName} Export`,
285
+ "",
286
+ "Open this directory as an Obsidian vault or give it to an LLMWiki-style knowledge workflow.",
287
+ "",
288
+ "Start with:",
289
+ "",
290
+ "- `Case.md`",
291
+ "- `Agent Console.md`",
292
+ "- `LLMWIKI.md`",
293
+ "- `graph.chain-insights.json` when present",
294
+ ""
295
+ ].join("\n");
296
+ }
297
+ function renderCaseMarkdown(input) {
298
+ return frontmatter({
299
+ type: "chain-insights-case",
300
+ case_id: input.caseInfo.id,
301
+ status: input.caseInfo.status,
302
+ tags: input.caseInfo.tags,
303
+ contains_sensitive_data: input.mode !== "public"
304
+ }) + [
305
+ `# ${input.caseInfo.name}`,
306
+ "",
307
+ `Case ID: \`${input.caseInfo.id}\``,
308
+ `Status: ${input.caseInfo.status}`,
309
+ `Evidence manifest: ${input.evidenceVerified ? "verified" : "failed"}`,
310
+ `Evidence files: ${input.evidenceCount}`,
311
+ `Entities: ${input.entityCount}`,
312
+ "",
313
+ "## Summary",
314
+ "",
315
+ input.caseInfo.description || "No description recorded.",
316
+ "",
317
+ "## Start Here",
318
+ "",
319
+ "- [[Agent Console]]",
320
+ "- [[LLMWIKI]]",
321
+ "- [[Sources/evidence-manifest]]",
322
+ ""
323
+ ].join("\n");
324
+ }
325
+ function renderAgentConsole(caseName) {
326
+ return [
327
+ "# Agent Console",
328
+ "",
329
+ `Case: [[Case|${caseName}]]`,
330
+ "",
331
+ "## Reading Order",
332
+ "",
333
+ "1. [[Case]]",
334
+ "2. [[LLMWIKI]]",
335
+ "3. `graph.chain-insights.json`",
336
+ "4. [[Sources/evidence-manifest]]",
337
+ "5. Entity and evidence notes linked from the case.",
338
+ "",
339
+ "## Agent Prompts",
340
+ "",
341
+ "- [[Prompts/Codex]]",
342
+ "- [[Prompts/Claude-Code]]",
343
+ "- [[Prompts/ChatGPT]]",
344
+ "",
345
+ "## Rules",
346
+ "",
347
+ "- Treat Chain Insights case evidence and manifests as canonical.",
348
+ "- Use Chain Insights tools for fresh graph facts.",
349
+ "- Preserve full blockchain addresses exactly unless this is a public redacted export.",
350
+ ""
351
+ ].join("\n");
352
+ }
353
+ function renderLlmWiki() {
354
+ return [
355
+ "# LLMWiki Entry",
356
+ "",
357
+ "This directory is a Chain Insights case export.",
358
+ "",
359
+ "Canonical machine files:",
360
+ "",
361
+ "- `manifest.chain-insights.json`",
362
+ "- `graph.chain-insights.json`",
363
+ "- `Graph.canvas`",
364
+ "",
365
+ "Human and agent notes:",
366
+ "",
367
+ "- `Case.md`",
368
+ "- `Agent Console.md`",
369
+ "- `Entities/`",
370
+ "- `Evidence/`",
371
+ "- `Prompts/`",
372
+ ""
373
+ ].join("\n");
374
+ }
375
+ function renderLlmsTxt() {
376
+ return [
377
+ "# Chain Insights Case Export",
378
+ "",
379
+ "Read these files first:",
380
+ "- Case.md",
381
+ "- Agent Console.md",
382
+ "- graph.chain-insights.json",
383
+ "- Entities/",
384
+ "- Evidence/",
385
+ "",
386
+ "Source of truth:",
387
+ "- manifest.chain-insights.json",
388
+ "- Sources/evidence-manifest.md",
389
+ ""
390
+ ].join("\n");
391
+ }
392
+ function renderPrompt(agentName) {
393
+ return [
394
+ `# ${agentName} Case Prompt`,
395
+ "",
396
+ "You are reading a Chain Insights case export.",
397
+ "",
398
+ "Treat `manifest.chain-insights.json`, `Sources/evidence-manifest.md`, and original case evidence as canonical.",
399
+ "Use generated prose for orientation, not as a replacement for evidence.",
400
+ "Use Chain Insights MCP tools for fresh graph facts when available.",
401
+ ""
402
+ ].join("\n");
403
+ }
404
+ //#endregion
405
+ //#region src/export/redaction.ts
406
+ const EVM_ADDRESS_RE = /\b0x[a-fA-F0-9]{40}\b/g;
407
+ const SUBSTRATE_ADDRESS_RE = /\b5[1-9A-HJ-NP-Za-km-z]{20,64}\b/g;
408
+ const SECRET_PATTERNS = [
409
+ /\bci_test_[A-Za-z0-9_-]+\b/g,
410
+ /\b(?:privateKey|walletPrivateKey|secret|token|authorization)\s*[:=]\s*["']?[^"'\s]+/gi,
411
+ /\b0x[a-fA-F0-9]{64}\b/g
412
+ ];
413
+ function createRedactor(mode) {
414
+ const aliases = /* @__PURE__ */ new Map();
415
+ const redactions = /* @__PURE__ */ new Set();
416
+ function aliasFor(address) {
417
+ const existing = aliases.get(address);
418
+ if (existing) return existing;
419
+ const alias = `addr_${String(aliases.size + 1).padStart(3, "0")}`;
420
+ aliases.set(address, alias);
421
+ redactions.add(`aliased:${alias}`);
422
+ return alias;
423
+ }
424
+ function redactSecrets(input) {
425
+ let output = input;
426
+ for (const pattern of SECRET_PATTERNS) output = output.replace(pattern, () => {
427
+ redactions.add("secret");
428
+ return "[redacted-secret]";
429
+ });
430
+ return output;
431
+ }
432
+ function text(input) {
433
+ let output = redactSecrets(input);
434
+ if (mode === "public") {
435
+ output = output.replace(SUBSTRATE_ADDRESS_RE, (match) => aliasFor(match));
436
+ output = output.replace(EVM_ADDRESS_RE, (match) => aliasFor(match));
437
+ }
438
+ return output;
439
+ }
440
+ function value(input) {
441
+ if (typeof input === "string") return text(input);
442
+ if (Array.isArray(input)) return input.map((item) => value(item));
443
+ if (input && typeof input === "object") return Object.fromEntries(Object.entries(input).map(([key, entry]) => [key, value(entry)]));
444
+ return input;
445
+ }
446
+ return {
447
+ text,
448
+ value,
449
+ aliasFor,
450
+ redactions: () => [...redactions].sort()
451
+ };
452
+ }
453
+ //#endregion
454
+ //#region src/export/index.ts
455
+ async function readEvidence(caseId) {
456
+ const paths = workspaceOutputPaths();
457
+ const dir = path.join(paths.casesRoot, caseId, "evidence");
458
+ const files = await readdir(dir).catch(() => []);
459
+ const docs = [];
460
+ for (const filename of files.filter((file) => file.endsWith(".md")).sort()) {
461
+ const { frontmatter, body } = parseFrontmatter(await readFile(path.join(dir, filename), "utf8"));
462
+ docs.push({
463
+ id: frontmatter["id"] || filename.replace(/\.md$/, ""),
464
+ filename,
465
+ source: frontmatter["source"] || "unknown",
466
+ timestamp: frontmatter["timestamp"] || "",
467
+ body
468
+ });
469
+ }
470
+ return docs;
471
+ }
472
+ async function writeFiles(root, entries) {
473
+ const written = [];
474
+ for (const [relativePath, content] of entries) written.push(await writePrivateFile(root, relativePath, content));
475
+ return written;
476
+ }
477
+ async function exportCase(rawOptions) {
478
+ const options = CaseExportOptionsSchema.parse(rawOptions);
479
+ const workspace = workspaceOutputPaths();
480
+ const caseInfo = await CaseStore.get(options.caseId);
481
+ const redactor = createRedactor(options.mode);
482
+ const evidenceVerification = await EvidenceStore.verifyManifest(options.caseId);
483
+ const evidenceDocs = await readEvidence(options.caseId);
484
+ const dossiers = await DossierStore.listSummaries(options.caseId);
485
+ const graph = redactor.value(await loadCaseExportGraph(options.caseId));
486
+ const canvas = graphToCanvas(graph);
487
+ const outputRoot = path.resolve(options.outputDir ?? path.join(workspace.root, "published", safeSlug(caseInfo.name)));
488
+ await mkdir(outputRoot, {
489
+ recursive: true,
490
+ mode: 448
491
+ });
492
+ const entries = [
493
+ ["README.md", renderReadme(redactor.text(caseInfo.name))],
494
+ ["Case.md", renderCaseMarkdown({
495
+ caseInfo: {
496
+ id: caseInfo.id,
497
+ name: redactor.text(caseInfo.name),
498
+ status: caseInfo.status,
499
+ tags: caseInfo.tags,
500
+ description: redactor.text(caseInfo.description)
501
+ },
502
+ mode: options.mode,
503
+ evidenceVerified: evidenceVerification.ok,
504
+ evidenceCount: evidenceVerification.count,
505
+ entityCount: dossiers.length
506
+ })],
507
+ ["LLMWIKI.md", renderLlmWiki()],
508
+ ["llms.txt", renderLlmsTxt()],
509
+ ["Agent Console.md", renderAgentConsole(redactor.text(caseInfo.name))],
510
+ ["Prompts/Codex.md", renderPrompt("Codex")],
511
+ ["Prompts/Claude-Code.md", renderPrompt("Claude Code")],
512
+ ["Prompts/ChatGPT.md", renderPrompt("ChatGPT")],
513
+ ["Sources/evidence-manifest.md", `# Evidence Manifest\n\nVerified: ${evidenceVerification.ok ? "yes" : "no"}\nEvidence files: ${evidenceVerification.count}\n`],
514
+ ["Sources/reports-index.md", "# Reports Index\n\nGraph and report artifacts are exported when present.\n"],
515
+ ["graph.chain-insights.json", JSON.stringify(graph, null, 2) + "\n"],
516
+ ["Graph.canvas", JSON.stringify(canvas, null, 2) + "\n"]
517
+ ];
518
+ for (const evidence of evidenceDocs) entries.push([path.join("Evidence", safeFilename(evidence.id)), redactor.text([
519
+ `# Evidence: ${evidence.source}`,
520
+ "",
521
+ `Source file: \`${evidence.filename}\``,
522
+ `Captured: ${evidence.timestamp || "unknown"}`,
523
+ "",
524
+ evidence.body,
525
+ ""
526
+ ].join("\n"))]);
527
+ const entityPaths = /* @__PURE__ */ new Set();
528
+ for (const dossier of dossiers) {
529
+ const entityId = options.mode === "public" ? redactor.aliasFor(dossier.address) : dossier.address;
530
+ const entityPath = path.join("Entities", safeFilename(entityId));
531
+ entityPaths.add(entityPath);
532
+ entries.push([entityPath, redactor.text([
533
+ `# Entity: ${entityId}`,
534
+ "",
535
+ `Type: ${dossier.type}`,
536
+ `First seen: ${dossier.firstSeen || "unknown"}`,
537
+ `Last seen: ${dossier.lastSeen || "unknown"}`,
538
+ `Risk tags: ${dossier.riskTags || "none"}`,
539
+ ""
540
+ ].join("\n"))]);
541
+ }
542
+ for (const [index, node] of graph.nodes.entries()) {
543
+ const entityId = graphNodeId(node, index);
544
+ const entityPath = entityNotePath(entityId);
545
+ if (entityPaths.has(entityPath)) continue;
546
+ entityPaths.add(entityPath);
547
+ entries.push([entityPath, [
548
+ `# Entity: ${entityId}`,
549
+ "",
550
+ `Address: ${String(node["address"] ?? entityId)}`,
551
+ `Roles: ${Array.isArray(node["roles"]) ? node["roles"].map(String).join(", ") || "none" : "none"}`,
552
+ `Node type: ${String(node["node_type"] ?? "unknown")}`,
553
+ "",
554
+ "## Graph Links",
555
+ "",
556
+ "- [[Graph.canvas]]",
557
+ ""
558
+ ].join("\n")]);
559
+ }
560
+ const files = await writeFiles(outputRoot, entries);
561
+ const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
562
+ const manifest = CaseExportManifestSchema.parse({
563
+ schema: "chain-insights.case_export.v1",
564
+ case_id: caseInfo.id,
565
+ case_name: redactor.text(caseInfo.name),
566
+ exported_at: exportedAt,
567
+ mode: options.mode,
568
+ target: options.target,
569
+ source_workspace: workspace.root,
570
+ verification: {
571
+ evidence_manifest_verified: evidenceVerification.ok,
572
+ verified_at: exportedAt,
573
+ evidence_count: evidenceVerification.count
574
+ },
575
+ files,
576
+ redactions: redactor.redactions(),
577
+ warnings: evidenceVerification.ok ? [] : [`Evidence manifest failed: ${(evidenceVerification.tampered ?? []).join(", ")}`]
578
+ });
579
+ const manifestFile = await writePrivateFile(outputRoot, "manifest.chain-insights.json", JSON.stringify(manifest, null, 2) + "\n");
580
+ return {
581
+ manifestPath: path.join(outputRoot, manifestFile.path),
582
+ outputDir: outputRoot,
583
+ fileCount: files.length + 1,
584
+ warnings: manifest.warnings,
585
+ nextFile: "Agent Console.md"
586
+ };
587
+ }
588
+ //#endregion
589
+ export { exportCase };
590
+
591
+ //# sourceMappingURL=export-BqTCO9lP.mjs.map