chain-insights 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +10 -0
- package/bin/install.cjs +252 -0
- package/bin/mcp-proxy.cjs +10 -0
- package/dist/active-BSrxLKwn.mjs +50 -0
- package/dist/active-BSrxLKwn.mjs.map +1 -0
- package/dist/active-Dv7Tu-O4.cjs +68 -0
- package/dist/app-BjjuQM0B.mjs +155 -0
- package/dist/app-BjjuQM0B.mjs.map +1 -0
- package/dist/app-Dq1TdB6p.cjs +161 -0
- package/dist/artifact-server-DoxJ7fCx.cjs +47 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs +48 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs.map +1 -0
- package/dist/assets/bg-pattern.png +0 -0
- package/dist/assets/logo.png +0 -0
- package/dist/call-args-DQA2QcRA.cjs +27 -0
- package/dist/call-args-Lk_wOJxd.mjs +29 -0
- package/dist/call-args-Lk_wOJxd.mjs.map +1 -0
- package/dist/capabilities-CB97WMA5.cjs +83 -0
- package/dist/capabilities-DliMBim-.mjs +84 -0
- package/dist/capabilities-DliMBim-.mjs.map +1 -0
- package/dist/cases-By7INiOa.mjs +6 -0
- package/dist/cases-CDcNU91B.cjs +9 -0
- package/dist/chunk-CZWwpsFl.cjs +43 -0
- package/dist/cli.cjs +752 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +753 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-D4Bq0rp9.mjs +111 -0
- package/dist/client-D4Bq0rp9.mjs.map +1 -0
- package/dist/client-D4fZgIaO.cjs +132 -0
- package/dist/config-Bmdl5hdk.cjs +67 -0
- package/dist/config-BwrBYmiC.mjs +44 -0
- package/dist/config-BwrBYmiC.mjs.map +1 -0
- package/dist/data-extractor-BNGj7ECT.cjs +347 -0
- package/dist/data-extractor-DFzsa5CS.mjs +336 -0
- package/dist/data-extractor-DFzsa5CS.mjs.map +1 -0
- package/dist/dossier-BsroDgD3.mjs +76 -0
- package/dist/dossier-BsroDgD3.mjs.map +1 -0
- package/dist/dossier-DtxREpPm.cjs +76 -0
- package/dist/evidence-BGcdKxuV.cjs +200 -0
- package/dist/evidence-BhvFW-y_.mjs +195 -0
- package/dist/evidence-BhvFW-y_.mjs.map +1 -0
- package/dist/format-Ce1RObVl.mjs +22 -0
- package/dist/format-Ce1RObVl.mjs.map +1 -0
- package/dist/format-DOrPvXEr.cjs +20 -0
- package/dist/frontmatter-D8wWCeOa.mjs +26 -0
- package/dist/frontmatter-D8wWCeOa.mjs.map +1 -0
- package/dist/frontmatter-DgAuai7E.cjs +35 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs +130 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs.map +1 -0
- package/dist/graph-normalizer-DeIj6Ses.cjs +133 -0
- package/dist/graph-reports-C4TBjCkM.mjs +63 -0
- package/dist/graph-reports-C4TBjCkM.mjs.map +1 -0
- package/dist/graph-reports-DU05YCei.cjs +64 -0
- package/dist/html-generator-CAv81IWH.cjs +85 -0
- package/dist/html-generator-V6Bp0uRb.mjs +68 -0
- package/dist/html-generator-V6Bp0uRb.mjs.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +9 -0
- package/dist/init-BjuFt54X.cjs +232 -0
- package/dist/init-CaOsHTIo.mjs +232 -0
- package/dist/init-CaOsHTIo.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +1257 -0
- package/dist/mcp-proxy.d.cts +12 -0
- package/dist/mcp-proxy.d.cts.map +1 -0
- package/dist/mcp-proxy.d.mts +12 -0
- package/dist/mcp-proxy.d.mts.map +1 -0
- package/dist/mcp-proxy.mjs +1255 -0
- package/dist/mcp-proxy.mjs.map +1 -0
- package/dist/output-root-CFYms3ad.cjs +43 -0
- package/dist/output-root-CmWM7aV2.mjs +33 -0
- package/dist/output-root-CmWM7aV2.mjs.map +1 -0
- package/dist/parser-BUIWW1OH.cjs +182 -0
- package/dist/parser-DO0_SssG.mjs +182 -0
- package/dist/parser-DO0_SssG.mjs.map +1 -0
- package/dist/public-tools-D4UI-Zb0.mjs +2554 -0
- package/dist/public-tools-D4UI-Zb0.mjs.map +1 -0
- package/dist/public-tools-XSpkz2ky.cjs +2556 -0
- package/dist/resolver-C2ZS7oC8.mjs +201 -0
- package/dist/resolver-C2ZS7oC8.mjs.map +1 -0
- package/dist/resolver-zYbu4wDV.cjs +203 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/runner-1Eq55OYb.cjs +148 -0
- package/dist/runner-BhUHbiHG.mjs +149 -0
- package/dist/runner-BhUHbiHG.mjs.map +1 -0
- package/dist/schema-4XpzDFQM.cjs +55 -0
- package/dist/schema-8d0rVIdZ.mjs +37 -0
- package/dist/schema-8d0rVIdZ.mjs.map +1 -0
- package/dist/schema-cache-9CksD7tX.mjs +34 -0
- package/dist/schema-cache-9CksD7tX.mjs.map +1 -0
- package/dist/schema-cache-CgWRCN2N.cjs +36 -0
- package/dist/selector-CkFcTXzz.cjs +10 -0
- package/dist/selector-xjm6NTHI.mjs +12 -0
- package/dist/selector-xjm6NTHI.mjs.map +1 -0
- package/dist/server-BkM5xrXb.mjs +45 -0
- package/dist/server-BkM5xrXb.mjs.map +1 -0
- package/dist/server-DXowbpfi.cjs +54 -0
- package/dist/session-BpNylyuJ.cjs +115 -0
- package/dist/session-CcTgYxsj.mjs +115 -0
- package/dist/session-CcTgYxsj.mjs.map +1 -0
- package/dist/setup-DOpKPrlx.cjs +81 -0
- package/dist/setup-DyrWHuwQ.mjs +80 -0
- package/dist/setup-DyrWHuwQ.mjs.map +1 -0
- package/dist/store-BiUhQOIf.cjs +230 -0
- package/dist/store-BoWE-Gtl.mjs +225 -0
- package/dist/store-BoWE-Gtl.mjs.map +1 -0
- package/dist/templates/graph.html +1406 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs +28 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs.map +1 -0
- package/dist/tool-visibility-CwgY205r.cjs +36 -0
- package/dist/tools-Cp2jAAAb.mjs +100 -0
- package/dist/tools-Cp2jAAAb.mjs.map +1 -0
- package/dist/tools-f_vJUZAF.cjs +139 -0
- package/dist/topup-server-BZuQifvh.cjs +940 -0
- package/dist/topup-server-DUjyFftI.mjs +919 -0
- package/dist/topup-server-DUjyFftI.mjs.map +1 -0
- package/dist/version-1gP19Lhi.mjs +8 -0
- package/dist/version-1gP19Lhi.mjs.map +1 -0
- package/dist/version-BNGtdpmH.cjs +18 -0
- package/dist/viz-BlCJe6Tk.mjs +35 -0
- package/dist/viz-BlCJe6Tk.mjs.map +1 -0
- package/dist/viz-ClezVXrJ.cjs +44 -0
- package/dist/wallet-BMelXBYP.mjs +104 -0
- package/dist/wallet-BMelXBYP.mjs.map +1 -0
- package/dist/wallet-RnvvSpV2.cjs +146 -0
- package/docs/architecture.md +145 -0
- package/docs/contributing.md +68 -0
- package/docs/debugging.md +68 -0
- package/docs/development.md +44 -0
- package/docs/graph-tools.md +251 -0
- package/docs/images/graph-mcp-iframe.png +0 -0
- package/docs/images/graph-visualization.png +0 -0
- package/docs/images/topup-page.png +0 -0
- package/docs/investigation-workspaces.md +151 -0
- package/docs/mcp-proxy.md +180 -0
- package/package.json +59 -0
- package/skills/chain-insights-developer-experience/SKILL.md +101 -0
- package/skills/chain-insights-investigation/SKILL.md +285 -0
- package/skills/chain-insights-investigation/agents/openai.yaml +4 -0
- package/skills/chain-insights-investigation/scripts/run-target-uat.sh +197 -0
- package/skills/chain-insights-trace-funds/SKILL.md +249 -0
- package/skills/ci-case/SKILL.md +43 -0
- package/skills/ci-status/SKILL.md +45 -0
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +75 -0
- package/skills/test-chain-insights-graphrag-mcp/agents/openai.yaml +4 -0
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +414 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { n as serializeFrontmatter, t as parseFrontmatter } from "./frontmatter-D8wWCeOa.mjs";
|
|
2
|
+
import { n as workspaceOutputPaths } from "./output-root-CmWM7aV2.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
//#region src/cases/schema.ts
|
|
7
|
+
const caseIdRegex = /^\d{8}_\d{3}_[a-z0-9][a-z0-9-]*$/;
|
|
8
|
+
const CaseStatusEnum = z.enum([
|
|
9
|
+
"open",
|
|
10
|
+
"active",
|
|
11
|
+
"suspended",
|
|
12
|
+
"closed"
|
|
13
|
+
]);
|
|
14
|
+
const CaseSchema = z.object({
|
|
15
|
+
id: z.string().regex(caseIdRegex, "Invalid case ID format"),
|
|
16
|
+
name: z.string().min(1).max(200),
|
|
17
|
+
status: CaseStatusEnum.default("open"),
|
|
18
|
+
created: z.string().datetime(),
|
|
19
|
+
updated: z.string().datetime(),
|
|
20
|
+
tags: z.array(z.string()).default([]),
|
|
21
|
+
description: z.string().default(""),
|
|
22
|
+
slug: z.string().optional()
|
|
23
|
+
});
|
|
24
|
+
z.object({
|
|
25
|
+
id: z.string().min(1),
|
|
26
|
+
caseId: z.string().regex(caseIdRegex),
|
|
27
|
+
source: z.string().min(1),
|
|
28
|
+
timestamp: z.string().datetime(),
|
|
29
|
+
queryParams: z.string().default("")
|
|
30
|
+
});
|
|
31
|
+
z.object({
|
|
32
|
+
address: z.string().min(1).max(100),
|
|
33
|
+
type: z.enum([
|
|
34
|
+
"eoa",
|
|
35
|
+
"contract",
|
|
36
|
+
"exchange",
|
|
37
|
+
"mixer",
|
|
38
|
+
"unknown"
|
|
39
|
+
]).default("unknown"),
|
|
40
|
+
firstSeen: z.string().datetime(),
|
|
41
|
+
lastSeen: z.string().datetime(),
|
|
42
|
+
riskTags: z.string().default("")
|
|
43
|
+
});
|
|
44
|
+
z.object({
|
|
45
|
+
sessionId: z.string().min(1),
|
|
46
|
+
caseId: z.string().regex(caseIdRegex),
|
|
47
|
+
startTime: z.string().datetime(),
|
|
48
|
+
endTime: z.string().optional(),
|
|
49
|
+
status: z.enum(["active", "ended"]).default("active")
|
|
50
|
+
});
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/cases/store.ts
|
|
53
|
+
const casesRoot = () => workspaceOutputPaths().casesRoot;
|
|
54
|
+
function caseDir(id) {
|
|
55
|
+
return path.join(casesRoot(), id);
|
|
56
|
+
}
|
|
57
|
+
function generateCaseId(name, existingIds) {
|
|
58
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
59
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 40);
|
|
60
|
+
const todayNums = existingIds.filter((id) => id.startsWith(date + "_")).map((id) => parseInt(id.split("_")[1] ?? "0", 10)).filter((n) => !isNaN(n));
|
|
61
|
+
const next = todayNums.length > 0 ? Math.max(...todayNums) + 1 : 1;
|
|
62
|
+
return `${date}_${String(next).padStart(3, "0")}_${slug}`;
|
|
63
|
+
}
|
|
64
|
+
const CaseStore = {
|
|
65
|
+
async create(input) {
|
|
66
|
+
const root = casesRoot();
|
|
67
|
+
await mkdir(root, { recursive: true });
|
|
68
|
+
const existingIds = await readdir(root).catch(() => []);
|
|
69
|
+
const id = generateCaseId(input.name, existingIds);
|
|
70
|
+
const slug = id.split("_").slice(2).join("_");
|
|
71
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
72
|
+
const tags = input.tags;
|
|
73
|
+
const dir = caseDir(id);
|
|
74
|
+
await mkdir(path.join(dir, "evidence"), { recursive: true });
|
|
75
|
+
await mkdir(path.join(dir, "dossiers"), { recursive: true });
|
|
76
|
+
const fm = {
|
|
77
|
+
id,
|
|
78
|
+
name: input.name,
|
|
79
|
+
status: "open",
|
|
80
|
+
created: now,
|
|
81
|
+
updated: now,
|
|
82
|
+
tags: tags.join(","),
|
|
83
|
+
description: input.description,
|
|
84
|
+
slug
|
|
85
|
+
};
|
|
86
|
+
const body = [
|
|
87
|
+
`# ${input.name}`,
|
|
88
|
+
"",
|
|
89
|
+
`Opened: ${now}`,
|
|
90
|
+
`Status: open`,
|
|
91
|
+
"",
|
|
92
|
+
"## Question",
|
|
93
|
+
"",
|
|
94
|
+
input.description || "TBD",
|
|
95
|
+
"",
|
|
96
|
+
"## Current Assessment",
|
|
97
|
+
"",
|
|
98
|
+
"TBD",
|
|
99
|
+
"",
|
|
100
|
+
"## Top Findings",
|
|
101
|
+
"",
|
|
102
|
+
"| Finding | Confidence | Evidence |",
|
|
103
|
+
"|---|---:|---|",
|
|
104
|
+
"",
|
|
105
|
+
"## Next Actions",
|
|
106
|
+
"",
|
|
107
|
+
"- TBD",
|
|
108
|
+
"",
|
|
109
|
+
"## Reports",
|
|
110
|
+
""
|
|
111
|
+
].join("\n");
|
|
112
|
+
await writeFile(path.join(dir, "case.md"), serializeFrontmatter(fm, body), { mode: 384 });
|
|
113
|
+
const manifest = JSON.stringify({
|
|
114
|
+
caseId: id,
|
|
115
|
+
entries: []
|
|
116
|
+
}, null, 2) + "\n";
|
|
117
|
+
await writeFile(path.join(dir, "manifest.json"), manifest, { mode: 384 });
|
|
118
|
+
return CaseSchema.parse({
|
|
119
|
+
id,
|
|
120
|
+
name: input.name,
|
|
121
|
+
status: "open",
|
|
122
|
+
created: now,
|
|
123
|
+
updated: now,
|
|
124
|
+
tags,
|
|
125
|
+
description: input.description,
|
|
126
|
+
slug
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
async setStatus(id, status) {
|
|
130
|
+
const dir = caseDir(id);
|
|
131
|
+
const filePath = path.join(dir, "case.md");
|
|
132
|
+
const { frontmatter, body } = parseFrontmatter(await readFile(filePath, "utf8"));
|
|
133
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
134
|
+
frontmatter["status"] = status;
|
|
135
|
+
frontmatter["updated"] = now;
|
|
136
|
+
await writeFile(filePath, serializeFrontmatter(frontmatter, body), { mode: 384 });
|
|
137
|
+
const tags = (frontmatter["tags"] ?? "").split(",").filter(Boolean);
|
|
138
|
+
return CaseSchema.parse({
|
|
139
|
+
id,
|
|
140
|
+
name: frontmatter["name"] ?? "",
|
|
141
|
+
status,
|
|
142
|
+
created: frontmatter["created"] ?? now,
|
|
143
|
+
updated: now,
|
|
144
|
+
tags,
|
|
145
|
+
description: frontmatter["description"] ?? ""
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
async list() {
|
|
149
|
+
const root = casesRoot();
|
|
150
|
+
try {
|
|
151
|
+
const ids = await readdir(root);
|
|
152
|
+
const cases = [];
|
|
153
|
+
for (const id of ids) try {
|
|
154
|
+
const { frontmatter } = parseFrontmatter(await readFile(path.join(caseDir(id), "case.md"), "utf8"));
|
|
155
|
+
cases.push({
|
|
156
|
+
id,
|
|
157
|
+
name: frontmatter["name"] ?? id,
|
|
158
|
+
status: frontmatter["status"] ?? "open",
|
|
159
|
+
created: frontmatter["created"] ?? ""
|
|
160
|
+
});
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const nodeErr = err;
|
|
163
|
+
if (nodeErr.code !== "ENOENT" && nodeErr.code !== "ENOTDIR") throw err;
|
|
164
|
+
}
|
|
165
|
+
return cases.sort((a, b) => b.created.localeCompare(a.created) || b.id.localeCompare(a.id)).map(({ id, name, status }) => ({
|
|
166
|
+
id,
|
|
167
|
+
name,
|
|
168
|
+
status
|
|
169
|
+
}));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err.code === "ENOENT") return [];
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
async get(id) {
|
|
176
|
+
const dir = caseDir(id);
|
|
177
|
+
const { frontmatter } = parseFrontmatter(await readFile(path.join(dir, "case.md"), "utf8"));
|
|
178
|
+
const tags = (frontmatter["tags"] ?? "").split(",").filter(Boolean);
|
|
179
|
+
return CaseSchema.parse({
|
|
180
|
+
id,
|
|
181
|
+
name: frontmatter["name"] ?? "",
|
|
182
|
+
status: frontmatter["status"] ?? "open",
|
|
183
|
+
created: frontmatter["created"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
184
|
+
updated: frontmatter["updated"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
185
|
+
tags,
|
|
186
|
+
description: frontmatter["description"] ?? ""
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
async loadContext(id) {
|
|
190
|
+
const dir = caseDir(id);
|
|
191
|
+
const { frontmatter } = parseFrontmatter(await readFile(path.join(dir, "case.md"), "utf8"));
|
|
192
|
+
const tags = (frontmatter["tags"] ?? "").split(",").filter(Boolean);
|
|
193
|
+
const { SessionStore } = await import("./session-CcTgYxsj.mjs");
|
|
194
|
+
const { DossierStore } = await import("./dossier-BsroDgD3.mjs");
|
|
195
|
+
const [latestSession, dossierSummaries, manifest] = await Promise.all([
|
|
196
|
+
SessionStore.getLatest(id),
|
|
197
|
+
DossierStore.listSummaries(id),
|
|
198
|
+
readFile(path.join(dir, "manifest.json"), "utf8").catch(() => "{\"entries\":[]}")
|
|
199
|
+
]);
|
|
200
|
+
const evidenceCount = JSON.parse(manifest).entries.length;
|
|
201
|
+
const lastSession = latestSession ? {
|
|
202
|
+
sessionId: latestSession.frontmatter["sessionId"] ?? "",
|
|
203
|
+
startTime: latestSession.frontmatter["startTime"] ?? "",
|
|
204
|
+
endTime: latestSession.frontmatter["endTime"] || void 0,
|
|
205
|
+
body: latestSession.body
|
|
206
|
+
} : null;
|
|
207
|
+
return {
|
|
208
|
+
case: {
|
|
209
|
+
id,
|
|
210
|
+
name: frontmatter["name"] ?? "",
|
|
211
|
+
status: frontmatter["status"] ?? "open",
|
|
212
|
+
created: frontmatter["created"] ?? "",
|
|
213
|
+
updated: frontmatter["updated"] ?? "",
|
|
214
|
+
tags
|
|
215
|
+
},
|
|
216
|
+
lastSession,
|
|
217
|
+
dossierSummaries,
|
|
218
|
+
evidenceCount
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
export { CaseStore, casesRoot, generateCaseId, CaseStatusEnum as n, CaseSchema as t };
|
|
224
|
+
|
|
225
|
+
//# sourceMappingURL=store-BoWE-Gtl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-BoWE-Gtl.mjs","names":["nodeErr"],"sources":["../src/cases/schema.ts","../src/cases/store.ts"],"sourcesContent":["import * as z from 'zod'\n\n// Case ID format: YYYYMMDD_NNN_slug (e.g. 20260511_001_tornado-mixer)\n// Regex rejects path traversal chars (../, shell chars) per T-03-01 threat model.\nconst caseIdRegex = /^\\d{8}_\\d{3}_[a-z0-9][a-z0-9-]*$/\n\nexport const CaseStatusEnum = z.enum(['open', 'active', 'suspended', 'closed'])\nexport type CaseStatus = z.infer<typeof CaseStatusEnum>\n\nexport const CaseSchema = z.object({\n id: z.string().regex(caseIdRegex, 'Invalid case ID format'),\n name: z.string().min(1).max(200),\n status: CaseStatusEnum.default('open'),\n created: z.string().datetime(),\n updated: z.string().datetime(),\n tags: z.array(z.string()).default([]),\n description: z.string().default(''),\n slug: z.string().optional(),\n})\nexport type Case = z.infer<typeof CaseSchema>\n\nexport const EvidenceSchema = z.object({\n id: z.string().min(1),\n caseId: z.string().regex(caseIdRegex),\n source: z.string().min(1),\n timestamp: z.string().datetime(),\n queryParams: z.string().default(''),\n})\nexport type Evidence = z.infer<typeof EvidenceSchema>\n\nexport const DossierSchema = z.object({\n address: z.string().min(1).max(100),\n type: z.enum(['eoa', 'contract', 'exchange', 'mixer', 'unknown']).default('unknown'),\n firstSeen: z.string().datetime(),\n lastSeen: z.string().datetime(),\n riskTags: z.string().default(''),\n})\nexport type Dossier = z.infer<typeof DossierSchema>\n\nexport const SessionSchema = z.object({\n sessionId: z.string().min(1),\n caseId: z.string().regex(caseIdRegex),\n startTime: z.string().datetime(),\n endTime: z.string().optional(),\n status: z.enum(['active', 'ended']).default('active'),\n})\nexport type Session = z.infer<typeof SessionSchema>\n","import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\nimport { CaseSchema, type Case, type CaseStatus } from './schema.js'\n\nexport const casesRoot = () => workspaceOutputPaths().casesRoot\n\nfunction caseDir(id: string): string {\n return path.join(casesRoot(), id)\n}\n\nexport function generateCaseId(name: string, existingIds: string[]): string {\n const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')\n const slug = name.toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n .slice(0, 40)\n const todayNums = existingIds\n .filter(id => id.startsWith(date + '_'))\n .map(id => parseInt(id.split('_')[1] ?? '0', 10))\n .filter(n => !isNaN(n))\n const next = todayNums.length > 0 ? Math.max(...todayNums) + 1 : 1\n return `${date}_${String(next).padStart(3, '0')}_${slug}`\n}\n\nexport const CaseStore = {\n async create(input: { name: string; tags: string[]; description: string }): Promise<Case> {\n const root = casesRoot()\n await mkdir(root, { recursive: true })\n const existingIds = await readdir(root).catch(() => [])\n const id = generateCaseId(input.name, existingIds)\n const slug = id.split('_').slice(2).join('_')\n const now = new Date().toISOString()\n const tags = input.tags\n\n const dir = caseDir(id)\n await mkdir(path.join(dir, 'evidence'), { recursive: true })\n await mkdir(path.join(dir, 'dossiers'), { recursive: true })\n\n const fm: Record<string, string> = {\n id,\n name: input.name,\n status: 'open',\n created: now,\n updated: now,\n tags: tags.join(','),\n description: input.description,\n slug,\n }\n const body = [\n `# ${input.name}`,\n '',\n `Opened: ${now}`,\n `Status: open`,\n '',\n '## Question',\n '',\n input.description || 'TBD',\n '',\n '## Current Assessment',\n '',\n 'TBD',\n '',\n '## Top Findings',\n '',\n '| Finding | Confidence | Evidence |',\n '|---|---:|---|',\n '',\n '## Next Actions',\n '',\n '- TBD',\n '',\n '## Reports',\n '',\n ].join('\\n')\n await writeFile(path.join(dir, 'case.md'), serializeFrontmatter(fm, body), { mode: 0o600 })\n\n const manifest = JSON.stringify({ caseId: id, entries: [] }, null, 2) + '\\n'\n await writeFile(path.join(dir, 'manifest.json'), manifest, { mode: 0o600 })\n\n return CaseSchema.parse({ id, name: input.name, status: 'open', created: now, updated: now, tags, description: input.description, slug })\n },\n\n async setStatus(id: string, status: CaseStatus): Promise<Case> {\n const dir = caseDir(id)\n const filePath = path.join(dir, 'case.md')\n const raw = await readFile(filePath, 'utf8')\n const { frontmatter, body } = parseFrontmatter(raw)\n const now = new Date().toISOString()\n frontmatter['status'] = status\n frontmatter['updated'] = now\n await writeFile(filePath, serializeFrontmatter(frontmatter, body), { mode: 0o600 })\n\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n return CaseSchema.parse({\n id,\n name: frontmatter['name'] ?? '',\n status,\n created: frontmatter['created'] ?? now,\n updated: now,\n tags,\n description: frontmatter['description'] ?? '',\n })\n },\n\n async list(): Promise<Array<{ id: string; name: string; status: string }>> {\n const root = casesRoot()\n try {\n const ids = await readdir(root)\n const cases: Array<{ id: string; name: string; status: string; created: string }> = []\n for (const id of ids) {\n try {\n const raw = await readFile(path.join(caseDir(id), 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n cases.push({\n id,\n name: frontmatter['name'] ?? id,\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? '',\n })\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code !== 'ENOENT' && nodeErr.code !== 'ENOTDIR') throw err\n }\n }\n return cases\n .sort((a, b) => b.created.localeCompare(a.created) || b.id.localeCompare(a.id))\n .map(({ id, name, status }) => ({ id, name, status }))\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code === 'ENOENT') return []\n throw err\n }\n },\n\n async get(id: string): Promise<Case> {\n const dir = caseDir(id)\n const raw = await readFile(path.join(dir, 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n return CaseSchema.parse({\n id,\n name: frontmatter['name'] ?? '',\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? new Date().toISOString(),\n updated: frontmatter['updated'] ?? new Date().toISOString(),\n tags,\n description: frontmatter['description'] ?? '',\n })\n },\n\n async loadContext(id: string): Promise<{\n case: { id: string; name: string; status: string; created: string; updated: string; tags: string[] };\n lastSession: { sessionId: string; startTime: string; endTime?: string; body: string } | null;\n dossierSummaries: Array<{ address: string; type: string; riskTags: string; firstSeen: string; lastSeen: string }>;\n evidenceCount: number;\n }> {\n const dir = caseDir(id)\n\n // Read case.md\n const raw = await readFile(path.join(dir, 'case.md'), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n const tags = (frontmatter['tags'] ?? '').split(',').filter(Boolean)\n\n // Lazy imports to avoid circular deps\n const { SessionStore } = await import('./session.js')\n const { DossierStore } = await import('./dossier.js')\n\n const [latestSession, dossierSummaries, manifest] = await Promise.all([\n SessionStore.getLatest(id),\n DossierStore.listSummaries(id),\n readFile(path.join(dir, 'manifest.json'), 'utf8').catch(() => '{\"entries\":[]}'),\n ])\n\n const manifestData = JSON.parse(manifest) as { entries: unknown[] }\n const evidenceCount = manifestData.entries.length\n\n const lastSession = latestSession\n ? {\n sessionId: latestSession.frontmatter['sessionId'] ?? '',\n startTime: latestSession.frontmatter['startTime'] ?? '',\n endTime: latestSession.frontmatter['endTime'] || undefined,\n body: latestSession.body,\n }\n : null\n\n return {\n case: {\n id,\n name: frontmatter['name'] ?? '',\n status: frontmatter['status'] ?? 'open',\n created: frontmatter['created'] ?? '',\n updated: frontmatter['updated'] ?? '',\n tags,\n },\n lastSession,\n dossierSummaries,\n evidenceCount,\n }\n },\n}\n"],"mappings":"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"}
|