raggrep 0.1.0 → 0.1.3

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/dist/cli/main.js CHANGED
@@ -548,10 +548,92 @@ function extractKeywords(content, name, maxKeywords = 50) {
548
548
  }
549
549
  return Array.from(keywords).slice(0, maxKeywords);
550
550
  }
551
+ function splitIdentifier(str) {
552
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/).map((s) => s.toLowerCase()).filter((s) => s.length > 1);
553
+ }
551
554
  function extractPathKeywords(filepath) {
552
- return filepath.split(/[/\\.]/).filter((p) => p.length > 2 && !COMMON_KEYWORDS.has(p.toLowerCase())).map((p) => p.toLowerCase());
555
+ const keywords = new Set;
556
+ const pathWithoutExt = filepath.replace(/\.[^.]+$/, "");
557
+ const segments = pathWithoutExt.split(/[/\\]/);
558
+ for (const segment of segments) {
559
+ if (segment.length < 2)
560
+ continue;
561
+ const lower = segment.toLowerCase();
562
+ if (!COMMON_KEYWORDS.has(lower) && lower.length > 2) {
563
+ keywords.add(lower);
564
+ }
565
+ const parts = splitIdentifier(segment);
566
+ for (const part of parts) {
567
+ if (!COMMON_KEYWORDS.has(part) && part.length > 2) {
568
+ keywords.add(part);
569
+ }
570
+ }
571
+ }
572
+ return Array.from(keywords);
573
+ }
574
+ function parsePathContext(filepath) {
575
+ const pathWithoutExt = filepath.replace(/\.[^.]+$/, "");
576
+ const allSegments = pathWithoutExt.split(/[/\\]/);
577
+ const filename = allSegments[allSegments.length - 1];
578
+ const dirSegments = allSegments.slice(0, -1);
579
+ const keywords = extractPathKeywords(filepath);
580
+ let layer;
581
+ const allLower = [...dirSegments, filename].map((s) => s.toLowerCase()).join(" ");
582
+ const filenameLower = filename.toLowerCase();
583
+ for (const [layerName, patterns] of Object.entries(LAYER_PATTERNS)) {
584
+ for (const pattern of patterns) {
585
+ if (filenameLower.includes(pattern)) {
586
+ layer = layerName;
587
+ break;
588
+ }
589
+ if (dirSegments.some((s) => s.toLowerCase() === pattern)) {
590
+ layer = layerName;
591
+ break;
592
+ }
593
+ }
594
+ if (layer)
595
+ break;
596
+ }
597
+ let domain;
598
+ const layerPatternSet = new Set(Object.values(LAYER_PATTERNS).flat());
599
+ const reversedSegments = [...dirSegments].reverse();
600
+ for (const segment of reversedSegments) {
601
+ const lower = segment.toLowerCase();
602
+ if (["src", "lib", "app", "packages", "modules"].includes(lower))
603
+ continue;
604
+ if (layerPatternSet.has(lower))
605
+ continue;
606
+ if (lower.length > 2) {
607
+ domain = lower;
608
+ break;
609
+ }
610
+ }
611
+ return {
612
+ segments: dirSegments,
613
+ layer,
614
+ domain,
615
+ depth: dirSegments.length,
616
+ keywords
617
+ };
618
+ }
619
+ function formatPathContextForEmbedding(pathContext) {
620
+ const parts = [];
621
+ if (pathContext.domain) {
622
+ parts.push(pathContext.domain);
623
+ }
624
+ if (pathContext.layer) {
625
+ parts.push(pathContext.layer);
626
+ }
627
+ const significantSegments = pathContext.segments.slice(-3).filter((s) => s.length > 2 && !["src", "lib", "app"].includes(s.toLowerCase()));
628
+ if (significantSegments.length > 0) {
629
+ parts.push(...significantSegments.map((s) => s.toLowerCase()));
630
+ }
631
+ if (parts.length === 0)
632
+ return "";
633
+ const unique = [...new Set(parts)];
634
+ return `[${unique.join(" ")}]`;
553
635
  }
