opencode-codebase-index 0.2.5 → 0.3.2

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/index.js CHANGED
@@ -491,7 +491,7 @@ var require_ignore = __commonJS({
491
491
  // path matching.
492
492
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
493
493
  // @returns {TestResult} true if a file is ignored
494
- test(path8, checkUnignored, mode) {
494
+ test(path9, checkUnignored, mode) {
495
495
  let ignored = false;
496
496
  let unignored = false;
497
497
  let matchedRule;
@@ -500,7 +500,7 @@ var require_ignore = __commonJS({
500
500
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
501
501
  return;
502
502
  }
503
- const matched = rule[mode].test(path8);
503
+ const matched = rule[mode].test(path9);
504
504
  if (!matched) {
505
505
  return;
506
506
  }
@@ -521,17 +521,17 @@ var require_ignore = __commonJS({
521
521
  var throwError = (message, Ctor) => {
522
522
  throw new Ctor(message);
523
523
  };
524
- var checkPath = (path8, originalPath, doThrow) => {
525
- if (!isString(path8)) {
524
+ var checkPath = (path9, originalPath, doThrow) => {
525
+ if (!isString(path9)) {
526
526
  return doThrow(
527
527
  `path must be a string, but got \`${originalPath}\``,
528
528
  TypeError
529
529
  );
530
530
  }
531
- if (!path8) {
531
+ if (!path9) {
532
532
  return doThrow(`path must not be empty`, TypeError);
533
533
  }
534
- if (checkPath.isNotRelative(path8)) {
534
+ if (checkPath.isNotRelative(path9)) {
535
535
  const r = "`path.relative()`d";
536
536
  return doThrow(
537
537
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -540,7 +540,7 @@ var require_ignore = __commonJS({
540
540
  }
541
541
  return true;
542
542
  };
543
- var isNotRelative = (path8) => REGEX_TEST_INVALID_PATH.test(path8);
543
+ var isNotRelative = (path9) => REGEX_TEST_INVALID_PATH.test(path9);
544
544
  checkPath.isNotRelative = isNotRelative;
545
545
  checkPath.convert = (p) => p;
546
546
  var Ignore2 = class {
@@ -570,19 +570,19 @@ var require_ignore = __commonJS({
570
570
  }
571
571
  // @returns {TestResult}
572
572
  _test(originalPath, cache, checkUnignored, slices) {
573
- const path8 = originalPath && checkPath.convert(originalPath);
573
+ const path9 = originalPath && checkPath.convert(originalPath);
574
574
  checkPath(
575
- path8,
575
+ path9,
576
576
  originalPath,
577
577
  this._strictPathCheck ? throwError : RETURN_FALSE
578
578
  );
579
- return this._t(path8, cache, checkUnignored, slices);
579
+ return this._t(path9, cache, checkUnignored, slices);
580
580
  }
581
- checkIgnore(path8) {
582
- if (!REGEX_TEST_TRAILING_SLASH.test(path8)) {
583
- return this.test(path8);
581
+ checkIgnore(path9) {
582
+ if (!REGEX_TEST_TRAILING_SLASH.test(path9)) {
583
+ return this.test(path9);
584
584
  }
585
- const slices = path8.split(SLASH2).filter(Boolean);
585
+ const slices = path9.split(SLASH2).filter(Boolean);
586
586
  slices.pop();
587
587
  if (slices.length) {
588
588
  const parent = this._t(
@@ -595,18 +595,18 @@ var require_ignore = __commonJS({
595
595
  return parent;
596
596
  }
597
597
  }
598
- return this._rules.test(path8, false, MODE_CHECK_IGNORE);
598
+ return this._rules.test(path9, false, MODE_CHECK_IGNORE);
599
599
  }
600
- _t(path8, cache, checkUnignored, slices) {
601
- if (path8 in cache) {
602
- return cache[path8];
600
+ _t(path9, cache, checkUnignored, slices) {
601
+ if (path9 in cache) {
602
+ return cache[path9];
603
603
  }
604
604
  if (!slices) {
605
- slices = path8.split(SLASH2).filter(Boolean);
605
+ slices = path9.split(SLASH2).filter(Boolean);
606
606
  }
607
607
  slices.pop();
608
608
  if (!slices.length) {
609
- return cache[path8] = this._rules.test(path8, checkUnignored, MODE_IGNORE);
609
+ return cache[path9] = this._rules.test(path9, checkUnignored, MODE_IGNORE);
610
610
  }
611
611
  const parent = this._t(
612
612
  slices.join(SLASH2) + SLASH2,
@@ -614,29 +614,29 @@ var require_ignore = __commonJS({
614
614
  checkUnignored,
615
615
  slices
616
616
  );
617
- return cache[path8] = parent.ignored ? parent : this._rules.test(path8, checkUnignored, MODE_IGNORE);
617
+ return cache[path9] = parent.ignored ? parent : this._rules.test(path9, checkUnignored, MODE_IGNORE);
618
618
  }
619
- ignores(path8) {
620
- return this._test(path8, this._ignoreCache, false).ignored;
619
+ ignores(path9) {
620
+ return this._test(path9, this._ignoreCache, false).ignored;
621
621
  }
622
622
  createFilter() {
623
- return (path8) => !this.ignores(path8);
623
+ return (path9) => !this.ignores(path9);
624
624
  }
625
625
  filter(paths) {
626
626
  return makeArray(paths).filter(this.createFilter());
627
627
  }
628
628
  // @returns {TestResult}
629
- test(path8) {
630
- return this._test(path8, this._testCache, true);
629
+ test(path9) {
630
+ return this._test(path9, this._testCache, true);
631
631
  }
632
632
  };
633
633
  var factory = (options) => new Ignore2(options);
634
- var isPathValid = (path8) => checkPath(path8 && checkPath.convert(path8), path8, RETURN_FALSE);
634
+ var isPathValid = (path9) => checkPath(path9 && checkPath.convert(path9), path9, RETURN_FALSE);
635
635
  var setupWindows = () => {
636
636
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
637
637
  checkPath.convert = makePosix;
638
638
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
639
- checkPath.isNotRelative = (path8) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path8) || isNotRelative(path8);
639
+ checkPath.isNotRelative = (path9) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path9) || isNotRelative(path9);
640
640
  };
641
641
  if (
642
642
  // Detect `process` so that it can run in browsers.
@@ -652,9 +652,10 @@ var require_ignore = __commonJS({
652
652
  });
653
653
 
654
654
  // src/index.ts
655
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
656
- import * as path7 from "path";
655
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
656
+ import * as path8 from "path";
657
657
  import * as os3 from "os";
658
+ import { fileURLToPath as fileURLToPath2 } from "url";
658
659
 
659
660
  // src/config/schema.ts
660
661
  var DEFAULT_INCLUDE = [
@@ -692,7 +693,10 @@ function getDefaultIndexingConfig() {
692
693
  maxChunksPerFile: 100,
693
694
  semanticOnly: false,
694
695
  retries: 3,
695
- retryDelayMs: 1e3
696
+ retryDelayMs: 1e3,
697
+ autoGc: true,
698
+ gcIntervalDays: 7,
699
+ gcOrphanThreshold: 100
696
700
  };
697
701
  }
698
702
  function getDefaultSearchConfig() {
@@ -704,8 +708,21 @@ function getDefaultSearchConfig() {
704
708
  contextLines: 0
705
709
  };
706
710
  }
711
+ function getDefaultDebugConfig() {
712
+ return {
713
+ enabled: false,
714
+ logLevel: "info",
715
+ logSearch: true,
716
+ logEmbedding: true,
717
+ logCache: true,
718
+ logGc: true,
719
+ logBranch: true,
720
+ metrics: true
721
+ };
722
+ }
707
723
  var VALID_PROVIDERS = ["auto", "github-copilot", "openai", "google", "ollama"];
708
724
  var VALID_SCOPES = ["project", "global"];
725
+ var VALID_LOG_LEVELS = ["error", "warn", "info", "debug"];
709
726
  function isValidProvider(value) {
710
727
  return typeof value === "string" && VALID_PROVIDERS.includes(value);
711
728
  }
@@ -715,10 +732,14 @@ function isValidScope(value) {
715
732
  function isStringArray(value) {
716
733
  return Array.isArray(value) && value.every((item) => typeof item === "string");
717
734
  }
735
+ function isValidLogLevel(value) {
736
+ return typeof value === "string" && VALID_LOG_LEVELS.includes(value);
737
+ }
718
738
  function parseConfig(raw) {
719
739
  const input = raw && typeof raw === "object" ? raw : {};
720
740
  const defaultIndexing = getDefaultIndexingConfig();
721
741
  const defaultSearch = getDefaultSearchConfig();
742
+ const defaultDebug = getDefaultDebugConfig();
722
743
  const rawIndexing = input.indexing && typeof input.indexing === "object" ? input.indexing : {};
723
744
  const indexing = {
724
745
  autoIndex: typeof rawIndexing.autoIndex === "boolean" ? rawIndexing.autoIndex : defaultIndexing.autoIndex,
@@ -727,7 +748,10 @@ function parseConfig(raw) {
727
748
  maxChunksPerFile: typeof rawIndexing.maxChunksPerFile === "number" ? Math.max(1, rawIndexing.maxChunksPerFile) : defaultIndexing.maxChunksPerFile,
728
749
  semanticOnly: typeof rawIndexing.semanticOnly === "boolean" ? rawIndexing.semanticOnly : defaultIndexing.semanticOnly,
729
750
  retries: typeof rawIndexing.retries === "number" ? rawIndexing.retries : defaultIndexing.retries,
730
- retryDelayMs: typeof rawIndexing.retryDelayMs === "number" ? rawIndexing.retryDelayMs : defaultIndexing.retryDelayMs
751
+ retryDelayMs: typeof rawIndexing.retryDelayMs === "number" ? rawIndexing.retryDelayMs : defaultIndexing.retryDelayMs,
752
+ autoGc: typeof rawIndexing.autoGc === "boolean" ? rawIndexing.autoGc : defaultIndexing.autoGc,
753
+ gcIntervalDays: typeof rawIndexing.gcIntervalDays === "number" ? Math.max(1, rawIndexing.gcIntervalDays) : defaultIndexing.gcIntervalDays,
754
+ gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold
731
755
  };
732
756
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
733
757
  const search = {
@@ -737,6 +761,17 @@ function parseConfig(raw) {
737
761
  hybridWeight: typeof rawSearch.hybridWeight === "number" ? Math.min(1, Math.max(0, rawSearch.hybridWeight)) : defaultSearch.hybridWeight,
738
762
  contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
739
763
  };
764
+ const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
765
+ const debug = {
766
+ enabled: typeof rawDebug.enabled === "boolean" ? rawDebug.enabled : defaultDebug.enabled,
767
+ logLevel: isValidLogLevel(rawDebug.logLevel) ? rawDebug.logLevel : defaultDebug.logLevel,
768
+ logSearch: typeof rawDebug.logSearch === "boolean" ? rawDebug.logSearch : defaultDebug.logSearch,
769
+ logEmbedding: typeof rawDebug.logEmbedding === "boolean" ? rawDebug.logEmbedding : defaultDebug.logEmbedding,
770
+ logCache: typeof rawDebug.logCache === "boolean" ? rawDebug.logCache : defaultDebug.logCache,
771
+ logGc: typeof rawDebug.logGc === "boolean" ? rawDebug.logGc : defaultDebug.logGc,
772
+ logBranch: typeof rawDebug.logBranch === "boolean" ? rawDebug.logBranch : defaultDebug.logBranch,
773
+ metrics: typeof rawDebug.metrics === "boolean" ? rawDebug.metrics : defaultDebug.metrics
774
+ };
740
775
  return {
741
776
  embeddingProvider: isValidProvider(input.embeddingProvider) ? input.embeddingProvider : "auto",
742
777
  embeddingModel: typeof input.embeddingModel === "string" ? input.embeddingModel : "auto",
@@ -744,7 +779,8 @@ function parseConfig(raw) {
744
779
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
745
780
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
746
781
  indexing,
747
- search
782
+ search,
783
+ debug
748
784
  };
749
785
  }
750
786
  var EMBEDDING_MODELS = {
@@ -809,6 +845,7 @@ function getDefaultModelForProvider(provider) {
809
845
  // src/indexer/index.ts
810
846
  import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync, promises as fsPromises2 } from "fs";
811
847
  import * as path5 from "path";
848
+ import { performance as performance2 } from "perf_hooks";
812
849
 
813
850
  // node_modules/eventemitter3/index.mjs
814
851
  var import_index = __toESM(require_eventemitter3(), 1);
@@ -2066,34 +2103,36 @@ var GoogleEmbeddingProvider = class {
2066
2103
  };
2067
2104
  }
2068
2105
  async embedBatch(texts) {
2069
- const embeddings = [];
2070
- let totalTokens = 0;
2071
- for (const text of texts) {
2072
- const response = await fetch(
2073
- `${this.credentials.baseUrl}/models/${this.modelInfo.model}:embedContent?key=${this.credentials.apiKey}`,
2074
- {
2075
- method: "POST",
2076
- headers: {
2077
- "Content-Type": "application/json"
2078
- },
2079
- body: JSON.stringify({
2080
- content: {
2081
- parts: [{ text }]
2082
- }
2083
- })
2106
+ const results = await Promise.all(
2107
+ texts.map(async (text) => {
2108
+ const response = await fetch(
2109
+ `${this.credentials.baseUrl}/models/${this.modelInfo.model}:embedContent?key=${this.credentials.apiKey}`,
2110
+ {
2111
+ method: "POST",
2112
+ headers: {
2113
+ "Content-Type": "application/json"
2114
+ },
2115
+ body: JSON.stringify({
2116
+ content: {
2117
+ parts: [{ text }]
2118
+ }
2119
+ })
2120
+ }
2121
+ );
2122
+ if (!response.ok) {
2123
+ const error = await response.text();
2124
+ throw new Error(`Google embedding API error: ${response.status} - ${error}`);
2084
2125
  }
2085
- );
2086
- if (!response.ok) {
2087
- const error = await response.text();
2088
- throw new Error(`Google embedding API error: ${response.status} - ${error}`);
2089
- }
2090
- const data = await response.json();
2091
- embeddings.push(data.embedding.values);
2092
- totalTokens += Math.ceil(text.length / 4);
2093
- }
2126
+ const data = await response.json();
2127
+ return {
2128
+ embedding: data.embedding.values,
2129
+ tokensUsed: Math.ceil(text.length / 4)
2130
+ };
2131
+ })
2132
+ );
2094
2133
  return {
2095
- embeddings,
2096
- totalTokensUsed: totalTokens
2134
+ embeddings: results.map((r) => r.embedding),
2135
+ totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
2097
2136
  };
2098
2137
  }
2099
2138
  getModelInfo() {
@@ -2127,16 +2166,10 @@ var OllamaEmbeddingProvider = class {
2127
2166
  };
2128
2167
  }
2129
2168
  async embedBatch(texts) {
2130
- const embeddings = [];
2131
- let totalTokens = 0;
2132
- for (const text of texts) {
2133
- const result = await this.embed(text);
2134
- embeddings.push(result.embedding);
2135
- totalTokens += result.tokensUsed;
2136
- }
2169
+ const results = await Promise.all(texts.map((text) => this.embed(text)));
2137
2170
  return {
2138
- embeddings,
2139
- totalTokensUsed: totalTokens
2171
+ embeddings: results.map((r) => r.embedding),
2172
+ totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
2140
2173
  };
2141
2174
  }
2142
2175
  getModelInfo() {
@@ -2322,6 +2355,298 @@ function padRight(str, length) {
2322
2355
  return str.padEnd(length);
2323
2356
  }
2324
2357
 
2358
+ // src/utils/logger.ts
2359
+ var LOG_LEVEL_PRIORITY = {
2360
+ error: 0,
2361
+ warn: 1,
2362
+ info: 2,
2363
+ debug: 3
2364
+ };
2365
+ function createEmptyMetrics() {
2366
+ return {
2367
+ filesScanned: 0,
2368
+ filesParsed: 0,
2369
+ parseMs: 0,
2370
+ chunksProcessed: 0,
2371
+ chunksEmbedded: 0,
2372
+ chunksFromCache: 0,
2373
+ chunksRemoved: 0,
2374
+ embeddingApiCalls: 0,
2375
+ embeddingTokensUsed: 0,
2376
+ embeddingErrors: 0,
2377
+ searchCount: 0,
2378
+ searchTotalMs: 0,
2379
+ searchAvgMs: 0,
2380
+ searchLastMs: 0,
2381
+ embeddingCallMs: 0,
2382
+ vectorSearchMs: 0,
2383
+ keywordSearchMs: 0,
2384
+ fusionMs: 0,
2385
+ cacheHits: 0,
2386
+ cacheMisses: 0,
2387
+ queryCacheHits: 0,
2388
+ queryCacheSimilarHits: 0,
2389
+ queryCacheMisses: 0,
2390
+ gcRuns: 0,
2391
+ gcOrphansRemoved: 0,
2392
+ gcChunksRemoved: 0,
2393
+ gcEmbeddingsRemoved: 0
2394
+ };
2395
+ }
2396
+ var Logger = class {
2397
+ config;
2398
+ metrics;
2399
+ logs = [];
2400
+ maxLogs = 1e3;
2401
+ constructor(config) {
2402
+ this.config = config;
2403
+ this.metrics = createEmptyMetrics();
2404
+ }
2405
+ shouldLog(level) {
2406
+ if (!this.config.enabled) return false;
2407
+ return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.config.logLevel];
2408
+ }
2409
+ log(level, category, message, data) {
2410
+ if (!this.shouldLog(level)) return;
2411
+ const entry = {
2412
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2413
+ level,
2414
+ category,
2415
+ message,
2416
+ data
2417
+ };
2418
+ this.logs.push(entry);
2419
+ if (this.logs.length > this.maxLogs) {
2420
+ this.logs.shift();
2421
+ }
2422
+ }
2423
+ search(level, message, data) {
2424
+ if (this.config.logSearch) {
2425
+ this.log(level, "search", message, data);
2426
+ }
2427
+ }
2428
+ embedding(level, message, data) {
2429
+ if (this.config.logEmbedding) {
2430
+ this.log(level, "embedding", message, data);
2431
+ }
2432
+ }
2433
+ cache(level, message, data) {
2434
+ if (this.config.logCache) {
2435
+ this.log(level, "cache", message, data);
2436
+ }
2437
+ }
2438
+ gc(level, message, data) {
2439
+ if (this.config.logGc) {
2440
+ this.log(level, "gc", message, data);
2441
+ }
2442
+ }
2443
+ branch(level, message, data) {
2444
+ if (this.config.logBranch) {
2445
+ this.log(level, "branch", message, data);
2446
+ }
2447
+ }
2448
+ info(message, data) {
2449
+ this.log("info", "general", message, data);
2450
+ }
2451
+ warn(message, data) {
2452
+ this.log("warn", "general", message, data);
2453
+ }
2454
+ error(message, data) {
2455
+ this.log("error", "general", message, data);
2456
+ }
2457
+ debug(message, data) {
2458
+ this.log("debug", "general", message, data);
2459
+ }
2460
+ recordIndexingStart() {
2461
+ if (!this.config.metrics) return;
2462
+ this.metrics.indexingStartTime = Date.now();
2463
+ }
2464
+ recordIndexingEnd() {
2465
+ if (!this.config.metrics) return;
2466
+ this.metrics.indexingEndTime = Date.now();
2467
+ }
2468
+ recordFilesScanned(count) {
2469
+ if (!this.config.metrics) return;
2470
+ this.metrics.filesScanned = count;
2471
+ }
2472
+ recordFilesParsed(count) {
2473
+ if (!this.config.metrics) return;
2474
+ this.metrics.filesParsed = count;
2475
+ }
2476
+ recordParseDuration(durationMs) {
2477
+ if (!this.config.metrics) return;
2478
+ this.metrics.parseMs = durationMs;
2479
+ }
2480
+ recordChunksProcessed(count) {
2481
+ if (!this.config.metrics) return;
2482
+ this.metrics.chunksProcessed += count;
2483
+ }
2484
+ recordChunksEmbedded(count) {
2485
+ if (!this.config.metrics) return;
2486
+ this.metrics.chunksEmbedded += count;
2487
+ }
2488
+ recordChunksFromCache(count) {
2489
+ if (!this.config.metrics) return;
2490
+ this.metrics.chunksFromCache += count;
2491
+ }
2492
+ recordChunksRemoved(count) {
2493
+ if (!this.config.metrics) return;
2494
+ this.metrics.chunksRemoved += count;
2495
+ }
2496
+ recordEmbeddingApiCall(tokens) {
2497
+ if (!this.config.metrics) return;
2498
+ this.metrics.embeddingApiCalls++;
2499
+ this.metrics.embeddingTokensUsed += tokens;
2500
+ }
2501
+ recordEmbeddingError() {
2502
+ if (!this.config.metrics) return;
2503
+ this.metrics.embeddingErrors++;
2504
+ }
2505
+ recordSearch(durationMs, breakdown) {
2506
+ if (!this.config.metrics) return;
2507
+ this.metrics.searchCount++;
2508
+ this.metrics.searchTotalMs += durationMs;
2509
+ this.metrics.searchLastMs = durationMs;
2510
+ this.metrics.searchAvgMs = this.metrics.searchTotalMs / this.metrics.searchCount;
2511
+ if (breakdown) {
2512
+ this.metrics.embeddingCallMs = breakdown.embeddingMs;
2513
+ this.metrics.vectorSearchMs = breakdown.vectorMs;
2514
+ this.metrics.keywordSearchMs = breakdown.keywordMs;
2515
+ this.metrics.fusionMs = breakdown.fusionMs;
2516
+ }
2517
+ }
2518
+ recordCacheHit() {
2519
+ if (!this.config.metrics) return;
2520
+ this.metrics.cacheHits++;
2521
+ }
2522
+ recordCacheMiss() {
2523
+ if (!this.config.metrics) return;
2524
+ this.metrics.cacheMisses++;
2525
+ }
2526
+ recordQueryCacheHit() {
2527
+ if (!this.config.metrics) return;
2528
+ this.metrics.queryCacheHits++;
2529
+ }
2530
+ recordQueryCacheSimilarHit() {
2531
+ if (!this.config.metrics) return;
2532
+ this.metrics.queryCacheSimilarHits++;
2533
+ }
2534
+ recordQueryCacheMiss() {
2535
+ if (!this.config.metrics) return;
2536
+ this.metrics.queryCacheMisses++;
2537
+ }
2538
+ recordGc(orphans, chunks, embeddings) {
2539
+ if (!this.config.metrics) return;
2540
+ this.metrics.gcRuns++;
2541
+ this.metrics.gcOrphansRemoved += orphans;
2542
+ this.metrics.gcChunksRemoved += chunks;
2543
+ this.metrics.gcEmbeddingsRemoved += embeddings;
2544
+ }
2545
+ getMetrics() {
2546
+ return { ...this.metrics };
2547
+ }
2548
+ getLogs(limit) {
2549
+ const logs = [...this.logs];
2550
+ if (limit) {
2551
+ return logs.slice(-limit);
2552
+ }
2553
+ return logs;
2554
+ }
2555
+ getLogsByCategory(category, limit) {
2556
+ const filtered = this.logs.filter((l) => l.category === category);
2557
+ if (limit) {
2558
+ return filtered.slice(-limit);
2559
+ }
2560
+ return filtered;
2561
+ }
2562
+ getLogsByLevel(level, limit) {
2563
+ const filtered = this.logs.filter((l) => l.level === level);
2564
+ if (limit) {
2565
+ return filtered.slice(-limit);
2566
+ }
2567
+ return filtered;
2568
+ }
2569
+ resetMetrics() {
2570
+ this.metrics = createEmptyMetrics();
2571
+ }
2572
+ clearLogs() {
2573
+ this.logs = [];
2574
+ }
2575
+ formatMetrics() {
2576
+ const m = this.metrics;
2577
+ const lines = [];
2578
+ lines.push("=== Metrics ===");
2579
+ if (m.indexingStartTime && m.indexingEndTime) {
2580
+ const duration = m.indexingEndTime - m.indexingStartTime;
2581
+ lines.push(`Indexing duration: ${(duration / 1e3).toFixed(2)}s`);
2582
+ }
2583
+ lines.push("");
2584
+ lines.push("Indexing:");
2585
+ lines.push(` Files scanned: ${m.filesScanned}`);
2586
+ lines.push(` Files parsed: ${m.filesParsed}`);
2587
+ lines.push(` Chunks processed: ${m.chunksProcessed}`);
2588
+ lines.push(` Chunks embedded: ${m.chunksEmbedded}`);
2589
+ lines.push(` Chunks from cache: ${m.chunksFromCache}`);
2590
+ lines.push(` Chunks removed: ${m.chunksRemoved}`);
2591
+ lines.push("");
2592
+ lines.push("Embedding API:");
2593
+ lines.push(` API calls: ${m.embeddingApiCalls}`);
2594
+ lines.push(` Tokens used: ${m.embeddingTokensUsed.toLocaleString()}`);
2595
+ lines.push(` Errors: ${m.embeddingErrors}`);
2596
+ if (m.searchCount > 0) {
2597
+ lines.push("");
2598
+ lines.push("Search:");
2599
+ lines.push(` Total searches: ${m.searchCount}`);
2600
+ lines.push(` Average time: ${m.searchAvgMs.toFixed(2)}ms`);
2601
+ lines.push(` Last search: ${m.searchLastMs.toFixed(2)}ms`);
2602
+ if (m.embeddingCallMs > 0) {
2603
+ lines.push(` - Embedding: ${m.embeddingCallMs.toFixed(2)}ms`);
2604
+ lines.push(` - Vector search: ${m.vectorSearchMs.toFixed(2)}ms`);
2605
+ lines.push(` - Keyword search: ${m.keywordSearchMs.toFixed(2)}ms`);
2606
+ lines.push(` - Fusion: ${m.fusionMs.toFixed(2)}ms`);
2607
+ }
2608
+ }
2609
+ const totalCacheOps = m.cacheHits + m.cacheMisses;
2610
+ if (totalCacheOps > 0) {
2611
+ lines.push("");
2612
+ lines.push("Cache:");
2613
+ lines.push(` Hits: ${m.cacheHits}`);
2614
+ lines.push(` Misses: ${m.cacheMisses}`);
2615
+ lines.push(` Hit rate: ${(m.cacheHits / totalCacheOps * 100).toFixed(1)}%`);
2616
+ }
2617
+ if (m.gcRuns > 0) {
2618
+ lines.push("");
2619
+ lines.push("Garbage Collection:");
2620
+ lines.push(` GC runs: ${m.gcRuns}`);
2621
+ lines.push(` Orphans removed: ${m.gcOrphansRemoved}`);
2622
+ lines.push(` Chunks removed: ${m.gcChunksRemoved}`);
2623
+ lines.push(` Embeddings removed: ${m.gcEmbeddingsRemoved}`);
2624
+ }
2625
+ return lines.join("\n");
2626
+ }
2627
+ formatRecentLogs(limit = 20) {
2628
+ const logs = this.getLogs(limit);
2629
+ if (logs.length === 0) {
2630
+ return "No logs recorded.";
2631
+ }
2632
+ return logs.map((l) => {
2633
+ const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
2634
+ return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
2635
+ }).join("\n");
2636
+ }
2637
+ isEnabled() {
2638
+ return this.config.enabled;
2639
+ }
2640
+ isMetricsEnabled() {
2641
+ return this.config.enabled && this.config.metrics;
2642
+ }
2643
+ };
2644
+ var globalLogger = null;
2645
+ function initializeLogger(config) {
2646
+ globalLogger = new Logger(config);
2647
+ return globalLogger;
2648
+ }
2649
+
2325
2650
  // src/native/index.ts
2326
2651
  import * as path3 from "path";
2327
2652
  import * as os2 from "os";
@@ -2450,6 +2775,21 @@ var VectorStore = class {
2450
2775
  metadata: JSON.parse(r.metadata)
2451
2776
  }));
2452
2777
  }
2778
+ getMetadata(id) {
2779
+ const result = this.inner.getMetadata(id);
2780
+ if (result === null || result === void 0) {
2781
+ return void 0;
2782
+ }
2783
+ return JSON.parse(result);
2784
+ }
2785
+ getMetadataBatch(ids) {
2786
+ const results = this.inner.getMetadataBatch(ids);
2787
+ const map = /* @__PURE__ */ new Map();
2788
+ for (const { key, metadata } of results) {
2789
+ map.set(key, JSON.parse(metadata));
2790
+ }
2791
+ return map;
2792
+ }
2453
2793
  };
2454
2794
  var CHARS_PER_TOKEN = 4;
2455
2795
  var MAX_BATCH_TOKENS = 7500;
@@ -2677,12 +3017,20 @@ var Database = class {
2677
3017
  upsertEmbedding(contentHash, embedding, chunkText, model) {
2678
3018
  this.inner.upsertEmbedding(contentHash, embedding, chunkText, model);
2679
3019
  }
3020
+ upsertEmbeddingsBatch(items) {
3021
+ if (items.length === 0) return;
3022
+ this.inner.upsertEmbeddingsBatch(items);
3023
+ }
2680
3024
  getMissingEmbeddings(contentHashes) {
2681
3025
  return this.inner.getMissingEmbeddings(contentHashes);
2682
3026
  }
2683
3027
  upsertChunk(chunk) {
2684
3028
  this.inner.upsertChunk(chunk);
2685
3029
  }
3030
+ upsertChunksBatch(chunks) {
3031
+ if (chunks.length === 0) return;
3032
+ this.inner.upsertChunksBatch(chunks);
3033
+ }
2686
3034
  getChunk(chunkId) {
2687
3035
  return this.inner.getChunk(chunkId) ?? null;
2688
3036
  }
@@ -2695,6 +3043,10 @@ var Database = class {
2695
3043
  addChunksToBranch(branch, chunkIds) {
2696
3044
  this.inner.addChunksToBranch(branch, chunkIds);
2697
3045
  }
3046
+ addChunksToBranchBatch(branch, chunkIds) {
3047
+ if (chunkIds.length === 0) return;
3048
+ this.inner.addChunksToBranchBatch(branch, chunkIds);
3049
+ }
2698
3050
  clearBranch(branch) {
2699
3051
  return this.inner.clearBranch(branch);
2700
3052
  }
@@ -2836,12 +3188,18 @@ var Indexer = class {
2836
3188
  failedBatchesPath = "";
2837
3189
  currentBranch = "default";
2838
3190
  baseBranch = "main";
3191
+ logger;
3192
+ queryEmbeddingCache = /* @__PURE__ */ new Map();
3193
+ maxQueryCacheSize = 100;
3194
+ queryCacheTtlMs = 5 * 60 * 1e3;
3195
+ querySimilarityThreshold = 0.85;
2839
3196
  constructor(projectRoot, config) {
2840
3197
  this.projectRoot = projectRoot;
2841
3198
  this.config = config;
2842
3199
  this.indexPath = this.getIndexPath();
2843
3200
  this.fileHashCachePath = path5.join(this.indexPath, "file-hashes.json");
2844
3201
  this.failedBatchesPath = path5.join(this.indexPath, "failed-batches.json");
3202
+ this.logger = initializeLogger(config.debug);
2845
3203
  }
2846
3204
  getIndexPath() {
2847
3205
  if (this.config.scope === "global") {
@@ -2920,6 +3278,11 @@ var Indexer = class {
2920
3278
  "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
2921
3279
  );
2922
3280
  }
3281
+ this.logger.info("Initializing indexer", {
3282
+ provider: this.detectedProvider.provider,
3283
+ model: this.detectedProvider.modelInfo.model,
3284
+ scope: this.config.scope
3285
+ });
2923
3286
  this.provider = createEmbeddingProvider(
2924
3287
  this.detectedProvider.credentials,
2925
3288
  this.detectedProvider.modelInfo
@@ -2951,15 +3314,54 @@ var Indexer = class {
2951
3314
  if (isGitRepo(this.projectRoot)) {
2952
3315
  this.currentBranch = getBranchOrDefault(this.projectRoot);
2953
3316
  this.baseBranch = getBaseBranch(this.projectRoot);
3317
+ this.logger.branch("info", "Detected git repository", {
3318
+ currentBranch: this.currentBranch,
3319
+ baseBranch: this.baseBranch
3320
+ });
2954
3321
  } else {
2955
3322
  this.currentBranch = "default";
2956
3323
  this.baseBranch = "default";
3324
+ this.logger.branch("debug", "Not a git repository, using default branch");
3325
+ }
3326
+ if (this.config.indexing.autoGc) {
3327
+ await this.maybeRunAutoGc();
3328
+ }
3329
+ }
3330
+ async maybeRunAutoGc() {
3331
+ if (!this.database) return;
3332
+ const lastGcTimestamp = this.database.getMetadata("lastGcTimestamp");
3333
+ const now = Date.now();
3334
+ const intervalMs = this.config.indexing.gcIntervalDays * 24 * 60 * 60 * 1e3;
3335
+ let shouldRunGc = false;
3336
+ if (!lastGcTimestamp) {
3337
+ shouldRunGc = true;
3338
+ } else {
3339
+ const lastGcTime = parseInt(lastGcTimestamp, 10);
3340
+ if (!isNaN(lastGcTime) && now - lastGcTime > intervalMs) {
3341
+ shouldRunGc = true;
3342
+ }
3343
+ }
3344
+ if (shouldRunGc) {
3345
+ await this.healthCheck();
3346
+ this.database.setMetadata("lastGcTimestamp", now.toString());
3347
+ }
3348
+ }
3349
+ async maybeRunOrphanGc() {
3350
+ if (!this.database) return;
3351
+ const stats = this.database.getStats();
3352
+ if (!stats) return;
3353
+ const orphanCount = stats.embeddingCount - stats.chunkCount;
3354
+ if (orphanCount > this.config.indexing.gcOrphanThreshold) {
3355
+ this.database.gcOrphanEmbeddings();
3356
+ this.database.gcOrphanChunks();
3357
+ this.database.setMetadata("lastGcTimestamp", Date.now().toString());
2957
3358
  }
2958
3359
  }
2959
3360
  migrateFromLegacyIndex() {
2960
3361
  if (!this.store || !this.database) return;
2961
3362
  const allMetadata = this.store.getAllMetadata();
2962
3363
  const chunkIds = [];
3364
+ const chunkDataBatch = [];
2963
3365
  for (const { key, metadata } of allMetadata) {
2964
3366
  const chunkData = {
2965
3367
  chunkId: key,
@@ -2971,10 +3373,13 @@ var Indexer = class {
2971
3373
  name: metadata.name,
2972
3374
  language: metadata.language
2973
3375
  };
2974
- this.database.upsertChunk(chunkData);
3376
+ chunkDataBatch.push(chunkData);
2975
3377
  chunkIds.push(key);
2976
3378
  }
2977
- this.database.addChunksToBranch(this.currentBranch || "default", chunkIds);
3379
+ if (chunkDataBatch.length > 0) {
3380
+ this.database.upsertChunksBatch(chunkDataBatch);
3381
+ }
3382
+ this.database.addChunksToBranchBatch(this.currentBranch || "default", chunkIds);
2978
3383
  }
2979
3384
  async ensureInitialized() {
2980
3385
  if (!this.store || !this.provider || !this.invertedIndex || !this.detectedProvider || !this.database) {
@@ -3000,6 +3405,8 @@ var Indexer = class {
3000
3405
  }
3001
3406
  async index(onProgress) {
3002
3407
  const { store, provider, invertedIndex, database, detectedProvider } = await this.ensureInitialized();
3408
+ this.logger.recordIndexingStart();
3409
+ this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
3003
3410
  const startTime = Date.now();
3004
3411
  const stats = {
3005
3412
  totalFiles: 0,
@@ -3029,6 +3436,11 @@ var Indexer = class {
3029
3436
  );
3030
3437
  stats.totalFiles = files.length;
3031
3438
  stats.skippedFiles = skipped;
3439
+ this.logger.recordFilesScanned(files.length);
3440
+ this.logger.cache("debug", "Scanning files for changes", {
3441
+ totalFiles: files.length,
3442
+ skippedFiles: skipped.length
3443
+ });
3032
3444
  const changedFiles = [];
3033
3445
  const unchangedFilePaths = /* @__PURE__ */ new Set();
3034
3446
  const currentFileHashes = /* @__PURE__ */ new Map();
@@ -3037,11 +3449,17 @@ var Indexer = class {
3037
3449
  currentFileHashes.set(f.path, currentHash);
3038
3450
  if (this.fileHashCache.get(f.path) === currentHash) {
3039
3451
  unchangedFilePaths.add(f.path);
3452
+ this.logger.recordCacheHit();
3040
3453
  } else {
3041
3454
  const content = await fsPromises2.readFile(f.path, "utf-8");
3042
3455
  changedFiles.push({ path: f.path, content, hash: currentHash });
3456
+ this.logger.recordCacheMiss();
3043
3457
  }
3044
3458
  }
3459
+ this.logger.cache("info", "File hash cache results", {
3460
+ unchanged: unchangedFilePaths.size,
3461
+ changed: changedFiles.length
3462
+ });
3045
3463
  onProgress?.({
3046
3464
  phase: "parsing",
3047
3465
  filesProcessed: 0,
@@ -3049,7 +3467,12 @@ var Indexer = class {
3049
3467
  chunksProcessed: 0,
3050
3468
  totalChunks: 0
3051
3469
  });
3470
+ const parseStartTime = performance2.now();
3052
3471
  const parsedFiles = parseFiles(changedFiles);
3472
+ const parseMs = performance2.now() - parseStartTime;
3473
+ this.logger.recordFilesParsed(parsedFiles.length);
3474
+ this.logger.recordParseDuration(parseMs);
3475
+ this.logger.debug("Parsed changed files", { parsedCount: parsedFiles.length, parseMs: parseMs.toFixed(2) });
3053
3476
  const existingChunks = /* @__PURE__ */ new Map();
3054
3477
  const existingChunksByFile = /* @__PURE__ */ new Map();
3055
3478
  for (const { key, metadata } of store.getAllMetadata()) {
@@ -3070,6 +3493,7 @@ var Indexer = class {
3070
3493
  }
3071
3494
  }
3072
3495
  }
3496
+ const chunkDataBatch = [];
3073
3497
  for (const parsed of parsedFiles) {
3074
3498
  currentFilePaths.add(parsed.path);
3075
3499
  if (parsed.chunks.length === 0) {
@@ -3097,7 +3521,7 @@ var Indexer = class {
3097
3521
  name: chunk.name,
3098
3522
  language: chunk.language
3099
3523
  };
3100
- database.upsertChunk(chunkData);
3524
+ chunkDataBatch.push(chunkData);
3101
3525
  if (existingChunks.get(id) === contentHash) {
3102
3526
  fileChunkCount++;
3103
3527
  continue;
@@ -3116,6 +3540,9 @@ var Indexer = class {
3116
3540
  fileChunkCount++;
3117
3541
  }
3118
3542
  }
3543
+ if (chunkDataBatch.length > 0) {
3544
+ database.upsertChunksBatch(chunkDataBatch);
3545
+ }
3119
3546
  let removedCount = 0;
3120
3547
  for (const [chunkId] of existingChunks) {
3121
3548
  if (!currentChunkIds.has(chunkId)) {
@@ -3127,9 +3554,16 @@ var Indexer = class {
3127
3554
  stats.totalChunks = pendingChunks.length;
3128
3555
  stats.existingChunks = currentChunkIds.size - pendingChunks.length;
3129
3556
  stats.removedChunks = removedCount;
3557
+ this.logger.recordChunksProcessed(currentChunkIds.size);
3558
+ this.logger.recordChunksRemoved(removedCount);
3559
+ this.logger.info("Chunk analysis complete", {
3560
+ pending: pendingChunks.length,
3561
+ existing: stats.existingChunks,
3562
+ removed: removedCount
3563
+ });
3130
3564
  if (pendingChunks.length === 0 && removedCount === 0) {
3131
3565
  database.clearBranch(this.currentBranch);
3132
- database.addChunksToBranch(this.currentBranch, Array.from(currentChunkIds));
3566
+ database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
3133
3567
  this.fileHashCache = currentFileHashes;
3134
3568
  this.saveFileHashCache();
3135
3569
  stats.durationMs = Date.now() - startTime;
@@ -3144,7 +3578,7 @@ var Indexer = class {
3144
3578
  }
3145
3579
  if (pendingChunks.length === 0) {
3146
3580
  database.clearBranch(this.currentBranch);
3147
- database.addChunksToBranch(this.currentBranch, Array.from(currentChunkIds));
3581
+ database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
3148
3582
  store.save();
3149
3583
  invertedIndex.save();
3150
3584
  this.fileHashCache = currentFileHashes;
@@ -3170,6 +3604,11 @@ var Indexer = class {
3170
3604
  const missingHashes = new Set(database.getMissingEmbeddings(allContentHashes));
3171
3605
  const chunksNeedingEmbedding = pendingChunks.filter((c) => missingHashes.has(c.contentHash));
3172
3606
  const chunksWithExistingEmbedding = pendingChunks.filter((c) => !missingHashes.has(c.contentHash));
3607
+ this.logger.cache("info", "Embedding cache lookup", {
3608
+ needsEmbedding: chunksNeedingEmbedding.length,
3609
+ fromCache: chunksWithExistingEmbedding.length
3610
+ });
3611
+ this.logger.recordChunksFromCache(chunksWithExistingEmbedding.length);
3173
3612
  for (const chunk of chunksWithExistingEmbedding) {
3174
3613
  const embeddingBuffer = database.getEmbedding(chunk.contentHash);
3175
3614
  if (embeddingBuffer) {
@@ -3208,13 +3647,16 @@ var Indexer = class {
3208
3647
  const message = getErrorMessage(error);
3209
3648
  if (isRateLimitError(error)) {
3210
3649
  rateLimitBackoffMs = Math.min(providerRateLimits.maxRetryMs, (rateLimitBackoffMs || providerRateLimits.minRetryMs) * 2);
3211
- console.error(
3212
- `Rate limited (attempt ${error.attemptNumber}/${error.retriesLeft + error.attemptNumber}): waiting ${rateLimitBackoffMs / 1e3}s before retry...`
3213
- );
3650
+ this.logger.embedding("warn", `Rate limited, backing off`, {
3651
+ attempt: error.attemptNumber,
3652
+ retriesLeft: error.retriesLeft,
3653
+ backoffMs: rateLimitBackoffMs
3654
+ });
3214
3655
  } else {
3215
- console.error(
3216
- `Embedding batch failed (attempt ${error.attemptNumber}): ${message}`
3217
- );
3656
+ this.logger.embedding("error", `Embedding batch failed`, {
3657
+ attempt: error.attemptNumber,
3658
+ error: message
3659
+ });
3218
3660
  }
3219
3661
  }
3220
3662
  }
@@ -3228,20 +3670,25 @@ var Indexer = class {
3228
3670
  metadata: chunk.metadata
3229
3671
  }));
3230
3672
  store.addBatch(items);
3231
- for (let i = 0; i < batch.length; i++) {
3232
- const chunk = batch[i];
3233
- const embedding = result.embeddings[i];
3234
- database.upsertEmbedding(
3235
- chunk.contentHash,
3236
- float32ArrayToBuffer(embedding),
3237
- chunk.text,
3238
- detectedProvider.modelInfo.model
3239
- );
3673
+ const embeddingBatchItems = batch.map((chunk, i) => ({
3674
+ contentHash: chunk.contentHash,
3675
+ embedding: float32ArrayToBuffer(result.embeddings[i]),
3676
+ chunkText: chunk.text,
3677
+ model: detectedProvider.modelInfo.model
3678
+ }));
3679
+ database.upsertEmbeddingsBatch(embeddingBatchItems);
3680
+ for (const chunk of batch) {
3240
3681
  invertedIndex.removeChunk(chunk.id);
3241
3682
  invertedIndex.addChunk(chunk.id, chunk.content);
3242
3683
  }
3243
3684
  stats.indexedChunks += batch.length;
3244
3685
  stats.tokensUsed += result.totalTokensUsed;
3686
+ this.logger.recordChunksEmbedded(batch.length);
3687
+ this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
3688
+ this.logger.embedding("debug", `Embedded batch`, {
3689
+ batchSize: batch.length,
3690
+ tokens: result.totalTokensUsed
3691
+ });
3245
3692
  onProgress?.({
3246
3693
  phase: "embedding",
3247
3694
  filesProcessed: files.length,
@@ -3252,7 +3699,11 @@ var Indexer = class {
3252
3699
  } catch (error) {
3253
3700
  stats.failedChunks += batch.length;
3254
3701
  this.addFailedBatch(batch, getErrorMessage(error));
3255
- console.error(`Failed to embed batch after retries: ${getErrorMessage(error)}`);
3702
+ this.logger.recordEmbeddingError();
3703
+ this.logger.embedding("error", `Failed to embed batch after retries`, {
3704
+ batchSize: batch.length,
3705
+ error: getErrorMessage(error)
3706
+ });
3256
3707
  }
3257
3708
  });
3258
3709
  }
@@ -3265,12 +3716,25 @@ var Indexer = class {
3265
3716
  totalChunks: pendingChunks.length
3266
3717
  });
3267
3718
  database.clearBranch(this.currentBranch);
3268
- database.addChunksToBranch(this.currentBranch, Array.from(currentChunkIds));
3719
+ database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
3269
3720
  store.save();
3270
3721
  invertedIndex.save();
3271
3722
  this.fileHashCache = currentFileHashes;
3272
3723
  this.saveFileHashCache();
3724
+ if (this.config.indexing.autoGc && stats.removedChunks > 0) {
3725
+ await this.maybeRunOrphanGc();
3726
+ }
3273
3727
  stats.durationMs = Date.now() - startTime;
3728
+ this.logger.recordIndexingEnd();
3729
+ this.logger.info("Indexing complete", {
3730
+ files: stats.totalFiles,
3731
+ indexed: stats.indexedChunks,
3732
+ existing: stats.existingChunks,
3733
+ removed: stats.removedChunks,
3734
+ failed: stats.failedChunks,
3735
+ tokens: stats.tokensUsed,
3736
+ durationMs: stats.durationMs
3737
+ });
3274
3738
  if (stats.failedChunks > 0) {
3275
3739
  stats.failedBatchesPath = this.failedBatchesPath;
3276
3740
  }
@@ -3283,18 +3747,96 @@ var Indexer = class {
3283
3747
  });
3284
3748
  return stats;
3285
3749
  }
3750
+ async getQueryEmbedding(query, provider) {
3751
+ const now = Date.now();
3752
+ const cached = this.queryEmbeddingCache.get(query);
3753
+ if (cached && now - cached.timestamp < this.queryCacheTtlMs) {
3754
+ this.logger.cache("debug", "Query embedding cache hit (exact)", { query: query.slice(0, 50) });
3755
+ this.logger.recordQueryCacheHit();
3756
+ return cached.embedding;
3757
+ }
3758
+ const similarMatch = this.findSimilarCachedQuery(query, now);
3759
+ if (similarMatch) {
3760
+ this.logger.cache("debug", "Query embedding cache hit (similar)", {
3761
+ query: query.slice(0, 50),
3762
+ similarTo: similarMatch.key.slice(0, 50),
3763
+ similarity: similarMatch.similarity.toFixed(3)
3764
+ });
3765
+ this.logger.recordQueryCacheSimilarHit();
3766
+ return similarMatch.embedding;
3767
+ }
3768
+ this.logger.cache("debug", "Query embedding cache miss", { query: query.slice(0, 50) });
3769
+ this.logger.recordQueryCacheMiss();
3770
+ const { embedding, tokensUsed } = await provider.embed(query);
3771
+ this.logger.recordEmbeddingApiCall(tokensUsed);
3772
+ if (this.queryEmbeddingCache.size >= this.maxQueryCacheSize) {
3773
+ const oldestKey = this.queryEmbeddingCache.keys().next().value;
3774
+ if (oldestKey) {
3775
+ this.queryEmbeddingCache.delete(oldestKey);
3776
+ }
3777
+ }
3778
+ this.queryEmbeddingCache.set(query, { embedding, timestamp: now });
3779
+ return embedding;
3780
+ }
3781
+ findSimilarCachedQuery(query, now) {
3782
+ const queryTokens = this.tokenize(query);
3783
+ if (queryTokens.size === 0) return null;
3784
+ let bestMatch = null;
3785
+ for (const [cachedQuery, { embedding, timestamp }] of this.queryEmbeddingCache) {
3786
+ if (now - timestamp >= this.queryCacheTtlMs) continue;
3787
+ const cachedTokens = this.tokenize(cachedQuery);
3788
+ const similarity = this.jaccardSimilarity(queryTokens, cachedTokens);
3789
+ if (similarity >= this.querySimilarityThreshold) {
3790
+ if (!bestMatch || similarity > bestMatch.similarity) {
3791
+ bestMatch = { key: cachedQuery, embedding, similarity };
3792
+ }
3793
+ }
3794
+ }
3795
+ return bestMatch;
3796
+ }
3797
+ tokenize(text) {
3798
+ return new Set(
3799
+ text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((t) => t.length > 1)
3800
+ );
3801
+ }
3802
+ jaccardSimilarity(a, b) {
3803
+ if (a.size === 0 && b.size === 0) return 1;
3804
+ if (a.size === 0 || b.size === 0) return 0;
3805
+ let intersection = 0;
3806
+ for (const token of a) {
3807
+ if (b.has(token)) intersection++;
3808
+ }
3809
+ const union = a.size + b.size - intersection;
3810
+ return intersection / union;
3811
+ }
3286
3812
  async search(query, limit, options) {
3813
+ const searchStartTime = performance2.now();
3287
3814
  const { store, provider, database } = await this.ensureInitialized();
3288
3815
  if (store.count() === 0) {
3816
+ this.logger.search("debug", "Search on empty index", { query });
3289
3817
  return [];
3290
3818
  }
3291
3819
  const maxResults = limit ?? this.config.search.maxResults;
3292
3820
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
3293
3821
  const filterByBranch = options?.filterByBranch ?? true;
3294
- const { embedding } = await provider.embed(query);
3822
+ this.logger.search("debug", "Starting search", {
3823
+ query,
3824
+ maxResults,
3825
+ hybridWeight,
3826
+ filterByBranch
3827
+ });
3828
+ const embeddingStartTime = performance2.now();
3829
+ const embedding = await this.getQueryEmbedding(query, provider);
3830
+ const embeddingMs = performance2.now() - embeddingStartTime;
3831
+ const vectorStartTime = performance2.now();
3295
3832
  const semanticResults = store.search(embedding, maxResults * 4);
3833
+ const vectorMs = performance2.now() - vectorStartTime;
3834
+ const keywordStartTime = performance2.now();
3296
3835
  const keywordResults = await this.keywordSearch(query, maxResults * 4);
3836
+ const keywordMs = performance2.now() - keywordStartTime;
3837
+ const fusionStartTime = performance2.now();
3297
3838
  const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
3839
+ const fusionMs = performance2.now() - fusionStartTime;
3298
3840
  let branchChunkIds = null;
3299
3841
  if (filterByBranch && this.currentBranch !== "default") {
3300
3842
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
@@ -3315,6 +3857,22 @@ var Indexer = class {
3315
3857
  }
3316
3858
  return true;
3317
3859
  }).slice(0, maxResults);
3860
+ const totalSearchMs = performance2.now() - searchStartTime;
3861
+ this.logger.recordSearch(totalSearchMs, {
3862
+ embeddingMs,
3863
+ vectorMs,
3864
+ keywordMs,
3865
+ fusionMs
3866
+ });
3867
+ this.logger.search("info", "Search complete", {
3868
+ query,
3869
+ results: filtered.length,
3870
+ totalMs: Math.round(totalSearchMs * 100) / 100,
3871
+ embeddingMs: Math.round(embeddingMs * 100) / 100,
3872
+ vectorMs: Math.round(vectorMs * 100) / 100,
3873
+ keywordMs: Math.round(keywordMs * 100) / 100,
3874
+ fusionMs: Math.round(fusionMs * 100) / 100
3875
+ });
3318
3876
  return Promise.all(
3319
3877
  filtered.map(async (r) => {
3320
3878
  let content = "";
@@ -3353,11 +3911,8 @@ var Indexer = class {
3353
3911
  if (scores.size === 0) {
3354
3912
  return [];
3355
3913
  }
3356
- const allMetadata = store.getAllMetadata();
3357
- const metadataMap = /* @__PURE__ */ new Map();
3358
- for (const { key, metadata } of allMetadata) {
3359
- metadataMap.set(key, metadata);
3360
- }
3914
+ const chunkIds = Array.from(scores.keys());
3915
+ const metadataMap = store.getMetadataBatch(chunkIds);
3361
3916
  const results = [];
3362
3917
  for (const [chunkId, score] of scores) {
3363
3918
  const metadata = metadataMap.get(chunkId);
@@ -3409,14 +3964,18 @@ var Indexer = class {
3409
3964
  };
3410
3965
  }
3411
3966
  async clearIndex() {
3412
- const { store, invertedIndex } = await this.ensureInitialized();
3967
+ const { store, invertedIndex, database } = await this.ensureInitialized();
3413
3968
  store.clear();
3414
3969
  store.save();
3415
3970
  invertedIndex.clear();
3416
3971
  invertedIndex.save();
3972
+ this.fileHashCache.clear();
3973
+ this.saveFileHashCache();
3974
+ database.clearBranch(this.currentBranch);
3417
3975
  }
3418
3976
  async healthCheck() {
3419
3977
  const { store, invertedIndex, database } = await this.ensureInitialized();
3978
+ this.logger.gc("info", "Starting health check");
3420
3979
  const allMetadata = store.getAllMetadata();
3421
3980
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
3422
3981
  for (const { key, metadata } of allMetadata) {
@@ -3443,6 +4002,13 @@ var Indexer = class {
3443
4002
  }
3444
4003
  const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
3445
4004
  const gcOrphanChunks = database.gcOrphanChunks();
4005
+ this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
4006
+ this.logger.gc("info", "Health check complete", {
4007
+ removedStale: removedCount,
4008
+ orphanEmbeddings: gcOrphanEmbeddings,
4009
+ orphanChunks: gcOrphanChunks,
4010
+ removedFiles: removedFilePaths.length
4011
+ });
3446
4012
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
3447
4013
  }
3448
4014
  async retryFailedBatches() {
@@ -3476,9 +4042,12 @@ var Indexer = class {
3476
4042
  invertedIndex.removeChunk(chunk.id);
3477
4043
  invertedIndex.addChunk(chunk.id, chunk.content);
3478
4044
  }
4045
+ this.logger.recordChunksEmbedded(batch.chunks.length);
4046
+ this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
3479
4047
  succeeded += batch.chunks.length;
3480
4048
  } catch (error) {
3481
4049
  failed += batch.chunks.length;
4050
+ this.logger.recordEmbeddingError();
3482
4051
  stillFailing.push({
3483
4052
  ...batch,
3484
4053
  attemptCount: batch.attemptCount + 1,
@@ -3513,6 +4082,9 @@ var Indexer = class {
3513
4082
  const { database } = await this.ensureInitialized();
3514
4083
  return database.getStats();
3515
4084
  }
4085
+ getLogger() {
4086
+ return this.logger;
4087
+ }
3516
4088
  };
3517
4089
 
3518
4090
  // node_modules/chokidar/index.js
@@ -3605,7 +4177,7 @@ var ReaddirpStream = class extends Readable {
3605
4177
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
3606
4178
  const statMethod = opts.lstat ? lstat : stat;
3607
4179
  if (wantBigintFsStats) {
3608
- this._stat = (path8) => statMethod(path8, { bigint: true });
4180
+ this._stat = (path9) => statMethod(path9, { bigint: true });
3609
4181
  } else {
3610
4182
  this._stat = statMethod;
3611
4183
  }
@@ -3630,8 +4202,8 @@ var ReaddirpStream = class extends Readable {
3630
4202
  const par = this.parent;
3631
4203
  const fil = par && par.files;
3632
4204
  if (fil && fil.length > 0) {
3633
- const { path: path8, depth } = par;
3634
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path8));
4205
+ const { path: path9, depth } = par;
4206
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path9));
3635
4207
  const awaited = await Promise.all(slice);
3636
4208
  for (const entry of awaited) {
3637
4209
  if (!entry)
@@ -3671,21 +4243,21 @@ var ReaddirpStream = class extends Readable {
3671
4243
  this.reading = false;
3672
4244
  }
3673
4245
  }
3674
- async _exploreDir(path8, depth) {
4246
+ async _exploreDir(path9, depth) {
3675
4247
  let files;
3676
4248
  try {
3677
- files = await readdir(path8, this._rdOptions);
4249
+ files = await readdir(path9, this._rdOptions);
3678
4250
  } catch (error) {
3679
4251
  this._onError(error);
3680
4252
  }
3681
- return { files, depth, path: path8 };
4253
+ return { files, depth, path: path9 };
3682
4254
  }
3683
- async _formatEntry(dirent, path8) {
4255
+ async _formatEntry(dirent, path9) {
3684
4256
  let entry;
3685
- const basename3 = this._isDirent ? dirent.name : dirent;
4257
+ const basename4 = this._isDirent ? dirent.name : dirent;
3686
4258
  try {
3687
- const fullPath = presolve(pjoin(path8, basename3));
3688
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
4259
+ const fullPath = presolve(pjoin(path9, basename4));
4260
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename4 };
3689
4261
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
3690
4262
  } catch (err) {
3691
4263
  this._onError(err);
@@ -4084,16 +4656,16 @@ var delFromSet = (main, prop, item) => {
4084
4656
  };
4085
4657
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
4086
4658
  var FsWatchInstances = /* @__PURE__ */ new Map();
4087
- function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
4659
+ function createFsWatchInstance(path9, options, listener, errHandler, emitRaw) {
4088
4660
  const handleEvent = (rawEvent, evPath) => {
4089
- listener(path8);
4090
- emitRaw(rawEvent, evPath, { watchedPath: path8 });
4091
- if (evPath && path8 !== evPath) {
4092
- fsWatchBroadcast(sp.resolve(path8, evPath), KEY_LISTENERS, sp.join(path8, evPath));
4661
+ listener(path9);
4662
+ emitRaw(rawEvent, evPath, { watchedPath: path9 });
4663
+ if (evPath && path9 !== evPath) {
4664
+ fsWatchBroadcast(sp.resolve(path9, evPath), KEY_LISTENERS, sp.join(path9, evPath));
4093
4665
  }
4094
4666
  };
4095
4667
  try {
4096
- return fs_watch(path8, {
4668
+ return fs_watch(path9, {
4097
4669
  persistent: options.persistent
4098
4670
  }, handleEvent);
4099
4671
  } catch (error) {
@@ -4109,12 +4681,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
4109
4681
  listener(val1, val2, val3);
4110
4682
  });
4111
4683
  };
4112
- var setFsWatchListener = (path8, fullPath, options, handlers) => {
4684
+ var setFsWatchListener = (path9, fullPath, options, handlers) => {
4113
4685
  const { listener, errHandler, rawEmitter } = handlers;
4114
4686
  let cont = FsWatchInstances.get(fullPath);
4115
4687
  let watcher;
4116
4688
  if (!options.persistent) {
4117
- watcher = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
4689
+ watcher = createFsWatchInstance(path9, options, listener, errHandler, rawEmitter);
4118
4690
  if (!watcher)
4119
4691
  return;
4120
4692
  return watcher.close.bind(watcher);
@@ -4125,7 +4697,7 @@ var setFsWatchListener = (path8, fullPath, options, handlers) => {
4125
4697
  addAndConvert(cont, KEY_RAW, rawEmitter);
4126
4698
  } else {
4127
4699
  watcher = createFsWatchInstance(
4128
- path8,
4700
+ path9,
4129
4701
  options,
4130
4702
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
4131
4703
  errHandler,
@@ -4140,7 +4712,7 @@ var setFsWatchListener = (path8, fullPath, options, handlers) => {
4140
4712
  cont.watcherUnusable = true;
4141
4713
  if (isWindows && error.code === "EPERM") {
4142
4714
  try {
4143
- const fd = await open(path8, "r");
4715
+ const fd = await open(path9, "r");
4144
4716
  await fd.close();
4145
4717
  broadcastErr(error);
4146
4718
  } catch (err) {
@@ -4171,7 +4743,7 @@ var setFsWatchListener = (path8, fullPath, options, handlers) => {
4171
4743
  };
4172
4744
  };
4173
4745
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
4174
- var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
4746
+ var setFsWatchFileListener = (path9, fullPath, options, handlers) => {
4175
4747
  const { listener, rawEmitter } = handlers;
4176
4748
  let cont = FsWatchFileInstances.get(fullPath);
4177
4749
  const copts = cont && cont.options;
@@ -4193,7 +4765,7 @@ var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
4193
4765
  });
4194
4766
  const currmtime = curr.mtimeMs;
4195
4767
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
4196
- foreach(cont.listeners, (listener2) => listener2(path8, curr));
4768
+ foreach(cont.listeners, (listener2) => listener2(path9, curr));
4197
4769
  }
4198
4770
  })
4199
4771
  };
@@ -4223,13 +4795,13 @@ var NodeFsHandler = class {
4223
4795
  * @param listener on fs change
4224
4796
  * @returns closer for the watcher instance
4225
4797
  */
4226
- _watchWithNodeFs(path8, listener) {
4798
+ _watchWithNodeFs(path9, listener) {
4227
4799
  const opts = this.fsw.options;
4228
- const directory = sp.dirname(path8);
4229
- const basename3 = sp.basename(path8);
4800
+ const directory = sp.dirname(path9);
4801
+ const basename4 = sp.basename(path9);
4230
4802
  const parent = this.fsw._getWatchedDir(directory);
4231
- parent.add(basename3);
4232
- const absolutePath = sp.resolve(path8);
4803
+ parent.add(basename4);
4804
+ const absolutePath = sp.resolve(path9);
4233
4805
  const options = {
4234
4806
  persistent: opts.persistent
4235
4807
  };
@@ -4238,13 +4810,13 @@ var NodeFsHandler = class {
4238
4810
  let closer;
4239
4811
  if (opts.usePolling) {
4240
4812
  const enableBin = opts.interval !== opts.binaryInterval;
4241
- options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
4242
- closer = setFsWatchFileListener(path8, absolutePath, options, {
4813
+ options.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
4814
+ closer = setFsWatchFileListener(path9, absolutePath, options, {
4243
4815
  listener,
4244
4816
  rawEmitter: this.fsw._emitRaw
4245
4817
  });
4246
4818
  } else {
4247
- closer = setFsWatchListener(path8, absolutePath, options, {
4819
+ closer = setFsWatchListener(path9, absolutePath, options, {
4248
4820
  listener,
4249
4821
  errHandler: this._boundHandleError,
4250
4822
  rawEmitter: this.fsw._emitRaw
@@ -4260,13 +4832,13 @@ var NodeFsHandler = class {
4260
4832
  if (this.fsw.closed) {
4261
4833
  return;
4262
4834
  }
4263
- const dirname4 = sp.dirname(file);
4264
- const basename3 = sp.basename(file);
4265
- const parent = this.fsw._getWatchedDir(dirname4);
4835
+ const dirname5 = sp.dirname(file);
4836
+ const basename4 = sp.basename(file);
4837
+ const parent = this.fsw._getWatchedDir(dirname5);
4266
4838
  let prevStats = stats;
4267
- if (parent.has(basename3))
4839
+ if (parent.has(basename4))
4268
4840
  return;
4269
- const listener = async (path8, newStats) => {
4841
+ const listener = async (path9, newStats) => {
4270
4842
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
4271
4843
  return;
4272
4844
  if (!newStats || newStats.mtimeMs === 0) {
@@ -4280,18 +4852,18 @@ var NodeFsHandler = class {
4280
4852
  this.fsw._emit(EV.CHANGE, file, newStats2);
4281
4853
  }
4282
4854
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
4283
- this.fsw._closeFile(path8);
4855
+ this.fsw._closeFile(path9);
4284
4856
  prevStats = newStats2;
4285
4857
  const closer2 = this._watchWithNodeFs(file, listener);
4286
4858
  if (closer2)
4287
- this.fsw._addPathCloser(path8, closer2);
4859
+ this.fsw._addPathCloser(path9, closer2);
4288
4860
  } else {
4289
4861
  prevStats = newStats2;
4290
4862
  }
4291
4863
  } catch (error) {
4292
- this.fsw._remove(dirname4, basename3);
4864
+ this.fsw._remove(dirname5, basename4);
4293
4865
  }
4294
- } else if (parent.has(basename3)) {
4866
+ } else if (parent.has(basename4)) {
4295
4867
  const at = newStats.atimeMs;
4296
4868
  const mt = newStats.mtimeMs;
4297
4869
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -4316,7 +4888,7 @@ var NodeFsHandler = class {
4316
4888
  * @param item basename of this item
4317
4889
  * @returns true if no more processing is needed for this entry.
4318
4890
  */
4319
- async _handleSymlink(entry, directory, path8, item) {
4891
+ async _handleSymlink(entry, directory, path9, item) {
4320
4892
  if (this.fsw.closed) {
4321
4893
  return;
4322
4894
  }
@@ -4326,7 +4898,7 @@ var NodeFsHandler = class {
4326
4898
  this.fsw._incrReadyCount();
4327
4899
  let linkPath;
4328
4900
  try {
4329
- linkPath = await fsrealpath(path8);
4901
+ linkPath = await fsrealpath(path9);
4330
4902
  } catch (e) {
4331
4903
  this.fsw._emitReady();
4332
4904
  return true;
@@ -4336,12 +4908,12 @@ var NodeFsHandler = class {
4336
4908
  if (dir.has(item)) {
4337
4909
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
4338
4910
  this.fsw._symlinkPaths.set(full, linkPath);
4339
- this.fsw._emit(EV.CHANGE, path8, entry.stats);
4911
+ this.fsw._emit(EV.CHANGE, path9, entry.stats);
4340
4912
  }
4341
4913
  } else {
4342
4914
  dir.add(item);
4343
4915
  this.fsw._symlinkPaths.set(full, linkPath);
4344
- this.fsw._emit(EV.ADD, path8, entry.stats);
4916
+ this.fsw._emit(EV.ADD, path9, entry.stats);
4345
4917
  }
4346
4918
  this.fsw._emitReady();
4347
4919
  return true;
@@ -4371,9 +4943,9 @@ var NodeFsHandler = class {
4371
4943
  return;
4372
4944
  }
4373
4945
  const item = entry.path;
4374
- let path8 = sp.join(directory, item);
4946
+ let path9 = sp.join(directory, item);
4375
4947
  current.add(item);
4376
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
4948
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path9, item)) {
4377
4949
  return;
4378
4950
  }
4379
4951
  if (this.fsw.closed) {
@@ -4382,8 +4954,8 @@ var NodeFsHandler = class {
4382
4954
  }
4383
4955
  if (item === target || !target && !previous.has(item)) {
4384
4956
  this.fsw._incrReadyCount();
4385
- path8 = sp.join(dir, sp.relative(dir, path8));
4386
- this._addToNodeFs(path8, initialAdd, wh, depth + 1);
4957
+ path9 = sp.join(dir, sp.relative(dir, path9));
4958
+ this._addToNodeFs(path9, initialAdd, wh, depth + 1);
4387
4959
  }
4388
4960
  }).on(EV.ERROR, this._boundHandleError);
4389
4961
  return new Promise((resolve4, reject) => {
@@ -4452,13 +5024,13 @@ var NodeFsHandler = class {
4452
5024
  * @param depth Child path actually targeted for watch
4453
5025
  * @param target Child path actually targeted for watch
4454
5026
  */
4455
- async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
5027
+ async _addToNodeFs(path9, initialAdd, priorWh, depth, target) {
4456
5028
  const ready = this.fsw._emitReady;
4457
- if (this.fsw._isIgnored(path8) || this.fsw.closed) {
5029
+ if (this.fsw._isIgnored(path9) || this.fsw.closed) {
4458
5030
  ready();
4459
5031
  return false;
4460
5032
  }
4461
- const wh = this.fsw._getWatchHelpers(path8);
5033
+ const wh = this.fsw._getWatchHelpers(path9);
4462
5034
  if (priorWh) {
4463
5035
  wh.filterPath = (entry) => priorWh.filterPath(entry);
4464
5036
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -4474,8 +5046,8 @@ var NodeFsHandler = class {
4474
5046
  const follow = this.fsw.options.followSymlinks;
4475
5047
  let closer;
4476
5048
  if (stats.isDirectory()) {
4477
- const absPath = sp.resolve(path8);
4478
- const targetPath = follow ? await fsrealpath(path8) : path8;
5049
+ const absPath = sp.resolve(path9);
5050
+ const targetPath = follow ? await fsrealpath(path9) : path9;
4479
5051
  if (this.fsw.closed)
4480
5052
  return;
4481
5053
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -4485,29 +5057,29 @@ var NodeFsHandler = class {
4485
5057
  this.fsw._symlinkPaths.set(absPath, targetPath);
4486
5058
  }
4487
5059
  } else if (stats.isSymbolicLink()) {
4488
- const targetPath = follow ? await fsrealpath(path8) : path8;
5060
+ const targetPath = follow ? await fsrealpath(path9) : path9;
4489
5061
  if (this.fsw.closed)
4490
5062
  return;
4491
5063
  const parent = sp.dirname(wh.watchPath);
4492
5064
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
4493
5065
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
4494
- closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
5066
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path9, wh, targetPath);
4495
5067
  if (this.fsw.closed)
4496
5068
  return;
4497
5069
  if (targetPath !== void 0) {
4498
- this.fsw._symlinkPaths.set(sp.resolve(path8), targetPath);
5070
+ this.fsw._symlinkPaths.set(sp.resolve(path9), targetPath);
4499
5071
  }
4500
5072
  } else {
4501
5073
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
4502
5074
  }
4503
5075
  ready();
4504
5076
  if (closer)
4505
- this.fsw._addPathCloser(path8, closer);
5077
+ this.fsw._addPathCloser(path9, closer);
4506
5078
  return false;
4507
5079
  } catch (error) {
4508
5080
  if (this.fsw._handleError(error)) {
4509
5081
  ready();
4510
- return path8;
5082
+ return path9;
4511
5083
  }
4512
5084
  }
4513
5085
  }
@@ -4550,24 +5122,24 @@ function createPattern(matcher) {
4550
5122
  }
4551
5123
  return () => false;
4552
5124
  }
4553
- function normalizePath(path8) {
4554
- if (typeof path8 !== "string")
5125
+ function normalizePath(path9) {
5126
+ if (typeof path9 !== "string")
4555
5127
  throw new Error("string expected");
4556
- path8 = sp2.normalize(path8);
4557
- path8 = path8.replace(/\\/g, "/");
5128
+ path9 = sp2.normalize(path9);
5129
+ path9 = path9.replace(/\\/g, "/");
4558
5130
  let prepend = false;
4559
- if (path8.startsWith("//"))
5131
+ if (path9.startsWith("//"))
4560
5132
  prepend = true;
4561
- path8 = path8.replace(DOUBLE_SLASH_RE, "/");
5133
+ path9 = path9.replace(DOUBLE_SLASH_RE, "/");
4562
5134
  if (prepend)
4563
- path8 = "/" + path8;
4564
- return path8;
5135
+ path9 = "/" + path9;
5136
+ return path9;
4565
5137
  }
4566
5138
  function matchPatterns(patterns, testString, stats) {
4567
- const path8 = normalizePath(testString);
5139
+ const path9 = normalizePath(testString);
4568
5140
  for (let index = 0; index < patterns.length; index++) {
4569
5141
  const pattern = patterns[index];
4570
- if (pattern(path8, stats)) {
5142
+ if (pattern(path9, stats)) {
4571
5143
  return true;
4572
5144
  }
4573
5145
  }
@@ -4605,19 +5177,19 @@ var toUnix = (string) => {
4605
5177
  }
4606
5178
  return str;
4607
5179
  };
4608
- var normalizePathToUnix = (path8) => toUnix(sp2.normalize(toUnix(path8)));
4609
- var normalizeIgnored = (cwd = "") => (path8) => {
4610
- if (typeof path8 === "string") {
4611
- return normalizePathToUnix(sp2.isAbsolute(path8) ? path8 : sp2.join(cwd, path8));
5180
+ var normalizePathToUnix = (path9) => toUnix(sp2.normalize(toUnix(path9)));
5181
+ var normalizeIgnored = (cwd = "") => (path9) => {
5182
+ if (typeof path9 === "string") {
5183
+ return normalizePathToUnix(sp2.isAbsolute(path9) ? path9 : sp2.join(cwd, path9));
4612
5184
  } else {
4613
- return path8;
5185
+ return path9;
4614
5186
  }
4615
5187
  };
4616
- var getAbsolutePath = (path8, cwd) => {
4617
- if (sp2.isAbsolute(path8)) {
4618
- return path8;
5188
+ var getAbsolutePath = (path9, cwd) => {
5189
+ if (sp2.isAbsolute(path9)) {
5190
+ return path9;
4619
5191
  }
4620
- return sp2.join(cwd, path8);
5192
+ return sp2.join(cwd, path9);
4621
5193
  };
4622
5194
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
4623
5195
  var DirEntry = class {
@@ -4682,10 +5254,10 @@ var WatchHelper = class {
4682
5254
  dirParts;
4683
5255
  followSymlinks;
4684
5256
  statMethod;
4685
- constructor(path8, follow, fsw) {
5257
+ constructor(path9, follow, fsw) {
4686
5258
  this.fsw = fsw;
4687
- const watchPath = path8;
4688
- this.path = path8 = path8.replace(REPLACER_RE, "");
5259
+ const watchPath = path9;
5260
+ this.path = path9 = path9.replace(REPLACER_RE, "");
4689
5261
  this.watchPath = watchPath;
4690
5262
  this.fullWatchPath = sp2.resolve(watchPath);
4691
5263
  this.dirParts = [];
@@ -4825,20 +5397,20 @@ var FSWatcher = class extends EventEmitter2 {
4825
5397
  this._closePromise = void 0;
4826
5398
  let paths = unifyPaths(paths_);
4827
5399
  if (cwd) {
4828
- paths = paths.map((path8) => {
4829
- const absPath = getAbsolutePath(path8, cwd);
5400
+ paths = paths.map((path9) => {
5401
+ const absPath = getAbsolutePath(path9, cwd);
4830
5402
  return absPath;
4831
5403
  });
4832
5404
  }
4833
- paths.forEach((path8) => {
4834
- this._removeIgnoredPath(path8);
5405
+ paths.forEach((path9) => {
5406
+ this._removeIgnoredPath(path9);
4835
5407
  });
4836
5408
  this._userIgnored = void 0;
4837
5409
  if (!this._readyCount)
4838
5410
  this._readyCount = 0;
4839
5411
  this._readyCount += paths.length;
4840
- Promise.all(paths.map(async (path8) => {
4841
- const res = await this._nodeFsHandler._addToNodeFs(path8, !_internal, void 0, 0, _origAdd);
5412
+ Promise.all(paths.map(async (path9) => {
5413
+ const res = await this._nodeFsHandler._addToNodeFs(path9, !_internal, void 0, 0, _origAdd);
4842
5414
  if (res)
4843
5415
  this._emitReady();
4844
5416
  return res;
@@ -4860,17 +5432,17 @@ var FSWatcher = class extends EventEmitter2 {
4860
5432
  return this;
4861
5433
  const paths = unifyPaths(paths_);
4862
5434
  const { cwd } = this.options;
4863
- paths.forEach((path8) => {
4864
- if (!sp2.isAbsolute(path8) && !this._closers.has(path8)) {
5435
+ paths.forEach((path9) => {
5436
+ if (!sp2.isAbsolute(path9) && !this._closers.has(path9)) {
4865
5437
  if (cwd)
4866
- path8 = sp2.join(cwd, path8);
4867
- path8 = sp2.resolve(path8);
5438
+ path9 = sp2.join(cwd, path9);
5439
+ path9 = sp2.resolve(path9);
4868
5440
  }
4869
- this._closePath(path8);
4870
- this._addIgnoredPath(path8);
4871
- if (this._watched.has(path8)) {
5441
+ this._closePath(path9);
5442
+ this._addIgnoredPath(path9);
5443
+ if (this._watched.has(path9)) {
4872
5444
  this._addIgnoredPath({
4873
- path: path8,
5445
+ path: path9,
4874
5446
  recursive: true
4875
5447
  });
4876
5448
  }
@@ -4934,38 +5506,38 @@ var FSWatcher = class extends EventEmitter2 {
4934
5506
  * @param stats arguments to be passed with event
4935
5507
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
4936
5508
  */
4937
- async _emit(event, path8, stats) {
5509
+ async _emit(event, path9, stats) {
4938
5510
  if (this.closed)
4939
5511
  return;
4940
5512
  const opts = this.options;
4941
5513
  if (isWindows)
4942
- path8 = sp2.normalize(path8);
5514
+ path9 = sp2.normalize(path9);
4943
5515
  if (opts.cwd)
4944
- path8 = sp2.relative(opts.cwd, path8);
4945
- const args = [path8];
5516
+ path9 = sp2.relative(opts.cwd, path9);
5517
+ const args = [path9];
4946
5518
  if (stats != null)
4947
5519
  args.push(stats);
4948
5520
  const awf = opts.awaitWriteFinish;
4949
5521
  let pw;
4950
- if (awf && (pw = this._pendingWrites.get(path8))) {
5522
+ if (awf && (pw = this._pendingWrites.get(path9))) {
4951
5523
  pw.lastChange = /* @__PURE__ */ new Date();
4952
5524
  return this;
4953
5525
  }
4954
5526
  if (opts.atomic) {
4955
5527
  if (event === EVENTS.UNLINK) {
4956
- this._pendingUnlinks.set(path8, [event, ...args]);
5528
+ this._pendingUnlinks.set(path9, [event, ...args]);
4957
5529
  setTimeout(() => {
4958
- this._pendingUnlinks.forEach((entry, path9) => {
5530
+ this._pendingUnlinks.forEach((entry, path10) => {
4959
5531
  this.emit(...entry);
4960
5532
  this.emit(EVENTS.ALL, ...entry);
4961
- this._pendingUnlinks.delete(path9);
5533
+ this._pendingUnlinks.delete(path10);
4962
5534
  });
4963
5535
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
4964
5536
  return this;
4965
5537
  }
4966
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
5538
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path9)) {
4967
5539
  event = EVENTS.CHANGE;
4968
- this._pendingUnlinks.delete(path8);
5540
+ this._pendingUnlinks.delete(path9);
4969
5541
  }
4970
5542
  }
4971
5543
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -4983,16 +5555,16 @@ var FSWatcher = class extends EventEmitter2 {
4983
5555
  this.emitWithAll(event, args);
4984
5556
  }
4985
5557
  };
4986
- this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
5558
+ this._awaitWriteFinish(path9, awf.stabilityThreshold, event, awfEmit);
4987
5559
  return this;
4988
5560
  }
4989
5561
  if (event === EVENTS.CHANGE) {
4990
- const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
5562
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path9, 50);
4991
5563
  if (isThrottled)
4992
5564
  return this;
4993
5565
  }
4994
5566
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
4995
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path8) : path8;
5567
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path9) : path9;
4996
5568
  let stats2;
4997
5569
  try {
4998
5570
  stats2 = await stat3(fullPath);
@@ -5023,23 +5595,23 @@ var FSWatcher = class extends EventEmitter2 {
5023
5595
  * @param timeout duration of time to suppress duplicate actions
5024
5596
  * @returns tracking object or false if action should be suppressed
5025
5597
  */
5026
- _throttle(actionType, path8, timeout) {
5598
+ _throttle(actionType, path9, timeout) {
5027
5599
  if (!this._throttled.has(actionType)) {
5028
5600
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
5029
5601
  }
5030
5602
  const action = this._throttled.get(actionType);
5031
5603
  if (!action)
5032
5604
  throw new Error("invalid throttle");
5033
- const actionPath = action.get(path8);
5605
+ const actionPath = action.get(path9);
5034
5606
  if (actionPath) {
5035
5607
  actionPath.count++;
5036
5608
  return false;
5037
5609
  }
5038
5610
  let timeoutObject;
5039
5611
  const clear = () => {
5040
- const item = action.get(path8);
5612
+ const item = action.get(path9);
5041
5613
  const count = item ? item.count : 0;
5042
- action.delete(path8);
5614
+ action.delete(path9);
5043
5615
  clearTimeout(timeoutObject);
5044
5616
  if (item)
5045
5617
  clearTimeout(item.timeoutObject);
@@ -5047,7 +5619,7 @@ var FSWatcher = class extends EventEmitter2 {
5047
5619
  };
5048
5620
  timeoutObject = setTimeout(clear, timeout);
5049
5621
  const thr = { timeoutObject, clear, count: 0 };
5050
- action.set(path8, thr);
5622
+ action.set(path9, thr);
5051
5623
  return thr;
5052
5624
  }
5053
5625
  _incrReadyCount() {
@@ -5061,44 +5633,44 @@ var FSWatcher = class extends EventEmitter2 {
5061
5633
  * @param event
5062
5634
  * @param awfEmit Callback to be called when ready for event to be emitted.
5063
5635
  */
5064
- _awaitWriteFinish(path8, threshold, event, awfEmit) {
5636
+ _awaitWriteFinish(path9, threshold, event, awfEmit) {
5065
5637
  const awf = this.options.awaitWriteFinish;
5066
5638
  if (typeof awf !== "object")
5067
5639
  return;
5068
5640
  const pollInterval = awf.pollInterval;
5069
5641
  let timeoutHandler;
5070
- let fullPath = path8;
5071
- if (this.options.cwd && !sp2.isAbsolute(path8)) {
5072
- fullPath = sp2.join(this.options.cwd, path8);
5642
+ let fullPath = path9;
5643
+ if (this.options.cwd && !sp2.isAbsolute(path9)) {
5644
+ fullPath = sp2.join(this.options.cwd, path9);
5073
5645
  }
5074
5646
  const now = /* @__PURE__ */ new Date();
5075
5647
  const writes = this._pendingWrites;
5076
5648
  function awaitWriteFinishFn(prevStat) {
5077
5649
  statcb(fullPath, (err, curStat) => {
5078
- if (err || !writes.has(path8)) {
5650
+ if (err || !writes.has(path9)) {
5079
5651
  if (err && err.code !== "ENOENT")
5080
5652
  awfEmit(err);
5081
5653
  return;
5082
5654
  }
5083
5655
  const now2 = Number(/* @__PURE__ */ new Date());
5084
5656
  if (prevStat && curStat.size !== prevStat.size) {
5085
- writes.get(path8).lastChange = now2;
5657
+ writes.get(path9).lastChange = now2;
5086
5658
  }
5087
- const pw = writes.get(path8);
5659
+ const pw = writes.get(path9);
5088
5660
  const df = now2 - pw.lastChange;
5089
5661
  if (df >= threshold) {
5090
- writes.delete(path8);
5662
+ writes.delete(path9);
5091
5663
  awfEmit(void 0, curStat);
5092
5664
  } else {
5093
5665
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
5094
5666
  }
5095
5667
  });
5096
5668
  }
5097
- if (!writes.has(path8)) {
5098
- writes.set(path8, {
5669
+ if (!writes.has(path9)) {
5670
+ writes.set(path9, {
5099
5671
  lastChange: now,
5100
5672
  cancelWait: () => {
5101
- writes.delete(path8);
5673
+ writes.delete(path9);
5102
5674
  clearTimeout(timeoutHandler);
5103
5675
  return event;
5104
5676
  }
@@ -5109,8 +5681,8 @@ var FSWatcher = class extends EventEmitter2 {
5109
5681
  /**
5110
5682
  * Determines whether user has asked to ignore this path.
5111
5683
  */
5112
- _isIgnored(path8, stats) {
5113
- if (this.options.atomic && DOT_RE.test(path8))
5684
+ _isIgnored(path9, stats) {
5685
+ if (this.options.atomic && DOT_RE.test(path9))
5114
5686
  return true;
5115
5687
  if (!this._userIgnored) {
5116
5688
  const { cwd } = this.options;
@@ -5120,17 +5692,17 @@ var FSWatcher = class extends EventEmitter2 {
5120
5692
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
5121
5693
  this._userIgnored = anymatch(list, void 0);
5122
5694
  }
5123
- return this._userIgnored(path8, stats);
5695
+ return this._userIgnored(path9, stats);
5124
5696
  }
5125
- _isntIgnored(path8, stat4) {
5126
- return !this._isIgnored(path8, stat4);
5697
+ _isntIgnored(path9, stat4) {
5698
+ return !this._isIgnored(path9, stat4);
5127
5699
  }
5128
5700
  /**
5129
5701
  * Provides a set of common helpers and properties relating to symlink handling.
5130
5702
  * @param path file or directory pattern being watched
5131
5703
  */
5132
- _getWatchHelpers(path8) {
5133
- return new WatchHelper(path8, this.options.followSymlinks, this);
5704
+ _getWatchHelpers(path9) {
5705
+ return new WatchHelper(path9, this.options.followSymlinks, this);
5134
5706
  }
5135
5707
  // Directory helpers
5136
5708
  // -----------------
@@ -5162,63 +5734,63 @@ var FSWatcher = class extends EventEmitter2 {
5162
5734
  * @param item base path of item/directory
5163
5735
  */
5164
5736
  _remove(directory, item, isDirectory) {
5165
- const path8 = sp2.join(directory, item);
5166
- const fullPath = sp2.resolve(path8);
5167
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path8) || this._watched.has(fullPath);
5168
- if (!this._throttle("remove", path8, 100))
5737
+ const path9 = sp2.join(directory, item);
5738
+ const fullPath = sp2.resolve(path9);
5739
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path9) || this._watched.has(fullPath);
5740
+ if (!this._throttle("remove", path9, 100))
5169
5741
  return;
5170
5742
  if (!isDirectory && this._watched.size === 1) {
5171
5743
  this.add(directory, item, true);
5172
5744
  }
5173
- const wp = this._getWatchedDir(path8);
5745
+ const wp = this._getWatchedDir(path9);
5174
5746
  const nestedDirectoryChildren = wp.getChildren();
5175
- nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
5747
+ nestedDirectoryChildren.forEach((nested) => this._remove(path9, nested));
5176
5748
  const parent = this._getWatchedDir(directory);
5177
5749
  const wasTracked = parent.has(item);
5178
5750
  parent.remove(item);
5179
5751
  if (this._symlinkPaths.has(fullPath)) {
5180
5752
  this._symlinkPaths.delete(fullPath);
5181
5753
  }
5182
- let relPath = path8;
5754
+ let relPath = path9;
5183
5755
  if (this.options.cwd)
5184
- relPath = sp2.relative(this.options.cwd, path8);
5756
+ relPath = sp2.relative(this.options.cwd, path9);
5185
5757
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
5186
5758
  const event = this._pendingWrites.get(relPath).cancelWait();
5187
5759
  if (event === EVENTS.ADD)
5188
5760
  return;
5189
5761
  }
5190
- this._watched.delete(path8);
5762
+ this._watched.delete(path9);
5191
5763
  this._watched.delete(fullPath);
5192
5764
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
5193
- if (wasTracked && !this._isIgnored(path8))
5194
- this._emit(eventName, path8);
5195
- this._closePath(path8);
5765
+ if (wasTracked && !this._isIgnored(path9))
5766
+ this._emit(eventName, path9);
5767
+ this._closePath(path9);
5196
5768
  }
5197
5769
  /**
5198
5770
  * Closes all watchers for a path
5199
5771
  */
5200
- _closePath(path8) {
5201
- this._closeFile(path8);
5202
- const dir = sp2.dirname(path8);
5203
- this._getWatchedDir(dir).remove(sp2.basename(path8));
5772
+ _closePath(path9) {
5773
+ this._closeFile(path9);
5774
+ const dir = sp2.dirname(path9);
5775
+ this._getWatchedDir(dir).remove(sp2.basename(path9));
5204
5776
  }
5205
5777
  /**
5206
5778
  * Closes only file-specific watchers
5207
5779
  */
5208
- _closeFile(path8) {
5209
- const closers = this._closers.get(path8);
5780
+ _closeFile(path9) {
5781
+ const closers = this._closers.get(path9);
5210
5782
  if (!closers)
5211
5783
  return;
5212
5784
  closers.forEach((closer) => closer());
5213
- this._closers.delete(path8);
5785
+ this._closers.delete(path9);
5214
5786
  }
5215
- _addPathCloser(path8, closer) {
5787
+ _addPathCloser(path9, closer) {
5216
5788
  if (!closer)
5217
5789
  return;
5218
- let list = this._closers.get(path8);
5790
+ let list = this._closers.get(path9);
5219
5791
  if (!list) {
5220
5792
  list = [];
5221
- this._closers.set(path8, list);
5793
+ this._closers.set(path9, list);
5222
5794
  }
5223
5795
  list.push(closer);
5224
5796
  }
@@ -5313,7 +5885,7 @@ var FileWatcher = class {
5313
5885
  return;
5314
5886
  }
5315
5887
  const changes = Array.from(this.pendingChanges.entries()).map(
5316
- ([path8, type]) => ({ path: path8, type })
5888
+ ([path9, type]) => ({ path: path9, type })
5317
5889
  );
5318
5890
  this.pendingChanges.clear();
5319
5891
  try {
@@ -5493,7 +6065,7 @@ var index_codebase = tool({
5493
6065
  estimateOnly: z.boolean().optional().default(false).describe("Only show cost estimate without indexing"),
5494
6066
  verbose: z.boolean().optional().default(false).describe("Show detailed info about skipped files and parsing failures")
5495
6067
  },
5496
- async execute(args) {
6068
+ async execute(args, context) {
5497
6069
  const indexer = getIndexer();
5498
6070
  if (args.estimateOnly) {
5499
6071
  const estimate = await indexer.estimateCost();
@@ -5502,7 +6074,19 @@ var index_codebase = tool({
5502
6074
  if (args.force) {
5503
6075
  await indexer.clearIndex();
5504
6076
  }
5505
- const stats = await indexer.index();
6077
+ const stats = await indexer.index((progress) => {
6078
+ context.metadata({
6079
+ title: formatProgressTitle(progress),
6080
+ metadata: {
6081
+ phase: progress.phase,
6082
+ filesProcessed: progress.filesProcessed,
6083
+ totalFiles: progress.totalFiles,
6084
+ chunksProcessed: progress.chunksProcessed,
6085
+ totalChunks: progress.totalChunks,
6086
+ percentage: calculatePercentage(progress)
6087
+ }
6088
+ });
6089
+ });
5506
6090
  return formatIndexStats(stats, args.verbose ?? false);
5507
6091
  }
5508
6092
  });
@@ -5540,6 +6124,51 @@ var index_health_check = tool({
5540
6124
  return lines.join("\n");
5541
6125
  }
5542
6126
  });
6127
+ var index_metrics = tool({
6128
+ description: "Get metrics and performance statistics for the codebase index. Shows indexing stats, search timings, cache hit rates, and API usage. Requires debug.enabled=true and debug.metrics=true in config.",
6129
+ args: {},
6130
+ async execute() {
6131
+ const indexer = getIndexer();
6132
+ const logger = indexer.getLogger();
6133
+ if (!logger.isEnabled()) {
6134
+ return 'Debug mode is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true,\n "metrics": true\n }\n}\n```';
6135
+ }
6136
+ if (!logger.isMetricsEnabled()) {
6137
+ return 'Metrics collection is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true,\n "metrics": true\n }\n}\n```';
6138
+ }
6139
+ return logger.formatMetrics();
6140
+ }
6141
+ });
6142
+ var index_logs = tool({
6143
+ description: "Get recent debug logs from the codebase indexer. Shows timestamped log entries with level and category. Requires debug.enabled=true in config.",
6144
+ args: {
6145
+ limit: z.number().optional().default(20).describe("Maximum number of log entries to return"),
6146
+ category: z.enum(["search", "embedding", "cache", "gc", "branch", "general"]).optional().describe("Filter by log category"),
6147
+ level: z.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by minimum log level")
6148
+ },
6149
+ async execute(args) {
6150
+ const indexer = getIndexer();
6151
+ const logger = indexer.getLogger();
6152
+ if (!logger.isEnabled()) {
6153
+ return 'Debug mode is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true\n }\n}\n```';
6154
+ }
6155
+ let logs;
6156
+ if (args.category) {
6157
+ logs = logger.getLogsByCategory(args.category, args.limit);
6158
+ } else if (args.level) {
6159
+ logs = logger.getLogsByLevel(args.level, args.limit);
6160
+ } else {
6161
+ logs = logger.getLogs(args.limit);
6162
+ }
6163
+ if (logs.length === 0) {
6164
+ return "No logs recorded yet. Logs are captured during indexing and search operations.";
6165
+ }
6166
+ return logs.map((l) => {
6167
+ const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
6168
+ return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
6169
+ }).join("\n");
6170
+ }
6171
+ });
5543
6172
  function formatIndexStats(stats, verbose = false) {
5544
6173
  const lines = [];
5545
6174
  if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
@@ -5601,12 +6230,90 @@ function formatStatus(status) {
5601
6230
  }
5602
6231
  return lines.join("\n");
5603
6232
  }
6233
+ function formatProgressTitle(progress) {
6234
+ switch (progress.phase) {
6235
+ case "scanning":
6236
+ return "Scanning files...";
6237
+ case "parsing":
6238
+ return `Parsing: ${progress.filesProcessed}/${progress.totalFiles} files`;
6239
+ case "embedding":
6240
+ return `Embedding: ${progress.chunksProcessed}/${progress.totalChunks} chunks`;
6241
+ case "storing":
6242
+ return "Storing index...";
6243
+ case "complete":
6244
+ return "Indexing complete";
6245
+ default:
6246
+ return "Indexing...";
6247
+ }
6248
+ }
6249
+ function calculatePercentage(progress) {
6250
+ if (progress.phase === "scanning") return 0;
6251
+ if (progress.phase === "complete") return 100;
6252
+ if (progress.phase === "parsing") {
6253
+ if (progress.totalFiles === 0) return 5;
6254
+ return Math.round(5 + progress.filesProcessed / progress.totalFiles * 15);
6255
+ }
6256
+ if (progress.phase === "embedding") {
6257
+ if (progress.totalChunks === 0) return 20;
6258
+ return Math.round(20 + progress.chunksProcessed / progress.totalChunks * 70);
6259
+ }
6260
+ if (progress.phase === "storing") return 95;
6261
+ return 0;
6262
+ }
6263
+
6264
+ // src/commands/loader.ts
6265
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
6266
+ import * as path7 from "path";
6267
+ function parseFrontmatter(content) {
6268
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
6269
+ const match = content.match(frontmatterRegex);
6270
+ if (!match) {
6271
+ return { frontmatter: {}, body: content.trim() };
6272
+ }
6273
+ const frontmatterLines = match[1].split("\n");
6274
+ const frontmatter = {};
6275
+ for (const line of frontmatterLines) {
6276
+ const colonIndex = line.indexOf(":");
6277
+ if (colonIndex > 0) {
6278
+ const key = line.slice(0, colonIndex).trim();
6279
+ const value = line.slice(colonIndex + 1).trim();
6280
+ frontmatter[key] = value;
6281
+ }
6282
+ }
6283
+ return { frontmatter, body: match[2].trim() };
6284
+ }
6285
+ function loadCommandsFromDirectory(commandsDir) {
6286
+ const commands = /* @__PURE__ */ new Map();
6287
+ if (!existsSync5(commandsDir)) {
6288
+ return commands;
6289
+ }
6290
+ const files = readdirSync2(commandsDir).filter((f) => f.endsWith(".md"));
6291
+ for (const file of files) {
6292
+ const filePath = path7.join(commandsDir, file);
6293
+ const content = readFileSync5(filePath, "utf-8");
6294
+ const { frontmatter, body } = parseFrontmatter(content);
6295
+ const name = path7.basename(file, ".md");
6296
+ const description = frontmatter.description || `Run the ${name} command`;
6297
+ commands.set(name, {
6298
+ description,
6299
+ template: body
6300
+ });
6301
+ }
6302
+ return commands;
6303
+ }
5604
6304
 
5605
6305
  // src/index.ts
6306
+ function getCommandsDir() {
6307
+ let currentDir = process.cwd();
6308
+ if (typeof import.meta !== "undefined" && import.meta.url) {
6309
+ currentDir = path8.dirname(fileURLToPath2(import.meta.url));
6310
+ }
6311
+ return path8.join(currentDir, "..", "commands");
6312
+ }
5606
6313
  function loadJsonFile(filePath) {
5607
6314
  try {
5608
- if (existsSync5(filePath)) {
5609
- const content = readFileSync5(filePath, "utf-8");
6315
+ if (existsSync6(filePath)) {
6316
+ const content = readFileSync6(filePath, "utf-8");
5610
6317
  return JSON.parse(content);
5611
6318
  }
5612
6319
  } catch {
@@ -5614,11 +6321,11 @@ function loadJsonFile(filePath) {
5614
6321
  return null;
5615
6322
  }
5616
6323
  function loadPluginConfig(projectRoot) {
5617
- const projectConfig = loadJsonFile(path7.join(projectRoot, ".opencode", "codebase-index.json"));
6324
+ const projectConfig = loadJsonFile(path8.join(projectRoot, ".opencode", "codebase-index.json"));
5618
6325
  if (projectConfig) {
5619
6326
  return projectConfig;
5620
6327
  }
5621
- const globalConfigPath = path7.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
6328
+ const globalConfigPath = path8.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
5622
6329
  const globalConfig = loadJsonFile(globalConfigPath);
5623
6330
  if (globalConfig) {
5624
6331
  return globalConfig;
@@ -5646,40 +6353,17 @@ var plugin = async ({ directory }) => {
5646
6353
  codebase_search,
5647
6354
  index_codebase,
5648
6355
  index_status,
5649
- index_health_check
6356
+ index_health_check,
6357
+ index_metrics,
6358
+ index_logs
5650
6359
  },
5651
6360
  async config(cfg) {
5652
6361
  cfg.command = cfg.command ?? {};
5653
- cfg.command["search"] = {
5654
- description: "Search codebase by meaning using semantic search",
5655
- template: `Use the \`codebase_search\` tool to find code related to: $ARGUMENTS
5656
-
5657
- If the index doesn't exist yet, run \`index_codebase\` first.
5658
-
5659
- Return the most relevant results with file paths and line numbers.`
5660
- };
5661
- cfg.command["find"] = {
5662
- description: "Find code using hybrid approach (semantic + grep)",
5663
- template: `Find code related to: $ARGUMENTS
5664
-
5665
- Strategy:
5666
- 1. First use \`codebase_search\` to find semantically related code
5667
- 2. From the results, identify specific function/class names
5668
- 3. Use grep to find all occurrences of those identifiers
5669
- 4. Combine findings into a comprehensive answer
5670
-
5671
- If the semantic index doesn't exist, run \`index_codebase\` first.`
5672
- };
5673
- cfg.command["index"] = {
5674
- description: "Index the codebase for semantic search",
5675
- template: `Run the \`index_codebase\` tool to create or update the semantic search index.
5676
-
5677
- Show progress and final statistics including:
5678
- - Number of files processed
5679
- - Number of chunks indexed
5680
- - Tokens used
5681
- - Duration`
5682
- };
6362
+ const commandsDir = getCommandsDir();
6363
+ const commands = loadCommandsFromDirectory(commandsDir);
6364
+ for (const [name, definition] of commands) {
6365
+ cfg.command[name] = definition;
6366
+ }
5683
6367
  }
5684
6368
  };
5685
6369
  };