vecfinder.js (23199B)
1 #!/usr/bin/env node 2 3 const fs = require('node:fs/promises'); 4 const path = require('node:path'); 5 const http = require('node:http'); 6 const readline = require('node:readline'); 7 const { stdin, stdout, stderr } = process; 8 const EventEmitter = require('node:events'); 9 const { execSync, spawn } = require('node:child_process'); 10 11 const ONE_GIB = 1073741824; 12 13 let config = { 14 endpoint: 'http://localhost:1234/v1/embeddings', 15 model: 'text-embedding-qwen3-0.6b-text-embedding', 16 apiKey: '', 17 parallelism: 2, 18 cacheMaxSize: ONE_GIB, 19 }; 20 21 let CACHE_DIR = null; 22 let CONFIG_PATH = null; 23 24 async function getWritableCacheDir(cacheBase) { 25 try { 26 await fs.mkdir(cacheBase, { recursive: true }); 27 await fs.access(cacheBase, fs.constants.W_OK); 28 return cacheBase; 29 } catch { 30 return null; 31 } 32 } 33 34 async function createSystemCacheDir() { 35 const cacheBase = '/var/cache/vecfinder'; 36 37 try { 38 const vecfinderGroupExists = await new Promise((resolve) => { 39 execSync('getent group vecfinder > /dev/null 2>&1', { stdio: 'ignore' }); 40 resolve(true); 41 }).catch(() => false); 42 43 if (!vecfinderGroupExists) { 44 return null; 45 } 46 47 const stat = await fs.stat('/var/cache').catch(() => null); 48 if (!stat || !(stat.mode & fs.constants.S_IWUSR)) { 49 return null; 50 } 51 52 try { 53 await fs.mkdir(cacheBase, { recursive: true }); 54 } catch (err) { 55 if (err.code !== 'EEXIST') return null; 56 } 57 58 const cacheStat = await fs.stat(cacheBase).catch(() => null); 59 if (!cacheStat) return null; 60 61 const mode = cacheStat.mode & 0o777; 62 const isGroupWritable = !!(mode & fs.constants.S_IWGRP); 63 64 if (isGroupWritable) { 65 return await getWritableCacheDir(cacheBase); 66 } 67 68 try { 69 execSync(`chmod 0770 "${cacheBase}"`, { stdio: 'ignore' }); 70 } catch { 71 return null; 72 } 73 74 return await getWritableCacheDir(cacheBase); 75 } catch { 76 return null; 77 } 78 } 79 80 async function discoverConfig() { 81 const userConfig = path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'vecfinder', 'config.json'); 82 const systemConfig1 = '/etc/vecfinder/config.json'; 83 const systemConfig2 = '/etc/vecfinder.json'; 84 85 if (await fileExists(userConfig)) { 86 CONFIG_PATH = userConfig; 87 CACHE_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.cache', 'vecfinder'); 88 return; 89 } 90 91 if (await fileExists(systemConfig1) || await fileExists(systemConfig2)) { 92 const systemConfig = await fileExists(systemConfig1) ? systemConfig1 : systemConfig2; 93 CONFIG_PATH = systemConfig; 94 95 try { 96 const data = JSON.parse(await fs.readFile(systemConfig, 'utf8')); 97 if (data.cacheDir && typeof data.cacheDir === 'string') { 98 CACHE_DIR = await getWritableCacheDir(data.cacheDir); 99 } 100 101 if (!CACHE_DIR) { 102 CACHE_DIR = await createSystemCacheDir(); 103 } 104 } catch {} 105 106 if (!CACHE_DIR) { 107 CACHE_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.cache', 'vecfinder'); 108 } 109 return; 110 } 111 112 CONFIG_PATH = userConfig; 113 CACHE_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.cache', 'vecfinder'); 114 } 115 116 async function loadConfig() { 117 await discoverConfig(); 118 119 if (!CONFIG_PATH || !(await fileExists(CONFIG_PATH))) { 120 return; 121 } 122 123 try { 124 const data = await fs.readFile(CONFIG_PATH, 'utf8'); 125 config = { ...config, ...JSON.parse(data) }; 126 } catch {} 127 } 128 129 let hasBat = null; 130 131 function checkBat() { 132 if (hasBat !== null) return hasBat; 133 try { 134 execSync('which bat', { stdio: 'ignore' }); 135 hasBat = true; 136 } catch { 137 hasBat = false; 138 } 139 return hasBat; 140 } 141 142 function stripAnsi(str) { 143 return str.replace(/\x1b\[[0-9;]*m/g, ''); 144 } 145 146 async function fileExists(filePath) { 147 try { 148 await fs.access(filePath); 149 return true; 150 } catch { 151 return false; 152 } 153 } 154 155 // Cache 156 function cachePath(filePath) { 157 return path.join(CACHE_DIR, filePath); 158 } 159 160 async function getCachedVector(filePath) { 161 try { 162 const cp = cachePath(filePath); 163 const [cacheStat, fileStat] = await Promise.all([fs.stat(cp), fs.stat(filePath)]); 164 if (fileStat.mtimeMs > cacheStat.mtimeMs) return null; 165 const buf = await fs.readFile(cp); 166 return Array.from(new Float64Array(buf.buffer, buf.byteOffset, buf.byteLength / 8)); 167 } catch { 168 return null; 169 } 170 } 171 172 async function setCachedVector(filePath, vector) { 173 try { 174 const cp = cachePath(filePath); 175 await fs.mkdir(path.dirname(cp), { recursive: true }); 176 await fs.writeFile(cp, Buffer.from(new Float64Array(vector).buffer)); 177 enforceCacheLimit(); 178 } catch {} 179 } 180 181 async function enforceCacheLimit() { 182 if (!CACHE_DIR) return; 183 184 try { 185 const files = []; 186 let totalSize = 0; 187 188 const pending = [CACHE_DIR]; 189 while (pending.length) { 190 const current = pending.pop(); 191 try { 192 const entries = await fs.readdir(current, { withFileTypes: true }); 193 for (const entry of entries) { 194 const fullPath = path.join(current, entry.name); 195 if (entry.isDirectory()) { 196 pending.push(fullPath); 197 } else { 198 const stat = await fs.stat(fullPath); 199 files.push({ path: fullPath, size: stat.size }); 200 totalSize += stat.size; 201 } 202 } 203 } catch {} 204 } 205 206 while (totalSize > config.cacheMaxSize && files.length > 0) { 207 const i = Math.floor(Math.random() * files.length); 208 const [removed] = files.splice(i, 1); 209 await fs.unlink(removed.path); 210 totalSize -= removed.size; 211 } 212 } catch {} 213 } 214 215 // Embedding 216 let currentQueryId = 0; 217 218 function normalize(vec) { 219 let norm = 0; 220 for (let i = 0; i < vec.length; i++) norm += vec[i] * vec[i]; 221 norm = Math.sqrt(norm); 222 if (norm === 0) return vec; 223 return vec.map(v => v / norm); 224 } 225 226 function dot(a, b) { 227 let sum = 0; 228 for (let i = 0; i < a.length; i++) sum += a[i] * b[i]; 229 return sum; 230 } 231 232 async function fetchEmbedding(text) { 233 return new Promise((resolve, reject) => { 234 const req = http.request(config.endpoint, { 235 method: 'POST', 236 headers: { 237 'Content-Type': 'application/json', 238 ...(config.apiKey ? { 'Authorization': `Bearer ${config.apiKey}` } : {}), 239 }, 240 }, (res) => { 241 let data = ''; 242 res.on('data', chunk => data += chunk); 243 res.on('end', () => { 244 try { 245 const json = JSON.parse(data); 246 if (!json.data?.[0]?.embedding) { 247 return reject(new Error('Invalid embedding response')); 248 } 249 resolve(json.data[0].embedding); 250 } catch (e) { 251 reject(e); 252 } 253 }); 254 }); 255 req.on('error', reject); 256 req.write(JSON.stringify({ model: config.model, input: text })); 257 req.end(); 258 }); 259 } 260 261 // Document store with events 262 class DocumentStore extends EventEmitter { 263 constructor() { 264 super(); 265 this.documents = new Map(); 266 this.queryVec = null; 267 this.consumersActive = 0; 268 } 269 270 async addDocument(id, content, metadata = {}, vector) { 271 if (!vector) { 272 const raw = await fetchEmbedding(content); 273 vector = normalize(raw); 274 } 275 this.documents.set(id, { content, vector, metadata }); 276 this.emit('documentAdded'); 277 } 278 279 setQuery(queryText) { 280 currentQueryId++; 281 const queryId = currentQueryId; 282 283 if (queryText.trim() === '') { 284 this.queryVec = null; 285 this.emit('queryUpdated'); 286 return; 287 } 288 289 fetchEmbedding(queryText).then(raw => { 290 if (queryId !== currentQueryId) return; 291 this.queryVec = normalize(raw); 292 this.emit('queryUpdated'); 293 }).catch(err => { 294 console.error(`Embedding error: ${err.message}`); 295 }); 296 } 297 298 getResults() { 299 const results = []; 300 for (const [id, doc] of this.documents.entries()) { 301 const score = this.queryVec ? dot(this.queryVec, doc.vector) : 0; 302 results.push({ 303 id, 304 score, 305 content: doc.content, 306 metadata: doc.metadata, 307 }); 308 } 309 310 if (this.queryVec) { 311 results.sort((a, b) => b.score - a.score); 312 } else { 313 results.sort((a, b) => a.id.localeCompare(b.id)); 314 } 315 316 return results; 317 } 318 } 319 320 // File indexing 321 async function isTextFile(filePath) { 322 try { 323 const chunk = await fs.readFile(filePath, { length: 512 }); 324 for (let i = 0; i < chunk.length; i++) { 325 if (chunk[i] === 0) return false; 326 } 327 return true; 328 } catch { 329 return false; 330 } 331 } 332 333 async function walkDir(dirPath) { 334 const ignore = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt']); 335 const files = []; 336 const pending = [dirPath]; 337 338 while (pending.length) { 339 const current = pending.pop(); 340 try { 341 const entries = await fs.readdir(current, { withFileTypes: true }); 342 for (const entry of entries) { 343 const fullPath = path.join(current, entry.name); 344 if (entry.isDirectory()) { 345 if (!ignore.has(entry.name)) pending.push(fullPath); 346 } else { 347 files.push(fullPath); 348 } 349 } 350 } catch {} 351 } 352 353 return files; 354 } 355 356 async function indexDirectory(store, dirPath) { 357 const files = await walkDir(dirPath); 358 const queue = []; 359 let totalFiles = 0; 360 let completedFiles = 0; 361 362 for (const filePath of files) { 363 const stats = await fs.stat(filePath).catch(() => null); 364 if (!stats || stats.size > 1024 * 1024) continue; 365 const text = await isTextFile(filePath); 366 if (!text) continue; 367 queue.push(filePath); 368 totalFiles++; 369 } 370 371 function consumer() { 372 if (queue.length === 0 && store.consumersActive === 0) return; 373 374 if (store.consumersActive >= config.parallelism || queue.length === 0) return; 375 376 store.consumersActive++; 377 const filePath = queue.shift(); 378 379 (async () => { 380 try { 381 const content = await fs.readFile(filePath, 'utf8'); 382 const id = path.relative(dirPath, filePath); 383 384 let vector = await getCachedVector(filePath); 385 if (vector) { 386 await store.addDocument(id, content, { path: filePath }, vector); 387 } else { 388 const raw = await fetchEmbedding(content); 389 vector = normalize(raw); 390 await store.addDocument(id, content, { path: filePath }, vector); 391 await setCachedVector(filePath, vector); 392 } 393 } catch {} 394 395 completedFiles++; 396 store.consumersActive--; 397 store.emit('documentAdded'); 398 if (queue.length > 0 || store.consumersActive > 0) { 399 setImmediate(consumer); 400 } 401 })(); 402 } 403 404 for (let i = 0; i < Math.min(config.parallelism, totalFiles); i++) { 405 setImmediate(consumer); 406 } 407 408 return new Promise(resolve => { 409 const checkComplete = () => { 410 if (completedFiles >= totalFiles && store.consumersActive === 0 && queue.length === 0) { 411 store.removeListener('documentAdded', checkComplete); 412 resolve(); 413 } 414 }; 415 store.on('documentAdded', checkComplete); 416 checkComplete(); 417 }); 418 } 419 420 // UI 421 class UI { 422 constructor(store, useBatFlag = null) { 423 this.store = store; 424 this.query = ''; 425 this.selectedIndex = 0; 426 this.listOffset = 0; 427 this.previewOffset = 0; 428 this.rl = null; 429 this._previewWrapped = null; 430 this._previewFileId = null; 431 this._previewWidth = null; 432 this.spinnerFrame = 0; 433 this.spinnerInterval = null; 434 this.useBat = useBatFlag !== null ? useBatFlag : checkBat(); 435 this._batOutput = null; 436 this._batFileId = null; 437 this._lastRenderState = null; 438 this._needsFullRedraw = false; 439 440 this.store.on('documentAdded', () => { 441 this._needsFullRedraw = true; 442 }); 443 this.store.on('queryUpdated', () => { 444 this.selectedIndex = 0; 445 this.listOffset = 0; 446 this.previewOffset = 0; 447 this._needsFullRedraw = true; 448 this.render(); 449 }); 450 } 451 452 init() { 453 this.rl = readline.createInterface({ input: stdin, output: stderr, terminal: true }); 454 stdin.on('keypress', (str, key) => this.handleKeyPress(str, key)); 455 this.rl.on('line', () => this.handleEnter()); 456 this.rl.on('close', () => this.cleanup()); 457 this.spinnerInterval = setInterval(() => { 458 this.spinnerFrame++; 459 this.render(); 460 }, 80); 461 this.render(); 462 } 463 464 cleanup() { 465 clearInterval(this.spinnerInterval); 466 stdin.removeAllListeners('keypress'); 467 if (this.rl) this.rl.close(); 468 } 469 470 adjustListOffset(maxHeight) { 471 const results = this.store.getResults(); 472 const topThreshold = this.listOffset + 2; 473 const bottomThreshold = this.listOffset + maxHeight - 3; 474 475 if (this.selectedIndex < topThreshold) { 476 this.listOffset = Math.max(0, this.selectedIndex - 2); 477 } else if (this.selectedIndex > bottomThreshold) { 478 this.listOffset = Math.min(results.length - maxHeight, this.selectedIndex - (maxHeight - 3)); 479 } 480 481 this.listOffset = Math.max(0, Math.min(this.listOffset, Math.max(0, results.length - maxHeight))); 482 } 483 484 getMaxPreviewOffset(maxHeight) { 485 if (this.useBat && this._batOutput) { 486 return Math.max(0, this._batOutput.split('\n').length - maxHeight); 487 } 488 return this._previewWrapped ? Math.max(0, this._previewWrapped.length - maxHeight) : 0; 489 } 490 491 handleKeyPress(str, key) { 492 if (!key) return; 493 494 if (key.name === 'enter') { 495 this.handleEnter(); 496 return; 497 } 498 499 if (key.name === 'up') { 500 if (this.selectedIndex > 0) { 501 this.selectedIndex--; 502 this.previewOffset = 0; 503 const height = process.stdout.rows || 24; 504 const maxHeight = height - 7; 505 this.adjustListOffset(maxHeight); 506 this.render(); 507 } 508 return; 509 } 510 511 if (key.name === 'down') { 512 const results = this.store.getResults(); 513 if (this.selectedIndex < results.length - 1) { 514 this.selectedIndex++; 515 this.previewOffset = 0; 516 const height = process.stdout.rows || 24; 517 const maxHeight = height - 7; 518 this.adjustListOffset(maxHeight); 519 this.render(); 520 } 521 return; 522 } 523 524 if (key.name === 'home') { 525 this.selectedIndex = 0; 526 this.previewOffset = 0; 527 const height = process.stdout.rows || 24; 528 const maxHeight = height - 7; 529 this.adjustListOffset(maxHeight); 530 this.render(); 531 return; 532 } 533 534 if (key.name === 'end') { 535 const results = this.store.getResults(); 536 this.selectedIndex = results.length - 1; 537 this.previewOffset = 0; 538 const height = process.stdout.rows || 24; 539 const maxHeight = height - 7; 540 this.adjustListOffset(maxHeight); 541 this.render(); 542 return; 543 } 544 545 if (key.name === 'pageup') { 546 this.previewOffset = Math.max(0, this.previewOffset - 10); 547 this.render(); 548 return; 549 } 550 551 if (key.name === 'pagedown') { 552 const height = process.stdout.rows || 24; 553 const maxHeight = height - 7; 554 const maxPreviewOffset = this.getMaxPreviewOffset(maxHeight); 555 this.previewOffset = Math.min(maxPreviewOffset, this.previewOffset + 10); 556 this.render(); 557 return; 558 } 559 560 if (key.name === 'backspace' || key.name === 'delete') { 561 this.query = this.query.slice(0, -1); 562 this.store.setQuery(this.query); 563 return; 564 } 565 566 if (str && str.length === 1 && !key.ctrl && !key.meta) { 567 this.query += str; 568 this.store.setQuery(this.query); 569 return; 570 } 571 } 572 573 handleEnter() { 574 this.cleanup(); 575 const results = this.store.getResults(); 576 if (results.length > 0 && this.selectedIndex < results.length) { 577 stdout.write(results[this.selectedIndex].id + '\n'); 578 } else { 579 stdout.write('\n'); 580 } 581 process.exit(0); 582 } 583 584 render() { 585 const width = process.stdout.columns || 80; 586 const height = process.stdout.rows || 24; 587 588 const leftWidth = Math.floor(width * 0.35); 589 const rightWidth = width - leftWidth - 1; 590 const headerHeight = 3; 591 const maxHeight = height - 4 - headerHeight; 592 593 const results = this.store.getResults(); 594 const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; 595 const spinner = this.store.consumersActive > 0 ? frames[this.spinnerFrame % frames.length] : '\u2713'; 596 597 const titleDecorWidth = '┤ VecFinder ├'.length; 598 const dashSpace = leftWidth - titleDecorWidth; 599 const titleCenterLeft = Math.floor(dashSpace / 2); 600 const titleCenterRight = dashSpace - titleCenterLeft; 601 602 const selectedResult = results[this.selectedIndex]; 603 604 const visibleIds = []; 605 for (let row = 0; row < maxHeight; row++) { 606 const result = results[row + this.listOffset]; 607 visibleIds.push(result ? result.id : null); 608 } 609 610 const currentState = { 611 width, 612 height, 613 leftWidth, 614 rightWidth, 615 query: this.query, 616 selectedIndex: this.selectedIndex, 617 listOffset: this.listOffset, 618 previewOffset: this.previewOffset, 619 spinner, 620 consumersActive: this.store.consumersActive, 621 selectedFileId: selectedResult?.id, 622 visibleIds, 623 batFileId: this._batFileId, 624 batOutputHash: this._batOutput ? this._batOutput.slice(0, 100) : null, 625 }; 626 627 if (!this._needsFullRedraw && this._lastRenderState) { 628 const s = this._lastRenderState; 629 if ( 630 s.width === currentState.width && 631 s.height === currentState.height && 632 s.leftWidth === currentState.leftWidth && 633 s.rightWidth === currentState.rightWidth && 634 s.query === currentState.query && 635 s.selectedIndex === currentState.selectedIndex && 636 s.listOffset === currentState.listOffset && 637 s.previewOffset === currentState.previewOffset && 638 s.spinner === currentState.spinner && 639 s.consumersActive === currentState.consumersActive && 640 s.selectedFileId === currentState.selectedFileId && 641 s.visibleIds.length === currentState.visibleIds.length && 642 s.visibleIds.every((id, i) => id === currentState.visibleIds[i]) && 643 s.batFileId === currentState.batFileId 644 ) { 645 return; 646 } 647 } 648 this._needsFullRedraw = false; 649 this._lastRenderState = currentState; 650 651 stderr.write('\x1b[2J\x1b[H'); 652 653 stderr.write('─'.repeat(titleCenterLeft) + '┤ VecFinder ├' + '─'.repeat(titleCenterRight) + '┬' + '─'.repeat(rightWidth) + '\n'); 654 655 let leftHeader = 'Files ' + spinner; 656 leftHeader = leftHeader.padEnd(leftWidth, ' ').slice(0, leftWidth); 657 658 let rightHeader = ''; 659 if (selectedResult) { 660 const title = 'File: ' + selectedResult.id; 661 rightHeader = title.padEnd(rightWidth, ' ').slice(0, rightWidth); 662 } else { 663 rightHeader = ''.padEnd(rightWidth, ' '); 664 } 665 666 stderr.write(leftHeader + '│' + rightHeader + '\n'); 667 668 stderr.write('─'.repeat(leftWidth) + '┼' + '─'.repeat(rightWidth) + '\n'); 669 670 for (let row = 0; row < maxHeight; row++) { 671 const result = results[row + this.listOffset]; 672 const isSelected = (row + this.listOffset === this.selectedIndex); 673 674 let left; 675 if (result) { 676 let name = result.id; 677 const maxName = leftWidth - 10; 678 if (name.length > maxName) name = '...' + name.slice(-(maxName - 3)); 679 if (this.query && result.score !== undefined) { 680 const score = result.score.toFixed(3); 681 left = ' ' + name.padEnd(leftWidth - score.length - 3, ' ') + score + ' '; 682 } else { 683 left = ' ' + name; 684 } 685 } else { 686 left = ''; 687 } 688 left = left.padEnd(leftWidth, ' ').slice(0, leftWidth); 689 690 let right = ''; 691 const isBatMode = this.useBat && selectedResult?.metadata?.path; 692 if (selectedResult) { 693 if (isBatMode) { 694 if (!this._batOutput || this._batFileId !== selectedResult.id) { 695 try { 696 this._batOutput = execSync(`bat --color=always --style=numbers --terminal-width=${rightWidth} --wrap=character "${selectedResult.metadata.path}"`, { 697 maxBuffer: 1024 * 1024, 698 encoding: 'utf8', 699 }); 700 } catch { 701 this._batOutput = null; 702 } 703 this._batFileId = selectedResult.id; 704 } 705 if (this._batOutput) { 706 const lines = this._batOutput.split('\n'); 707 const visibleLines = lines.slice(this.previewOffset, this.previewOffset + maxHeight); 708 const line = visibleLines[row] || ''; 709 const stripped = stripAnsi(line); 710 const padding = rightWidth - stripped.length; 711 right = line + ' '.repeat(Math.max(0, padding)) + '\x1b[0m'; 712 } 713 } else { 714 if (!this._previewWrapped || this._previewFileId !== selectedResult.id || this._previewWidth !== rightWidth) { 715 const lines = selectedResult.content.split('\n'); 716 this._previewWrapped = []; 717 for (const line of lines) { 718 if (line.length === 0) { 719 this._previewWrapped.push(''); 720 } else { 721 for (let i = 0; i < line.length; i += rightWidth) { 722 this._previewWrapped.push(line.slice(i, i + rightWidth)); 723 } 724 } 725 } 726 this._previewFileId = selectedResult.id; 727 this._previewWidth = rightWidth; 728 } 729 right = this._previewWrapped[row + this.previewOffset] || ''; 730 right = right.padEnd(rightWidth, ' ').slice(0, rightWidth); 731 } 732 } 733 734 if (isSelected) { 735 stderr.write(`\x1b[7m${left}\x1b[0m│${right}`); 736 } else { 737 stderr.write(`${left}│${right}`); 738 } 739 stderr.write('\n'); 740 } 741 742 stderr.write('─'.repeat(leftWidth) + '┴' + '─'.repeat(rightWidth) + '\n'); 743 stderr.write(`Query: ${this.query}`); 744 } 745 } 746 747 function printUsage() { 748 stdout.write(`Usage: vecfinder [options] 749 vecfinder --query <text> [-k N] 750 751 Options: 752 -h, --help Show this help message 753 --query <text> Search directory for best match (non-interactive) 754 -k <number> Number of results to return (default: 1, requires --query) 755 --no-bat Disable bat syntax highlighting (interactive mode) 756 `); 757 } 758 759 // Main 760 async function main() { 761 const args = process.argv.slice(2); 762 const queryArgIdx = args.indexOf('--query'); 763 const hasQueryFlag = queryArgIdx >= 0; 764 const queryText = hasQueryFlag ? (args[queryArgIdx + 1] || null) : null; 765 const kArgIdx = args.indexOf('-k'); 766 const k = kArgIdx >= 0 ? Math.max(1, parseInt(args[kArgIdx + 1], 10) || 1) : 1; 767 const showHelp = args.includes('--help') || args.includes('-h'); 768 769 await loadConfig(); 770 await fs.mkdir(CACHE_DIR, { recursive: true }); 771 772 if (showHelp) { 773 printUsage(); 774 process.exit(0); 775 } 776 777 const store = new DocumentStore(); 778 779 if (hasQueryFlag) { 780 if (!queryText) { 781 printUsage(); 782 stderr.write('Error: --query requires a text argument\n'); 783 process.exit(1); 784 } 785 786 await indexDirectory(store, process.cwd()); 787 const raw = await fetchEmbedding(queryText); 788 store.queryVec = normalize(raw); 789 const results = store.getResults().slice(0, k); 790 for (const r of results) { 791 stdout.write(r.id + '\n'); 792 } 793 process.exit(0); 794 } 795 796 const useBatFlag = args.includes('--no-bat') ? false : null; 797 const ui = new UI(store, useBatFlag); 798 799 ui.init(); 800 801 indexDirectory(store, process.cwd()); 802 } 803 804 main().catch(err => { 805 console.error(`Fatal error: ${err.message}`); 806 process.exit(1); 807 });