abb-opencode-local-rag 0.1.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.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +416 -0
  3. package/README.es.md +416 -0
  4. package/README.fr.md +416 -0
  5. package/README.md +491 -0
  6. package/README.pt-BR.md +416 -0
  7. package/README.zh-CN.md +416 -0
  8. package/dist/bin/install-skills.d.ts +20 -0
  9. package/dist/bin/install-skills.d.ts.map +1 -0
  10. package/dist/bin/install-skills.js +195 -0
  11. package/dist/bin/install-skills.js.map +1 -0
  12. package/dist/chunker/index.d.ts +24 -0
  13. package/dist/chunker/index.d.ts.map +1 -0
  14. package/dist/chunker/index.js +2 -0
  15. package/dist/chunker/index.js.map +1 -0
  16. package/dist/chunker/semantic-chunker.d.ts +97 -0
  17. package/dist/chunker/semantic-chunker.d.ts.map +1 -0
  18. package/dist/chunker/semantic-chunker.js +294 -0
  19. package/dist/chunker/semantic-chunker.js.map +1 -0
  20. package/dist/chunker/sentence-splitter.d.ts +28 -0
  21. package/dist/chunker/sentence-splitter.d.ts.map +1 -0
  22. package/dist/chunker/sentence-splitter.js +219 -0
  23. package/dist/chunker/sentence-splitter.js.map +1 -0
  24. package/dist/cli/common.d.ts +65 -0
  25. package/dist/cli/common.d.ts.map +1 -0
  26. package/dist/cli/common.js +138 -0
  27. package/dist/cli/common.js.map +1 -0
  28. package/dist/cli/delete.d.ts +8 -0
  29. package/dist/cli/delete.d.ts.map +1 -0
  30. package/dist/cli/delete.js +173 -0
  31. package/dist/cli/delete.js.map +1 -0
  32. package/dist/cli/file-collection.d.ts +2 -0
  33. package/dist/cli/file-collection.d.ts.map +1 -0
  34. package/dist/cli/file-collection.js +53 -0
  35. package/dist/cli/file-collection.js.map +1 -0
  36. package/dist/cli/ingest.d.ts +100 -0
  37. package/dist/cli/ingest.d.ts.map +1 -0
  38. package/dist/cli/ingest.js +363 -0
  39. package/dist/cli/ingest.js.map +1 -0
  40. package/dist/cli/list.d.ts +35 -0
  41. package/dist/cli/list.d.ts.map +1 -0
  42. package/dist/cli/list.js +210 -0
  43. package/dist/cli/list.js.map +1 -0
  44. package/dist/cli/options.d.ts +100 -0
  45. package/dist/cli/options.d.ts.map +1 -0
  46. package/dist/cli/options.js +241 -0
  47. package/dist/cli/options.js.map +1 -0
  48. package/dist/cli/query.d.ts +24 -0
  49. package/dist/cli/query.d.ts.map +1 -0
  50. package/dist/cli/query.js +191 -0
  51. package/dist/cli/query.js.map +1 -0
  52. package/dist/cli/read-neighbors.d.ts +11 -0
  53. package/dist/cli/read-neighbors.d.ts.map +1 -0
  54. package/dist/cli/read-neighbors.js +224 -0
  55. package/dist/cli/read-neighbors.js.map +1 -0
  56. package/dist/cli/status.d.ts +8 -0
  57. package/dist/cli/status.d.ts.map +1 -0
  58. package/dist/cli/status.js +80 -0
  59. package/dist/cli/status.js.map +1 -0
  60. package/dist/cli/sync.d.ts +8 -0
  61. package/dist/cli/sync.d.ts.map +1 -0
  62. package/dist/cli/sync.js +244 -0
  63. package/dist/cli/sync.js.map +1 -0
  64. package/dist/cli-main.d.ts +12 -0
  65. package/dist/cli-main.d.ts.map +1 -0
  66. package/dist/cli-main.js +63 -0
  67. package/dist/cli-main.js.map +1 -0
  68. package/dist/embedder/index.d.ts +85 -0
  69. package/dist/embedder/index.d.ts.map +1 -0
  70. package/dist/embedder/index.js +284 -0
  71. package/dist/embedder/index.js.map +1 -0
  72. package/dist/features/list.d.ts +37 -0
  73. package/dist/features/list.d.ts.map +1 -0
  74. package/dist/features/list.js +40 -0
  75. package/dist/features/list.js.map +1 -0
  76. package/dist/features/sync.d.ts +207 -0
  77. package/dist/features/sync.d.ts.map +1 -0
  78. package/dist/features/sync.js +380 -0
  79. package/dist/features/sync.js.map +1 -0
  80. package/dist/index.d.ts +3 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +53 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/ingest/compute.d.ts +86 -0
  85. package/dist/ingest/compute.d.ts.map +1 -0
  86. package/dist/ingest/compute.js +177 -0
  87. package/dist/ingest/compute.js.map +1 -0
  88. package/dist/ingest/file.d.ts +27 -0
  89. package/dist/ingest/file.d.ts.map +1 -0
  90. package/dist/ingest/file.js +67 -0
  91. package/dist/ingest/file.js.map +1 -0
  92. package/dist/ingest/visual.d.ts +45 -0
  93. package/dist/ingest/visual.d.ts.map +1 -0
  94. package/dist/ingest/visual.js +234 -0
  95. package/dist/ingest/visual.js.map +1 -0
  96. package/dist/parser/docx-parser.d.ts +12 -0
  97. package/dist/parser/docx-parser.d.ts.map +1 -0
  98. package/dist/parser/docx-parser.js +328 -0
  99. package/dist/parser/docx-parser.js.map +1 -0
  100. package/dist/parser/html-parser.d.ts +18 -0
  101. package/dist/parser/html-parser.d.ts.map +1 -0
  102. package/dist/parser/html-parser.js +102 -0
  103. package/dist/parser/html-parser.js.map +1 -0
  104. package/dist/parser/index.d.ts +214 -0
  105. package/dist/parser/index.d.ts.map +1 -0
  106. package/dist/parser/index.js +454 -0
  107. package/dist/parser/index.js.map +1 -0
  108. package/dist/parser/pdf-extract.d.ts +81 -0
  109. package/dist/parser/pdf-extract.d.ts.map +1 -0
  110. package/dist/parser/pdf-extract.js +112 -0
  111. package/dist/parser/pdf-extract.js.map +1 -0
  112. package/dist/parser/pdf-filter.d.ts +117 -0
  113. package/dist/parser/pdf-filter.d.ts.map +1 -0
  114. package/dist/parser/pdf-filter.js +528 -0
  115. package/dist/parser/pdf-filter.js.map +1 -0
  116. package/dist/parser/title-extractor.d.ts +69 -0
  117. package/dist/parser/title-extractor.d.ts.map +1 -0
  118. package/dist/parser/title-extractor.js +145 -0
  119. package/dist/parser/title-extractor.js.map +1 -0
  120. package/dist/pdf-visual/captioner.d.ts +16 -0
  121. package/dist/pdf-visual/captioner.d.ts.map +1 -0
  122. package/dist/pdf-visual/captioner.js +63 -0
  123. package/dist/pdf-visual/captioner.js.map +1 -0
  124. package/dist/pdf-visual/captioners/fast.d.ts +7 -0
  125. package/dist/pdf-visual/captioners/fast.d.ts.map +1 -0
  126. package/dist/pdf-visual/captioners/fast.js +103 -0
  127. package/dist/pdf-visual/captioners/fast.js.map +1 -0
  128. package/dist/pdf-visual/captioners/quality.d.ts +7 -0
  129. package/dist/pdf-visual/captioners/quality.d.ts.map +1 -0
  130. package/dist/pdf-visual/captioners/quality.js +127 -0
  131. package/dist/pdf-visual/captioners/quality.js.map +1 -0
  132. package/dist/pdf-visual/captioners/shared.d.ts +44 -0
  133. package/dist/pdf-visual/captioners/shared.d.ts.map +1 -0
  134. package/dist/pdf-visual/captioners/shared.js +104 -0
  135. package/dist/pdf-visual/captioners/shared.js.map +1 -0
  136. package/dist/pdf-visual/detector.d.ts +9 -0
  137. package/dist/pdf-visual/detector.d.ts.map +1 -0
  138. package/dist/pdf-visual/detector.js +234 -0
  139. package/dist/pdf-visual/detector.js.map +1 -0
  140. package/dist/pdf-visual/index.d.ts +13 -0
  141. package/dist/pdf-visual/index.d.ts.map +1 -0
  142. package/dist/pdf-visual/index.js +45 -0
  143. package/dist/pdf-visual/index.js.map +1 -0
  144. package/dist/pdf-visual/renderer.d.ts +9 -0
  145. package/dist/pdf-visual/renderer.d.ts.map +1 -0
  146. package/dist/pdf-visual/renderer.js +177 -0
  147. package/dist/pdf-visual/renderer.js.map +1 -0
  148. package/dist/pdf-visual/types.d.ts +62 -0
  149. package/dist/pdf-visual/types.d.ts.map +1 -0
  150. package/dist/pdf-visual/types.js +32 -0
  151. package/dist/pdf-visual/types.js.map +1 -0
  152. package/dist/server/error-utils.d.ts +79 -0
  153. package/dist/server/error-utils.d.ts.map +1 -0
  154. package/dist/server/error-utils.js +148 -0
  155. package/dist/server/error-utils.js.map +1 -0
  156. package/dist/server/index.d.ts +258 -0
  157. package/dist/server/index.d.ts.map +1 -0
  158. package/dist/server/index.js +1104 -0
  159. package/dist/server/index.js.map +1 -0
  160. package/dist/server/list-scanner.d.ts +52 -0
  161. package/dist/server/list-scanner.d.ts.map +1 -0
  162. package/dist/server/list-scanner.js +72 -0
  163. package/dist/server/list-scanner.js.map +1 -0
  164. package/dist/server/tool-definitions.d.ts +8 -0
  165. package/dist/server/tool-definitions.d.ts.map +1 -0
  166. package/dist/server/tool-definitions.js +181 -0
  167. package/dist/server/tool-definitions.js.map +1 -0
  168. package/dist/server/tool-input.d.ts +37 -0
  169. package/dist/server/tool-input.d.ts.map +1 -0
  170. package/dist/server/tool-input.js +216 -0
  171. package/dist/server/tool-input.js.map +1 -0
  172. package/dist/server/types.d.ts +331 -0
  173. package/dist/server/types.d.ts.map +1 -0
  174. package/dist/server/types.js +3 -0
  175. package/dist/server/types.js.map +1 -0
  176. package/dist/server-main.d.ts +46 -0
  177. package/dist/server-main.d.ts.map +1 -0
  178. package/dist/server-main.js +242 -0
  179. package/dist/server-main.js.map +1 -0
  180. package/dist/utils/base-dirs.d.ts +212 -0
  181. package/dist/utils/base-dirs.d.ts.map +1 -0
  182. package/dist/utils/base-dirs.js +422 -0
  183. package/dist/utils/base-dirs.js.map +1 -0
  184. package/dist/utils/errors.d.ts +24 -0
  185. package/dist/utils/errors.d.ts.map +1 -0
  186. package/dist/utils/errors.js +53 -0
  187. package/dist/utils/errors.js.map +1 -0
  188. package/dist/utils/limits.d.ts +26 -0
  189. package/dist/utils/limits.d.ts.map +1 -0
  190. package/dist/utils/limits.js +28 -0
  191. package/dist/utils/limits.js.map +1 -0
  192. package/dist/utils/list-sources.d.ts +47 -0
  193. package/dist/utils/list-sources.d.ts.map +1 -0
  194. package/dist/utils/list-sources.js +50 -0
  195. package/dist/utils/list-sources.js.map +1 -0
  196. package/dist/utils/raw-data-utils.d.ts +131 -0
  197. package/dist/utils/raw-data-utils.d.ts.map +1 -0
  198. package/dist/utils/raw-data-utils.js +255 -0
  199. package/dist/utils/raw-data-utils.js.map +1 -0
  200. package/dist/utils/scan.d.ts +126 -0
  201. package/dist/utils/scan.d.ts.map +1 -0
  202. package/dist/utils/scan.js +221 -0
  203. package/dist/utils/scan.js.map +1 -0
  204. package/dist/utils/scope-match.d.ts +43 -0
  205. package/dist/utils/scope-match.d.ts.map +1 -0
  206. package/dist/utils/scope-match.js +87 -0
  207. package/dist/utils/scope-match.js.map +1 -0
  208. package/dist/utils/sensitive-path.d.ts +23 -0
  209. package/dist/utils/sensitive-path.d.ts.map +1 -0
  210. package/dist/utils/sensitive-path.js +91 -0
  211. package/dist/utils/sensitive-path.js.map +1 -0
  212. package/dist/utils/sync-path-key.d.ts +20 -0
  213. package/dist/utils/sync-path-key.d.ts.map +1 -0
  214. package/dist/utils/sync-path-key.js +33 -0
  215. package/dist/utils/sync-path-key.js.map +1 -0
  216. package/dist/vectordb/index.d.ts +168 -0
  217. package/dist/vectordb/index.d.ts.map +1 -0
  218. package/dist/vectordb/index.js +619 -0
  219. package/dist/vectordb/index.js.map +1 -0
  220. package/dist/vectordb/search-filters.d.ts +39 -0
  221. package/dist/vectordb/search-filters.d.ts.map +1 -0
  222. package/dist/vectordb/search-filters.js +136 -0
  223. package/dist/vectordb/search-filters.js.map +1 -0
  224. package/dist/vectordb/types.d.ts +196 -0
  225. package/dist/vectordb/types.d.ts.map +1 -0
  226. package/dist/vectordb/types.js +224 -0
  227. package/dist/vectordb/types.js.map +1 -0
  228. package/package.json +105 -0
  229. package/skills/mcp-local-rag/SKILL.md +308 -0
  230. package/skills/mcp-local-rag/references/cli-reference.md +175 -0
  231. package/skills/mcp-local-rag/references/html-ingestion.md +78 -0
  232. package/skills/mcp-local-rag/references/query-optimization.md +57 -0
  233. package/skills/mcp-local-rag/references/result-refinement.md +56 -0