554
- var COMMON_KEYWORDS;
636
+ var COMMON_KEYWORDS, LAYER_PATTERNS;
555
637
  var init_keywords = __esm(() => {
556
638
  COMMON_KEYWORDS = new Set([
557
639
  "const",
@@ -621,6 +703,19 @@ var init_keywords = __esm(() => {
621
703
  "has",
622
704
  "have"
623
705
  ]);
706
+ LAYER_PATTERNS = {
707
+ controller: ["controller", "controllers", "handler", "handlers", "route", "routes", "api"],
708
+ service: ["service", "services", "usecase", "usecases", "application"],
709
+ repository: ["repository", "repositories", "repo", "repos", "dao", "store", "storage"],
710
+ model: ["model", "models", "entity", "entities", "schema", "schemas"],
711
+ util: ["util", "utils", "utility", "utilities", "helper", "helpers", "common", "shared"],
712
+ config: ["config", "configs", "configuration", "settings"],
713
+ middleware: ["middleware", "middlewares", "interceptor", "interceptors"],
714
+ domain: ["domain", "core", "business"],
715
+ infrastructure: ["infrastructure", "infra", "external", "adapters"],
716
+ presentation: ["presentation", "view", "views", "component", "components", "ui"],
717
+ test: ["test", "tests", "spec", "specs", "__tests__", "__test__"]
718
+ };
624
719
  });
625
720
 
626
721
  // src/utils/tieredIndex.ts
@@ -799,7 +894,12 @@ class SemanticModule {
799
894
  if (parsedChunks.length === 0) {
800
895
  return null;
801
896
  }
802
- const chunkContents = parsedChunks.map((c) => c.content);
897
+ const pathContext = parsePathContext(filepath);
898
+ const pathPrefix = formatPathContextForEmbedding(pathContext);
899
+ const chunkContents = parsedChunks.map((c) => {
900
+ const namePrefix = c.name ? `${c.name}: ` : "";
901
+ return `${pathPrefix} ${namePrefix}${c.content}`;
902
+ });
803
903
  const embeddings = await getEmbeddings(chunkContents);
804
904
  const chunks = parsedChunks.map((pc) => ({
805
905
  id: generateChunkId(filepath, pc.startLine, pc.endLine),
@@ -825,13 +925,20 @@ class SemanticModule {
825
925
  const keywords = extractKeywords(pc.content, pc.name);
826
926
  keywords.forEach((k) => allKeywords.add(k));
827
927
  }
928
+ pathContext.keywords.forEach((k) => allKeywords.add(k));
828
929
  const fileSummary = {
829
930
  filepath,
830
931
  chunkCount: chunks.length,
831
932
  chunkTypes,
832
933
  keywords: Array.from(allKeywords),
833
934
  exports,
834
- lastModified: stats.lastModified
935
+ lastModified: stats.lastModified,
936
+ pathContext: {
937
+ segments: pathContext.segments,
938
+ layer: pathContext.layer,
939
+ domain: pathContext.domain,
940
+ depth: pathContext.depth
941
+ }
835
942
  };
836
943
  this.pendingSummaries.set(filepath, fileSummary);
837
944
  return {
@@ -908,11 +1015,32 @@ class SemanticModule {
908
1015
  for (const result of bm25Results) {
909
1016
  bm25Scores.set(result.id, normalizeScore(result.score, 3));
910
1017
  }
1018
+ const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
1019
+ const pathBoosts = new Map;
1020
+ for (const filepath of candidateFiles) {
1021
+ const summary = symbolicIndex.getFileSummary(filepath);
1022
+ if (summary?.pathContext) {
1023
+ let boost = 0;
1024
+ const ctx2 = summary.pathContext;
1025
+ if (ctx2.domain && queryTerms.some((t) => ctx2.domain.includes(t) || t.includes(ctx2.domain))) {
1026
+ boost += 0.1;
1027
+ }
1028
+ if (ctx2.layer && queryTerms.some((t) => ctx2.layer.includes(t) || t.includes(ctx2.layer))) {
1029
+ boost += 0.05;
1030
+ }
1031
+ const segmentMatch = ctx2.segments.some((seg) => queryTerms.some((t) => seg.toLowerCase().includes(t) || t.includes(seg.toLowerCase())));
1032
+ if (segmentMatch) {
1033
+ boost += 0.05;
1034
+ }
1035
+ pathBoosts.set(filepath, boost);
1036
+ }
1037
+ }
911
1038
  const results = [];
912
1039
  for (const { filepath, chunk, embedding } of allChunksData) {
913
1040
  const semanticScore = cosineSimilarity(queryEmbedding, embedding);
914
1041
  const bm25Score = bm25Scores.get(chunk.id) || 0;
915
- const hybridScore = SEMANTIC_WEIGHT * semanticScore + BM25_WEIGHT * bm25Score;
1042
+ const pathBoost = pathBoosts.get(filepath) || 0;
1043
+ const hybridScore = SEMANTIC_WEIGHT * semanticScore + BM25_WEIGHT * bm25Score + pathBoost;
916
1044
  if (hybridScore >= minScore || bm25Score > 0.3) {
917
1045
  results.push({
918
1046
  filepath,
@@ -921,7 +1049,8 @@ class SemanticModule {
921
1049
  moduleId: this.id,
922
1050
  context: {
923
1051
  semanticScore,
924
- bm25Score
1052
+ bm25Score,
1053
+ pathBoost
925
1054
  }
926
1055
  });
927
1056
  }
@@ -960,6 +1089,7 @@ var init_semantic = __esm(() => {
960
1089
  init_config2();
961
1090
  init_parseCode();
962
1091
  init_tieredIndex();
1092
+ init_keywords();
963
1093
  });
964
1094
 
965
1095
  // src/modules/registry.ts
@@ -991,18 +1121,183 @@ var init_registry = __esm(() => {
991
1121
  registry = new ModuleRegistryImpl;
992
1122
  });
993
1123
 
1124
+ // src/indexer/watcher.ts
1125
+ import { watch } from "chokidar";
1126
+ import * as path5 from "path";
1127
+ async function watchDirectory(rootDir, options = {}) {
1128
+ const {
1129
+ debounceMs = DEFAULT_DEBOUNCE_MS,
1130
+ verbose = false,
1131
+ model,
1132
+ onIndexStart,
1133
+ onIndexComplete,
1134
+ onFileChange,
1135
+ onError
1136
+ } = options;
1137
+ rootDir = path5.resolve(rootDir);
1138
+ const config = await loadConfig(rootDir);
1139
+ const watchPatterns = config.extensions.map((ext) => `**/*${ext}`);
1140
+ const ignorePatterns = [
1141
+ ...config.ignorePaths.map((p) => `**/${p}/**`),
1142
+ `**/${config.indexDir}/**`
1143
+ ];
1144
+ let isRunning = true;
1145
+ let isIndexing = false;
1146
+ let pendingChanges = new Map;
1147
+ let debounceTimer = null;
1148
+ let watcher = null;
1149
+ async function processPendingChanges() {
1150
+ if (!isRunning || isIndexing || pendingChanges.size === 0) {
1151
+ return;
1152
+ }
1153
+ isIndexing = true;
1154
+ const changes = new Map(pendingChanges);
1155
+ pendingChanges.clear();
1156
+ try {
1157
+ const filesToIndex = [];
1158
+ const filesToDelete = [];
1159
+ for (const [filepath, event] of changes) {
1160
+ if (event === "unlink") {
1161
+ filesToDelete.push(filepath);
1162
+ } else {
1163
+ filesToIndex.push(filepath);
1164
+ }
1165
+ }
1166
+ if (filesToDelete.length > 0) {
1167
+ if (verbose) {
1168
+ console.log(`
1169
+ [Watch] Cleaning up ${filesToDelete.length} deleted file(s)...`);
1170
+ }
1171
+ await cleanupIndex(rootDir, { verbose: false });
1172
+ }
1173
+ if (filesToIndex.length > 0) {
1174
+ if (onIndexStart) {
1175
+ onIndexStart(filesToIndex);
1176
+ }
1177
+ if (verbose) {
1178
+ console.log(`
1179
+ [Watch] Indexing ${filesToIndex.length} changed file(s)...`);
1180
+ }
1181
+ const results = await indexDirectory(rootDir, {
1182
+ model,
1183
+ verbose: false
1184
+ });
1185
+ if (onIndexComplete) {
1186
+ onIndexComplete(results);
1187
+ }
1188
+ for (const result of results) {
1189
+ if (result.indexed > 0 || result.errors > 0) {
1190
+ console.log(`[Watch] ${result.moduleId}: ${result.indexed} indexed, ${result.errors} errors`);
1191
+ }
1192
+ }
1193
+ }
1194
+ } catch (error) {
1195
+ const err = error instanceof Error ? error : new Error(String(error));
1196
+ console.error("[Watch] Error during indexing:", err.message);
1197
+ if (onError) {
1198
+ onError(err);
1199
+ }
1200
+ } finally {
1201
+ isIndexing = false;
1202
+ if (pendingChanges.size > 0) {
1203
+ scheduleProcessing();
1204
+ }
1205
+ }
1206
+ }
1207
+ function scheduleProcessing() {
1208
+ if (debounceTimer) {
1209
+ clearTimeout(debounceTimer);
1210
+ }
1211
+ if (pendingChanges.size >= MAX_BATCH_SIZE) {
1212
+ processPendingChanges();
1213
+ return;
1214
+ }
1215
+ debounceTimer = setTimeout(() => {
1216
+ debounceTimer = null;
1217
+ processPendingChanges();
1218
+ }, debounceMs);
1219
+ }
1220
+ function handleFileEvent(event, filepath) {
1221
+ if (!isRunning)
1222
+ return;
1223
+ const relativePath = path5.relative(rootDir, filepath);
1224
+ for (const ignorePath of config.ignorePaths) {
1225
+ if (relativePath.startsWith(ignorePath) || relativePath.includes(`/${ignorePath}/`)) {
1226
+ return;
1227
+ }
1228
+ }
1229
+ if (onFileChange) {
1230
+ onFileChange(event, relativePath);
1231
+ }
1232
+ if (verbose) {
1233
+ const symbol = event === "add" ? "+" : event === "unlink" ? "-" : "~";
1234
+ console.log(`[Watch] ${symbol} ${relativePath}`);
1235
+ }
1236
+ pendingChanges.set(relativePath, event);
1237
+ scheduleProcessing();
1238
+ }
1239
+ watcher = watch(watchPatterns, {
1240
+ cwd: rootDir,
1241
+ ignored: ignorePatterns,
1242
+ persistent: true,
1243
+ ignoreInitial: true,
1244
+ awaitWriteFinish: {
1245
+ stabilityThreshold: 100,
1246
+ pollInterval: 50
1247
+ },
1248
+ usePolling: false,
1249
+ atomic: true
1250
+ });
1251
+ watcher.on("add", (filepath) => handleFileEvent("add", path5.join(rootDir, filepath)));
1252
+ watcher.on("change", (filepath) => handleFileEvent("change", path5.join(rootDir, filepath)));
1253
+ watcher.on("unlink", (filepath) => handleFileEvent("unlink", path5.join(rootDir, filepath)));
1254
+ watcher.on("error", (error) => {
1255
+ const err = error instanceof Error ? error : new Error(String(error));
1256
+ console.error("[Watch] Watcher error:", err);
1257
+ if (onError) {
1258
+ onError(err);
1259
+ }
1260
+ });
1261
+ await new Promise((resolve2) => {
1262
+ watcher.on("ready", () => {
1263
+ resolve2();
1264
+ });
1265
+ });
1266
+ return {
1267
+ stop: async () => {
1268
+ isRunning = false;
1269
+ if (debounceTimer) {
1270
+ clearTimeout(debounceTimer);
1271
+ debounceTimer = null;
1272
+ }
1273
+ if (watcher) {
1274
+ await watcher.close();
1275
+ watcher = null;
1276
+ }
1277
+ },
1278
+ isRunning: () => isRunning
1279
+ };
1280
+ }
1281
+ var DEFAULT_DEBOUNCE_MS = 300, MAX_BATCH_SIZE = 100;
1282
+ var init_watcher = __esm(() => {
1283
+ init_config2();
1284
+ init_indexer();
1285
+ });
1286
+
994
1287
  // src/indexer/index.ts
995
1288
  var exports_indexer = {};
996
1289
  __export(exports_indexer, {
1290
+ watchDirectory: () => watchDirectory,
997
1291
  indexDirectory: () => indexDirectory,
1292
+ getIndexStatus: () => getIndexStatus,
998
1293
  cleanupIndex: () => cleanupIndex
999
1294
  });
1000
1295
  import { glob } from "glob";
1001
1296
  import * as fs3 from "fs/promises";
1002
- import * as path5 from "path";
1297
+ import * as path6 from "path";
1003
1298
  async function indexDirectory(rootDir, options = {}) {
1004
1299
  const verbose = options.verbose ?? false;
1005
- rootDir = path5.resolve(rootDir);
1300
+ rootDir = path6.resolve(rootDir);
1006
1301
  console.log(`Indexing directory: ${rootDir}`);
1007
1302
  const config = await loadConfig(rootDir);
1008
1303
  await registerBuiltInModules();
@@ -1037,11 +1332,11 @@ async function indexDirectory(rootDir, options = {}) {
1037
1332
  rootDir,
1038
1333
  config,
1039
1334
  readFile: async (filepath) => {
1040
- const fullPath = path5.isAbsolute(filepath) ? filepath : path5.join(rootDir, filepath);
1335
+ const fullPath = path6.isAbsolute(filepath) ? filepath : path6.join(rootDir, filepath);
1041
1336
  return fs3.readFile(fullPath, "utf-8");
1042
1337
  },
1043
1338
  getFileStats: async (filepath) => {
1044
- const fullPath = path5.isAbsolute(filepath) ? filepath : path5.join(rootDir, filepath);
1339
+ const fullPath = path6.isAbsolute(filepath) ? filepath : path6.join(rootDir, filepath);
1045
1340
  const stats = await fs3.stat(fullPath);
1046
1341
  return { lastModified: stats.mtime.toISOString() };
1047
1342
  }
@@ -1065,17 +1360,17 @@ async function indexWithModule(rootDir, files, module, config, verbose) {
1065
1360
  rootDir,
1066
1361
  config,
1067
1362
  readFile: async (filepath) => {
1068
- const fullPath = path5.isAbsolute(filepath) ? filepath : path5.join(rootDir, filepath);
1363
+ const fullPath = path6.isAbsolute(filepath) ? filepath : path6.join(rootDir, filepath);
1069
1364
  return fs3.readFile(fullPath, "utf-8");
1070
1365
  },
1071
1366
  getFileStats: async (filepath) => {
1072
- const fullPath = path5.isAbsolute(filepath) ? filepath : path5.join(rootDir, filepath);
1367
+ const fullPath = path6.isAbsolute(filepath) ? filepath : path6.join(rootDir, filepath);
1073
1368
  const stats = await fs3.stat(fullPath);
1074
1369
  return { lastModified: stats.mtime.toISOString() };
1075
1370
  }
1076
1371
  };
1077
1372
  for (const filepath of files) {
1078
- const relativePath = path5.relative(rootDir, filepath);
1373
+ const relativePath = path6.relative(rootDir, filepath);
1079
1374
  try {
1080
1375
  const stats = await fs3.stat(filepath);
1081
1376
  const lastModified = stats.mtime.toISOString();
@@ -1144,13 +1439,13 @@ async function loadModuleManifest(rootDir, moduleId, config) {
1144
1439
  }
1145
1440
  async function writeModuleManifest(rootDir, moduleId, manifest, config) {
1146
1441
  const manifestPath = getModuleManifestPath(rootDir, moduleId, config);
1147
- await fs3.mkdir(path5.dirname(manifestPath), { recursive: true });
1442
+ await fs3.mkdir(path6.dirname(manifestPath), { recursive: true });
1148
1443
  await fs3.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1149
1444
  }
1150
1445
  async function writeFileIndex(rootDir, moduleId, filepath, fileIndex, config) {
1151
1446
  const indexPath = getModuleIndexPath(rootDir, moduleId, config);
1152
- const indexFilePath = path5.join(indexPath, filepath.replace(/\.[^.]+$/, ".json"));
1153
- await fs3.mkdir(path5.dirname(indexFilePath), { recursive: true });
1447
+ const indexFilePath = path6.join(indexPath, filepath.replace(/\.[^.]+$/, ".json"));
1448
+ await fs3.mkdir(path6.dirname(indexFilePath), { recursive: true });
1154
1449
  await fs3.writeFile(indexFilePath, JSON.stringify(fileIndex, null, 2));
1155
1450
  }
1156
1451
  async function updateGlobalManifest(rootDir, modules, config) {
@@ -1160,12 +1455,12 @@ async function updateGlobalManifest(rootDir, modules, config) {
1160
1455
  lastUpdated: new Date().toISOString(),
1161
1456
  modules: modules.map((m) => m.id)
1162
1457
  };
1163
- await fs3.mkdir(path5.dirname(manifestPath), { recursive: true });
1458
+ await fs3.mkdir(path6.dirname(manifestPath), { recursive: true });
1164
1459
  await fs3.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1165
1460
  }
1166
1461
  async function cleanupIndex(rootDir, options = {}) {
1167
1462
  const verbose = options.verbose ?? false;
1168
- rootDir = path5.resolve(rootDir);
1463
+ rootDir = path6.resolve(rootDir);
1169
1464
  console.log(`Cleaning up index in: ${rootDir}`);
1170
1465
  const config = await loadConfig(rootDir);
1171
1466
  await registerBuiltInModules();
@@ -1195,7 +1490,7 @@ async function cleanupModuleIndex(rootDir, moduleId, config, verbose) {
1195
1490
  const filesToRemove = [];
1196
1491
  const updatedFiles = {};
1197
1492
  for (const [filepath, entry] of Object.entries(manifest.files)) {
1198
- const fullPath = path5.join(rootDir, filepath);
1493
+ const fullPath = path6.join(rootDir, filepath);
1199
1494
  try {
1200
1495
  await fs3.access(fullPath);
1201
1496
  updatedFiles[filepath] = entry;
@@ -1209,7 +1504,7 @@ async function cleanupModuleIndex(rootDir, moduleId, config, verbose) {
1209
1504
  }
1210
1505
  }
1211
1506
  for (const filepath of filesToRemove) {
1212
- const indexFilePath = path5.join(indexPath, filepath.replace(/\.[^.]+$/, ".json"));
1507
+ const indexFilePath = path6.join(indexPath, filepath.replace(/\.[^.]+$/, ".json"));
1213
1508
  try {
1214
1509
  await fs3.unlink(indexFilePath);
1215
1510
  } catch {}
@@ -1225,7 +1520,7 @@ async function cleanupEmptyDirectories(dir) {
1225
1520
  const entries = await fs3.readdir(dir, { withFileTypes: true });
1226
1521
  for (const entry of entries) {
1227
1522
  if (entry.isDirectory()) {
1228
- const subDir = path5.join(dir, entry.name);
1523
+ const subDir = path6.join(dir, entry.name);
1229
1524
  await cleanupEmptyDirectories(subDir);
1230
1525
  }
1231
1526
  }
@@ -1239,9 +1534,66 @@ async function cleanupEmptyDirectories(dir) {
1239
1534
  return false;
1240
1535
  }
1241
1536
  }
1537
+ async function getIndexStatus(rootDir) {
1538
+ rootDir = path6.resolve(rootDir);
1539
+ const config = await loadConfig(rootDir);
1540
+ const indexDir = path6.join(rootDir, config.indexDir);
1541
+ const status = {
1542
+ exists: false,
1543
+ rootDir,
1544
+ indexDir,
1545
+ modules: [],
1546
+ totalFiles: 0
1547
+ };
1548
+ try {
1549
+ await fs3.access(indexDir);
1550
+ } catch {
1551
+ return status;
1552
+ }
1553
+ try {
1554
+ const globalManifestPath = getGlobalManifestPath(rootDir, config);
1555
+ const content = await fs3.readFile(globalManifestPath, "utf-8");
1556
+ const globalManifest = JSON.parse(content);
1557
+ status.exists = true;
1558
+ status.lastUpdated = globalManifest.lastUpdated;
1559
+ for (const moduleId of globalManifest.modules) {
1560
+ try {
1561
+ const manifest = await loadModuleManifest(rootDir, moduleId, config);
1562
+ const fileCount = Object.keys(manifest.files).length;
1563
+ status.modules.push({
1564
+ id: moduleId,
1565
+ fileCount,
1566
+ lastUpdated: manifest.lastUpdated
1567
+ });
1568
+ status.totalFiles += fileCount;
1569
+ } catch {}
1570
+ }
1571
+ } catch {
1572
+ try {
1573
+ const entries = await fs3.readdir(path6.join(indexDir, "index"));
1574
+ if (entries.length > 0) {
1575
+ status.exists = true;
1576
+ for (const entry of entries) {
1577
+ try {
1578
+ const manifest = await loadModuleManifest(rootDir, entry, config);
1579
+ const fileCount = Object.keys(manifest.files).length;
1580
+ status.modules.push({
1581
+ id: entry,
1582
+ fileCount,
1583
+ lastUpdated: manifest.lastUpdated
1584
+ });
1585
+ status.totalFiles += fileCount;
1586
+ } catch {}
1587
+ }
1588
+ }
1589
+ } catch {}
1590
+ }
1591
+ return status;
1592
+ }
1242
1593
  var init_indexer = __esm(() => {
1243
1594
  init_config2();
1244
1595
  init_registry();
1596
+ init_watcher();
1245
1597
  });
1246
1598
 
1247
1599
  // src/search/index.ts
@@ -1251,9 +1603,9 @@ __export(exports_search, {
1251
1603
  formatSearchResults: () => formatSearchResults
1252
1604
  });
1253
1605
  import * as fs4 from "fs/promises";
1254
- import * as path6 from "path";
1606
+ import * as path7 from "path";
1255
1607
  async function search(rootDir, query, options = {}) {
1256
- rootDir = path6.resolve(rootDir);
1608
+ rootDir = path7.resolve(rootDir);
1257
1609
  console.log(`Searching for: "${query}"`);
1258
1610
  const config = await loadConfig(rootDir);
1259
1611
  await registerBuiltInModules();
@@ -1294,7 +1646,7 @@ function createSearchContext(rootDir, moduleId, config) {
1294
1646
  config,
1295
1647
  loadFileIndex: async (filepath) => {
1296
1648
  const hasExtension = /\.[^./]+$/.test(filepath);
1297
- const indexFilePath = hasExtension ? path6.join(indexPath, filepath.replace(/\.[^.]+$/, ".json")) : path6.join(indexPath, filepath + ".json");
1649
+ const indexFilePath = hasExtension ? path7.join(indexPath, filepath.replace(/\.[^.]+$/, ".json")) : path7.join(indexPath, filepath + ".json");
1298
1650
  try {
1299
1651
  const content = await fs4.readFile(indexFilePath, "utf-8");
1300
1652
  return JSON.parse(content);
@@ -1306,8 +1658,8 @@ function createSearchContext(rootDir, moduleId, config) {
1306
1658
  const files = [];
1307
1659
  await traverseDirectory(indexPath, files, indexPath);
1308
1660
  return files.filter((f) => f.endsWith(".json") && !f.endsWith("manifest.json")).map((f) => {
1309
- const relative3 = path6.relative(indexPath, f);
1310
- return relative3.replace(/\.json$/, "");
1661
+ const relative4 = path7.relative(indexPath, f);
1662
+ return relative4.replace(/\.json$/, "");
1311
1663
  });
1312
1664
  }
1313
1665
  };
@@ -1316,7 +1668,7 @@ async function traverseDirectory(dir, files, basePath) {
1316
1668
  try {
1317
1669
  const entries = await fs4.readdir(dir, { withFileTypes: true });
1318
1670
  for (const entry of entries) {
1319
- const fullPath = path6.join(dir, entry.name);
1671
+ const fullPath = path7.join(dir, entry.name);
1320
1672
  if (entry.isDirectory()) {
1321
1673
  await traverseDirectory(fullPath, files, basePath);
1322
1674
  } else if (entry.isFile()) {
@@ -1373,12 +1725,38 @@ var init_search = __esm(() => {
1373
1725
 
1374
1726
  // src/cli/main.ts
1375
1727
  init_embeddings();
1728
+ import { createRequire } from "module";
1729
+ var require2 = createRequire(import.meta.url);
1730
+ var pkg = require2("../../package.json");
1731
+ var VERSION = pkg.version;
1376
1732
  var args = process.argv.slice(2);
1377
1733
  var command = args[0];
1734
+ if (command === "--version" || command === "-v") {
1735
+ console.log(`raggrep v${VERSION}`);
1736
+ process.exit(0);
1737
+ }
1738
+ function formatTimeAgo(date) {
1739
+ const now = new Date;
1740
+ const diffMs = now.getTime() - date.getTime();
1741
+ const diffSecs = Math.floor(diffMs / 1000);
1742
+ const diffMins = Math.floor(diffSecs / 60);
1743
+ const diffHours = Math.floor(diffMins / 60);
1744
+ const diffDays = Math.floor(diffHours / 24);
1745
+ if (diffSecs < 60)
1746
+ return "just now";
1747
+ if (diffMins < 60)
1748
+ return `${diffMins}m ago`;
1749
+ if (diffHours < 24)
1750
+ return `${diffHours}h ago`;
1751
+ if (diffDays < 7)
1752
+ return `${diffDays}d ago`;
1753
+ return date.toLocaleDateString();
1754
+ }
1378
1755
  function parseFlags(args2) {
1379
1756
  const flags = {
1380
1757
  help: false,
1381
1758
  verbose: false,
1759
+ watch: false,
1382
1760
  remaining: []
1383
1761
  };
1384
1762
  for (let i = 0;i < args2.length; i++) {
@@ -1387,6 +1765,8 @@ function parseFlags(args2) {
1387
1765
  flags.help = true;
1388
1766
  } else if (arg === "--verbose" || arg === "-v") {
1389
1767
  flags.verbose = true;
1768
+ } else if (arg === "--watch" || arg === "-w") {
1769
+ flags.watch = true;
1390
1770
  } else if (arg === "--model" || arg === "-m") {
1391
1771
  const modelName = args2[++i];
1392
1772
  if (modelName && modelName in EMBEDDING_MODELS) {
@@ -1436,6 +1816,7 @@ Usage:
1436
1816
  raggrep index [options]
1437
1817
 
1438
1818
  Options:
1819
+ -w, --watch Watch for file changes and re-index automatically
1439
1820
  -m, --model <name> Embedding model to use (default: all-MiniLM-L6-v2)
1440
1821
  -v, --verbose Show detailed progress
1441
1822
  -h, --help Show this help message
@@ -1447,12 +1828,13 @@ Model Cache: ${getCacheDir()}
1447
1828
 
1448
1829
  Examples:
1449
1830
  raggrep index
1831
+ raggrep index --watch
1450
1832
  raggrep index --model bge-small-en-v1.5
1451
1833
  raggrep index --verbose
1452
1834
  `);
1453
1835
  process.exit(0);
1454
1836
  }
1455
- const { indexDirectory: indexDirectory2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
1837
+ const { indexDirectory: indexDirectory2, watchDirectory: watchDirectory2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
1456
1838
  console.log("RAGgrep Indexer");
1457
1839
  console.log(`================
1458
1840
  `);
@@ -1471,6 +1853,39 @@ Examples:
1471
1853
  console.error("Error during indexing:", error);
1472
1854
  process.exit(1);
1473
1855
  }
1856
+ if (flags.watch) {
1857
+ console.log(`
1858
+ ┌─────────────────────────────────────────┐`);
1859
+ console.log("│ Watching for changes... (Ctrl+C to stop) │");
1860
+ console.log(`└─────────────────────────────────────────┘
1861
+ `);
1862
+ try {
1863
+ const watcher = await watchDirectory2(process.cwd(), {
1864
+ model: flags.model,
1865
+ verbose: flags.verbose,
1866
+ onFileChange: (event, filepath) => {
1867
+ if (flags.verbose) {
1868
+ const symbol = event === "add" ? "+" : event === "unlink" ? "-" : "~";
1869
+ console.log(` ${symbol} ${filepath}`);
1870
+ }
1871
+ }
1872
+ });
1873
+ const shutdown = async () => {
1874
+ console.log(`
1875
+
1876
+ Stopping watcher...`);
1877
+ await watcher.stop();
1878
+ console.log("Done.");
1879
+ process.exit(0);
1880
+ };
1881
+ process.on("SIGINT", shutdown);
1882
+ process.on("SIGTERM", shutdown);
1883
+ await new Promise(() => {});
1884
+ } catch (error) {
1885
+ console.error("Error starting watcher:", error);
1886
+ process.exit(1);
1887
+ }
1888
+ }
1474
1889
  break;
1475
1890
  }
1476
1891
  case "query": {
@@ -1487,6 +1902,10 @@ Options:
1487
1902
  -t, --type <ext> Filter by file extension (e.g., ts, tsx, js)
1488
1903
  -h, --help Show this help message
1489
1904
 
1905
+ Note:
1906
+ If the current directory has not been indexed, raggrep will
1907
+ automatically index it before searching.
1908
+
1490
1909
  Examples:
1491
1910
  raggrep query "user authentication"
1492
1911
  raggrep query "handle errors" --top 5
@@ -1496,16 +1915,36 @@ Examples:
1496
1915
  process.exit(0);
1497
1916
  }
1498
1917
  const { search: search2, formatSearchResults: formatSearchResults2 } = await Promise.resolve().then(() => (init_search(), exports_search));
1918
+ const { getIndexStatus: getIndexStatus2, indexDirectory: indexDirectory2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
1499
1919
  const query = flags.remaining[0];
1500
1920
  if (!query) {
1501
1921
  console.error("Usage: raggrep query <search query>");
1502
1922
  console.error('Run "raggrep query --help" for more information.');
1503
1923
  process.exit(1);
1504
1924
  }
1505
- console.log("RAGgrep Search");
1506
- console.log(`==============
1507
- `);
1508
1925
  try {
1926
+ const status = await getIndexStatus2(process.cwd());
1927
+ if (!status.exists) {
1928
+ console.log(`No index found. Indexing directory first...
1929
+ `);
1930
+ console.log("RAGgrep Indexer");
1931
+ console.log(`================
1932
+ `);
1933
+ const indexResults = await indexDirectory2(process.cwd(), {
1934
+ model: flags.model,
1935
+ verbose: false
1936
+ });
1937
+ console.log(`
1938
+ ================`);
1939
+ console.log("Summary:");
1940
+ for (const result of indexResults) {
1941
+ console.log(` ${result.moduleId}: ${result.indexed} indexed, ${result.skipped} skipped, ${result.errors} errors`);
1942
+ }
1943
+ console.log("");
1944
+ }
1945
+ console.log("RAGgrep Search");
1946
+ console.log(`==============
1947
+ `);
1509
1948
  const filePatterns = flags.fileType ? [`*.${flags.fileType}`] : undefined;
1510
1949
  const results = await search2(process.cwd(), query, {
1511
1950
  topK: flags.topK ?? 10,
@@ -1561,9 +2000,72 @@ Examples:
1561
2000
  }
1562
2001
  break;
1563
2002
  }
2003
+ case "status": {
2004
+ if (flags.help) {
2005
+ console.log(`
2006
+ raggrep status - Show the current state of the index
2007
+
2008
+ Usage:
2009
+ raggrep status [options]
2010
+
2011
+ Options:
2012
+ -h, --help Show this help message
2013
+
2014
+ Description:
2015
+ Displays information about the index in the current directory,
2016
+ including whether it exists, how many files are indexed, and
2017
+ when it was last updated.
2018
+
2019
+ Examples:
2020
+ raggrep status
2021
+ `);
2022
+ process.exit(0);
2023
+ }
2024
+ const { getIndexStatus: getIndexStatus2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
2025
+ try {
2026
+ const status = await getIndexStatus2(process.cwd());
2027
+ if (!status.exists) {
2028
+ console.log(`
2029
+ ┌─────────────────────────────────────────┐
2030
+ │ RAGgrep Status │
2031
+ ├─────────────────────────────────────────┤
2032
+ │ ○ Not indexed │
2033
+ └─────────────────────────────────────────┘
2034
+
2035
+ Directory: ${status.rootDir}
2036
+
2037
+ Run "raggrep index" to create an index.
2038
+ `);
2039
+ } else {
2040
+ const date = status.lastUpdated ? new Date(status.lastUpdated) : null;
2041
+ const timeAgo = date ? formatTimeAgo(date) : "unknown";
2042
+ console.log(`
2043
+ ┌─────────────────────────────────────────┐
2044
+ │ RAGgrep Status │
2045
+ ├─────────────────────────────────────────┤
2046
+ │ ● Indexed │
2047
+ └─────────────────────────────────────────┘
2048
+
2049
+ Files: ${status.totalFiles.toString().padEnd(10)} Updated: ${timeAgo}
2050
+ Location: ${status.indexDir}
2051
+ `);
2052
+ if (status.modules.length > 0) {
2053
+ console.log(" Modules:");
2054
+ for (const mod of status.modules) {
2055
+ console.log(` └─ ${mod.id} (${mod.fileCount} files)`);
2056
+ }
2057
+ console.log("");
2058
+ }
2059
+ }
2060
+ } catch (error) {
2061
+ console.error("Error getting status:", error);
2062
+ process.exit(1);
2063
+ }
2064
+ break;
2065
+ }
1564
2066
  default:
1565
2067
  console.log(`
1566
- raggrep - Local filesystem-based RAG system for codebases
2068
+ raggrep v${VERSION} - Local filesystem-based RAG system for codebases
1567
2069
 
1568
2070
  Usage:
1569
2071
  raggrep <command> [options]
@@ -1571,16 +2073,17 @@ Usage:
1571
2073
  Commands:
1572
2074
  index Index the current directory
1573
2075
  query Search the indexed codebase
2076
+ status Show the current state of the index
1574
2077
  cleanup Remove stale index entries for deleted files
1575
2078
 
1576
2079
  Options:
1577
- -h, --help Show help for a command
2080
+ -h, --help Show help for a command
2081
+ -v, --version Show version number
1578
2082
 
1579
2083
  Examples:
1580
2084
  raggrep index
1581
- raggrep index --model bge-small-en-v1.5
1582
2085
  raggrep query "user login"
1583
- raggrep query "handle errors" --top 5
2086
+ raggrep status
1584
2087
  raggrep cleanup
1585
2088
 
1586
2089
  Run 'raggrep <command> --help' for more information.
@@ -1593,4 +2096,4 @@ Run 'raggrep <command> --help' for more information.
1593
2096
  }
1594
2097
  main();
1595
2098
 
1596
- //# debugId=8E6C629D8A1BA14F64756E2164756E21
2099
+ //# debugId=0D3D8495D3A140B664756E2164756E21