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.
- 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 +129 -118
- package/docs/investigation-workspaces.md +14 -0
- package/docs/mcp-proxy.md +20 -2
- package/package.json +1 -1
- package/skills/chain-insights-bittensor-cypher/SKILL.md +24 -0
- package/skills/chain-insights-cypher/SKILL.md +16 -1
- package/skills/chain-insights-cypher/references/memgraph-examples.md +193 -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
|
@@ -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
|
|
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-
|
|
78
|
+
//# sourceMappingURL=dossier-Bjpcbcxa.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dossier-
|
|
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
|