viberag 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 (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. package/scripts/check-node-version.js +37 -0
@@ -0,0 +1,298 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { createDirectoryNode, createFileNode, deserializeNode, serializeNode, } from './node.js';
5
+ import { computeDirectoryHash, computeFileHash, hasValidExtension, isBinaryFile, } from './hash.js';
6
+ import { compareTrees } from './diff.js';
7
+ import { loadGitignore, getGlobIgnorePatterns } from '../gitignore/index.js';
8
+ export * from './node.js';
9
+ export * from './hash.js';
10
+ export * from './diff.js';
11
+ /**
12
+ * A Merkle tree for efficient codebase change detection.
13
+ *
14
+ * The tree is content-addressed: if a file's content doesn't change,
15
+ * its hash stays the same. Directory hashes are computed from their
16
+ * children's hashes, so unchanged subtrees have unchanged hashes.
17
+ */
18
+ export class MerkleTree {
19
+ constructor(root, fileCount, buildStats) {
20
+ /** Root node of the tree */
21
+ Object.defineProperty(this, "root", {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value: void 0
26
+ });
27
+ /** Total number of files in the tree */
28
+ Object.defineProperty(this, "fileCount", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ });
34
+ /** Build statistics (populated after build) */
35
+ Object.defineProperty(this, "buildStats", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: void 0
40
+ });
41
+ this.root = root;
42
+ this.fileCount = fileCount;
43
+ this.buildStats = buildStats;
44
+ }
45
+ /**
46
+ * Build a Merkle tree from the filesystem.
47
+ *
48
+ * Uses .gitignore for exclusions instead of hardcoded patterns.
49
+ * If extensions is provided, only files with those extensions are included.
50
+ * If extensions is empty/undefined, all text files are included.
51
+ *
52
+ * @param projectRoot - Absolute path to project root
53
+ * @param extensions - File extensions to include (e.g., [".py", ".ts"]), or empty for all
54
+ * @param _excludePatterns - DEPRECATED: Use .gitignore instead. This parameter is ignored.
55
+ * @param previousTree - Previous tree for mtime optimization
56
+ */
57
+ static async build(projectRoot, extensions, _excludePatterns, previousTree) {
58
+ // Build a lookup map from the previous tree for mtime optimization
59
+ const previousNodes = previousTree
60
+ ? buildNodeLookup(previousTree.root)
61
+ : new Map();
62
+ // Initialize build stats
63
+ const stats = {
64
+ filesScanned: 0,
65
+ filesIndexed: 0,
66
+ cacheHits: 0,
67
+ cacheMisses: 0,
68
+ filesSkipped: 0,
69
+ };
70
+ // Load gitignore rules for both fast-glob (upfront filtering) and post-filtering
71
+ const [gitignore, globIgnorePatterns] = await Promise.all([
72
+ loadGitignore(projectRoot),
73
+ getGlobIgnorePatterns(projectRoot),
74
+ ]);
75
+ // Find all files - use gitignore patterns upfront to avoid scanning excluded dirs
76
+ const pattern = '**/*';
77
+ const files = await fg(pattern, {
78
+ cwd: projectRoot,
79
+ dot: true,
80
+ onlyFiles: true,
81
+ followSymbolicLinks: false, // Skip symlinks
82
+ // Apply gitignore patterns upfront so fast-glob skips excluded directories
83
+ ignore: globIgnorePatterns,
84
+ });
85
+ stats.filesScanned = files.length;
86
+ // Filter using gitignore and optionally by extension
87
+ const validFiles = files.filter(relativePath => {
88
+ // Apply gitignore rules
89
+ if (gitignore.ignores(relativePath)) {
90
+ return false;
91
+ }
92
+ // If extensions specified, filter by extension
93
+ if (extensions.length > 0 &&
94
+ !hasValidExtension(relativePath, extensions)) {
95
+ return false;
96
+ }
97
+ return true;
98
+ });
99
+ // Build file nodes
100
+ const fileNodes = new Map();
101
+ for (const relativePath of validFiles) {
102
+ const absolutePath = path.join(projectRoot, relativePath);
103
+ try {
104
+ // Get file stats (use lstat to detect symlinks)
105
+ const fileStats = await fs.lstat(absolutePath);
106
+ // Skip symlinks
107
+ if (fileStats.isSymbolicLink()) {
108
+ stats.filesSkipped++;
109
+ continue;
110
+ }
111
+ // Check if it's a binary file
112
+ const binary = await isBinaryFile(absolutePath);
113
+ if (binary) {
114
+ stats.filesSkipped++;
115
+ continue;
116
+ }
117
+ const size = fileStats.size;
118
+ const mtime = fileStats.mtimeMs;
119
+ // Check if we can reuse hash from previous tree (mtime optimization)
120
+ let hash;
121
+ const prevNode = previousNodes.get(relativePath);
122
+ if (prevNode &&
123
+ prevNode.type === 'file' &&
124
+ prevNode.size === size &&
125
+ prevNode.mtime === mtime) {
126
+ // File unchanged - reuse cached hash
127
+ hash = prevNode.hash;
128
+ stats.cacheHits++;
129
+ }
130
+ else {
131
+ // File is new or changed - compute hash
132
+ hash = await computeFileHash(absolutePath);
133
+ stats.cacheMisses++;
134
+ }
135
+ const node = createFileNode(relativePath, hash, size, mtime);
136
+ fileNodes.set(relativePath, node);
137
+ stats.filesIndexed++;
138
+ }
139
+ catch {
140
+ // Skip files we can't read
141
+ stats.filesSkipped++;
142
+ continue;
143
+ }
144
+ }
145
+ // Build directory structure
146
+ const root = buildDirectoryTree(fileNodes);
147
+ return new MerkleTree(root, stats.filesIndexed, stats);
148
+ }
149
+ /**
150
+ * Compare this tree with another tree.
151
+ *
152
+ * @param other - The other tree (usually the new/current tree)
153
+ * @returns TreeDiff with new, modified, and deleted files
154
+ */
155
+ compare(other) {
156
+ return compareTrees(this.root, other.root);
157
+ }
158
+ /**
159
+ * Serialize the tree to a plain object for JSON storage.
160
+ */
161
+ toJSON() {
162
+ if (!this.root) {
163
+ return null;
164
+ }
165
+ return serializeNode(this.root);
166
+ }
167
+ /**
168
+ * Deserialize a tree from a plain object.
169
+ */
170
+ static fromJSON(data) {
171
+ const emptyStats = {
172
+ filesScanned: 0,
173
+ filesIndexed: 0,
174
+ cacheHits: 0,
175
+ cacheMisses: 0,
176
+ filesSkipped: 0,
177
+ };
178
+ if (!data) {
179
+ return new MerkleTree(null, 0, emptyStats);
180
+ }
181
+ const root = deserializeNode(data);
182
+ const fileCount = countFiles(root);
183
+ return new MerkleTree(root, fileCount, emptyStats);
184
+ }
185
+ /**
186
+ * Create an empty tree.
187
+ */
188
+ static empty() {
189
+ return new MerkleTree(null, 0, {
190
+ filesScanned: 0,
191
+ filesIndexed: 0,
192
+ cacheHits: 0,
193
+ cacheMisses: 0,
194
+ filesSkipped: 0,
195
+ });
196
+ }
197
+ }
198
+ /**
199
+ * Build a lookup map from path to node for quick access.
200
+ */
201
+ function buildNodeLookup(root) {
202
+ const lookup = new Map();
203
+ if (!root) {
204
+ return lookup;
205
+ }
206
+ function traverse(node) {
207
+ lookup.set(node.path, node);
208
+ if (node.children) {
209
+ for (const child of node.children.values()) {
210
+ traverse(child);
211
+ }
212
+ }
213
+ }
214
+ traverse(root);
215
+ return lookup;
216
+ }
217
+ /**
218
+ * Build a directory tree from a flat map of file nodes.
219
+ */
220
+ function buildDirectoryTree(fileNodes) {
221
+ if (fileNodes.size === 0) {
222
+ return null;
223
+ }
224
+ // Build intermediate directory nodes
225
+ const allNodes = new Map(fileNodes);
226
+ const dirChildren = new Map();
227
+ // Collect children for each directory
228
+ for (const [filePath] of fileNodes) {
229
+ const parts = filePath.split('/');
230
+ // Build path to each ancestor directory
231
+ for (let i = 1; i <= parts.length; i++) {
232
+ // Parent directory path (empty string for root)
233
+ const dirPath = parts.slice(0, i - 1).join('/');
234
+ const childName = parts[i - 1];
235
+ const childPath = i === parts.length ? filePath : parts.slice(0, i).join('/');
236
+ if (!dirChildren.has(dirPath)) {
237
+ dirChildren.set(dirPath, new Map());
238
+ }
239
+ // Add the immediate child to its parent directory
240
+ const childNode = allNodes.get(childPath);
241
+ if (childNode) {
242
+ dirChildren.get(dirPath).set(childName, childNode);
243
+ }
244
+ }
245
+ }
246
+ // Build directory nodes bottom-up (deepest first)
247
+ const dirPaths = [...dirChildren.keys()].sort((a, b) => b.split('/').length - a.split('/').length);
248
+ for (const dirPath of dirPaths) {
249
+ if (dirPath === '')
250
+ continue; // Skip root for now
251
+ const children = dirChildren.get(dirPath);
252
+ // Collect actual child nodes (may include already-built directories)
253
+ const childNodes = new Map();
254
+ for (const [name, child] of children) {
255
+ // Check if there's a directory node we built
256
+ const builtDir = allNodes.get(child.path);
257
+ if (builtDir) {
258
+ childNodes.set(name, builtDir);
259
+ }
260
+ }
261
+ // Compute directory hash and create node
262
+ const hash = computeDirectoryHash(childNodes);
263
+ const dirNode = createDirectoryNode(dirPath, hash, childNodes);
264
+ allNodes.set(dirPath, dirNode);
265
+ // Update parent's reference to this directory
266
+ const parentPath = dirPath.includes('/')
267
+ ? dirPath.slice(0, dirPath.lastIndexOf('/'))
268
+ : '';
269
+ const dirName = dirPath.includes('/')
270
+ ? dirPath.slice(dirPath.lastIndexOf('/') + 1)
271
+ : dirPath;
272
+ if (dirChildren.has(parentPath)) {
273
+ dirChildren.get(parentPath).set(dirName, dirNode);
274
+ }
275
+ }
276
+ // Build root node
277
+ const rootChildren = dirChildren.get('');
278
+ if (!rootChildren || rootChildren.size === 0) {
279
+ return null;
280
+ }
281
+ const hash = computeDirectoryHash(rootChildren);
282
+ return createDirectoryNode('', hash, rootChildren);
283
+ }
284
+ /**
285
+ * Count the number of files in a tree.
286
+ */
287
+ function countFiles(node) {
288
+ if (node.type === 'file') {
289
+ return 1;
290
+ }
291
+ let count = 0;
292
+ if (node.children) {
293
+ for (const child of node.children.values()) {
294
+ count += countFiles(child);
295
+ }
296
+ }
297
+ return count;
298
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Type of a Merkle tree node.
3
+ */
4
+ export type NodeType = 'file' | 'directory';
5
+ /**
6
+ * A node in the Merkle tree.
7
+ *
8
+ * Files have hash = SHA256(content).
9
+ * Directories have hash = SHA256(sorted child name+hash pairs).
10
+ */
11
+ export interface MerkleNode {
12
+ /** SHA256 hash of content (file) or children (directory) */
13
+ hash: string;
14
+ /** Node type */
15
+ type: NodeType;
16
+ /** Path relative to project root */
17
+ path: string;
18
+ /** Child nodes (directories only) */
19
+ children?: Map<string, MerkleNode>;
20
+ /** File size in bytes (files only) */
21
+ size?: number;
22
+ /** File modification time in Unix ms (files only) */
23
+ mtime?: number;
24
+ }
25
+ /**
26
+ * Serialized form of a MerkleNode for JSON storage.
27
+ */
28
+ export interface SerializedNode {
29
+ hash: string;
30
+ type: NodeType;
31
+ path: string;
32
+ children?: Record<string, SerializedNode>;
33
+ size?: number;
34
+ mtime?: number;
35
+ }
36
+ /**
37
+ * Serialize a MerkleNode to a plain object for JSON storage.
38
+ */
39
+ export declare function serializeNode(node: MerkleNode): SerializedNode;
40
+ /**
41
+ * Deserialize a plain object back to a MerkleNode.
42
+ */
43
+ export declare function deserializeNode(data: SerializedNode): MerkleNode;
44
+ /**
45
+ * Create a file node.
46
+ */
47
+ export declare function createFileNode(path: string, hash: string, size: number, mtime: number): MerkleNode;
48
+ /**
49
+ * Create a directory node.
50
+ */
51
+ export declare function createDirectoryNode(path: string, hash: string, children: Map<string, MerkleNode>): MerkleNode;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Serialize a MerkleNode to a plain object for JSON storage.
3
+ */
4
+ export function serializeNode(node) {
5
+ const serialized = {
6
+ hash: node.hash,
7
+ type: node.type,
8
+ path: node.path,
9
+ };
10
+ if (node.children) {
11
+ serialized.children = {};
12
+ for (const [name, child] of node.children) {
13
+ serialized.children[name] = serializeNode(child);
14
+ }
15
+ }
16
+ if (node.size !== undefined) {
17
+ serialized.size = node.size;
18
+ }
19
+ if (node.mtime !== undefined) {
20
+ serialized.mtime = node.mtime;
21
+ }
22
+ return serialized;
23
+ }
24
+ /**
25
+ * Deserialize a plain object back to a MerkleNode.
26
+ */
27
+ export function deserializeNode(data) {
28
+ const node = {
29
+ hash: data.hash,
30
+ type: data.type,
31
+ path: data.path,
32
+ };
33
+ if (data.children) {
34
+ node.children = new Map();
35
+ for (const [name, childData] of Object.entries(data.children)) {
36
+ node.children.set(name, deserializeNode(childData));
37
+ }
38
+ }
39
+ if (data.size !== undefined) {
40
+ node.size = data.size;
41
+ }
42
+ if (data.mtime !== undefined) {
43
+ node.mtime = data.mtime;
44
+ }
45
+ return node;
46
+ }
47
+ /**
48
+ * Create a file node.
49
+ */
50
+ export function createFileNode(path, hash, size, mtime) {
51
+ return {
52
+ hash,
53
+ type: 'file',
54
+ path,
55
+ size,
56
+ mtime,
57
+ };
58
+ }
59
+ /**
60
+ * Create a directory node.
61
+ */
62
+ export function createDirectoryNode(path, hash, children) {
63
+ return {
64
+ hash,
65
+ type: 'directory',
66
+ path,
67
+ children,
68
+ };
69
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Filter builder for LanceDB WHERE clauses.
3
+ *
4
+ * Converts SearchFilters to LanceDB filter strings.
5
+ */
6
+ import type { SearchFilters } from './types.js';
7
+ /**
8
+ * Build a LanceDB WHERE clause from search filters.
9
+ *
10
+ * @param filters - Search filters to convert
11
+ * @returns Filter string for LanceDB WHERE clause, or undefined if no filters
12
+ */
13
+ export declare function buildFilterClause(filters: SearchFilters | undefined): string | undefined;
14
+ /**
15
+ * Build a name match filter for definition mode.
16
+ *
17
+ * @param symbolName - Symbol name to match
18
+ * @param typeFilter - Optional type filter
19
+ * @returns Filter string for LanceDB WHERE clause
20
+ */
21
+ export declare function buildDefinitionFilter(symbolName: string, typeFilter?: ('function' | 'class' | 'method' | 'module')[]): string;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Filter builder for LanceDB WHERE clauses.
3
+ *
4
+ * Converts SearchFilters to LanceDB filter strings.
5
+ */
6
+ /**
7
+ * Build a LanceDB WHERE clause from search filters.
8
+ *
9
+ * @param filters - Search filters to convert
10
+ * @returns Filter string for LanceDB WHERE clause, or undefined if no filters
11
+ */
12
+ export function buildFilterClause(filters) {
13
+ if (!filters)
14
+ return undefined;
15
+ const conditions = [];
16
+ // Path prefix filter
17
+ if (filters.pathPrefix) {
18
+ const escaped = escapeSqlString(filters.pathPrefix);
19
+ conditions.push(`filepath LIKE '${escaped}%'`);
20
+ }
21
+ // Path contains (ALL must match)
22
+ if (filters.pathContains && filters.pathContains.length > 0) {
23
+ for (const str of filters.pathContains) {
24
+ const escaped = escapeSqlString(str);
25
+ conditions.push(`filepath LIKE '%${escaped}%'`);
26
+ }
27
+ }
28
+ // Path not contains (NONE must match)
29
+ if (filters.pathNotContains && filters.pathNotContains.length > 0) {
30
+ for (const str of filters.pathNotContains) {
31
+ const escaped = escapeSqlString(str);
32
+ conditions.push(`filepath NOT LIKE '%${escaped}%'`);
33
+ }
34
+ }
35
+ // Type filter (chunk type: function, class, method, module)
36
+ if (filters.type && filters.type.length > 0) {
37
+ const types = filters.type.map(t => `'${escapeSqlString(t)}'`).join(', ');
38
+ conditions.push(`type IN (${types})`);
39
+ }
40
+ // Extension filter
41
+ if (filters.extension && filters.extension.length > 0) {
42
+ const extensions = filters.extension
43
+ .map(e => `'${escapeSqlString(e)}'`)
44
+ .join(', ');
45
+ conditions.push(`extension IN (${extensions})`);
46
+ }
47
+ // Is exported filter
48
+ if (filters.isExported !== undefined) {
49
+ conditions.push(`is_exported = ${filters.isExported}`);
50
+ }
51
+ // Decorator contains filter
52
+ if (filters.decoratorContains) {
53
+ const escaped = escapeSqlString(filters.decoratorContains);
54
+ conditions.push(`decorator_names LIKE '%${escaped}%'`);
55
+ }
56
+ // Has docstring filter
57
+ if (filters.hasDocstring !== undefined) {
58
+ if (filters.hasDocstring) {
59
+ conditions.push(`docstring IS NOT NULL AND docstring != ''`);
60
+ }
61
+ else {
62
+ conditions.push(`(docstring IS NULL OR docstring = '')`);
63
+ }
64
+ }
65
+ if (conditions.length === 0) {
66
+ return undefined;
67
+ }
68
+ return conditions.join(' AND ');
69
+ }
70
+ /**
71
+ * Build a name match filter for definition mode.
72
+ *
73
+ * @param symbolName - Symbol name to match
74
+ * @param typeFilter - Optional type filter
75
+ * @returns Filter string for LanceDB WHERE clause
76
+ */
77
+ export function buildDefinitionFilter(symbolName, typeFilter) {
78
+ const conditions = [];
79
+ // Exact name match
80
+ const escaped = escapeSqlString(symbolName);
81
+ conditions.push(`name = '${escaped}'`);
82
+ // Type filter for definitions (exclude module chunks)
83
+ if (typeFilter && typeFilter.length > 0) {
84
+ const types = typeFilter.map(t => `'${escapeSqlString(t)}'`).join(', ');
85
+ conditions.push(`type IN (${types})`);
86
+ }
87
+ else {
88
+ // Default: look for function, class, method definitions
89
+ conditions.push(`type IN ('function', 'class', 'method')`);
90
+ }
91
+ return conditions.join(' AND ');
92
+ }
93
+ /**
94
+ * Escape a string for use in SQL LIKE clause.
95
+ */
96
+ function escapeSqlString(str) {
97
+ // Escape single quotes by doubling them
98
+ // Also escape % and _ which are LIKE wildcards
99
+ return str.replace(/'/g, "''").replace(/%/g, '\\%').replace(/_/g, '\\_');
100
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Full-text search (BM25) using LanceDB.
3
+ */
4
+ import type { Table } from '@lancedb/lancedb';
5
+ import type { SearchResult } from './types.js';
6
+ /**
7
+ * Options for FTS search.
8
+ */
9
+ export interface FtsSearchOptions {
10
+ /** Maximum number of results */
11
+ limit: number;
12
+ /** LanceDB WHERE clause filter */
13
+ filterClause?: string;
14
+ /** Minimum score threshold (0-1) */
15
+ minScore?: number;
16
+ }
17
+ /**
18
+ * Ensure FTS index exists on the text column.
19
+ * Creates the index if it doesn't exist.
20
+ *
21
+ * @param table - LanceDB table to index
22
+ */
23
+ export declare function ensureFtsIndex(table: Table): Promise<void>;
24
+ /**
25
+ * Perform full-text search using BM25.
26
+ *
27
+ * @param table - LanceDB table to search
28
+ * @param query - Search query string
29
+ * @param options - Search options
30
+ * @returns Array of search results with FTS scores
31
+ */
32
+ export declare function ftsSearch(table: Table, query: string, options: FtsSearchOptions | number): Promise<SearchResult[]>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Full-text search (BM25) using LanceDB.
3
+ */
4
+ import * as lancedb from '@lancedb/lancedb';
5
+ /**
6
+ * Ensure FTS index exists on the text column.
7
+ * Creates the index if it doesn't exist.
8
+ *
9
+ * @param table - LanceDB table to index
10
+ */
11
+ export async function ensureFtsIndex(table) {
12
+ const indices = await table.listIndices();
13
+ const hasFtsIndex = indices.some(idx => idx.columns.includes('text') && idx.indexType === 'FTS');
14
+ if (!hasFtsIndex) {
15
+ await table.createIndex('text', {
16
+ config: lancedb.Index.fts(),
17
+ });
18
+ }
19
+ }
20
+ /**
21
+ * Perform full-text search using BM25.
22
+ *
23
+ * @param table - LanceDB table to search
24
+ * @param query - Search query string
25
+ * @param options - Search options
26
+ * @returns Array of search results with FTS scores
27
+ */
28
+ export async function ftsSearch(table, query, options) {
29
+ // Support legacy signature: ftsSearch(table, query, limit)
30
+ const opts = typeof options === 'number' ? { limit: options } : options;
31
+ // Ensure FTS index exists
32
+ await ensureFtsIndex(table);
33
+ let searchQuery = table.search(query, 'fts').limit(opts.limit);
34
+ // Apply filter if provided
35
+ if (opts.filterClause) {
36
+ searchQuery = searchQuery.where(opts.filterClause);
37
+ }
38
+ const results = await searchQuery.toArray();
39
+ return results
40
+ .map((row, index) => {
41
+ const chunk = row;
42
+ // BM25 score (higher is better)
43
+ // Normalize by rank for consistent scoring
44
+ const ftsScore = chunk._score ?? 1 / (index + 1);
45
+ return {
46
+ id: chunk.id,
47
+ text: chunk.text,
48
+ filepath: chunk.filepath,
49
+ filename: chunk.filename,
50
+ name: chunk.name,
51
+ type: chunk.type,
52
+ startLine: chunk.start_line,
53
+ endLine: chunk.end_line,
54
+ score: ftsScore,
55
+ ftsScore,
56
+ signature: chunk.signature,
57
+ isExported: chunk.is_exported,
58
+ };
59
+ })
60
+ .filter(r => !opts.minScore || r.score >= opts.minScore);
61
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Hybrid search combining vector and FTS with RRF reranking.
3
+ */
4
+ import type { SearchResult } from './types.js';
5
+ /**
6
+ * Combine vector and FTS results using Reciprocal Rank Fusion.
7
+ *
8
+ * RRF formula: score = sum(1 / (k + rank))
9
+ * where k is a constant (typically 60) and rank is 1-indexed.
10
+ *
11
+ * @param vectorResults - Results from vector search
12
+ * @param ftsResults - Results from FTS search
13
+ * @param limit - Maximum number of results to return
14
+ * @param vectorWeight - Weight for vector results (0.0-1.0, default 0.7)
15
+ * @returns Combined and reranked results
16
+ */
17
+ export declare function hybridRerank(vectorResults: SearchResult[], ftsResults: SearchResult[], limit: number, vectorWeight?: number): SearchResult[];