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.
- package/LICENSE +661 -0
- package/README.md +219 -0
- package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
- package/dist/cli/__tests__/mcp-setup.test.js +597 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +238 -0
- package/dist/cli/commands/handlers.d.ts +57 -0
- package/dist/cli/commands/handlers.js +231 -0
- package/dist/cli/commands/index.d.ts +2 -0
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/mcp-setup.d.ts +107 -0
- package/dist/cli/commands/mcp-setup.js +509 -0
- package/dist/cli/commands/useRagCommands.d.ts +23 -0
- package/dist/cli/commands/useRagCommands.js +180 -0
- package/dist/cli/components/CleanWizard.d.ts +17 -0
- package/dist/cli/components/CleanWizard.js +169 -0
- package/dist/cli/components/InitWizard.d.ts +20 -0
- package/dist/cli/components/InitWizard.js +370 -0
- package/dist/cli/components/McpSetupWizard.d.ts +37 -0
- package/dist/cli/components/McpSetupWizard.js +387 -0
- package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
- package/dist/cli/components/SearchResultsDisplay.js +130 -0
- package/dist/cli/components/WelcomeBanner.d.ts +10 -0
- package/dist/cli/components/WelcomeBanner.js +26 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/data/mcp-editors.d.ts +80 -0
- package/dist/cli/data/mcp-editors.js +270 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli-bundle.cjs +5269 -0
- package/dist/common/commands/terminalSetup.d.ts +2 -0
- package/dist/common/commands/terminalSetup.js +144 -0
- package/dist/common/components/CommandSuggestions.d.ts +9 -0
- package/dist/common/components/CommandSuggestions.js +20 -0
- package/dist/common/components/StaticWithResize.d.ts +23 -0
- package/dist/common/components/StaticWithResize.js +62 -0
- package/dist/common/components/StatusBar.d.ts +8 -0
- package/dist/common/components/StatusBar.js +64 -0
- package/dist/common/components/TextInput.d.ts +12 -0
- package/dist/common/components/TextInput.js +239 -0
- package/dist/common/components/index.d.ts +3 -0
- package/dist/common/components/index.js +3 -0
- package/dist/common/hooks/index.d.ts +4 -0
- package/dist/common/hooks/index.js +4 -0
- package/dist/common/hooks/useCommandHistory.d.ts +7 -0
- package/dist/common/hooks/useCommandHistory.js +51 -0
- package/dist/common/hooks/useCtrlC.d.ts +9 -0
- package/dist/common/hooks/useCtrlC.js +40 -0
- package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
- package/dist/common/hooks/useKittyKeyboard.js +26 -0
- package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
- package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
- package/dist/common/hooks/useTerminalResize.d.ts +28 -0
- package/dist/common/hooks/useTerminalResize.js +51 -0
- package/dist/common/hooks/useTextBuffer.d.ts +13 -0
- package/dist/common/hooks/useTextBuffer.js +165 -0
- package/dist/common/index.d.ts +13 -0
- package/dist/common/index.js +17 -0
- package/dist/common/types.d.ts +162 -0
- package/dist/common/types.js +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +66 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +837 -0
- package/dist/mcp/watcher.d.ts +86 -0
- package/dist/mcp/watcher.js +334 -0
- package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
- package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
- package/dist/rag/__tests__/helpers.d.ts +30 -0
- package/dist/rag/__tests__/helpers.js +67 -0
- package/dist/rag/__tests__/merkle.test.d.ts +5 -0
- package/dist/rag/__tests__/merkle.test.js +161 -0
- package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
- package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
- package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
- package/dist/rag/__tests__/multi-language.test.js +535 -0
- package/dist/rag/__tests__/rag.test.d.ts +10 -0
- package/dist/rag/__tests__/rag.test.js +311 -0
- package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
- package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
- package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
- package/dist/rag/__tests__/search-filters.test.js +250 -0
- package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
- package/dist/rag/__tests__/search-modes.test.js +133 -0
- package/dist/rag/config/index.d.ts +61 -0
- package/dist/rag/config/index.js +111 -0
- package/dist/rag/constants.d.ts +41 -0
- package/dist/rag/constants.js +57 -0
- package/dist/rag/embeddings/fastembed.d.ts +62 -0
- package/dist/rag/embeddings/fastembed.js +124 -0
- package/dist/rag/embeddings/gemini.d.ts +26 -0
- package/dist/rag/embeddings/gemini.js +116 -0
- package/dist/rag/embeddings/index.d.ts +10 -0
- package/dist/rag/embeddings/index.js +9 -0
- package/dist/rag/embeddings/local-4b.d.ts +28 -0
- package/dist/rag/embeddings/local-4b.js +51 -0
- package/dist/rag/embeddings/local.d.ts +29 -0
- package/dist/rag/embeddings/local.js +119 -0
- package/dist/rag/embeddings/mistral.d.ts +22 -0
- package/dist/rag/embeddings/mistral.js +85 -0
- package/dist/rag/embeddings/openai.d.ts +22 -0
- package/dist/rag/embeddings/openai.js +85 -0
- package/dist/rag/embeddings/types.d.ts +37 -0
- package/dist/rag/embeddings/types.js +1 -0
- package/dist/rag/gitignore/index.d.ts +57 -0
- package/dist/rag/gitignore/index.js +178 -0
- package/dist/rag/index.d.ts +15 -0
- package/dist/rag/index.js +25 -0
- package/dist/rag/indexer/chunker.d.ts +129 -0
- package/dist/rag/indexer/chunker.js +1352 -0
- package/dist/rag/indexer/index.d.ts +6 -0
- package/dist/rag/indexer/index.js +6 -0
- package/dist/rag/indexer/indexer.d.ts +73 -0
- package/dist/rag/indexer/indexer.js +356 -0
- package/dist/rag/indexer/types.d.ts +68 -0
- package/dist/rag/indexer/types.js +47 -0
- package/dist/rag/logger/index.d.ts +20 -0
- package/dist/rag/logger/index.js +75 -0
- package/dist/rag/manifest/index.d.ts +50 -0
- package/dist/rag/manifest/index.js +97 -0
- package/dist/rag/merkle/diff.d.ts +26 -0
- package/dist/rag/merkle/diff.js +95 -0
- package/dist/rag/merkle/hash.d.ts +34 -0
- package/dist/rag/merkle/hash.js +165 -0
- package/dist/rag/merkle/index.d.ts +68 -0
- package/dist/rag/merkle/index.js +298 -0
- package/dist/rag/merkle/node.d.ts +51 -0
- package/dist/rag/merkle/node.js +69 -0
- package/dist/rag/search/filters.d.ts +21 -0
- package/dist/rag/search/filters.js +100 -0
- package/dist/rag/search/fts.d.ts +32 -0
- package/dist/rag/search/fts.js +61 -0
- package/dist/rag/search/hybrid.d.ts +17 -0
- package/dist/rag/search/hybrid.js +58 -0
- package/dist/rag/search/index.d.ts +89 -0
- package/dist/rag/search/index.js +367 -0
- package/dist/rag/search/types.d.ts +130 -0
- package/dist/rag/search/types.js +4 -0
- package/dist/rag/search/vector.d.ts +25 -0
- package/dist/rag/search/vector.js +44 -0
- package/dist/rag/storage/index.d.ts +92 -0
- package/dist/rag/storage/index.js +287 -0
- package/dist/rag/storage/lancedb-native.d.ts +7 -0
- package/dist/rag/storage/lancedb-native.js +10 -0
- package/dist/rag/storage/schema.d.ts +23 -0
- package/dist/rag/storage/schema.js +50 -0
- package/dist/rag/storage/types.d.ts +100 -0
- package/dist/rag/storage/types.js +68 -0
- package/package.json +67 -0
- 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[];
|