chain-insights 0.3.4 → 0.3.6
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 +28 -11
- package/dist/canvas-Cn-maEIh.mjs +203 -0
- package/dist/canvas-Cn-maEIh.mjs.map +1 -0
- package/dist/canvas-p-oKCMjc.cjs +251 -0
- package/dist/cases-Bz_9XKEw.cjs +19 -0
- package/dist/cases-TVcAifxu.mjs +16 -0
- package/dist/cases-TVcAifxu.mjs.map +1 -0
- package/dist/cli.cjs +74 -28
- package/dist/cli.mjs +74 -28
- package/dist/cli.mjs.map +1 -1
- package/dist/{data-extractor-DZUJu1Bz.mjs → data-extractor-B4nHw1wZ.mjs} +2 -2
- package/dist/{data-extractor-DZUJu1Bz.mjs.map → data-extractor-B4nHw1wZ.mjs.map} +1 -1
- package/dist/{data-extractor-Cavd7wHk.cjs → data-extractor-DS4rzy3M.cjs} +1 -1
- package/dist/{export-BqTCO9lP.mjs → export-CBhcJuZ6.mjs} +8 -205
- package/dist/export-CBhcJuZ6.mjs.map +1 -0
- package/dist/{export-DsXgtCwO.cjs → export-D4v4-6F4.cjs} +16 -214
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{init-DLBL_nVG.mjs → init-CKQ6F07J.mjs} +22 -5
- package/dist/init-CKQ6F07J.mjs.map +1 -0
- package/dist/{init-zqbd7i-_.cjs → init-Dhw8F23z.cjs} +21 -4
- package/dist/mcp-proxy.cjs +20 -20
- package/dist/mcp-proxy.mjs +20 -20
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-wJoAFDFa.mjs → public-tools-CyUZEz9B.mjs} +3 -3
- package/dist/{public-tools-wJoAFDFa.mjs.map → public-tools-CyUZEz9B.mjs.map} +1 -1
- package/dist/{public-tools-BvMb3H2P.cjs → public-tools-xfVNz9NE.cjs} +2 -2
- package/dist/{runner-BhZ4lnF1.cjs → runner-CVo41fjz.cjs} +2 -2
- package/dist/{runner-DIJSbkjc.mjs → runner-DWuSy1Se.mjs} +3 -3
- package/dist/{runner-DIJSbkjc.mjs.map → runner-DWuSy1Se.mjs.map} +1 -1
- package/dist/{selector-CF2o5gxN.mjs → selector-BvXM9jbe.mjs} +2 -2
- package/dist/{selector-CF2o5gxN.mjs.map → selector-BvXM9jbe.mjs.map} +1 -1
- package/dist/{selector-DfAMZEC9.cjs → selector-Dps_ZFxq.cjs} +1 -1
- package/dist/{store-CTtqQtaE.mjs → store-C2B_AssI.mjs} +2 -2
- package/dist/{store-CTtqQtaE.mjs.map → store-C2B_AssI.mjs.map} +1 -1
- package/dist/{store-CqPfs47P.cjs → store-CQhU8dz8.cjs} +0 -18
- package/dist/vault-B2y78Ypu.cjs +560 -0
- package/dist/vault-z35Dohdq.mjs +560 -0
- package/dist/vault-z35Dohdq.mjs.map +1 -0
- package/dist/{viz-Dqp3C5kb.cjs → viz-D1620cBX.cjs} +3 -3
- package/dist/{viz-5y24S5X1.mjs → viz-DB5XFG1z.mjs} +4 -4
- package/dist/{viz-5y24S5X1.mjs.map → viz-DB5XFG1z.mjs.map} +1 -1
- package/docs/graph-tools.md +24 -8
- package/docs/investigation-workspaces.md +36 -10
- package/docs/knowledge-exports.md +6 -2
- package/docs/mcp-proxy.md +29 -7
- package/docs/obsidian-vault.md +130 -0
- package/package.json +1 -1
- package/skills/chain-insights-developer-experience/SKILL.md +2 -2
- package/skills/chain-insights-investigation/SKILL.md +1 -1
- package/dist/cases-Cp9DUbEV.mjs +0 -6
- package/dist/cases-sTY5aXav.cjs +0 -9
- package/dist/export-BqTCO9lP.mjs.map +0 -1
- package/dist/init-DLBL_nVG.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -46,14 +46,20 @@ npm install -g .
|
|
|
46
46
|
cia --version
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Create an investigation
|
|
49
|
+
Create an investigation vault:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
mkdir -p ./chain-insights-investigations
|
|
53
53
|
cd ./chain-insights-investigations
|
|
54
54
|
cia init .
|
|
55
|
+
cia obsidian open .
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
Chain Insights workspaces are Obsidian-compatible vaults and plain local
|
|
59
|
+
folders. Obsidian is a first-class review UI, but it is not required to use the
|
|
60
|
+
workspace files. See the
|
|
61
|
+
[Obsidian vault workflow](docs/obsidian-vault.md).
|
|
62
|
+
|
|
57
63
|
## Configure GraphRAG MCP Endpoint
|
|
58
64
|
|
|
59
65
|
`cia` uses `graphMcpEndpoint` for all GraphRAG MCP calls. The npm package does
|
|
@@ -114,8 +120,11 @@ GraphRAG MCP endpoint.
|
|
|
114
120
|
Hosted GraphRAG MCP lets new users try `graph_query` with a small public free
|
|
115
121
|
quota before setting up paid access. The default public free graph_query quota
|
|
116
122
|
is 10 execution seconds per IP per UTC day. Use `usage_status` to see the
|
|
117
|
-
current caller quota.
|
|
118
|
-
|
|
123
|
+
current caller quota. Prepared wallet users receive the daily public grant
|
|
124
|
+
first, then paid access continues automatically after the grant is exhausted.
|
|
125
|
+
If you do not have a prepared wallet yet, use bounded single `graph_query`
|
|
126
|
+
calls for the demo, then prepare a wallet or use an invited tester access key
|
|
127
|
+
when the quota is exhausted.
|
|
119
128
|
|
|
120
129
|
Open a case and run a small investigation:
|
|
121
130
|
|
|
@@ -137,9 +146,11 @@ cia case show 1
|
|
|
137
146
|
find reports cases -maxdepth 3 -type f | sort
|
|
138
147
|
```
|
|
139
148
|
|
|
140
|
-
## Export
|
|
149
|
+
## Export Only When Sharing
|
|
141
150
|
|
|
142
|
-
|
|
151
|
+
Normal local work happens in the investigation vault. Export only when you need
|
|
152
|
+
to share a case, hand it off to a partner, ingest it into LLM Wiki, or archive a
|
|
153
|
+
review checkpoint.
|
|
143
154
|
|
|
144
155
|
```bash
|
|
145
156
|
cia case evidence verify 1
|
|
@@ -150,10 +161,11 @@ The export writes Markdown notes, `manifest.chain-insights.json`,
|
|
|
150
161
|
`graph.chain-insights.json`, `Graph.canvas`, LLM Wiki entrypoints, and prompts
|
|
151
162
|
for Codex, Claude Code, and ChatGPT under `published/<case-slug>/`.
|
|
152
163
|
|
|
153
|
-
Private exports may include full addresses. Use `--mode
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
[
|
|
164
|
+
Private exports may include full addresses. Use `--mode partner` for controlled
|
|
165
|
+
handoff after review. Use `--mode public` only for shareable demos; public mode
|
|
166
|
+
aliases addresses and removes secrets by default. Vault workflow guidance lives
|
|
167
|
+
in [Obsidian vault workflow](docs/obsidian-vault.md); export bundle details
|
|
168
|
+
live in [Knowledge exports](docs/knowledge-exports.md).
|
|
157
169
|
|
|
158
170
|
## Demo
|
|
159
171
|
|
|
@@ -173,6 +185,10 @@ cia mcp call graph_query_batch \
|
|
|
173
185
|
'queries=[{"id":"count","query":"USE live_topology MATCH (n) RETURN count(n) AS count LIMIT 1"},{"id":"archive_flows","query":"USE archive_topology MATCH (src:Address)-[f:FLOWS_TO]->(dst:Address) RETURN f.period_granularity AS granularity, src.address AS source, dst.address AS target LIMIT 3"},{"id":"facts_sample","query":"USE facts MATCH (a:Address)-[:HAS_FEATURE]->(f:AddressFeature) RETURN a.address AS address, f.sent_count AS sent_count LIMIT 3"}]'
|
|
174
186
|
```
|
|
175
187
|
|
|
188
|
+
For no-wallet public demos, prefer the single-query example first. Batch calls
|
|
189
|
+
reserve worst-case execution time and can ask for paid access even when a small
|
|
190
|
+
free balance remains.
|
|
191
|
+
|
|
176
192
|
Run suspect topology without requiring an incident timestamp:
|
|
177
193
|
|
|
178
194
|
```bash
|
|
@@ -249,8 +265,9 @@ reports under the workspace instead of embedding large payloads in case notes.
|
|
|
249
265
|
| Doc | Use it for |
|
|
250
266
|
| --- | --- |
|
|
251
267
|
| [Graph tools](docs/graph-tools.md) | GraphRAG MCP layers, `graph_query`, `graph_query_batch`, AML tool contracts, graph reports, evidence pointers |
|
|
252
|
-
| [
|
|
253
|
-
| [
|
|
268
|
+
| [Obsidian vault workflow](docs/obsidian-vault.md) | Create an investigation vault, open Obsidian, refresh live notes, and use VS Code, Codex, Claude Code, and LLM Wiki overlays |
|
|
269
|
+
| [Investigation workspaces](docs/investigation-workspaces.md) | `cia init`, Obsidian-compatible vault layout, live note refresh, evidence, dossiers, imports, templates, sessions, reports |
|
|
270
|
+
| [Knowledge exports](docs/knowledge-exports.md) | Portable and redacted bundles for sharing, partner handoff, LLM Wiki ingestion, and archive |
|
|
254
271
|
| [MCP proxy](docs/mcp-proxy.md) | Stdio proxy behavior, endpoint configuration, agent installers, local tools, auth modes, Inspector validation |
|
|
255
272
|
| [Architecture](docs/architecture.md) | Product layers, data flow, local storage, security model, config keys |
|
|
256
273
|
| [Development](docs/development.md) | Build, test, and local install commands |
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/export/paths.ts
|
|
6
|
+
function safeSlug(value) {
|
|
7
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "case-export";
|
|
8
|
+
}
|
|
9
|
+
function safeFilename(value) {
|
|
10
|
+
const parsed = path.parse(value);
|
|
11
|
+
return `${safeSlug(parsed.name)}${parsed.ext.toLowerCase().replace(/[^.a-z0-9]/g, "") || ".md"}`;
|
|
12
|
+
}
|
|
13
|
+
function assertInsideDirectory(root, candidate) {
|
|
14
|
+
const resolvedRoot = path.resolve(root);
|
|
15
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
16
|
+
const relative = path.relative(resolvedRoot, resolvedCandidate);
|
|
17
|
+
if (relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative)) return;
|
|
18
|
+
throw new Error(`Refusing to write outside export directory: ${candidate}`);
|
|
19
|
+
}
|
|
20
|
+
async function assertNoSymlink(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
if ((await lstat(filePath)).isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${filePath}`);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.code === "ENOENT") return;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function writePrivateFile(root, relativePath, content) {
|
|
29
|
+
const filePath = path.join(root, relativePath);
|
|
30
|
+
assertInsideDirectory(root, filePath);
|
|
31
|
+
await mkdir(path.dirname(filePath), {
|
|
32
|
+
recursive: true,
|
|
33
|
+
mode: 448
|
|
34
|
+
});
|
|
35
|
+
await assertNoSymlink(filePath);
|
|
36
|
+
await writeFile(filePath, content, { mode: 384 });
|
|
37
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
38
|
+
return {
|
|
39
|
+
path: relativePath,
|
|
40
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
41
|
+
bytes
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/export/schema.ts
|
|
46
|
+
const CaseExportTargetSchema = z.enum(["obsidian-llmwiki"]);
|
|
47
|
+
const CaseExportModeSchema = z.enum([
|
|
48
|
+
"private",
|
|
49
|
+
"partner",
|
|
50
|
+
"public"
|
|
51
|
+
]);
|
|
52
|
+
const caseIdRegex = /^\d{8}_\d{3}_[a-z0-9][a-z0-9-]*$/;
|
|
53
|
+
const CaseExportOptionsSchema = z.object({
|
|
54
|
+
caseId: z.string().regex(caseIdRegex),
|
|
55
|
+
target: CaseExportTargetSchema.default("obsidian-llmwiki"),
|
|
56
|
+
mode: CaseExportModeSchema.default("private"),
|
|
57
|
+
outputDir: z.string().optional()
|
|
58
|
+
});
|
|
59
|
+
const ExportedFileSchema = z.object({
|
|
60
|
+
path: z.string().min(1),
|
|
61
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
62
|
+
bytes: z.number().int().nonnegative()
|
|
63
|
+
});
|
|
64
|
+
const CaseExportManifestSchema = z.object({
|
|
65
|
+
schema: z.literal("chain-insights.case_export.v1"),
|
|
66
|
+
case_id: z.string().regex(caseIdRegex),
|
|
67
|
+
case_name: z.string().min(1),
|
|
68
|
+
exported_at: z.string().datetime(),
|
|
69
|
+
mode: CaseExportModeSchema,
|
|
70
|
+
target: CaseExportTargetSchema,
|
|
71
|
+
source_workspace: z.string().min(1),
|
|
72
|
+
verification: z.object({
|
|
73
|
+
evidence_manifest_verified: z.boolean(),
|
|
74
|
+
verified_at: z.string().datetime(),
|
|
75
|
+
evidence_count: z.number().int().nonnegative()
|
|
76
|
+
}),
|
|
77
|
+
files: z.array(ExportedFileSchema),
|
|
78
|
+
redactions: z.array(z.string()),
|
|
79
|
+
warnings: z.array(z.string())
|
|
80
|
+
});
|
|
81
|
+
const JsonCanvasNodeSchema = z.object({
|
|
82
|
+
id: z.string().min(1),
|
|
83
|
+
type: z.enum([
|
|
84
|
+
"text",
|
|
85
|
+
"file",
|
|
86
|
+
"link",
|
|
87
|
+
"group"
|
|
88
|
+
]),
|
|
89
|
+
x: z.number(),
|
|
90
|
+
y: z.number(),
|
|
91
|
+
width: z.number().positive(),
|
|
92
|
+
height: z.number().positive(),
|
|
93
|
+
text: z.string().optional(),
|
|
94
|
+
file: z.string().optional(),
|
|
95
|
+
url: z.string().optional(),
|
|
96
|
+
label: z.string().optional(),
|
|
97
|
+
color: z.string().optional()
|
|
98
|
+
});
|
|
99
|
+
const JsonCanvasEdgeSchema = z.object({
|
|
100
|
+
id: z.string().min(1),
|
|
101
|
+
fromNode: z.string().min(1),
|
|
102
|
+
toNode: z.string().min(1),
|
|
103
|
+
fromSide: z.enum([
|
|
104
|
+
"top",
|
|
105
|
+
"right",
|
|
106
|
+
"bottom",
|
|
107
|
+
"left"
|
|
108
|
+
]).optional(),
|
|
109
|
+
toSide: z.enum([
|
|
110
|
+
"top",
|
|
111
|
+
"right",
|
|
112
|
+
"bottom",
|
|
113
|
+
"left"
|
|
114
|
+
]).optional(),
|
|
115
|
+
toEnd: z.enum(["none", "arrow"]).optional(),
|
|
116
|
+
label: z.string().optional(),
|
|
117
|
+
color: z.string().optional()
|
|
118
|
+
});
|
|
119
|
+
const JsonCanvasSchema = z.object({
|
|
120
|
+
nodes: z.array(JsonCanvasNodeSchema),
|
|
121
|
+
edges: z.array(JsonCanvasEdgeSchema)
|
|
122
|
+
});
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/export/canvas.ts
|
|
125
|
+
function roleColor(roles) {
|
|
126
|
+
if (roles.includes("victim")) return "1";
|
|
127
|
+
if (roles.includes("suspect") || roles.includes("scam_candidate")) return "2";
|
|
128
|
+
if (roles.includes("deposit")) return "3";
|
|
129
|
+
if (roles.includes("exchange")) return "5";
|
|
130
|
+
if (roles.includes("service")) return "6";
|
|
131
|
+
return "#808080";
|
|
132
|
+
}
|
|
133
|
+
function nodeRoles(node) {
|
|
134
|
+
return Array.isArray(node["roles"]) ? node["roles"].map(String) : [];
|
|
135
|
+
}
|
|
136
|
+
function nodeLabel(node) {
|
|
137
|
+
return String(node["address"] ?? node["id"] ?? "unknown");
|
|
138
|
+
}
|
|
139
|
+
function graphNodeId(node, index) {
|
|
140
|
+
return String(node["id"] ?? node["address"] ?? `node-${index + 1}`);
|
|
141
|
+
}
|
|
142
|
+
function entityNotePath(entityId) {
|
|
143
|
+
return `Entities/${safeFilename(entityId)}`;
|
|
144
|
+
}
|
|
145
|
+
function graphToCanvas(graph) {
|
|
146
|
+
const nodes = [{
|
|
147
|
+
id: "case",
|
|
148
|
+
type: "file",
|
|
149
|
+
file: "Case.md",
|
|
150
|
+
x: 0,
|
|
151
|
+
y: 0,
|
|
152
|
+
width: 360,
|
|
153
|
+
height: 120,
|
|
154
|
+
color: "4"
|
|
155
|
+
}];
|
|
156
|
+
const nodeIdMap = /* @__PURE__ */ new Map();
|
|
157
|
+
graph.nodes.forEach((node, index) => {
|
|
158
|
+
const rawId = graphNodeId(node, index);
|
|
159
|
+
const canvasId = `entity-${index + 1}`;
|
|
160
|
+
nodeIdMap.set(rawId, canvasId);
|
|
161
|
+
nodes.push({
|
|
162
|
+
id: canvasId,
|
|
163
|
+
type: "file",
|
|
164
|
+
file: entityNotePath(rawId),
|
|
165
|
+
x: 420 + index % 4 * 340,
|
|
166
|
+
y: Math.floor(index / 4) * 220,
|
|
167
|
+
width: 300,
|
|
168
|
+
height: 120,
|
|
169
|
+
color: roleColor(nodeRoles(node))
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
const edges = graph.edges.flatMap((edge, index) => {
|
|
173
|
+
const from = nodeIdMap.get(String(edge["source"] ?? ""));
|
|
174
|
+
const to = nodeIdMap.get(String(edge["target"] ?? ""));
|
|
175
|
+
if (!from || !to) return [];
|
|
176
|
+
return [{
|
|
177
|
+
id: `edge-${index + 1}`,
|
|
178
|
+
fromNode: from,
|
|
179
|
+
toNode: to,
|
|
180
|
+
fromSide: "right",
|
|
181
|
+
toSide: "left",
|
|
182
|
+
toEnd: "arrow",
|
|
183
|
+
label: String(edge["edge_type"] ?? "related_to")
|
|
184
|
+
}];
|
|
185
|
+
});
|
|
186
|
+
for (const [index, node] of graph.nodes.entries()) edges.push({
|
|
187
|
+
id: `case-link-${index + 1}`,
|
|
188
|
+
fromNode: "case",
|
|
189
|
+
toNode: `entity-${index + 1}`,
|
|
190
|
+
fromSide: "right",
|
|
191
|
+
toSide: "left",
|
|
192
|
+
toEnd: "arrow",
|
|
193
|
+
label: nodeLabel(node)
|
|
194
|
+
});
|
|
195
|
+
return JsonCanvasSchema.parse({
|
|
196
|
+
nodes,
|
|
197
|
+
edges
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
export { CaseExportOptionsSchema as a, writePrivateFile as c, CaseExportManifestSchema as i, graphNodeId as n, safeFilename as o, graphToCanvas as r, safeSlug as s, entityNotePath as t };
|
|
202
|
+
|
|
203
|
+
//# sourceMappingURL=canvas-Cn-maEIh.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-Cn-maEIh.mjs","names":[],"sources":["../src/export/paths.ts","../src/export/schema.ts","../src/export/canvas.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { lstat, mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport function safeSlug(value: string): string {\n const slug = value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80)\n return slug || 'case-export'\n}\n\nexport function safeFilename(value: string): string {\n const parsed = path.parse(value)\n const name = safeSlug(parsed.name)\n const ext = parsed.ext.toLowerCase().replace(/[^.a-z0-9]/g, '')\n return `${name}${ext || '.md'}`\n}\n\nexport function assertInsideDirectory(root: string, candidate: string): void {\n const resolvedRoot = path.resolve(root)\n const resolvedCandidate = path.resolve(candidate)\n const relative = path.relative(resolvedRoot, resolvedCandidate)\n if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) return\n throw new Error(`Refusing to write outside export directory: ${candidate}`)\n}\n\nexport async function assertNoSymlink(filePath: string): Promise<void> {\n try {\n const stat = await lstat(filePath)\n if (stat.isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${filePath}`)\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return\n throw err\n }\n}\n\nexport async function writePrivateFile(\n root: string,\n relativePath: string,\n content: string,\n): Promise<{ path: string; sha256: string; bytes: number }> {\n const filePath = path.join(root, relativePath)\n assertInsideDirectory(root, filePath)\n await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 })\n await assertNoSymlink(filePath)\n await writeFile(filePath, content, { mode: 0o600 })\n const bytes = Buffer.byteLength(content, 'utf8')\n const sha256 = createHash('sha256').update(content).digest('hex')\n return { path: relativePath, sha256, bytes }\n}\n","import * as z from 'zod'\n\nexport const CaseExportTargetSchema = z.enum(['obsidian-llmwiki'])\nexport type CaseExportTarget = z.infer<typeof CaseExportTargetSchema>\n\nexport const CaseExportModeSchema = z.enum(['private', 'partner', 'public'])\nexport type CaseExportMode = z.infer<typeof CaseExportModeSchema>\n\nconst caseIdRegex = /^\\d{8}_\\d{3}_[a-z0-9][a-z0-9-]*$/\n\nexport const CaseExportOptionsSchema = z.object({\n caseId: z.string().regex(caseIdRegex),\n target: CaseExportTargetSchema.default('obsidian-llmwiki'),\n mode: CaseExportModeSchema.default('private'),\n outputDir: z.string().optional(),\n})\nexport type CaseExportOptions = z.infer<typeof CaseExportOptionsSchema>\n\nexport const ExportedFileSchema = z.object({\n path: z.string().min(1),\n sha256: z.string().regex(/^[a-f0-9]{64}$/),\n bytes: z.number().int().nonnegative(),\n})\nexport type ExportedFile = z.infer<typeof ExportedFileSchema>\n\nexport const CaseExportManifestSchema = z.object({\n schema: z.literal('chain-insights.case_export.v1'),\n case_id: z.string().regex(caseIdRegex),\n case_name: z.string().min(1),\n exported_at: z.string().datetime(),\n mode: CaseExportModeSchema,\n target: CaseExportTargetSchema,\n source_workspace: z.string().min(1),\n verification: z.object({\n evidence_manifest_verified: z.boolean(),\n verified_at: z.string().datetime(),\n evidence_count: z.number().int().nonnegative(),\n }),\n files: z.array(ExportedFileSchema),\n redactions: z.array(z.string()),\n warnings: z.array(z.string()),\n})\nexport type CaseExportManifest = z.infer<typeof CaseExportManifestSchema>\n\nexport const JsonCanvasNodeSchema = z.object({\n id: z.string().min(1),\n type: z.enum(['text', 'file', 'link', 'group']),\n x: z.number(),\n y: z.number(),\n width: z.number().positive(),\n height: z.number().positive(),\n text: z.string().optional(),\n file: z.string().optional(),\n url: z.string().optional(),\n label: z.string().optional(),\n color: z.string().optional(),\n})\n\nexport const JsonCanvasEdgeSchema = z.object({\n id: z.string().min(1),\n fromNode: z.string().min(1),\n toNode: z.string().min(1),\n fromSide: z.enum(['top', 'right', 'bottom', 'left']).optional(),\n toSide: z.enum(['top', 'right', 'bottom', 'left']).optional(),\n toEnd: z.enum(['none', 'arrow']).optional(),\n label: z.string().optional(),\n color: z.string().optional(),\n})\n\nexport const JsonCanvasSchema = z.object({\n nodes: z.array(JsonCanvasNodeSchema),\n edges: z.array(JsonCanvasEdgeSchema),\n})\nexport type JsonCanvas = z.infer<typeof JsonCanvasSchema>\n\nexport type CaseExportResult = {\n manifestPath: string\n outputDir: string\n fileCount: number\n warnings: string[]\n nextFile: string\n}\n","import { safeFilename } from './paths.js'\nimport { JsonCanvasSchema, type JsonCanvas } from './schema.js'\n\nfunction roleColor(roles: string[]): string {\n if (roles.includes('victim')) return '1'\n if (roles.includes('suspect') || roles.includes('scam_candidate')) return '2'\n if (roles.includes('deposit')) return '3'\n if (roles.includes('exchange')) return '5'\n if (roles.includes('service')) return '6'\n return '#808080'\n}\n\nfunction nodeRoles(node: Record<string, unknown>): string[] {\n return Array.isArray(node['roles']) ? node['roles'].map(String) : []\n}\n\nfunction nodeLabel(node: Record<string, unknown>): string {\n return String(node['address'] ?? node['id'] ?? 'unknown')\n}\n\nexport function graphNodeId(node: Record<string, unknown>, index: number): string {\n return String(node['id'] ?? node['address'] ?? `node-${index + 1}`)\n}\n\nexport function entityNotePath(entityId: string): string {\n return `Entities/${safeFilename(entityId)}`\n}\n\nexport function graphToCanvas(graph: { nodes: Record<string, unknown>[]; edges: Record<string, unknown>[] }): JsonCanvas {\n const nodes = [\n {\n id: 'case',\n type: 'file' as const,\n file: 'Case.md',\n x: 0,\n y: 0,\n width: 360,\n height: 120,\n color: '4',\n },\n ]\n\n const nodeIdMap = new Map<string, string>()\n graph.nodes.forEach((node, index) => {\n const rawId = graphNodeId(node, index)\n const canvasId = `entity-${index + 1}`\n nodeIdMap.set(rawId, canvasId)\n nodes.push({\n id: canvasId,\n type: 'file' as const,\n file: entityNotePath(rawId),\n x: 420 + (index % 4) * 340,\n y: Math.floor(index / 4) * 220,\n width: 300,\n height: 120,\n color: roleColor(nodeRoles(node)),\n })\n })\n\n const edges = graph.edges.flatMap((edge, index) => {\n const from = nodeIdMap.get(String(edge['source'] ?? ''))\n const to = nodeIdMap.get(String(edge['target'] ?? ''))\n if (!from || !to) return []\n return [{\n id: `edge-${index + 1}`,\n fromNode: from,\n toNode: to,\n fromSide: 'right' as const,\n toSide: 'left' as const,\n toEnd: 'arrow' as const,\n label: String(edge['edge_type'] ?? 'related_to'),\n }]\n })\n\n for (const [index, node] of graph.nodes.entries()) {\n edges.push({\n id: `case-link-${index + 1}`,\n fromNode: 'case',\n toNode: `entity-${index + 1}`,\n fromSide: 'right' as const,\n toSide: 'left' as const,\n toEnd: 'arrow' as const,\n label: nodeLabel(node),\n })\n }\n\n return JsonCanvasSchema.parse({ nodes, edges })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-DakpK96I.cjs");
|
|
2
|
+
let node_path = require("node:path");
|
|
3
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
4
|
+
let node_fs_promises = require("node:fs/promises");
|
|
5
|
+
let zod = require("zod");
|
|
6
|
+
zod = require_chunk.__toESM(zod, 1);
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
8
|
+
//#region src/export/paths.ts
|
|
9
|
+
function safeSlug(value) {
|
|
10
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "case-export";
|
|
11
|
+
}
|
|
12
|
+
function safeFilename(value) {
|
|
13
|
+
const parsed = node_path.default.parse(value);
|
|
14
|
+
return `${safeSlug(parsed.name)}${parsed.ext.toLowerCase().replace(/[^.a-z0-9]/g, "") || ".md"}`;
|
|
15
|
+
}
|
|
16
|
+
function assertInsideDirectory(root, candidate) {
|
|
17
|
+
const resolvedRoot = node_path.default.resolve(root);
|
|
18
|
+
const resolvedCandidate = node_path.default.resolve(candidate);
|
|
19
|
+
const relative = node_path.default.relative(resolvedRoot, resolvedCandidate);
|
|
20
|
+
if (relative === "" || !relative.startsWith("..") && !node_path.default.isAbsolute(relative)) return;
|
|
21
|
+
throw new Error(`Refusing to write outside export directory: ${candidate}`);
|
|
22
|
+
}
|
|
23
|
+
async function assertNoSymlink(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
if ((await (0, node_fs_promises.lstat)(filePath)).isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${filePath}`);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code === "ENOENT") return;
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function writePrivateFile(root, relativePath, content) {
|
|
32
|
+
const filePath = node_path.default.join(root, relativePath);
|
|
33
|
+
assertInsideDirectory(root, filePath);
|
|
34
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), {
|
|
35
|
+
recursive: true,
|
|
36
|
+
mode: 448
|
|
37
|
+
});
|
|
38
|
+
await assertNoSymlink(filePath);
|
|
39
|
+
await (0, node_fs_promises.writeFile)(filePath, content, { mode: 384 });
|
|
40
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
41
|
+
return {
|
|
42
|
+
path: relativePath,
|
|
43
|
+
sha256: (0, node_crypto.createHash)("sha256").update(content).digest("hex"),
|
|
44
|
+
bytes
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/export/schema.ts
|
|
49
|
+
const CaseExportTargetSchema = zod.enum(["obsidian-llmwiki"]);
|
|
50
|
+
const CaseExportModeSchema = zod.enum([
|
|
51
|
+
"private",
|
|
52
|
+
"partner",
|
|
53
|
+
"public"
|
|
54
|
+
]);
|
|
55
|
+
const caseIdRegex = /^\d{8}_\d{3}_[a-z0-9][a-z0-9-]*$/;
|
|
56
|
+
const CaseExportOptionsSchema = zod.object({
|
|
57
|
+
caseId: zod.string().regex(caseIdRegex),
|
|
58
|
+
target: CaseExportTargetSchema.default("obsidian-llmwiki"),
|
|
59
|
+
mode: CaseExportModeSchema.default("private"),
|
|
60
|
+
outputDir: zod.string().optional()
|
|
61
|
+
});
|
|
62
|
+
const ExportedFileSchema = zod.object({
|
|
63
|
+
path: zod.string().min(1),
|
|
64
|
+
sha256: zod.string().regex(/^[a-f0-9]{64}$/),
|
|
65
|
+
bytes: zod.number().int().nonnegative()
|
|
66
|
+
});
|
|
67
|
+
const CaseExportManifestSchema = zod.object({
|
|
68
|
+
schema: zod.literal("chain-insights.case_export.v1"),
|
|
69
|
+
case_id: zod.string().regex(caseIdRegex),
|
|
70
|
+
case_name: zod.string().min(1),
|
|
71
|
+
exported_at: zod.string().datetime(),
|
|
72
|
+
mode: CaseExportModeSchema,
|
|
73
|
+
target: CaseExportTargetSchema,
|
|
74
|
+
source_workspace: zod.string().min(1),
|
|
75
|
+
verification: zod.object({
|
|
76
|
+
evidence_manifest_verified: zod.boolean(),
|
|
77
|
+
verified_at: zod.string().datetime(),
|
|
78
|
+
evidence_count: zod.number().int().nonnegative()
|
|
79
|
+
}),
|
|
80
|
+
files: zod.array(ExportedFileSchema),
|
|
81
|
+
redactions: zod.array(zod.string()),
|
|
82
|
+
warnings: zod.array(zod.string())
|
|
83
|
+
});
|
|
84
|
+
const JsonCanvasNodeSchema = zod.object({
|
|
85
|
+
id: zod.string().min(1),
|
|
86
|
+
type: zod.enum([
|
|
87
|
+
"text",
|
|
88
|
+
"file",
|
|
89
|
+
"link",
|
|
90
|
+
"group"
|
|
91
|
+
]),
|
|
92
|
+
x: zod.number(),
|
|
93
|
+
y: zod.number(),
|
|
94
|
+
width: zod.number().positive(),
|
|
95
|
+
height: zod.number().positive(),
|
|
96
|
+
text: zod.string().optional(),
|
|
97
|
+
file: zod.string().optional(),
|
|
98
|
+
url: zod.string().optional(),
|
|
99
|
+
label: zod.string().optional(),
|
|
100
|
+
color: zod.string().optional()
|
|
101
|
+
});
|
|
102
|
+
const JsonCanvasEdgeSchema = zod.object({
|
|
103
|
+
id: zod.string().min(1),
|
|
104
|
+
fromNode: zod.string().min(1),
|
|
105
|
+
toNode: zod.string().min(1),
|
|
106
|
+
fromSide: zod.enum([
|
|
107
|
+
"top",
|
|
108
|
+
"right",
|
|
109
|
+
"bottom",
|
|
110
|
+
"left"
|
|
111
|
+
]).optional(),
|
|
112
|
+
toSide: zod.enum([
|
|
113
|
+
"top",
|
|
114
|
+
"right",
|
|
115
|
+
"bottom",
|
|
116
|
+
"left"
|
|
117
|
+
]).optional(),
|
|
118
|
+
toEnd: zod.enum(["none", "arrow"]).optional(),
|
|
119
|
+
label: zod.string().optional(),
|
|
120
|
+
color: zod.string().optional()
|
|
121
|
+
});
|
|
122
|
+
const JsonCanvasSchema = zod.object({
|
|
123
|
+
nodes: zod.array(JsonCanvasNodeSchema),
|
|
124
|
+
edges: zod.array(JsonCanvasEdgeSchema)
|
|
125
|
+
});
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/export/canvas.ts
|
|
128
|
+
function roleColor(roles) {
|
|
129
|
+
if (roles.includes("victim")) return "1";
|
|
130
|
+
if (roles.includes("suspect") || roles.includes("scam_candidate")) return "2";
|
|
131
|
+
if (roles.includes("deposit")) return "3";
|
|
132
|
+
if (roles.includes("exchange")) return "5";
|
|
133
|
+
if (roles.includes("service")) return "6";
|
|
134
|
+
return "#808080";
|
|
135
|
+
}
|
|
136
|
+
function nodeRoles(node) {
|
|
137
|
+
return Array.isArray(node["roles"]) ? node["roles"].map(String) : [];
|
|
138
|
+
}
|
|
139
|
+
function nodeLabel(node) {
|
|
140
|
+
return String(node["address"] ?? node["id"] ?? "unknown");
|
|
141
|
+
}
|
|
142
|
+
function graphNodeId(node, index) {
|
|
143
|
+
return String(node["id"] ?? node["address"] ?? `node-${index + 1}`);
|
|
144
|
+
}
|
|
145
|
+
function entityNotePath(entityId) {
|
|
146
|
+
return `Entities/${safeFilename(entityId)}`;
|
|
147
|
+
}
|
|
148
|
+
function graphToCanvas(graph) {
|
|
149
|
+
const nodes = [{
|
|
150
|
+
id: "case",
|
|
151
|
+
type: "file",
|
|
152
|
+
file: "Case.md",
|
|
153
|
+
x: 0,
|
|
154
|
+
y: 0,
|
|
155
|
+
width: 360,
|
|
156
|
+
height: 120,
|
|
157
|
+
color: "4"
|
|
158
|
+
}];
|
|
159
|
+
const nodeIdMap = /* @__PURE__ */ new Map();
|
|
160
|
+
graph.nodes.forEach((node, index) => {
|
|
161
|
+
const rawId = graphNodeId(node, index);
|
|
162
|
+
const canvasId = `entity-${index + 1}`;
|
|
163
|
+
nodeIdMap.set(rawId, canvasId);
|
|
164
|
+
nodes.push({
|
|
165
|
+
id: canvasId,
|
|
166
|
+
type: "file",
|
|
167
|
+
file: entityNotePath(rawId),
|
|
168
|
+
x: 420 + index % 4 * 340,
|
|
169
|
+
y: Math.floor(index / 4) * 220,
|
|
170
|
+
width: 300,
|
|
171
|
+
height: 120,
|
|
172
|
+
color: roleColor(nodeRoles(node))
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
const edges = graph.edges.flatMap((edge, index) => {
|
|
176
|
+
const from = nodeIdMap.get(String(edge["source"] ?? ""));
|
|
177
|
+
const to = nodeIdMap.get(String(edge["target"] ?? ""));
|
|
178
|
+
if (!from || !to) return [];
|
|
179
|
+
return [{
|
|
180
|
+
id: `edge-${index + 1}`,
|
|
181
|
+
fromNode: from,
|
|
182
|
+
toNode: to,
|
|
183
|
+
fromSide: "right",
|
|
184
|
+
toSide: "left",
|
|
185
|
+
toEnd: "arrow",
|
|
186
|
+
label: String(edge["edge_type"] ?? "related_to")
|
|
187
|
+
}];
|
|
188
|
+
});
|
|
189
|
+
for (const [index, node] of graph.nodes.entries()) edges.push({
|
|
190
|
+
id: `case-link-${index + 1}`,
|
|
191
|
+
fromNode: "case",
|
|
192
|
+
toNode: `entity-${index + 1}`,
|
|
193
|
+
fromSide: "right",
|
|
194
|
+
toSide: "left",
|
|
195
|
+
toEnd: "arrow",
|
|
196
|
+
label: nodeLabel(node)
|
|
197
|
+
});
|
|
198
|
+
return JsonCanvasSchema.parse({
|
|
199
|
+
nodes,
|
|
200
|
+
edges
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
Object.defineProperty(exports, "CaseExportManifestSchema", {
|
|
205
|
+
enumerable: true,
|
|
206
|
+
get: function() {
|
|
207
|
+
return CaseExportManifestSchema;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
Object.defineProperty(exports, "CaseExportOptionsSchema", {
|
|
211
|
+
enumerable: true,
|
|
212
|
+
get: function() {
|
|
213
|
+
return CaseExportOptionsSchema;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
Object.defineProperty(exports, "entityNotePath", {
|
|
217
|
+
enumerable: true,
|
|
218
|
+
get: function() {
|
|
219
|
+
return entityNotePath;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
Object.defineProperty(exports, "graphNodeId", {
|
|
223
|
+
enumerable: true,
|
|
224
|
+
get: function() {
|
|
225
|
+
return graphNodeId;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
Object.defineProperty(exports, "graphToCanvas", {
|
|
229
|
+
enumerable: true,
|
|
230
|
+
get: function() {
|
|
231
|
+
return graphToCanvas;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
Object.defineProperty(exports, "safeFilename", {
|
|
235
|
+
enumerable: true,
|
|
236
|
+
get: function() {
|
|
237
|
+
return safeFilename;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
Object.defineProperty(exports, "safeSlug", {
|
|
241
|
+
enumerable: true,
|
|
242
|
+
get: function() {
|
|
243
|
+
return safeSlug;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
Object.defineProperty(exports, "writePrivateFile", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function() {
|
|
249
|
+
return writePrivateFile;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-DakpK96I.cjs");
|
|
2
|
+
const require_dossier = require("./dossier-BXy57V4-.cjs");
|
|
3
|
+
const require_store = require("./store-CQhU8dz8.cjs");
|
|
4
|
+
const require_evidence = require("./evidence-CvEesemA.cjs");
|
|
5
|
+
const require_session = require("./session-BT7VpbAd.cjs");
|
|
6
|
+
//#region src/cases/index.ts
|
|
7
|
+
var cases_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
|
8
|
+
CaseStore: () => require_store.CaseStore,
|
|
9
|
+
DossierStore: () => require_dossier.DossierStore,
|
|
10
|
+
EvidenceStore: () => require_evidence.EvidenceStore,
|
|
11
|
+
SessionStore: () => require_session.SessionStore
|
|
12
|
+
});
|
|
13
|
+
//#endregion
|
|
14
|
+
Object.defineProperty(exports, "cases_exports", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function() {
|
|
17
|
+
return cases_exports;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { t as DossierStore } from "./dossier-Bjpcbcxa.mjs";
|
|
3
|
+
import { t as CaseStore } from "./store-C2B_AssI.mjs";
|
|
4
|
+
import { t as EvidenceStore } from "./evidence-D96PTzOQ.mjs";
|
|
5
|
+
import { t as SessionStore } from "./session-DROyhebe.mjs";
|
|
6
|
+
//#region src/cases/index.ts
|
|
7
|
+
var cases_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
CaseStore: () => CaseStore,
|
|
9
|
+
DossierStore: () => DossierStore,
|
|
10
|
+
EvidenceStore: () => EvidenceStore,
|
|
11
|
+
SessionStore: () => SessionStore
|
|
12
|
+
});
|
|
13
|
+
//#endregion
|
|
14
|
+
export { cases_exports as t };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=cases-TVcAifxu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cases-TVcAifxu.mjs","names":[],"sources":["../src/cases/index.ts"],"sourcesContent":["// Stable public surface for the cases module.\nexport { CaseStore, generateCaseId } from './store.js'\nexport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\nexport { CaseSchema, EvidenceSchema, DossierSchema, SessionSchema, CaseStatusEnum } from './schema.js'\nexport type { Case, Evidence, Dossier, Session, CaseStatus } from './schema.js'\nexport { EvidenceStore } from './evidence.js'\nexport { DossierStore } from './dossier.js'\nexport { SessionStore } from './session.js'\n"],"mappings":""}
|