@tekmidian/pai 0.5.6 → 0.6.0
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/ARCHITECTURE.md +72 -1
- package/README.md +107 -3
- package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
- package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
- package/dist/cli/index.mjs +1897 -1569
- package/dist/cli/index.mjs.map +1 -1
- package/dist/clusters-JIDQW65f.mjs +201 -0
- package/dist/clusters-JIDQW65f.mjs.map +1 -0
- package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
- package/dist/config-BuhHWyOK.mjs.map +1 -0
- package/dist/daemon/index.mjs +12 -9
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
- package/dist/daemon-D3hYb5_C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +4597 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
- package/dist/db-BtuN768f.mjs.map +1 -0
- package/dist/db-DdUperSl.mjs +110 -0
- package/dist/db-DdUperSl.mjs.map +1 -0
- package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
- package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
- package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
- package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
- package/dist/helpers-BEST-4Gx.mjs +420 -0
- package/dist/helpers-BEST-4Gx.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +19 -4
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/capture-session-summary.mjs +38 -0
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +6 -12
- package/dist/hooks/cleanup-session-files.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +105 -111
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +26 -17
- package/dist/hooks/initialize-session.mjs.map +4 -4
- package/dist/hooks/inject-observations.mjs +220 -0
- package/dist/hooks/inject-observations.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +18 -2
- package/dist/hooks/load-core-context.mjs.map +4 -4
- package/dist/hooks/load-project-context.mjs +102 -97
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/observe.mjs +354 -0
- package/dist/hooks/observe.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +174 -90
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/sync-todo-to-md.mjs +31 -33
- package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
- package/dist/index.d.mts +32 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -9
- package/dist/indexer-D53l5d1U.mjs +1 -0
- package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
- package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
- package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
- package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
- package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
- package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
- package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
- package/dist/note-context-BK24bX8Y.mjs +126 -0
- package/dist/note-context-BK24bX8Y.mjs.map +1 -0
- package/dist/postgres-CKf-EDtS.mjs +846 -0
- package/dist/postgres-CKf-EDtS.mjs.map +1 -0
- package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
- package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
- package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
- package/dist/search-DC1qhkKn.mjs.map +1 -0
- package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
- package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
- package/dist/state-C6_vqz7w.mjs +102 -0
- package/dist/state-C6_vqz7w.mjs.map +1 -0
- package/dist/stop-words-BaMEGVeY.mjs +326 -0
- package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
- package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
- package/dist/sync-BOsnEj2-.mjs.map +1 -0
- package/dist/themes-BvYF0W8T.mjs +148 -0
- package/dist/themes-BvYF0W8T.mjs.map +1 -0
- package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
- package/dist/tools-DcaJlYDN.mjs.map +1 -0
- package/dist/trace-CRx9lPuc.mjs +137 -0
- package/dist/trace-CRx9lPuc.mjs.map +1 -0
- package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
- package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
- package/dist/zettelkasten-cdajbnPr.mjs +708 -0
- package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
- package/package.json +1 -2
- package/src/hooks/ts/capture-all-events.ts +6 -0
- package/src/hooks/ts/lib/project-utils/index.ts +50 -0
- package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
- package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
- package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
- package/src/hooks/ts/lib/project-utils.ts +40 -999
- package/src/hooks/ts/post-tool-use/observe.ts +327 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
- package/src/hooks/ts/session-start/initialize-session.ts +7 -1
- package/src/hooks/ts/session-start/inject-observations.ts +254 -0
- package/src/hooks/ts/session-start/load-core-context.ts +7 -0
- package/src/hooks/ts/session-start/load-project-context.ts +8 -1
- package/src/hooks/ts/stop/stop-hook.ts +28 -0
- package/templates/claude-md.template.md +7 -74
- package/templates/skills/user/.gitkeep +0 -0
- package/dist/chunker-CbnBe0s0.mjs +0 -191
- package/dist/chunker-CbnBe0s0.mjs.map +0 -1
- package/dist/config-Cf92lGX_.mjs.map +0 -1
- package/dist/daemon-D9evGlgR.mjs.map +0 -1
- package/dist/db-4lSqLFb8.mjs.map +0 -1
- package/dist/db-Dp8VXIMR.mjs +0 -212
- package/dist/db-Dp8VXIMR.mjs.map +0 -1
- package/dist/indexer-CMPOiY1r.mjs.map +0 -1
- package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
- package/dist/mcp/index.d.mts +0 -1
- package/dist/mcp/index.mjs +0 -500
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/postgres-FXrHDPcE.mjs +0 -358
- package/dist/postgres-FXrHDPcE.mjs.map +0 -1
- package/dist/schemas-BFIgGntb.mjs +0 -3405
- package/dist/schemas-BFIgGntb.mjs.map +0 -1
- package/dist/search-_oHfguA5.mjs.map +0 -1
- package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
- package/dist/tools-DV_lsiCc.mjs.map +0 -1
- package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
- package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
- package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
- package/templates/README.md +0 -181
- package/templates/skills/createskill-skill.template.md +0 -78
- package/templates/skills/history-system.template.md +0 -371
- package/templates/skills/hook-system.template.md +0 -913
- package/templates/skills/sessions-skill.template.md +0 -102
- package/templates/skills/skill-system.template.md +0 -214
- package/templates/skills/terminal-tabs.template.md +0 -120
- package/templates/templates.md +0 -20
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as yieldToEventLoop, f as sha256File, l as chunkMarkdown, n as chunkId } from "./helpers-BEST-4Gx.mjs";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, normalize, relative } from "node:path";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
4
|
|
|
6
|
-
//#region src/memory/vault
|
|
5
|
+
//#region src/memory/vault/walk.ts
|
|
7
6
|
/**
|
|
8
|
-
* Vault
|
|
9
|
-
*
|
|
10
|
-
* Indexes an entire Obsidian vault (or any markdown knowledge base), following
|
|
11
|
-
* symlinks, deduplicating files by inode, parsing wikilinks, and computing
|
|
12
|
-
* per-file health metrics (orphan detection, dead links).
|
|
13
|
-
*
|
|
14
|
-
* Key differences from the project indexer (indexer.ts):
|
|
15
|
-
* - Follows symbolic links (project indexer skips them)
|
|
16
|
-
* - Deduplicates files with the same inode (same content reachable via multiple paths)
|
|
17
|
-
* - Parses [[wikilinks]] and builds a directed link graph
|
|
18
|
-
* - Resolves wikilinks using Obsidian's shortest-match algorithm
|
|
19
|
-
* - Computes health metrics per file: inbound/outbound link counts, dead links, orphans
|
|
7
|
+
* Vault directory walker — follows symlinks with cycle detection.
|
|
20
8
|
*/
|
|
21
9
|
/** Maximum number of .md files to collect from a vault. */
|
|
22
10
|
const VAULT_MAX_FILES = 1e4;
|
|
23
11
|
/** Maximum recursion depth for vault directory walks. */
|
|
24
12
|
const VAULT_MAX_DEPTH = 10;
|
|
25
|
-
/** Number of files to process before yielding to the event loop. */
|
|
26
|
-
const VAULT_YIELD_EVERY = 10;
|
|
27
13
|
/**
|
|
28
14
|
* Directories to always skip, at any depth, during vault walks.
|
|
29
15
|
* Includes standard build/VCS noise plus Obsidian-specific directories.
|
|
@@ -46,15 +32,6 @@ const VAULT_SKIP_DIRS = new Set([
|
|
|
46
32
|
".obsidian",
|
|
47
33
|
".trash"
|
|
48
34
|
]);
|
|
49
|
-
function sha256File(content) {
|
|
50
|
-
return createHash("sha256").update(content).digest("hex");
|
|
51
|
-
}
|
|
52
|
-
function chunkId(projectId, path, chunkIndex, startLine, endLine) {
|
|
53
|
-
return createHash("sha256").update(`${projectId}:${path}:${chunkIndex}:${startLine}:${endLine}`).digest("hex");
|
|
54
|
-
}
|
|
55
|
-
function yieldToEventLoop() {
|
|
56
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
57
|
-
}
|
|
58
35
|
/**
|
|
59
36
|
* Recursively collect all .md files under a vault root, following symlinks.
|
|
60
37
|
*
|
|
@@ -66,11 +43,8 @@ function yieldToEventLoop() {
|
|
|
66
43
|
* Using the real stat (not lstat) ensures that symlinked dirs resolve to
|
|
67
44
|
* their actual inode, preventing infinite loops.
|
|
68
45
|
*
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
71
|
-
* @param acc Shared accumulator (mutated in place for early exit).
|
|
72
|
-
* @param visited Set of "device:inode" strings for visited directories.
|
|
73
|
-
* @param depth Current recursion depth.
|
|
46
|
+
* @param vaultRoot Absolute path to the vault root directory.
|
|
47
|
+
* @param opts Optional overrides for maxFiles and maxDepth.
|
|
74
48
|
*/
|
|
75
49
|
function walkVaultMdFiles(vaultRoot, opts) {
|
|
76
50
|
const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;
|
|
@@ -137,6 +111,9 @@ function walkVaultMdFiles(vaultRoot, opts) {
|
|
|
137
111
|
if (existsSync(vaultRoot)) walk(vaultRoot, 0);
|
|
138
112
|
return results;
|
|
139
113
|
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/memory/vault/deduplicate.ts
|
|
140
117
|
/**
|
|
141
118
|
* Group vault files by inode identity (device + inode).
|
|
142
119
|
*
|
|
@@ -168,8 +145,11 @@ function deduplicateByInode(files) {
|
|
|
168
145
|
}
|
|
169
146
|
return result;
|
|
170
147
|
}
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/memory/vault/parse-links.ts
|
|
171
151
|
/**
|
|
172
|
-
* Parse all
|
|
152
|
+
* Parse all links from markdown content.
|
|
173
153
|
*
|
|
174
154
|
* Handles:
|
|
175
155
|
* - Standard wikilinks: [[Target Note]]
|
|
@@ -177,11 +157,16 @@ function deduplicateByInode(files) {
|
|
|
177
157
|
* - Heading anchors: [[Target Note#Heading]] (stripped for resolution)
|
|
178
158
|
* - Embeds: ![[Target Note]]
|
|
179
159
|
* - Frontmatter wikilinks (YAML between --- delimiters)
|
|
160
|
+
* - Markdown links: [text](path/to/note.md)
|
|
161
|
+
* - Markdown embeds: 
|
|
162
|
+
*
|
|
163
|
+
* External URLs (http://, https://, mailto:, etc.) are excluded — only
|
|
164
|
+
* relative paths are treated as vault links.
|
|
180
165
|
*
|
|
181
166
|
* @param content Raw markdown file content.
|
|
182
167
|
* @returns Array of parsed links in document order.
|
|
183
168
|
*/
|
|
184
|
-
function
|
|
169
|
+
function parseLinks(content) {
|
|
185
170
|
const links = [];
|
|
186
171
|
const lines = content.split("\n");
|
|
187
172
|
let frontmatterEnd = 0;
|
|
@@ -190,9 +175,11 @@ function parseWikilinks(content) {
|
|
|
190
175
|
if (closingIdx !== -1) frontmatterEnd = content.slice(0, closingIdx + 4).split("\n").length - 1;
|
|
191
176
|
}
|
|
192
177
|
const wikilinkRe = /(!?)\[\[([^\]]+?)\]\]/g;
|
|
178
|
+
const mdLinkRe = /(!)?\[([^\]]*)\]\(([^)]+)\)/g;
|
|
193
179
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
194
180
|
const line = lines[lineIdx];
|
|
195
181
|
const lineNumber = lineIdx + 1;
|
|
182
|
+
const isFrontmatter = lineIdx < frontmatterEnd;
|
|
196
183
|
wikilinkRe.lastIndex = 0;
|
|
197
184
|
let match;
|
|
198
185
|
while ((match = wikilinkRe.exec(line)) !== null) {
|
|
@@ -204,17 +191,47 @@ function parseWikilinks(content) {
|
|
|
204
191
|
const hashIdx = beforePipe.indexOf("#");
|
|
205
192
|
const raw = hashIdx === -1 ? beforePipe.trim() : beforePipe.slice(0, hashIdx).trim();
|
|
206
193
|
if (!raw) continue;
|
|
207
|
-
const isFrontmatter = lineIdx < frontmatterEnd;
|
|
208
194
|
links.push({
|
|
209
195
|
raw,
|
|
210
196
|
alias: alias?.trim() ?? null,
|
|
211
197
|
lineNumber,
|
|
212
|
-
isEmbed: isEmbed && !isFrontmatter
|
|
198
|
+
isEmbed: isEmbed && !isFrontmatter,
|
|
199
|
+
isMdLink: false
|
|
213
200
|
});
|
|
214
201
|
}
|
|
202
|
+
if (!isFrontmatter) {
|
|
203
|
+
mdLinkRe.lastIndex = 0;
|
|
204
|
+
while ((match = mdLinkRe.exec(line)) !== null) {
|
|
205
|
+
const isEmbed = match[1] === "!";
|
|
206
|
+
const displayText = match[2];
|
|
207
|
+
let target = match[3].trim();
|
|
208
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;
|
|
209
|
+
if (target.startsWith("#")) continue;
|
|
210
|
+
const hashIdx = target.indexOf("#");
|
|
211
|
+
if (hashIdx !== -1) target = target.slice(0, hashIdx);
|
|
212
|
+
try {
|
|
213
|
+
target = decodeURIComponent(target);
|
|
214
|
+
} catch {}
|
|
215
|
+
const raw = target.replace(/\.md$/i, "").trim();
|
|
216
|
+
if (!raw) continue;
|
|
217
|
+
links.push({
|
|
218
|
+
raw,
|
|
219
|
+
alias: displayText || null,
|
|
220
|
+
lineNumber,
|
|
221
|
+
isEmbed,
|
|
222
|
+
isMdLink: true
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
215
226
|
}
|
|
216
227
|
return links;
|
|
217
228
|
}
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/memory/vault/name-index.ts
|
|
232
|
+
/**
|
|
233
|
+
* Name index builder for Obsidian wikilink resolution.
|
|
234
|
+
*/
|
|
218
235
|
/**
|
|
219
236
|
* Build a name lookup index for Obsidian wikilink resolution.
|
|
220
237
|
*
|
|
@@ -232,6 +249,12 @@ function buildNameIndex(files) {
|
|
|
232
249
|
}
|
|
233
250
|
return index;
|
|
234
251
|
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/memory/vault/resolve.ts
|
|
255
|
+
/**
|
|
256
|
+
* Wikilink resolver using Obsidian's shortest-match algorithm.
|
|
257
|
+
*/
|
|
235
258
|
/**
|
|
236
259
|
* Resolve a wikilink target to a vault-relative path using Obsidian's rules.
|
|
237
260
|
*
|
|
@@ -295,9 +318,28 @@ function commonPrefixLength(a, b) {
|
|
|
295
318
|
else break;
|
|
296
319
|
return count;
|
|
297
320
|
}
|
|
321
|
+
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/memory/vault/indexer.ts
|
|
298
324
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
325
|
+
* Main vault indexing orchestrator.
|
|
326
|
+
*
|
|
327
|
+
* Indexes an entire Obsidian vault (or any markdown knowledge base), following
|
|
328
|
+
* symlinks, deduplicating files by inode, parsing wikilinks, and computing
|
|
329
|
+
* per-file health metrics (orphan detection, dead links).
|
|
330
|
+
*
|
|
331
|
+
* Key differences from the project indexer (indexer/sync.ts):
|
|
332
|
+
* - Follows symbolic links (project indexer skips them)
|
|
333
|
+
* - Deduplicates files with the same inode (same content reachable via multiple paths)
|
|
334
|
+
* - Parses [[wikilinks]] and builds a directed link graph
|
|
335
|
+
* - Resolves wikilinks using Obsidian's shortest-match algorithm
|
|
336
|
+
* - Computes health metrics per file: inbound/outbound link counts, dead links, orphans
|
|
337
|
+
*/
|
|
338
|
+
/** Number of files to process before yielding to the event loop. */
|
|
339
|
+
const VAULT_YIELD_EVERY = 1;
|
|
340
|
+
/**
|
|
341
|
+
* Index an entire Obsidian vault (or markdown knowledge base) using the
|
|
342
|
+
* async StorageBackend interface.
|
|
301
343
|
*
|
|
302
344
|
* Steps:
|
|
303
345
|
* 1. Walk vault root, following symlinks.
|
|
@@ -306,7 +348,7 @@ function commonPrefixLength(a, b) {
|
|
|
306
348
|
* 4. For each canonical file:
|
|
307
349
|
* a. SHA-256 hash for change detection — skip unchanged files.
|
|
308
350
|
* b. Read content, chunk with chunkMarkdown().
|
|
309
|
-
* c. Insert chunks into memory_chunks and memory_fts.
|
|
351
|
+
* c. Insert chunks into backend (memory_chunks and memory_fts).
|
|
310
352
|
* d. Upsert vault_files row.
|
|
311
353
|
* 5. Record aliases in vault_aliases.
|
|
312
354
|
* 6. Rebuild vault_name_index table.
|
|
@@ -317,11 +359,11 @@ function commonPrefixLength(a, b) {
|
|
|
317
359
|
* 8. Compute and upsert health metrics (vault_health).
|
|
318
360
|
* 9. Return statistics.
|
|
319
361
|
*
|
|
320
|
-
* @param
|
|
362
|
+
* @param backend StorageBackend to write to.
|
|
321
363
|
* @param vaultProjectId Registry project ID for the vault "project".
|
|
322
364
|
* @param vaultRoot Absolute path to the vault root directory.
|
|
323
365
|
*/
|
|
324
|
-
async function indexVault(
|
|
366
|
+
async function indexVault(backend, vaultProjectId, vaultRoot) {
|
|
325
367
|
const startTime = Date.now();
|
|
326
368
|
const result = {
|
|
327
369
|
filesIndexed: 0,
|
|
@@ -336,28 +378,6 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
336
378
|
const allFiles = walkVaultMdFiles(vaultRoot);
|
|
337
379
|
const inodeGroups = deduplicateByInode(allFiles);
|
|
338
380
|
const nameIndex = buildNameIndex(allFiles);
|
|
339
|
-
const selectFileHash = db.prepare("SELECT hash FROM vault_files WHERE vault_path = ?");
|
|
340
|
-
const deleteOldChunkIds = db.prepare("SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?");
|
|
341
|
-
const deleteFts = db.prepare("DELETE FROM memory_fts WHERE id = ?");
|
|
342
|
-
const deleteChunks = db.prepare("DELETE FROM memory_chunks WHERE project_id = ? AND path = ?");
|
|
343
|
-
const insertChunk = db.prepare(`
|
|
344
|
-
INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)
|
|
345
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
346
|
-
`);
|
|
347
|
-
const insertFts = db.prepare(`
|
|
348
|
-
INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)
|
|
349
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
350
|
-
`);
|
|
351
|
-
const upsertVaultFile = db.prepare(`
|
|
352
|
-
INSERT INTO vault_files (vault_path, inode, device, hash, title, indexed_at)
|
|
353
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
354
|
-
ON CONFLICT(vault_path) DO UPDATE SET
|
|
355
|
-
inode = excluded.inode,
|
|
356
|
-
device = excluded.device,
|
|
357
|
-
hash = excluded.hash,
|
|
358
|
-
title = excluded.title,
|
|
359
|
-
indexed_at = excluded.indexed_at
|
|
360
|
-
`);
|
|
361
381
|
await yieldToEventLoop();
|
|
362
382
|
let filesSinceYield = 0;
|
|
363
383
|
for (const group of inodeGroups) {
|
|
@@ -375,120 +395,136 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
375
395
|
continue;
|
|
376
396
|
}
|
|
377
397
|
const hash = sha256File(content);
|
|
378
|
-
if (
|
|
398
|
+
if ((await backend.getVaultFile(canonical.vaultRelPath))?.hash === hash) {
|
|
379
399
|
result.filesSkipped++;
|
|
380
400
|
continue;
|
|
381
401
|
}
|
|
382
|
-
|
|
383
|
-
db.transaction(() => {
|
|
384
|
-
for (const row of oldChunkIds) deleteFts.run(row.id);
|
|
385
|
-
deleteChunks.run(vaultProjectId, canonical.vaultRelPath);
|
|
386
|
-
})();
|
|
402
|
+
await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);
|
|
387
403
|
const chunks = chunkMarkdown(content);
|
|
388
404
|
const updatedAt = Date.now();
|
|
389
405
|
const titleMatch = /^#\s+(.+)$/m.exec(content);
|
|
390
406
|
const title = titleMatch ? titleMatch[1].trim() : basename(canonical.vaultRelPath, ".md");
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
407
|
+
const chunkRows = [];
|
|
408
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
409
|
+
const chunk = chunks[i];
|
|
410
|
+
const id = chunkId(vaultProjectId, canonical.vaultRelPath, i, chunk.startLine, chunk.endLine);
|
|
411
|
+
chunkRows.push({
|
|
412
|
+
id,
|
|
413
|
+
projectId: vaultProjectId,
|
|
414
|
+
source: "vault",
|
|
415
|
+
tier: "topic",
|
|
416
|
+
path: canonical.vaultRelPath,
|
|
417
|
+
startLine: chunk.startLine,
|
|
418
|
+
endLine: chunk.endLine,
|
|
419
|
+
hash: chunk.hash,
|
|
420
|
+
text: chunk.text,
|
|
421
|
+
updatedAt
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (chunkRows.length > 0) await backend.insertChunks(chunkRows);
|
|
425
|
+
const vaultFileRow = {
|
|
426
|
+
vaultPath: canonical.vaultRelPath,
|
|
427
|
+
inode: canonical.inode,
|
|
428
|
+
device: canonical.device,
|
|
429
|
+
hash,
|
|
430
|
+
title,
|
|
431
|
+
indexedAt: updatedAt
|
|
432
|
+
};
|
|
433
|
+
await backend.upsertVaultFile(vaultFileRow);
|
|
400
434
|
result.filesIndexed++;
|
|
401
435
|
result.chunksCreated += chunks.length;
|
|
402
436
|
}
|
|
403
437
|
await yieldToEventLoop();
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
438
|
+
const allAliases = [];
|
|
439
|
+
for (const group of inodeGroups) for (const alias of group.aliases) {
|
|
440
|
+
allAliases.push({
|
|
441
|
+
vaultPath: alias.vaultRelPath,
|
|
442
|
+
canonicalPath: group.canonical.vaultRelPath,
|
|
443
|
+
inode: alias.inode,
|
|
444
|
+
device: alias.device
|
|
445
|
+
});
|
|
446
|
+
result.aliasesRecorded++;
|
|
447
|
+
}
|
|
448
|
+
const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));
|
|
449
|
+
for (const canonPath of canonicalPaths) await backend.deleteVaultAliases(canonPath);
|
|
450
|
+
if (allAliases.length > 0) await backend.upsertVaultAliases(allAliases);
|
|
415
451
|
await yieldToEventLoop();
|
|
416
|
-
db.exec("DELETE FROM vault_name_index");
|
|
417
|
-
const insertNameIndex = db.prepare(`
|
|
418
|
-
INSERT OR REPLACE INTO vault_name_index (name, vault_path) VALUES (?, ?)
|
|
419
|
-
`);
|
|
420
|
-
const insertNameIndexTx = db.transaction((entries) => {
|
|
421
|
-
for (const [name, path] of entries) insertNameIndex.run(name, path);
|
|
422
|
-
});
|
|
423
452
|
const nameEntries = [];
|
|
424
|
-
for (const [name, paths] of nameIndex) for (const path of paths) nameEntries.push(
|
|
425
|
-
|
|
453
|
+
for (const [name, paths] of nameIndex) for (const path of paths) nameEntries.push({
|
|
454
|
+
name,
|
|
455
|
+
vaultPath: path
|
|
456
|
+
});
|
|
457
|
+
await backend.replaceNameIndex(nameEntries);
|
|
426
458
|
await yieldToEventLoop();
|
|
427
|
-
db.exec("DELETE FROM vault_links");
|
|
428
|
-
const insertLink = db.prepare(`
|
|
429
|
-
INSERT OR IGNORE INTO vault_links
|
|
430
|
-
(source_path, target_raw, target_path, link_type, line_number)
|
|
431
|
-
VALUES (?, ?, ?, ?, ?)
|
|
432
|
-
`);
|
|
433
459
|
const linkRows = [];
|
|
460
|
+
const allSourcePaths = [];
|
|
461
|
+
let linkParseYield = 0;
|
|
434
462
|
for (const group of inodeGroups) {
|
|
463
|
+
if (linkParseYield++ % VAULT_YIELD_EVERY === 0) await yieldToEventLoop();
|
|
435
464
|
const { canonical } = group;
|
|
465
|
+
allSourcePaths.push(canonical.vaultRelPath);
|
|
436
466
|
let content;
|
|
437
467
|
try {
|
|
438
468
|
content = readFileSync(canonical.absPath, "utf8");
|
|
439
469
|
} catch {
|
|
440
470
|
continue;
|
|
441
471
|
}
|
|
442
|
-
const parsedLinks =
|
|
472
|
+
const parsedLinks = parseLinks(content);
|
|
443
473
|
for (const link of parsedLinks) {
|
|
444
474
|
const target = resolveWikilink(link.raw, nameIndex, canonical.vaultRelPath);
|
|
475
|
+
let linkType;
|
|
476
|
+
if (link.isMdLink) linkType = link.isEmbed ? "md-embed" : "md-link";
|
|
477
|
+
else linkType = link.isEmbed ? "embed" : "wikilink";
|
|
445
478
|
linkRows.push({
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
target,
|
|
449
|
-
linkType
|
|
479
|
+
sourcePath: canonical.vaultRelPath,
|
|
480
|
+
targetRaw: link.raw,
|
|
481
|
+
targetPath: target,
|
|
482
|
+
linkType,
|
|
450
483
|
lineNumber: link.lineNumber
|
|
451
484
|
});
|
|
452
485
|
}
|
|
453
486
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
487
|
+
const LINK_BATCH_SIZE = 500;
|
|
488
|
+
for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {
|
|
489
|
+
const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);
|
|
490
|
+
const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));
|
|
491
|
+
await backend.replaceLinksForSources(batchSources, batchLinks);
|
|
492
|
+
await yieldToEventLoop();
|
|
493
|
+
}
|
|
457
494
|
result.linksExtracted = linkRows.length;
|
|
458
|
-
result.deadLinksFound = linkRows.filter((r) => r.
|
|
495
|
+
result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;
|
|
459
496
|
await yieldToEventLoop();
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const upsertHealth = db.prepare(`
|
|
469
|
-
INSERT INTO vault_health
|
|
470
|
-
(vault_path, inbound_count, outbound_count, dead_link_count, is_orphan, computed_at)
|
|
471
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
472
|
-
ON CONFLICT(vault_path) DO UPDATE SET
|
|
473
|
-
inbound_count = excluded.inbound_count,
|
|
474
|
-
outbound_count = excluded.outbound_count,
|
|
475
|
-
dead_link_count = excluded.dead_link_count,
|
|
476
|
-
is_orphan = excluded.is_orphan,
|
|
477
|
-
computed_at = excluded.computed_at
|
|
478
|
-
`);
|
|
497
|
+
const outboundMap = /* @__PURE__ */ new Map();
|
|
498
|
+
const deadMap = /* @__PURE__ */ new Map();
|
|
499
|
+
const inboundMap = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const row of linkRows) {
|
|
501
|
+
outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);
|
|
502
|
+
if (row.targetPath === null) deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);
|
|
503
|
+
else inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);
|
|
504
|
+
}
|
|
479
505
|
const computedAt = Date.now();
|
|
480
506
|
let orphanCount = 0;
|
|
481
|
-
|
|
482
|
-
|
|
507
|
+
const HEALTH_BATCH_SIZE = 500;
|
|
508
|
+
for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {
|
|
509
|
+
const healthRows = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE).map((group) => {
|
|
483
510
|
const path = group.canonical.vaultRelPath;
|
|
484
511
|
const inbound = inboundMap.get(path) ?? 0;
|
|
485
512
|
const outbound = outboundMap.get(path) ?? 0;
|
|
486
513
|
const dead = deadMap.get(path) ?? 0;
|
|
487
|
-
const isOrphan = inbound === 0
|
|
514
|
+
const isOrphan = inbound === 0;
|
|
488
515
|
if (isOrphan) orphanCount++;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
516
|
+
return {
|
|
517
|
+
vaultPath: path,
|
|
518
|
+
inboundCount: inbound,
|
|
519
|
+
outboundCount: outbound,
|
|
520
|
+
deadLinkCount: dead,
|
|
521
|
+
isOrphan,
|
|
522
|
+
computedAt
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
await backend.upsertVaultHealth(healthRows);
|
|
526
|
+
await yieldToEventLoop();
|
|
527
|
+
}
|
|
492
528
|
result.orphansFound = orphanCount;
|
|
493
529
|
result.elapsed = Date.now() - startTime;
|
|
494
530
|
return result;
|
|
@@ -496,4 +532,4 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
496
532
|
|
|
497
533
|
//#endregion
|
|
498
534
|
export { indexVault };
|
|
499
|
-
//# sourceMappingURL=vault-indexer-
|
|
535
|
+
//# sourceMappingURL=vault-indexer-Bi2cRmn7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-indexer-Bi2cRmn7.mjs","names":[],"sources":["../src/memory/vault/walk.ts","../src/memory/vault/deduplicate.ts","../src/memory/vault/parse-links.ts","../src/memory/vault/name-index.ts","../src/memory/vault/resolve.ts","../src/memory/vault/indexer.ts"],"sourcesContent":["/**\n * Vault directory walker — follows symlinks with cycle detection.\n */\n\nimport { statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/** Maximum number of .md files to collect from a vault. */\nconst VAULT_MAX_FILES = 10_000;\n\n/** Maximum recursion depth for vault directory walks. */\nconst VAULT_MAX_DEPTH = 10;\n\n/**\n * Directories to always skip, at any depth, during vault walks.\n * Includes standard build/VCS noise plus Obsidian-specific directories.\n */\nconst VAULT_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\",\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\",\n \".next\",\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n // Obsidian internals\n \".obsidian\",\n \".trash\",\n]);\n\n/**\n * Recursively collect all .md files under a vault root, following symlinks.\n *\n * Symlink-following behaviour:\n * - Symbolic links to files: followed if the target is a .md file\n * - Symbolic links to directories: followed with cycle detection via inode\n *\n * Cycle detection is based on the real inode of each visited directory.\n * Using the real stat (not lstat) ensures that symlinked dirs resolve to\n * their actual inode, preventing infinite loops.\n *\n * @param vaultRoot Absolute path to the vault root directory.\n * @param opts Optional overrides for maxFiles and maxDepth.\n */\nexport function walkVaultMdFiles(\n vaultRoot: string,\n opts?: { maxFiles?: number; maxDepth?: number },\n): VaultFile[] {\n const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;\n const maxDepth = opts?.maxDepth ?? VAULT_MAX_DEPTH;\n\n const results: VaultFile[] = [];\n const visitedDirs = new Set<string>();\n\n function walk(dir: string, depth: number): void {\n if (results.length >= maxFiles) return;\n if (depth > maxDepth) return;\n\n // Get the real inode of this directory (follows symlinks on the dir itself)\n let dirStat: ReturnType<typeof statSync>;\n try {\n dirStat = statSync(dir);\n } catch {\n return; // Unreadable or broken symlink — skip\n }\n\n const dirKey = `${dirStat.dev}:${dirStat.ino}`;\n if (visitedDirs.has(dirKey)) return; // Cycle detected\n visitedDirs.add(dirKey);\n\n let entries: import(\"node:fs\").Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: \"utf8\" });\n } catch {\n return; // Unreadable directory — skip\n }\n\n for (const entry of entries) {\n if (results.length >= maxFiles) break;\n if (VAULT_SKIP_DIRS.has(entry.name)) continue;\n\n const full = join(dir, entry.name);\n\n if (entry.isSymbolicLink()) {\n // Follow the symlink — resolve to real target\n let targetStat: ReturnType<typeof statSync>;\n try {\n targetStat = statSync(full); // statSync follows symlinks\n } catch {\n continue; // Broken symlink — skip\n }\n\n if (targetStat.isDirectory()) {\n if (!VAULT_SKIP_DIRS.has(entry.name)) {\n walk(full, depth + 1);\n }\n } else if (targetStat.isFile() && entry.name.endsWith(\".md\")) {\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: targetStat.ino,\n device: targetStat.dev,\n });\n }\n } else if (entry.isDirectory()) {\n walk(full, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n let fileStat: ReturnType<typeof statSync>;\n try {\n fileStat = statSync(full);\n } catch {\n continue;\n }\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: fileStat.ino,\n device: fileStat.dev,\n });\n }\n }\n }\n\n if (existsSync(vaultRoot)) {\n walk(vaultRoot, 0);\n }\n\n return results;\n}\n","/**\n * Inode-based deduplication for vault files.\n */\n\nimport type { VaultFile, InodeGroup } from \"./types.js\";\n\n/**\n * Group vault files by inode identity (device + inode).\n *\n * Within each group, the canonical file is chosen as the one with the\n * fewest path separators (shallowest), breaking ties by shortest string.\n * All other group members become aliases.\n */\nexport function deduplicateByInode(files: VaultFile[]): InodeGroup[] {\n const groups = new Map<string, VaultFile[]>();\n\n for (const file of files) {\n const key = `${file.device}:${file.inode}`;\n const existing = groups.get(key);\n if (existing) {\n existing.push(file);\n } else {\n groups.set(key, [file]);\n }\n }\n\n const result: InodeGroup[] = [];\n\n for (const group of groups.values()) {\n if (group.length === 0) continue;\n\n // Sort: fewest path separators first, then shortest string\n const sorted = [...group].sort((a, b) => {\n const aDepth = (a.vaultRelPath.match(/\\//g) ?? []).length;\n const bDepth = (b.vaultRelPath.match(/\\//g) ?? []).length;\n if (aDepth !== bDepth) return aDepth - bDepth;\n return a.vaultRelPath.length - b.vaultRelPath.length;\n });\n\n const [canonical, ...aliases] = sorted as [VaultFile, ...VaultFile[]];\n result.push({ canonical, aliases });\n }\n\n return result;\n}\n","/**\n * Markdown link parser for vault files.\n *\n * Handles wikilinks ([[target]]), markdown links ([text](path)),\n * embeds (![[target]]), and frontmatter wikilinks.\n */\n\nimport type { ParsedLink } from \"./types.js\";\n\n/**\n * Parse all links from markdown content.\n *\n * Handles:\n * - Standard wikilinks: [[Target Note]]\n * - Aliased wikilinks: [[Target Note|Display Text]]\n * - Heading anchors: [[Target Note#Heading]] (stripped for resolution)\n * - Embeds: ![[Target Note]]\n * - Frontmatter wikilinks (YAML between --- delimiters)\n * - Markdown links: [text](path/to/note.md)\n * - Markdown embeds: \n *\n * External URLs (http://, https://, mailto:, etc.) are excluded — only\n * relative paths are treated as vault links.\n *\n * @param content Raw markdown file content.\n * @returns Array of parsed links in document order.\n */\nexport function parseLinks(content: string): ParsedLink[] {\n const links: ParsedLink[] = [];\n const lines = content.split(\"\\n\");\n\n // Determine frontmatter range (YAML between opening and closing ---)\n let frontmatterEnd = 0;\n if (content.startsWith(\"---\")) {\n const closingIdx = content.indexOf(\"\\n---\", 3);\n if (closingIdx !== -1) {\n frontmatterEnd = content.slice(0, closingIdx + 4).split(\"\\n\").length - 1;\n }\n }\n\n // Regex for [[wikilinks]] and ![[embeds]]\n const wikilinkRe = /(!?)\\[\\[([^\\]]+?)\\]\\]/g;\n\n // Regex for markdown links [text](target) and embeds \n const mdLinkRe = /(!)?\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx]!;\n const lineNumber = lineIdx + 1; // 1-indexed\n const isFrontmatter = lineIdx < frontmatterEnd;\n\n // --- Wikilinks ---\n wikilinkRe.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = wikilinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const inner = match[2]!;\n\n // Split on first | for alias\n const pipeIdx = inner.indexOf(\"|\");\n const beforePipe = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx);\n const alias = pipeIdx === -1 ? null : inner.slice(pipeIdx + 1);\n\n // Strip heading anchor (everything after #)\n const hashIdx = beforePipe.indexOf(\"#\");\n const raw = hashIdx === -1 ? beforePipe.trim() : beforePipe.slice(0, hashIdx).trim();\n\n if (!raw) continue; // Skip links with empty targets (e.g. [[#Heading]])\n\n links.push({\n raw,\n alias: alias?.trim() ?? null,\n lineNumber,\n isEmbed: isEmbed && !isFrontmatter,\n isMdLink: false,\n });\n }\n\n // --- Markdown links --- (skip inside frontmatter)\n if (!isFrontmatter) {\n mdLinkRe.lastIndex = 0;\n while ((match = mdLinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const displayText = match[2]!;\n let target = match[3]!.trim();\n\n // Skip external URLs\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;\n\n // Skip pure anchor links (#heading)\n if (target.startsWith(\"#\")) continue;\n\n // Strip heading anchor from target\n const hashIdx = target.indexOf(\"#\");\n if (hashIdx !== -1) target = target.slice(0, hashIdx);\n\n // URL-decode (Obsidian encodes spaces as %20 in md links)\n try {\n target = decodeURIComponent(target);\n } catch {\n // Malformed encoding — use as-is\n }\n\n // Strip .md extension for resolution (resolveWikilink adds it back)\n const raw = target.replace(/\\.md$/i, \"\").trim();\n if (!raw) continue;\n\n links.push({\n raw,\n alias: displayText || null,\n lineNumber,\n isEmbed,\n isMdLink: true,\n });\n }\n }\n }\n\n return links;\n}\n\n/** @deprecated Use {@link parseLinks} instead. */\nexport const parseWikilinks = parseLinks;\n","/**\n * Name index builder for Obsidian wikilink resolution.\n */\n\nimport { basename } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/**\n * Build a name lookup index for Obsidian wikilink resolution.\n *\n * Maps lowercase filename (without .md extension) to all vault-relative paths\n * that share that name. Includes both canonical paths and alias paths so that\n * wikilinks resolve regardless of which path the file is accessed through.\n */\nexport function buildNameIndex(files: VaultFile[]): Map<string, string[]> {\n const index = new Map<string, string[]>();\n\n for (const file of files) {\n const name = basename(file.vaultRelPath, \".md\").toLowerCase();\n const existing = index.get(name);\n if (existing) {\n existing.push(file.vaultRelPath);\n } else {\n index.set(name, [file.vaultRelPath]);\n }\n }\n\n return index;\n}\n","/**\n * Wikilink resolver using Obsidian's shortest-match algorithm.\n */\n\nimport { normalize, basename, dirname } from \"node:path\";\n\n/**\n * Resolve a wikilink target to a vault-relative path using Obsidian's rules.\n *\n * Resolution algorithm:\n * 1. If raw contains \"/\", attempt exact path match (with and without .md).\n * 2. Normalize: lowercase the raw target, strip .md extension.\n * 3. Look up in the name index (all files with that basename).\n * 4. If exactly one match, return it.\n * 5. If multiple matches, pick the one closest to the source file\n * (longest common directory prefix, then shortest overall path).\n * 6. If no matches, return null (dead link).\n *\n * @param raw The raw link target (heading-stripped, pipe-stripped).\n * @param nameIndex Map from lowercase basename-without-ext to vault paths.\n * @param sourcePath Vault-relative path of the file containing the link.\n * @returns Vault-relative path of the resolved target, or null.\n */\nexport function resolveWikilink(\n raw: string,\n nameIndex: Map<string, string[]>,\n sourcePath: string,\n): string | null {\n if (!raw) return null;\n\n // Case 1: path contains \"/\" — try exact match with and without .md\n if (raw.includes(\"/\")) {\n const normalized = normalize(raw);\n const normalizedMd = normalized.endsWith(\".md\") ? normalized : normalized + \".md\";\n\n // Check if any indexed path matches (case-insensitive for macOS compatibility)\n for (const [, paths] of nameIndex) {\n for (const p of paths) {\n if (p === normalizedMd || p === normalized) return p;\n if (p.toLowerCase() === normalizedMd.toLowerCase()) return p;\n }\n }\n // Fall through to name lookup in case the path prefix was wrong\n }\n\n // Normalize the raw target for name lookup.\n // Use the basename only — Obsidian resolves by filename, not full path.\n const rawBase = basename(raw)\n .replace(/\\.md$/i, \"\")\n .toLowerCase()\n .trim();\n\n if (!rawBase) return null;\n\n const candidates = nameIndex.get(rawBase);\n\n if (!candidates || candidates.length === 0) {\n return null; // Dead link\n }\n\n if (candidates.length === 1) {\n return candidates[0]!;\n }\n\n // Multiple matches — pick the one closest to the source file\n const sourceDir = dirname(sourcePath);\n\n let bestPath: string | null = null;\n let bestPrefixLen = -1;\n let bestPathLen = Infinity;\n\n for (const candidate of candidates) {\n const candidateDir = dirname(candidate);\n const prefixLen = commonPrefixLength(sourceDir, candidateDir);\n const pathLen = candidate.length;\n\n if (\n prefixLen > bestPrefixLen ||\n (prefixLen === bestPrefixLen && pathLen < bestPathLen)\n ) {\n bestPrefixLen = prefixLen;\n bestPathLen = pathLen;\n bestPath = candidate;\n }\n }\n\n return bestPath;\n}\n\n/**\n * Compute the length of the common prefix between two directory paths,\n * measured in path segments (not raw characters).\n *\n * Example: \"a/b/c\" and \"a/b/d\" → 2 (common: \"a\", \"b\")\n */\nfunction commonPrefixLength(a: string, b: string): number {\n if (a === \".\" && b === \".\") return 0;\n const aParts = a === \".\" ? [] : a.split(\"/\");\n const bParts = b === \".\" ? [] : b.split(\"/\");\n let count = 0;\n const len = Math.min(aParts.length, bParts.length);\n for (let i = 0; i < len; i++) {\n if (aParts[i] === bParts[i]) {\n count++;\n } else {\n break;\n }\n }\n return count;\n}\n","/**\n * Main vault indexing orchestrator.\n *\n * Indexes an entire Obsidian vault (or any markdown knowledge base), following\n * symlinks, deduplicating files by inode, parsing wikilinks, and computing\n * per-file health metrics (orphan detection, dead links).\n *\n * Key differences from the project indexer (indexer/sync.ts):\n * - Follows symbolic links (project indexer skips them)\n * - Deduplicates files with the same inode (same content reachable via multiple paths)\n * - Parses [[wikilinks]] and builds a directed link graph\n * - Resolves wikilinks using Obsidian's shortest-match algorithm\n * - Computes health metrics per file: inbound/outbound link counts, dead links, orphans\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport type {\n StorageBackend,\n VaultFileRow,\n VaultAliasRow,\n VaultLinkRow,\n VaultHealthRow,\n VaultNameEntry,\n ChunkRow,\n} from \"../../storage/interface.js\";\nimport { chunkMarkdown } from \"../chunker.js\";\nimport { sha256File, chunkId, yieldToEventLoop } from \"../indexer/helpers.js\";\nimport { walkVaultMdFiles } from \"./walk.js\";\nimport { deduplicateByInode } from \"./deduplicate.js\";\nimport { parseLinks } from \"./parse-links.js\";\nimport { buildNameIndex } from \"./name-index.js\";\nimport { resolveWikilink } from \"./resolve.js\";\nimport type { VaultIndexResult } from \"./types.js\";\n\n/** Number of files to process before yielding to the event loop. */\nconst VAULT_YIELD_EVERY = 1;\n\n/**\n * Index an entire Obsidian vault (or markdown knowledge base) using the\n * async StorageBackend interface.\n *\n * Steps:\n * 1. Walk vault root, following symlinks.\n * 2. Deduplicate by inode — each unique file is indexed once.\n * 3. Build a name index for wikilink resolution.\n * 4. For each canonical file:\n * a. SHA-256 hash for change detection — skip unchanged files.\n * b. Read content, chunk with chunkMarkdown().\n * c. Insert chunks into backend (memory_chunks and memory_fts).\n * d. Upsert vault_files row.\n * 5. Record aliases in vault_aliases.\n * 6. Rebuild vault_name_index table.\n * 7. Rebuild vault_links:\n * a. Parse [[wikilinks]] from each canonical file.\n * b. Resolve each link with resolveWikilink().\n * c. Insert into vault_links.\n * 8. Compute and upsert health metrics (vault_health).\n * 9. Return statistics.\n *\n * @param backend StorageBackend to write to.\n * @param vaultProjectId Registry project ID for the vault \"project\".\n * @param vaultRoot Absolute path to the vault root directory.\n */\nexport async function indexVault(\n backend: StorageBackend,\n vaultProjectId: number,\n vaultRoot: string,\n): Promise<VaultIndexResult> {\n const startTime = Date.now();\n\n const result: VaultIndexResult = {\n filesIndexed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n aliasesRecorded: 0,\n linksExtracted: 0,\n deadLinksFound: 0,\n orphansFound: 0,\n elapsed: 0,\n };\n\n // Step 1: Walk vault, collecting all .md files (follows symlinks)\n const allFiles = walkVaultMdFiles(vaultRoot);\n\n // Step 2: Deduplicate by inode\n const inodeGroups = deduplicateByInode(allFiles);\n\n // Step 3: Build name index (from all files including aliases, for resolution)\n const nameIndex = buildNameIndex(allFiles);\n\n // Step 4: Index each canonical file\n await yieldToEventLoop();\n let filesSinceYield = 0;\n\n for (const group of inodeGroups) {\n if (filesSinceYield >= VAULT_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const { canonical } = group;\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n result.filesSkipped++;\n continue;\n }\n\n const hash = sha256File(content);\n\n // Change detection: skip if hash is unchanged\n const existing = await backend.getVaultFile(canonical.vaultRelPath);\n if (existing?.hash === hash) {\n result.filesSkipped++;\n continue;\n }\n\n // Delete old chunks for this vault path\n await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);\n\n // Chunk the content\n const chunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n // Extract title from first H1 heading or filename\n const titleMatch = /^#\\s+(.+)$/m.exec(content);\n const title = titleMatch\n ? titleMatch[1]!.trim()\n : basename(canonical.vaultRelPath, \".md\");\n\n // Build chunk rows\n const chunkRows: ChunkRow[] = [];\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(\n vaultProjectId,\n canonical.vaultRelPath,\n i,\n chunk.startLine,\n chunk.endLine,\n );\n chunkRows.push({\n id,\n projectId: vaultProjectId,\n source: \"vault\",\n tier: \"topic\",\n path: canonical.vaultRelPath,\n startLine: chunk.startLine,\n endLine: chunk.endLine,\n hash: chunk.hash,\n text: chunk.text,\n updatedAt,\n });\n }\n\n if (chunkRows.length > 0) {\n await backend.insertChunks(chunkRows);\n }\n\n // Upsert vault file record\n const vaultFileRow: VaultFileRow = {\n vaultPath: canonical.vaultRelPath,\n inode: canonical.inode,\n device: canonical.device,\n hash,\n title,\n indexedAt: updatedAt,\n };\n await backend.upsertVaultFile(vaultFileRow);\n\n result.filesIndexed++;\n result.chunksCreated += chunks.length;\n }\n\n // Step 5: Record aliases in vault_aliases\n await yieldToEventLoop();\n\n const allAliases: VaultAliasRow[] = [];\n for (const group of inodeGroups) {\n for (const alias of group.aliases) {\n allAliases.push({\n vaultPath: alias.vaultRelPath,\n canonicalPath: group.canonical.vaultRelPath,\n inode: alias.inode,\n device: alias.device,\n });\n result.aliasesRecorded++;\n }\n }\n\n const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));\n for (const canonPath of canonicalPaths) {\n await backend.deleteVaultAliases(canonPath);\n }\n if (allAliases.length > 0) {\n await backend.upsertVaultAliases(allAliases);\n }\n\n // Step 6: Rebuild vault_name_index\n await yieldToEventLoop();\n\n const nameEntries: VaultNameEntry[] = [];\n for (const [name, paths] of nameIndex) {\n for (const path of paths) {\n nameEntries.push({ name, vaultPath: path });\n }\n }\n await backend.replaceNameIndex(nameEntries);\n\n // Step 7: Rebuild vault_links\n await yieldToEventLoop();\n\n const linkRows: VaultLinkRow[] = [];\n const allSourcePaths: string[] = [];\n\n let linkParseYield = 0;\n for (const group of inodeGroups) {\n if (linkParseYield++ % VAULT_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const { canonical } = group;\n allSourcePaths.push(canonical.vaultRelPath);\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n continue;\n }\n\n const parsedLinks = parseLinks(content);\n for (const link of parsedLinks) {\n const target = resolveWikilink(link.raw, nameIndex, canonical.vaultRelPath);\n let linkType: string;\n if (link.isMdLink) {\n linkType = link.isEmbed ? \"md-embed\" : \"md-link\";\n } else {\n linkType = link.isEmbed ? \"embed\" : \"wikilink\";\n }\n linkRows.push({\n sourcePath: canonical.vaultRelPath,\n targetRaw: link.raw,\n targetPath: target,\n linkType,\n lineNumber: link.lineNumber,\n });\n }\n }\n\n // Replace all links for all sources in batches of 500\n const LINK_BATCH_SIZE = 500;\n for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {\n const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);\n const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));\n await backend.replaceLinksForSources(batchSources, batchLinks);\n await yieldToEventLoop();\n }\n\n result.linksExtracted = linkRows.length;\n result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;\n\n // Step 8: Compute and upsert vault_health metrics\n await yieldToEventLoop();\n\n const outboundMap = new Map<string, number>();\n const deadMap = new Map<string, number>();\n const inboundMap = new Map<string, number>();\n\n for (const row of linkRows) {\n outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);\n if (row.targetPath === null) {\n deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);\n } else {\n inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);\n }\n }\n\n const computedAt = Date.now();\n let orphanCount = 0;\n\n const HEALTH_BATCH_SIZE = 500;\n for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {\n const batch = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE);\n const healthRows: VaultHealthRow[] = batch.map((group) => {\n const path = group.canonical.vaultRelPath;\n const inbound = inboundMap.get(path) ?? 0;\n const outbound = outboundMap.get(path) ?? 0;\n const dead = deadMap.get(path) ?? 0;\n const isOrphan = inbound === 0;\n if (isOrphan) orphanCount++;\n return {\n vaultPath: path,\n inboundCount: inbound,\n outboundCount: outbound,\n deadLinkCount: dead,\n isOrphan,\n computedAt,\n };\n });\n await backend.upsertVaultHealth(healthRows);\n await yieldToEventLoop();\n }\n\n result.orphansFound = orphanCount;\n result.elapsed = Date.now() - startTime;\n\n return result;\n}\n"],"mappings":"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"}
|