chain-insights 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +20 -7
  2. package/dist/canvas-Cn-maEIh.mjs +203 -0
  3. package/dist/canvas-Cn-maEIh.mjs.map +1 -0
  4. package/dist/canvas-p-oKCMjc.cjs +251 -0
  5. package/dist/cases-Bz_9XKEw.cjs +19 -0
  6. package/dist/cases-TVcAifxu.mjs +16 -0
  7. package/dist/cases-TVcAifxu.mjs.map +1 -0
  8. package/dist/cli.cjs +74 -28
  9. package/dist/cli.mjs +74 -28
  10. package/dist/cli.mjs.map +1 -1
  11. package/dist/{data-extractor-DZUJu1Bz.mjs → data-extractor-B4nHw1wZ.mjs} +2 -2
  12. package/dist/{data-extractor-DZUJu1Bz.mjs.map → data-extractor-B4nHw1wZ.mjs.map} +1 -1
  13. package/dist/{data-extractor-Cavd7wHk.cjs → data-extractor-DS4rzy3M.cjs} +1 -1
  14. package/dist/{export-BqTCO9lP.mjs → export-CBhcJuZ6.mjs} +8 -205
  15. package/dist/export-CBhcJuZ6.mjs.map +1 -0
  16. package/dist/{export-DsXgtCwO.cjs → export-D4v4-6F4.cjs} +16 -214
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.mjs +1 -1
  19. package/dist/{init-DLBL_nVG.mjs → init-CKQ6F07J.mjs} +22 -5
  20. package/dist/init-CKQ6F07J.mjs.map +1 -0
  21. package/dist/{init-zqbd7i-_.cjs → init-Dhw8F23z.cjs} +21 -4
  22. package/dist/mcp-proxy.cjs +20 -20
  23. package/dist/mcp-proxy.mjs +20 -20
  24. package/dist/mcp-proxy.mjs.map +1 -1
  25. package/dist/{public-tools-wJoAFDFa.mjs → public-tools-CyUZEz9B.mjs} +3 -3
  26. package/dist/{public-tools-wJoAFDFa.mjs.map → public-tools-CyUZEz9B.mjs.map} +1 -1
  27. package/dist/{public-tools-BvMb3H2P.cjs → public-tools-xfVNz9NE.cjs} +2 -2
  28. package/dist/{runner-BhZ4lnF1.cjs → runner-CVo41fjz.cjs} +2 -2
  29. package/dist/{runner-DIJSbkjc.mjs → runner-DWuSy1Se.mjs} +3 -3
  30. package/dist/{runner-DIJSbkjc.mjs.map → runner-DWuSy1Se.mjs.map} +1 -1
  31. package/dist/{selector-CF2o5gxN.mjs → selector-BvXM9jbe.mjs} +2 -2
  32. package/dist/{selector-CF2o5gxN.mjs.map → selector-BvXM9jbe.mjs.map} +1 -1
  33. package/dist/{selector-DfAMZEC9.cjs → selector-Dps_ZFxq.cjs} +1 -1
  34. package/dist/{store-CTtqQtaE.mjs → store-C2B_AssI.mjs} +2 -2
  35. package/dist/{store-CTtqQtaE.mjs.map → store-C2B_AssI.mjs.map} +1 -1
  36. package/dist/{store-CqPfs47P.cjs → store-CQhU8dz8.cjs} +0 -18
  37. package/dist/vault-B2y78Ypu.cjs +560 -0
  38. package/dist/vault-z35Dohdq.mjs +560 -0
  39. package/dist/vault-z35Dohdq.mjs.map +1 -0
  40. package/dist/{viz-Dqp3C5kb.cjs → viz-D1620cBX.cjs} +3 -3
  41. package/dist/{viz-5y24S5X1.mjs → viz-DB5XFG1z.mjs} +4 -4
  42. package/dist/{viz-5y24S5X1.mjs.map → viz-DB5XFG1z.mjs.map} +1 -1
  43. package/docs/graph-tools.md +15 -6
  44. package/docs/investigation-workspaces.md +38 -9
  45. package/docs/knowledge-exports.md +204 -0
  46. package/docs/mcp-proxy.md +15 -3
  47. package/docs/obsidian-vault.md +130 -0
  48. package/package.json +1 -1
  49. package/skills/chain-insights-developer-experience/SKILL.md +2 -2
  50. package/skills/chain-insights-investigation/SKILL.md +1 -1
  51. package/dist/cases-Cp9DUbEV.mjs +0 -6
  52. package/dist/cases-sTY5aXav.cjs +0 -9
  53. package/dist/export-BqTCO9lP.mjs.map +0 -1
  54. 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 workspace:
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
@@ -137,9 +143,11 @@ cia case show 1
137
143
  find reports cases -maxdepth 3 -type f | sort
138
144
  ```
139
145
 
140
- ## Export To Obsidian, LLMWiki, And Agents
146
+ ## Export Only When Sharing
141
147
 
142
- After a case has evidence, export a local knowledge bundle:
148
+ Normal local work happens in the investigation vault. Export only when you need
149
+ to share a case, hand it off to a partner, ingest it into LLM Wiki, or archive a
150
+ review checkpoint.
143
151
 
144
152
  ```bash
145
153
  cia case evidence verify 1
@@ -147,11 +155,14 @@ cia case export 1 --target obsidian-llmwiki --mode private
147
155
  ```
148
156
 
149
157
  The export writes Markdown notes, `manifest.chain-insights.json`,
150
- `graph.chain-insights.json`, `Graph.canvas`, LLMWiki entrypoints, and prompts
158
+ `graph.chain-insights.json`, `Graph.canvas`, LLM Wiki entrypoints, and prompts
151
159
  for Codex, Claude Code, and ChatGPT under `published/<case-slug>/`.
152
160
 
153
- Private exports may include full addresses. Use `--mode public` only for
154
- shareable demos; public mode aliases addresses and removes secrets by default.
161
+ Private exports may include full addresses. Use `--mode partner` for controlled
162
+ handoff after review. Use `--mode public` only for shareable demos; public mode
163
+ aliases addresses and removes secrets by default. Vault workflow guidance lives
164
+ in [Obsidian vault workflow](docs/obsidian-vault.md); export bundle details
165
+ live in [Knowledge exports](docs/knowledge-exports.md).
155
166
 
156
167
  ## Demo
157
168
 
@@ -247,7 +258,9 @@ reports under the workspace instead of embedding large payloads in case notes.
247
258
  | Doc | Use it for |
248
259
  | --- | --- |
249
260
  | [Graph tools](docs/graph-tools.md) | GraphRAG MCP layers, `graph_query`, `graph_query_batch`, AML tool contracts, graph reports, evidence pointers |
250
- | [Investigation workspaces](docs/investigation-workspaces.md) | `cia init`, case layout, evidence, dossiers, imports, templates, sessions, reports |
261
+ | [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 |
262
+ | [Investigation workspaces](docs/investigation-workspaces.md) | `cia init`, Obsidian-compatible vault layout, live note refresh, evidence, dossiers, imports, templates, sessions, reports |
263
+ | [Knowledge exports](docs/knowledge-exports.md) | Portable and redacted bundles for sharing, partner handoff, LLM Wiki ingestion, and archive |
251
264
  | [MCP proxy](docs/mcp-proxy.md) | Stdio proxy behavior, endpoint configuration, agent installers, local tools, auth modes, Inspector validation |
252
265
  | [Architecture](docs/architecture.md) | Product layers, data flow, local storage, security model, config keys |
253
266
  | [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":""}