@@ -0,0 +1,221 @@
1
+ // Shared bounded directory scan for supported document files.
2
+ //
3
+ // The single bounded directory walker behind the CLI `ingest` walker, the CLI
4
+ // `list` walker, and the MCP server's `list_files` scan: bounded depth, symlink
5
+ // skipping, exclude-path filtering, and supported-extension matching.
6
+ //
7
+ // The four collect predicates live in `classifyScanEntry` so a path a caller
8
+ // names explicitly (`classifyRequestedPath`) is judged by the same rules as a
9
+ // path the walk discovers — sync accepts both, and only one of them used to be
10
+ // filtered.
11
+ //
12
+ // Presentation (warning wording, when/where warnings are surfaced) and
13
+ // post-processing (sort/dedup) stay with each caller — this helper returns
14
+ // structured coverage facts (`unreadableDirs`, `depthLimitedDirs`,
15
+ // `skippedSymlinks`, and the derived `depthLimited`) so callers preserve their
16
+ // own, intentionally-different, user-facing messages. The path-granular facts
17
+ // let a caller tell an unobserved region apart from an observed one instead of
18
+ // treating any gap as a whole-scan failure.
19
+ import { lstat, readdir, realpath } from 'node:fs/promises';
20
+ import { basename, dirname, extname, join } from 'node:path';
21
+ import { SUPPORTED_EXTENSIONS } from '../parser/index.js';
22
+ import { MAX_SCAN_DEPTH } from './limits.js';
23
+ import { isInScope, isUnderOrEqual, shouldVisitDir } from './scope-match.js';
24
+ /**
25
+ * Canonical identity key for the `list`/`list_files` cross-reference: a file's
26
+ * realpath, falling back to the input path when realpath fails (orphaned or
27
+ * raw-data entries). Matching ingested DB entries against scanned files by this
28
+ * key recognizes the same physical file across symlinked spellings (prefix or
29
+ * alias). Storage, lookup, and display still use the normal resolve() path —
30
+ * realpath here is the file-identity comparison, not a user-facing value.
31
+ */
32
+ export async function realpathForMatch(filePath) {
33
+ try {
34
+ return await realpath(filePath);
35
+ }
36
+ catch {
37
+ return filePath;
38
+ }
39
+ }
40
+ /**
41
+ * Canonical form of one explicitly requested path: its parent chain resolved
42
+ * through symbolic links, with the final component appended verbatim. `null` when
43
+ * the parent chain cannot be resolved at all — absent, or a directory this
44
+ * process may not traverse — which a caller must treat as "not contained",
45
+ * because telling those cases apart would report the state of paths outside its
46
+ * configured roots.
47
+ *
48
+ * Only the parent chain is resolved, because the requested entry itself is judged
49
+ * by {@link classifyRequestedPath}'s `lstat`: a symbolic link named directly
50
+ * inside a root is an in-root entry that is refused as a link, not a path to be
51
+ * reported by whatever it points at.
52
+ *
53
+ * `realpath` here is the containment (security) boundary, the same role it plays
54
+ * in `DocumentParser.validateFilePath` — never a spelling anything is stored,
55
+ * looked up, or displayed under. Those stay `resolve()`-only.
56
+ */
57
+ export async function canonicalizeRequestedPath(path) {
58
+ try {
59
+ return join(await realpath(dirname(path)), basename(path));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * True when `fullPath` sits under one of the configured excluded prefixes (the
67
+ * database or cache directory).
68
+ *
69
+ * Case-folded on Windows, whose filesystem is case-insensitive: the prefixes are
70
+ * built with `resolve()` only, which preserves whatever case `BASE_DIRS` and
71
+ * `DB_PATH` were spelled in, so a raw comparison let `C:\Docs\lancedb\raw.md`
72
+ * past `c:\docs\lancedb\`. That is worse than a plain miss, because sync's prune
73
+ * guard compares case-folded keys (`toSyncPathKey`): the internals were ingested
74
+ * and then could never be pruned. Both sides now agree.
75
+ *
76
+ * Exact-or-descendant via `isUnderOrEqual`: the prefixes carry a trailing
77
+ * separator, so `startsWith` matched the directory's contents but not the
78
+ * directory itself, and the walk descended into it once per run.
79
+ *
80
+ * Purely lexical — no `realpath`, `stat`, or any other syscall, because this runs
81
+ * once per directory entry on the walk shared with `list_files`, CLI `list`, and
82
+ * CLI `ingest`.
83
+ */
84
+ function isUnderExcludedPrefix(fullPath, excludePaths, platform) {
85
+ const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
86
+ const candidate = fold(fullPath);
87
+ return excludePaths.some((prefix) => isUnderOrEqual(candidate, fold(prefix)));
88
+ }
89
+ /**
90
+ * The collect predicates of {@link bfsCollectSupportedFiles} as one decision, so
91
+ * a discovered directory entry and an explicitly requested path
92
+ * ({@link classifyRequestedPath}) are judged by exactly the same rules instead of
93
+ * by two implementations that can drift.
94
+ *
95
+ * Evaluation order is part of the contract and matches the walk: a symbolic link
96
+ * is reported as a link even under an excluded prefix, and a directory is
97
+ * accepted without any extension test.
98
+ *
99
+ * `platform` is a parameter rather than a direct `process.platform` read — the
100
+ * same reason `toSyncPathKey` takes one: the Windows exclusion semantics
101
+ * ({@link isUnderExcludedPrefix}) must be provable on a macOS/Linux machine. The
102
+ * default leaves every call site unchanged.
103
+ *
104
+ * Both `Dirent` (from `readdir`) and `Stats` (from `lstat`) satisfy
105
+ * {@link EntryTypeFacts} structurally.
106
+ */
107
+ export function classifyScanEntry(fullPath, entry, excludePaths, platform = process.platform) {
108
+ if (entry.isSymbolicLink())
109
+ return 'symlink';
110
+ if (isUnderExcludedPrefix(fullPath, excludePaths, platform))
111
+ return 'excluded';
112
+ if (entry.isDirectory())
113
+ return 'directory';
114
+ if (!entry.isFile())
115
+ return 'irregular';
116
+ return SUPPORTED_EXTENSIONS.has(extname(fullPath).toLowerCase()) ? 'file' : 'unsupported';
117
+ }
118
+ /**
119
+ * Classify one explicitly requested path with {@link classifyScanEntry}, so a
120
+ * path a caller names is subject to the same predicates as a path the walker
121
+ * discovers.
122
+ *
123
+ * `lstat` rather than `stat`, so a symbolic link is reported as a link instead of
124
+ * as whatever it points at; and `lstat` rather than any read, so a caller can
125
+ * refuse the path before its bytes cost anything — reading a FIFO blocks forever,
126
+ * and reading through a link reaches outside the configured roots.
127
+ *
128
+ * Any stat failure is `'missing'`: an unreachable path and an absent one are the
129
+ * same non-answer to "what is here".
130
+ */
131
+ export async function classifyRequestedPath(path, excludePaths, platform = process.platform) {
132
+ try {
133
+ return classifyScanEntry(path, await lstat(path), excludePaths, platform);
134
+ }
135
+ catch {
136
+ return 'missing';
137
+ }
138
+ }
139
+ /**
140
+ * Bounded BFS scan of a single root, collecting every supported file up to
141
+ * `maxDepth` levels deep, counted from `rootPath` itself. Symlinks are skipped
142
+ * (never followed) and recorded in `skippedSymlinks`; paths under any
143
+ * `excludePaths` prefix are filtered out. A per-directory `readdir` failure is
144
+ * captured into `unreadableDirs` and does not abort the scan (best-effort per
145
+ * directory); a branch pruned at `maxDepth` is captured into `depthLimitedDirs`.
146
+ *
147
+ * When `scope` is provided (non-empty), the predicate is pushed into the
148
+ * traversal: a directory is visited only if it is in-scope or an ancestor of
149
+ * some scope prefix, and a file is collected only if it is in-scope. A root that
150
+ * intersects no prefix is skipped without any `readdir`. An absent/empty `scope`
151
+ * leaves traversal and collection byte-for-byte unchanged.
152
+ *
153
+ * `platform` only selects how the exclusion comparison treats case (see
154
+ * {@link classifyScanEntry}); it defaults to the host, so every existing call is
155
+ * unchanged.
156
+ *
157
+ * Does not sort, dedupe, or emit warnings — callers handle those so their
158
+ * existing output contracts are preserved.
159
+ */
160
+ export async function bfsCollectSupportedFiles(rootPath, excludePaths, maxDepth = MAX_SCAN_DEPTH, scope, platform = process.platform) {
161
+ const files = [];
162
+ const unreadableDirs = [];
163
+ const depthLimitedDirs = [];
164
+ const skippedSymlinks = [];
165
+ // Scope pushdown (shared with scanBaseDir via scope-match): visit a directory
166
+ // only if it is in-scope or an ancestor of the scoped subtree, and collect a
167
+ // file only if it is in-scope. A root intersecting no prefix is skipped
168
+ // without any `readdir`; absent scope leaves traversal/collection unchanged.
169
+ const queue = shouldVisitDir(rootPath, scope)
170
+ ? [{ dirPath: rootPath, depth: 0 }]
171
+ : [];
172
+ while (queue.length > 0) {
173
+ const { dirPath, depth } = queue.shift();
174
+ if (depth >= maxDepth) {
175
+ // `dirPath` was reached but never read, so it is the first unvisited
176
+ // directory of this branch: it and all its descendants are unobserved.
177
+ depthLimitedDirs.push(dirPath);
178
+ continue;
179
+ }
180
+ // TypeScript's `readdir` has overloads keyed on the options shape; pin the
181
+ // encoding to `'utf8'` and cast so the loop operates on string-encoded
182
+ // Dirent entries (matches the rest of the codebase).
183
+ let entries;
184
+ try {
185
+ entries = (await readdir(dirPath, {
186
+ withFileTypes: true,
187
+ encoding: 'utf8',
188
+ }));
189
+ }
190
+ catch (error) {
191
+ const code = error && typeof error === 'object' && 'code' in error
192
+ ? (error.code ?? 'UNKNOWN')
193
+ : 'UNKNOWN';
194
+ unreadableDirs.push({ dirPath, code });
195
+ continue;
196
+ }
197
+ for (const entry of entries) {
198
+ const fullPath = join(dirPath, entry.name);
199
+ const kind = classifyScanEntry(fullPath, entry, excludePaths, platform);
200
+ if (kind === 'symlink') {
201
+ skippedSymlinks.push(fullPath);
202
+ }
203
+ else if (kind === 'directory') {
204
+ if (shouldVisitDir(fullPath, scope)) {
205
+ queue.push({ dirPath: fullPath, depth: depth + 1 });
206
+ }
207
+ }
208
+ else if (kind === 'file' && isInScope(fullPath, scope)) {
209
+ files.push(fullPath);
210
+ }
211
+ }
212
+ }
213
+ return {
214
+ files,
215
+ unreadableDirs,
216
+ depthLimitedDirs,
217
+ skippedSymlinks,
218
+ depthLimited: depthLimitedDirs.length > 0,
219
+ };
220
+ }
221
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/utils/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ export interface NormalizedScopePrefix {
2
+ exact: string;
3
+ descendant: string;
4
+ }
5
+ /** Normalize one scope prefix for both in-memory and LanceDB matching. */
6
+ export declare function normalizeScopePrefix(prefix: string): NormalizedScopePrefix;
7
+ /**
8
+ * True when `path` equals `prefix` or is a descendant of it, using a separator
9
+ * boundary so `/foo/bar` does not match `/foo/barista`. The separator is derived
10
+ * from `prefix`; trailing separators on `prefix` are normalized so `/a/b`,
11
+ * `/a/b/`, and `/a/b//` are equivalent. `path` is compared verbatim, matching
12
+ * the SQL contract in `buildPrefixPredicate`.
13
+ */
14
+ export declare function isUnderOrEqual(path: string, prefix: string): boolean;
15
+ /**
16
+ * True when `path` is under-or-equal any prefix in `prefixes` (union). Empty or
17
+ * undefined `prefixes` semantics are the caller's concern; an empty list yields
18
+ * false (membership against no prefixes).
19
+ */
20
+ export declare function matchesAnyScope(path: string, prefixes: string[]): boolean;
21
+ /**
22
+ * Return the prefixes in `scope` that are NOT absolute paths (server-OS path
23
+ * style, via `node:path` `isAbsolute` — the same path style scope matching
24
+ * uses). A non-absolute prefix matches nothing under the exact-or-descendant
25
+ * contract, so both list surfaces surface these as non-fatal warnings while
26
+ * preserving the "matches nothing" result semantics. Input order is preserved.
27
+ */
28
+ export declare function nonAbsolutePrefixes(scope: string[]): string[];
29
+ /**
30
+ * Directory-visit predicate for the scoped BFS walk, shared by both walkers so
31
+ * the boundary semantics live in one place. Visit `dir` when there is no scope,
32
+ * when `dir` is in-scope (under-or-equal a prefix), or when `dir` is an ancestor
33
+ * of some prefix (must be descended to reach the scoped subtree). An absent or
34
+ * empty `scope` visits every directory (traversal unchanged).
35
+ */
36
+ export declare function shouldVisitDir(dir: string, scope?: string[]): boolean;
37
+ /**
38
+ * File-collect predicate for the scoped BFS walk, shared by both walkers.
39
+ * Collect `path` when there is no scope, or when `path` is in-scope. An absent
40
+ * or empty `scope` collects every supported file (collection unchanged).
41
+ */
42
+ export declare function isInScope(path: string, scope?: string[]): boolean;
43
+ //# sourceMappingURL=scope-match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-match.d.ts","sourceRoot":"","sources":["../../src/utils/scope-match.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,CAO1E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAGpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAEzE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAGrE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAEjE"}
@@ -0,0 +1,87 @@
1
+ // Boundary-safe, exact-or-descendant path prefix matcher shared by both BFS
2
+ // walkers and both list surfaces (MCP `list_files` and the `list` CLI).
3
+ //
4
+ // This is the JS counterpart of `vectordb`'s SQL `buildPrefixPredicate`
5
+ // (`src/vectordb/index.ts`): both implement the same exact-or-descendant
6
+ // contract (`path = prefix OR path startsWith prefix + separator`) with the
7
+ // same separator-boundary and trailing-separator normalization, so `/foo/bar`
8
+ // matches `/foo/bar` and `/foo/bar/x.md` but not `/foo/barista`. Both reference
9
+ // #146's boundary rules — a change to one must be mirrored in the other.
10
+ import { isAbsolute, sep as PATH_SEP } from 'node:path';
11
+ // A slash identifies slash-style paths even when a legal filename segment
12
+ // contains a backslash. Pure backslash-style paths retain Windows support.
13
+ function deriveSeparator(prefix) {
14
+ return prefix.includes('/') ? '/' : prefix.includes('\\') ? '\\' : PATH_SEP;
15
+ }
16
+ // Strip trailing separators so `/a/b`, `/a/b/`, `/a/b//` normalize alike. A
17
+ // prefix of only separators (e.g. a lone posix root `/`) is kept as a single
18
+ // separator so its descendant boundary is `/<sep>` rather than empty.
19
+ function stripTrailingSeparators(prefix, separator) {
20
+ let end = prefix.length;
21
+ while (end > 0 && prefix[end - 1] === separator) {
22
+ end--;
23
+ }
24
+ if (end === 0) {
25
+ return separator;
26
+ }
27
+ return prefix.slice(0, end);
28
+ }
29
+ /** Normalize one scope prefix for both in-memory and LanceDB matching. */
30
+ export function normalizeScopePrefix(prefix) {
31
+ const separator = deriveSeparator(prefix);
32
+ const exact = stripTrailingSeparators(prefix, separator);
33
+ return {
34
+ exact,
35
+ descendant: exact.endsWith(separator) ? exact : exact + separator,
36
+ };
37
+ }
38
+ /**
39
+ * True when `path` equals `prefix` or is a descendant of it, using a separator
40
+ * boundary so `/foo/bar` does not match `/foo/barista`. The separator is derived
41
+ * from `prefix`; trailing separators on `prefix` are normalized so `/a/b`,
42
+ * `/a/b/`, and `/a/b//` are equivalent. `path` is compared verbatim, matching
43
+ * the SQL contract in `buildPrefixPredicate`.
44
+ */
45
+ export function isUnderOrEqual(path, prefix) {
46
+ const { exact, descendant } = normalizeScopePrefix(prefix);
47
+ return path === exact || path.startsWith(descendant);
48
+ }
49
+ /**
50
+ * True when `path` is under-or-equal any prefix in `prefixes` (union). Empty or
51
+ * undefined `prefixes` semantics are the caller's concern; an empty list yields
52
+ * false (membership against no prefixes).
53
+ */
54
+ export function matchesAnyScope(path, prefixes) {
55
+ return prefixes.some((prefix) => isUnderOrEqual(path, prefix));
56
+ }
57
+ /**
58
+ * Return the prefixes in `scope` that are NOT absolute paths (server-OS path
59
+ * style, via `node:path` `isAbsolute` — the same path style scope matching
60
+ * uses). A non-absolute prefix matches nothing under the exact-or-descendant
61
+ * contract, so both list surfaces surface these as non-fatal warnings while
62
+ * preserving the "matches nothing" result semantics. Input order is preserved.
63
+ */
64
+ export function nonAbsolutePrefixes(scope) {
65
+ return scope.filter((prefix) => !isAbsolute(prefix));
66
+ }
67
+ /**
68
+ * Directory-visit predicate for the scoped BFS walk, shared by both walkers so
69
+ * the boundary semantics live in one place. Visit `dir` when there is no scope,
70
+ * when `dir` is in-scope (under-or-equal a prefix), or when `dir` is an ancestor
71
+ * of some prefix (must be descended to reach the scoped subtree). An absent or
72
+ * empty `scope` visits every directory (traversal unchanged).
73
+ */
74
+ export function shouldVisitDir(dir, scope) {
75
+ if (!scope || scope.length === 0)
76
+ return true;
77
+ return matchesAnyScope(dir, scope) || scope.some((prefix) => isUnderOrEqual(prefix, dir));
78
+ }
79
+ /**
80
+ * File-collect predicate for the scoped BFS walk, shared by both walkers.
81
+ * Collect `path` when there is no scope, or when `path` is in-scope. An absent
82
+ * or empty `scope` collects every supported file (collection unchanged).
83
+ */
84
+ export function isInScope(path, scope) {
85
+ return !scope || scope.length === 0 || matchesAnyScope(path, scope);
86
+ }
87
+ //# sourceMappingURL=scope-match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-match.js","sourceRoot":"","sources":["../../src/utils/scope-match.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wEAAwE;AACxE,EAAE;AACF,wEAAwE;AACxE,yEAAyE;AACzE,4EAA4E;AAC5E,8EAA8E;AAC9E,gFAAgF;AAChF,yEAAyE;AAEzE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEvD,0EAA0E;AAC1E,2EAA2E;AAC3E,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC7E,CAAC;AAED,4EAA4E;AAC5E,6EAA6E;AAC7E,sEAAsE;AACtE,SAAS,uBAAuB,CAAC,MAAc,EAAE,SAAiB;IAChE,IAAI,GAAG,GAAG,MAAM,CAAC,MAAM,CAAA;IACvB,OAAO,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAChD,GAAG,EAAE,CAAA;IACP,CAAC;IACD,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;QACd,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAC7B,CAAC;AAOD,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACzC,MAAM,KAAK,GAAG,uBAAuB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACxD,OAAO;QACL,KAAK;QACL,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,SAAS;KAClE,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,MAAc;IACzD,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAA;IAC1D,OAAO,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAkB;IAC9D,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAe;IACjD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,KAAgB;IAC1D,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,OAAO,eAAe,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAgB;IACtD,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACrE,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Returns the literal prefixes joined with their `realpath`-resolved forms.
3
+ * Without canonicalization macOS would let `/etc` (which realpaths to
4
+ * `/private/etc`) slip past once the resolver normalizes the path. The
5
+ * literal is always kept so a realpath failure cannot weaken the policy.
6
+ */
7
+ export declare function buildSensitivePrefixes(realpathSyncFn?: (p: string) => string): string[];
8
+ /**
9
+ * Returns a user-facing error string when `value` resolves to a sensitive
10
+ * system or credential directory. Returns `undefined` when the path is
11
+ * acceptable.
12
+ *
13
+ * `flagName` is interpolated into the error message so the surfacing
14
+ * surface (CLI flag, env var, ...) is visible at the call site. The CLI uses
15
+ * `'--base-dir'`; the server entry point uses `'BASE_DIR'` or `'BASE_DIRS'`
16
+ * to attribute the rejection to the env var actually consulted.
17
+ *
18
+ * The trailing-separator check on system prefixes guards against sibling
19
+ * paths like `/etcetera`. Both the `~/.ssh` and the expanded form are
20
+ * rejected so the policy holds when `$HOME` is unset.
21
+ */
22
+ export declare function checkSensitivePath(value: string, flagName: string): string | undefined;
23
+ //# sourceMappingURL=sensitive-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive-path.d.ts","sourceRoot":"","sources":["../../src/utils/sensitive-path.ts"],"names":[],"mappings":"AA4BA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAqB,GACnD,MAAM,EAAE,CAcV;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0BtF"}
@@ -0,0 +1,91 @@
1
+ // Sensitive-path policy shared by the CLI and the MCP server entry point.
2
+ //
3
+ // Both entry points must refuse to use system or credential directories as
4
+ // document roots: pre-multi-root code only enforced this at the CLI surface,
5
+ // which left a gap where `BASE_DIRS=["/etc"]` in the MCP server's environment
6
+ // would be silently accepted. This module owns the single source of truth
7
+ // for the policy so the CLI (`cli/options.ts` and `cli/common.ts`) and the
8
+ // server entry point (`server-main.ts`) cannot drift.
9
+ //
10
+ // The policy is intentionally simple — a small allow-list-by-exclusion of
11
+ // system mount points and credential directories under `$HOME`. It is not a
12
+ // general-purpose sandboxing mechanism; the parser layer (`DocumentParser`)
13
+ // remains the authoritative path-traversal / symlink-escape boundary.
14
+ import { realpathSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ // Normalize a path for comparison: forward-slash separators throughout and
17
+ // lower-case on Windows (case-insensitive filesystem). Used by the security
18
+ // boundary checks below so `C:\Users\me\.ssh` and `c:/users/me/.ssh` resolve
19
+ // to the same comparable form.
20
+ function toComparable(p) {
21
+ const slashed = p.replace(/\\/g, '/');
22
+ return process.platform === 'win32' ? slashed.toLowerCase() : slashed;
23
+ }
24
+ const SENSITIVE_PATH_LITERALS = ['/etc', '/usr', '/sys', '/proc', '/var'];
25
+ /**
26
+ * Returns the literal prefixes joined with their `realpath`-resolved forms.
27
+ * Without canonicalization macOS would let `/etc` (which realpaths to
28
+ * `/private/etc`) slip past once the resolver normalizes the path. The
29
+ * literal is always kept so a realpath failure cannot weaken the policy.
30
+ */
31
+ export function buildSensitivePrefixes(realpathSyncFn = realpathSync) {
32
+ const set = new Set();
33
+ for (const literal of SENSITIVE_PATH_LITERALS) {
34
+ set.add(literal);
35
+ try {
36
+ const canonical = realpathSyncFn(literal);
37
+ if (typeof canonical === 'string' && canonical.length > 0) {
38
+ set.add(canonical);
39
+ }
40
+ }
41
+ catch {
42
+ // realpath unavailable on this platform; literal already retained.
43
+ }
44
+ }
45
+ return [...set];
46
+ }
47
+ const SENSITIVE_PATH_PREFIXES = buildSensitivePrefixes();
48
+ /**
49
+ * Directories under `$HOME` that hold credentials and must never be opened
50
+ * as document roots even when the user expands the path themselves.
51
+ */
52
+ const SENSITIVE_HOME_PREFIXES = ['.ssh', '.gnupg'];
53
+ /**
54
+ * Returns a user-facing error string when `value` resolves to a sensitive
55
+ * system or credential directory. Returns `undefined` when the path is
56
+ * acceptable.
57
+ *
58
+ * `flagName` is interpolated into the error message so the surfacing
59
+ * surface (CLI flag, env var, ...) is visible at the call site. The CLI uses
60
+ * `'--base-dir'`; the server entry point uses `'BASE_DIR'` or `'BASE_DIRS'`
61
+ * to attribute the rejection to the env var actually consulted.
62
+ *
63
+ * The trailing-separator check on system prefixes guards against sibling
64
+ * paths like `/etcetera`. Both the `~/.ssh` and the expanded form are
65
+ * rejected so the policy holds when `$HOME` is unset.
66
+ */
67
+ export function checkSensitivePath(value, flagName) {
68
+ const home = process.env['HOME'] || homedir();
69
+ const expanded = value.startsWith('~/') ? `${home}/${value.slice(2)}` : value;
70
+ const valueCmp = toComparable(expanded);
71
+ for (const prefix of SENSITIVE_PATH_PREFIXES) {
72
+ const prefixCmp = toComparable(prefix);
73
+ if (valueCmp === prefixCmp || valueCmp.startsWith(`${prefixCmp}/`)) {
74
+ return `Refusing to use sensitive system path for ${flagName}: ${value}`;
75
+ }
76
+ }
77
+ for (const dir of SENSITIVE_HOME_PREFIXES) {
78
+ if (home.length > 0) {
79
+ const homePathCmp = toComparable(`${home}/${dir}`);
80
+ if (valueCmp === homePathCmp || valueCmp.startsWith(`${homePathCmp}/`)) {
81
+ return `Refusing to use sensitive system path for ${flagName}: ${value}`;
82
+ }
83
+ }
84
+ // Unexpanded `~/...` form — caught even when `home` is empty.
85
+ if (value === `~/${dir}` || value.startsWith(`~/${dir}/`)) {
86
+ return `Refusing to use sensitive system path for ${flagName}: ${value}`;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ //# sourceMappingURL=sensitive-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive-path.js","sourceRoot":"","sources":["../../src/utils/sensitive-path.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve `path` into the absolute, normalized key sync uses to reconcile disk
3
+ * state against database state. Windows keys are case-folded because its
4
+ * filesystem is case-insensitive; POSIX keys keep their case.
5
+ *
6
+ * `platform` is a parameter rather than a direct `process.platform` read, and it
7
+ * selects the resolver as well as the case-fold, so the Windows branch is
8
+ * provable on a macOS/Linux machine — the host-bound `resolve()` would otherwise
9
+ * turn a Windows path into a cwd-relative POSIX one.
10
+ *
11
+ * Resolution is purely lexical, with no filesystem canonicalization: symbolic
12
+ * links are intentionally not followed, and a path that no longer exists on disk
13
+ * must still yield a key.
14
+ *
15
+ * The key is an internal reconciliation identity. It never replaces the verbatim
16
+ * stored `filePath` spellings used for deletion, nor the shared scope contract
17
+ * used by the query, list, and source-classification flows.
18
+ */
19
+ export declare function toSyncPathKey(path: string, platform?: NodeJS.Platform): string;
20
+ //# sourceMappingURL=sync-path-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-path-key.d.ts","sourceRoot":"","sources":["../../src/utils/sync-path-key.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAKhG"}
@@ -0,0 +1,33 @@
1
+ // Sync-only comparison-key generation.
2
+ //
3
+ // This module deliberately holds no boundary logic. Containment is delegated to
4
+ // the unchanged `isUnderOrEqual` in `scope-match.ts`, composed by the caller as
5
+ // `isUnderOrEqual(toSyncPathKey(candidate), toSyncPathKey(prefix))`, so the
6
+ // exact-or-descendant, separator-boundary, and trailing-separator semantics
7
+ // stay in exactly one place.
8
+ import { posix, win32 } from 'node:path';
9
+ /**
10
+ * Resolve `path` into the absolute, normalized key sync uses to reconcile disk
11
+ * state against database state. Windows keys are case-folded because its
12
+ * filesystem is case-insensitive; POSIX keys keep their case.
13
+ *
14
+ * `platform` is a parameter rather than a direct `process.platform` read, and it
15
+ * selects the resolver as well as the case-fold, so the Windows branch is
16
+ * provable on a macOS/Linux machine — the host-bound `resolve()` would otherwise
17
+ * turn a Windows path into a cwd-relative POSIX one.
18
+ *
19
+ * Resolution is purely lexical, with no filesystem canonicalization: symbolic
20
+ * links are intentionally not followed, and a path that no longer exists on disk
21
+ * must still yield a key.
22
+ *
23
+ * The key is an internal reconciliation identity. It never replaces the verbatim
24
+ * stored `filePath` spellings used for deletion, nor the shared scope contract
25
+ * used by the query, list, and source-classification flows.
26
+ */
27
+ export function toSyncPathKey(path, platform = process.platform) {
28
+ if (platform === 'win32') {
29
+ return win32.resolve(path).toLowerCase();
30
+ }
31
+ return posix.resolve(path);
32
+ }
33
+ //# sourceMappingURL=sync-path-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-path-key.js","sourceRoot":"","sources":["../../src/utils/sync-path-key.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,4EAA4E;AAC5E,4EAA4E;AAC5E,6BAA6B;AAE7B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAExC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IACtF,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC5B,CAAC"}