opencode-codebase-index 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -685,7 +685,8 @@ var DEFAULT_INCLUDE = [
685
685
  "**/*.{sh,bash,zsh}",
686
686
  "**/*.{txt,html,htm}",
687
687
  "**/*.zig",
688
- "**/*.gd"
688
+ "**/*.gd",
689
+ "**/*.metal"
689
690
  ];
690
691
  var DEFAULT_EXCLUDE = [
691
692
  "**/node_modules/**",
@@ -876,6 +877,9 @@ function isValidProvider(value) {
876
877
  return typeof value === "string" && Object.keys(EMBEDDING_MODELS).includes(value);
877
878
  }
878
879
  function isValidModel(value, provider) {
880
+ if (typeof value === "string" && provider === "ollama" && value.trim().length > 0) {
881
+ return true;
882
+ }
879
883
  return typeof value === "string" && Object.keys(EMBEDDING_MODELS[provider]).includes(value);
880
884
  }
881
885
  function isValidScope(value) {
@@ -2458,6 +2462,9 @@ function loadOpenCodeAuth() {
2458
2462
  return {};
2459
2463
  }
2460
2464
  async function detectEmbeddingProvider(preferredProvider, model) {
2465
+ if (preferredProvider === "ollama") {
2466
+ return detectOllamaProvider(model);
2467
+ }
2461
2468
  const credentials = await getProviderCredentials(preferredProvider);
2462
2469
  if (credentials) {
2463
2470
  if (!model) {
@@ -2473,10 +2480,16 @@ async function detectEmbeddingProvider(preferredProvider, model) {
2473
2480
  );
2474
2481
  }
2475
2482
  const providerModels = EMBEDDING_MODELS[preferredProvider];
2483
+ const modelInfo = Object.values(providerModels).find((candidate) => candidate.model === model);
2484
+ if (!modelInfo) {
2485
+ throw new Error(
2486
+ `Model '${model}' is not supported by provider '${preferredProvider}'`
2487
+ );
2488
+ }
2476
2489
  return {
2477
2490
  provider: preferredProvider,
2478
2491
  credentials,
2479
- modelInfo: providerModels[model]
2492
+ modelInfo
2480
2493
  };
2481
2494
  }
2482
2495
  throw new Error(
@@ -2485,6 +2498,13 @@ async function detectEmbeddingProvider(preferredProvider, model) {
2485
2498
  }
2486
2499
  async function tryDetectProvider() {
2487
2500
  for (const provider of autoDetectProviders) {
2501
+ if (provider === "ollama") {
2502
+ const ollamaProvider = await tryDetectOllamaProvider();
2503
+ if (ollamaProvider) {
2504
+ return ollamaProvider;
2505
+ }
2506
+ continue;
2507
+ }
2488
2508
  const credentials = await getProviderCredentials(provider);
2489
2509
  if (credentials) {
2490
2510
  return {
@@ -2552,32 +2572,105 @@ function getGoogleCredentials() {
2552
2572
  }
2553
2573
  return null;
2554
2574
  }
2555
- async function getOllamaCredentials() {
2556
- const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
2575
+ async function fetchOllama(url, init) {
2576
+ const controller = new AbortController();
2577
+ const timeoutId = setTimeout(() => controller.abort(), 2e3);
2557
2578
  try {
2558
- const controller = new AbortController();
2559
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
2560
- const response = await fetch(`${baseUrl}/api/tags`, {
2561
- signal: controller.signal
2562
- });
2579
+ return await fetch(url, { ...init, signal: controller.signal });
2580
+ } finally {
2563
2581
  clearTimeout(timeoutId);
2582
+ }
2583
+ }
2584
+ async function getOllamaCredentials() {
2585
+ const baseUrl = (process.env.OLLAMA_HOST || "http://localhost:11434").replace(/\/+$/, "");
2586
+ try {
2587
+ const response = await fetchOllama(`${baseUrl}/api/tags`);
2564
2588
  if (response.ok) {
2565
- const data = await response.json();
2566
- const hasEmbeddingModel = data.models?.some(
2567
- (m) => m.name.includes("nomic-embed") || m.name.includes("mxbai-embed") || m.name.includes("all-minilm")
2568
- );
2569
- if (hasEmbeddingModel) {
2570
- return {
2571
- provider: "ollama",
2572
- baseUrl
2573
- };
2574
- }
2589
+ return {
2590
+ provider: "ollama",
2591
+ baseUrl
2592
+ };
2575
2593
  }
2576
2594
  } catch {
2577
2595
  return null;
2578
2596
  }
2579
2597
  return null;
2580
2598
  }
2599
+ function findCatalogOllamaModel(model) {
2600
+ const stableName = model.endsWith(":latest") ? model.slice(0, -":latest".length) : model;
2601
+ return Object.values(EMBEDDING_MODELS.ollama).find((candidate) => candidate.model === stableName) ?? null;
2602
+ }
2603
+ function getPositiveIntegerMetadata(modelInfo, suffix) {
2604
+ const values = Object.entries(modelInfo).filter(([key]) => key.endsWith(suffix)).map(([, value]) => value).filter((value) => typeof value === "number" && Number.isInteger(value) && value > 0);
2605
+ return values.length === 1 ? values[0] : null;
2606
+ }
2607
+ async function fetchOllamaModelInfo(credentials, model) {
2608
+ const response = await fetchOllama(`${credentials.baseUrl}/api/show`, {
2609
+ method: "POST",
2610
+ headers: { "Content-Type": "application/json" },
2611
+ body: JSON.stringify({ model })
2612
+ });
2613
+ if (!response.ok) {
2614
+ return null;
2615
+ }
2616
+ const data = await response.json();
2617
+ if (!data.capabilities?.includes("embedding") || !data.model_info) {
2618
+ return null;
2619
+ }
2620
+ const dimensions = getPositiveIntegerMetadata(data.model_info, ".embedding_length");
2621
+ const maxTokens = getPositiveIntegerMetadata(data.model_info, ".context_length");
2622
+ if (!dimensions || !maxTokens) {
2623
+ return null;
2624
+ }
2625
+ return {
2626
+ provider: "ollama",
2627
+ model,
2628
+ dimensions,
2629
+ maxTokens,
2630
+ costPer1MTokens: 0
2631
+ };
2632
+ }
2633
+ async function listOllamaModels(credentials) {
2634
+ const response = await fetchOllama(`${credentials.baseUrl}/api/tags`);
2635
+ if (!response.ok) {
2636
+ return [];
2637
+ }
2638
+ const data = await response.json();
2639
+ return [...new Set((data.models ?? []).map((entry) => entry.name ?? entry.model).filter((name) => typeof name === "string" && name.trim().length > 0))];
2640
+ }
2641
+ async function detectOllamaProvider(model) {
2642
+ const credentials = await getOllamaCredentials();
2643
+ if (!credentials) {
2644
+ throw new Error("Preferred provider 'ollama' is not configured or authenticated");
2645
+ }
2646
+ const requestedModel = model?.trim();
2647
+ if (requestedModel) {
2648
+ const catalogModel = findCatalogOllamaModel(requestedModel);
2649
+ if (catalogModel) {
2650
+ return { provider: "ollama", credentials, modelInfo: catalogModel };
2651
+ }
2652
+ }
2653
+ const candidates = requestedModel ? [requestedModel] : await listOllamaModels(credentials);
2654
+ for (const candidate of candidates) {
2655
+ const modelInfo = await fetchOllamaModelInfo(credentials, candidate);
2656
+ if (modelInfo) {
2657
+ return {
2658
+ provider: "ollama",
2659
+ credentials,
2660
+ modelInfo: findCatalogOllamaModel(candidate) ?? modelInfo
2661
+ };
2662
+ }
2663
+ }
2664
+ const detail = model ? `Model '${model}' is not installed or is not embedding-capable` : "No installed embedding-capable Ollama model was found";
2665
+ throw new Error(detail);
2666
+ }
2667
+ async function tryDetectOllamaProvider() {
2668
+ try {
2669
+ return await detectOllamaProvider();
2670
+ } catch {
2671
+ return null;
2672
+ }
2673
+ }
2581
2674
  function getProviderDisplayName(provider) {
2582
2675
  switch (provider) {
2583
2676
  case "github-copilot":
@@ -2917,6 +3010,7 @@ var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider extends BaseEmbeddi
2917
3010
  // src/embeddings/providers/ollama.ts
2918
3011
  var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider extends BaseEmbeddingProvider {
2919
3012
  static MIN_TRUNCATION_CHARS = 512;
3013
+ static REQUEST_TIMEOUT_MS = 12e4;
2920
3014
  constructor(credentials, modelInfo) {
2921
3015
  super(credentials, modelInfo);
2922
3016
  }
@@ -2991,22 +3085,45 @@ var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider extends BaseEmbeddi
2991
3085
  }
2992
3086
  }
2993
3087
  async embedSingle(text) {
2994
- const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
2995
- method: "POST",
2996
- headers: {
2997
- "Content-Type": "application/json"
2998
- },
2999
- body: JSON.stringify({
3000
- model: this.modelInfo.model,
3001
- prompt: text,
3002
- truncate: false
3003
- })
3004
- });
3088
+ const controller = new AbortController();
3089
+ const timeout = setTimeout(
3090
+ () => controller.abort(),
3091
+ _OllamaEmbeddingProvider.REQUEST_TIMEOUT_MS
3092
+ );
3093
+ let response;
3094
+ try {
3095
+ response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
3096
+ method: "POST",
3097
+ headers: {
3098
+ "Content-Type": "application/json"
3099
+ },
3100
+ body: JSON.stringify({
3101
+ model: this.modelInfo.model,
3102
+ prompt: text,
3103
+ truncate: false
3104
+ }),
3105
+ signal: controller.signal
3106
+ });
3107
+ } catch (error) {
3108
+ if (error instanceof Error && error.name === "AbortError") {
3109
+ throw new Error(
3110
+ `Ollama embedding request timed out after ${_OllamaEmbeddingProvider.REQUEST_TIMEOUT_MS}ms`
3111
+ );
3112
+ }
3113
+ throw error;
3114
+ } finally {
3115
+ clearTimeout(timeout);
3116
+ }
3005
3117
  if (!response.ok) {
3006
3118
  const error = (await response.text()).slice(0, 500);
3007
3119
  throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
3008
3120
  }
3009
3121
  const data = await response.json();
3122
+ if (!Array.isArray(data.embedding) || data.embedding.length !== this.modelInfo.dimensions || data.embedding.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
3123
+ throw new Error(
3124
+ `Ollama returned an invalid embedding; expected ${this.modelInfo.dimensions} finite dimensions`
3125
+ );
3126
+ }
3010
3127
  return {
3011
3128
  embedding: data.embedding,
3012
3129
  tokensUsed: this.estimateTokens(text)
@@ -3918,7 +4035,9 @@ function mapChunk(c) {
3918
4035
  return {
3919
4036
  content: c.content,
3920
4037
  startLine: c.startLine ?? c.start_line,
4038
+ startCol: c.startCol ?? c.start_col,
3921
4039
  endLine: c.endLine ?? c.end_line,
4040
+ endCol: c.endCol ?? c.end_col,
3922
4041
  chunkType: c.chunkType ?? c.chunk_type,
3923
4042
  name: c.name ?? void 0,
3924
4043
  language: c.language
@@ -4039,6 +4158,7 @@ function getEmbeddingHeaderParts(chunk, filePath) {
4039
4158
  javascript: "JavaScript",
4040
4159
  python: "Python",
4041
4160
  rust: "Rust",
4161
+ swift: "Swift",
4042
4162
  go: "Go",
4043
4163
  java: "Java"
4044
4164
  };
@@ -4047,7 +4167,16 @@ function getEmbeddingHeaderParts(chunk, filePath) {
4047
4167
  function: "function",
4048
4168
  arrow_function: "arrow function",
4049
4169
  method_definition: "method",
4170
+ method_declaration: "method",
4171
+ protocol_function_declaration: "protocol requirement",
4172
+ init_declaration: "initializer",
4173
+ deinit_declaration: "deinitializer",
4174
+ subscript_declaration: "subscript",
4050
4175
  class_declaration: "class",
4176
+ actor_declaration: "actor",
4177
+ extension_declaration: "extension",
4178
+ protocol_declaration: "protocol",
4179
+ struct_declaration: "struct",
4051
4180
  interface_declaration: "interface",
4052
4181
  type_alias_declaration: "type alias",
4053
4182
  enum_declaration: "enum",
@@ -4741,16 +4870,6 @@ function resolveWorktreeFallbackPath(projectRoot, relativePath) {
4741
4870
  const fallbackPath = path8.join(mainRepoRoot, relativePath);
4742
4871
  return (0, import_fs6.existsSync)(fallbackPath) ? fallbackPath : null;
4743
4872
  }
4744
- function resolveWorktreeFallbackProjectIndexPath(projectRoot, host) {
4745
- const inheritedHostPath = resolveWorktreeFallbackPath(projectRoot, getProjectIndexRelativePath(host));
4746
- if (inheritedHostPath) {
4747
- return inheritedHostPath;
4748
- }
4749
- if (host !== "opencode") {
4750
- return resolveWorktreeFallbackPath(projectRoot, OPENCODE_PROJECT_INDEX_RELATIVE_PATH);
4751
- }
4752
- return null;
4753
- }
4754
4873
  function getHostProjectIndexRelativePath(host) {
4755
4874
  return getProjectIndexRelativePath(host);
4756
4875
  }
@@ -4852,6 +4971,9 @@ function resolveProjectIndexPath(projectRoot, scope, host = "opencode") {
4852
4971
  if (hasHostProjectConfig(projectRoot, host)) {
4853
4972
  return localIndexPath;
4854
4973
  }
4974
+ if (resolveWorktreeMainRepoRoot(projectRoot)) {
4975
+ return localIndexPath;
4976
+ }
4855
4977
  const hostFallback = resolveWorktreeFallbackPath(projectRoot, getProjectIndexRelativePath(host));
4856
4978
  if (hostFallback) {
4857
4979
  return hostFallback;
@@ -5376,40 +5498,6 @@ function releaseIndexLock(lease) {
5376
5498
  }
5377
5499
  return true;
5378
5500
  }
5379
- async function withIndexLock(indexPath, operation, callback, options = {}) {
5380
- const lease = acquireIndexLock(indexPath, operation);
5381
- let result;
5382
- let callbackError;
5383
- let callbackFailed = false;
5384
- try {
5385
- result = await callback(lease);
5386
- } catch (error) {
5387
- callbackFailed = true;
5388
- callbackError = error;
5389
- }
5390
- if (!callbackFailed && options.completeRecoveries !== false) {
5391
- try {
5392
- completeLeaseRecovery(lease);
5393
- } catch (error) {
5394
- callbackFailed = true;
5395
- callbackError = error;
5396
- }
5397
- }
5398
- let releaseError;
5399
- try {
5400
- if (!releaseIndexLock(lease)) {
5401
- releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
5402
- }
5403
- } catch (error) {
5404
- releaseError = error;
5405
- }
5406
- if (releaseError !== void 0) {
5407
- if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
5408
- throw releaseError;
5409
- }
5410
- if (callbackFailed) throw callbackError;
5411
- return result;
5412
- }
5413
5501
  function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
5414
5502
  if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
5415
5503
  temporaryCounter += 1;
@@ -5444,8 +5532,18 @@ function completeLeaseRecovery(lease) {
5444
5532
  }
5445
5533
 
5446
5534
  // src/indexer/index.ts
5447
- var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
5448
- var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
5535
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "swift", "php", "apex", "zig", "gdscript", "matlab", "bash", "c", "cpp", "metal"]);
5536
+ var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex", "php"]);
5537
+ var CALL_GRAPH_RESOLUTION_VERSION = "4";
5538
+ var PHP_FUNCTION_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5539
+ "function_declaration",
5540
+ "function",
5541
+ "function_definition"
5542
+ ]);
5543
+ var PHP_CLASS_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5544
+ "class_declaration",
5545
+ "class_definition"
5546
+ ]);
5449
5547
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5450
5548
  "function_declaration",
5451
5549
  "function",
@@ -5458,6 +5556,9 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5458
5556
  "enum_declaration",
5459
5557
  "function_definition",
5460
5558
  "class_definition",
5559
+ "class_specifier",
5560
+ "struct_specifier",
5561
+ "namespace_definition",
5461
5562
  "decorated_definition",
5462
5563
  "method_declaration",
5463
5564
  "type_declaration",
@@ -5473,6 +5574,14 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5473
5574
  "test_declaration",
5474
5575
  "struct_declaration",
5475
5576
  "union_declaration",
5577
+ // Synthetic Swift declarations or declarations specific to tree-sitter-swift.
5578
+ "actor_declaration",
5579
+ "extension_declaration",
5580
+ "protocol_declaration",
5581
+ "protocol_function_declaration",
5582
+ "init_declaration",
5583
+ "deinit_declaration",
5584
+ "subscript_declaration",
5476
5585
  // GDScript declarations whose names participate in the call graph.
5477
5586
  // `function_definition` and `class_definition` are already in the set
5478
5587
  // above (shared with Python/C/Bash and Python, respectively).
@@ -5482,6 +5591,53 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5482
5591
  "const_statement",
5483
5592
  "class_name_statement"
5484
5593
  ]);
5594
+ var C_FAMILY_TYPE_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set(["class_specifier", "struct_specifier"]);
5595
+ function isCompatibleCFamilyCallTarget(language, callType, symbolKind) {
5596
+ if (language !== "c" && language !== "cpp") return true;
5597
+ if (symbolKind === "namespace_definition") return callType === "Import";
5598
+ const isTypeSymbol = C_FAMILY_TYPE_SYMBOL_CHUNK_TYPES.has(symbolKind);
5599
+ if (callType === "Constructor" || callType === "Inherits" || callType === "Implements") {
5600
+ return isTypeSymbol;
5601
+ }
5602
+ return !isTypeSymbol;
5603
+ }
5604
+ var EXECUTABLE_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
5605
+ "function_declaration",
5606
+ "function",
5607
+ "arrow_function",
5608
+ "method_definition",
5609
+ "function_definition",
5610
+ "method_declaration",
5611
+ "function_item",
5612
+ "protocol_function_declaration",
5613
+ "init_declaration",
5614
+ "deinit_declaration",
5615
+ "subscript_declaration",
5616
+ "constructor_definition",
5617
+ "trigger_declaration",
5618
+ "test_declaration"
5619
+ ]);
5620
+ function findEnclosingSymbol(symbols, line, column) {
5621
+ let best;
5622
+ for (const symbol of symbols) {
5623
+ if (line < symbol.startLine || line > symbol.endLine) continue;
5624
+ if (column !== void 0 && (line === symbol.startLine && column < symbol.startCol || line === symbol.endLine && column >= symbol.endCol)) {
5625
+ continue;
5626
+ }
5627
+ if (!best) {
5628
+ best = symbol;
5629
+ continue;
5630
+ }
5631
+ const span = symbol.endLine - symbol.startLine;
5632
+ const bestSpan = best.endLine - best.startLine;
5633
+ const isNarrowerPositionRange = column !== void 0 && span === bestSpan && symbol.startLine === best.startLine && symbol.endLine === best.endLine && symbol.startCol >= best.startCol && symbol.endCol <= best.endCol && (symbol.startCol > best.startCol || symbol.endCol < best.endCol);
5634
+ const isMoreSpecificTie = span === bestSpan && symbol.startLine === best.startLine && EXECUTABLE_SYMBOL_CHUNK_TYPES.has(symbol.kind) && !EXECUTABLE_SYMBOL_CHUNK_TYPES.has(best.kind);
5635
+ if (span < bestSpan || span === bestSpan && symbol.startLine > best.startLine || isNarrowerPositionRange || isMoreSpecificTie) {
5636
+ best = symbol;
5637
+ }
5638
+ }
5639
+ return best;
5640
+ }
5485
5641
  function float32ArrayToBuffer(arr) {
5486
5642
  const float32 = new Float32Array(arr);
5487
5643
  return Buffer.from(float32.buffer);
@@ -5566,6 +5722,8 @@ function hasBlameMetadata(metadata) {
5566
5722
  }
5567
5723
  var INDEX_METADATA_VERSION = "1";
5568
5724
  var EMBEDDING_STRATEGY_VERSION = "2";
5725
+ var SWIFT_PARSER_VERSION = "1";
5726
+ var METAL_PARSER_VERSION = "1";
5569
5727
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
5570
5728
  var RANK_HYBRID_CACHE_LIMIT = 256;
5571
5729
  function createPendingChunkStorageText(texts) {
@@ -5888,15 +6046,25 @@ function chunkTypeBoost(chunkType) {
5888
6046
  case "function_declaration":
5889
6047
  case "method":
5890
6048
  case "method_definition":
6049
+ case "method_declaration":
6050
+ case "protocol_function_declaration":
6051
+ case "init_declaration":
6052
+ case "deinit_declaration":
6053
+ case "subscript_declaration":
5891
6054
  case "class":
5892
6055
  case "class_declaration":
6056
+ case "actor_declaration":
6057
+ case "extension_declaration":
5893
6058
  return 0.2;
5894
6059
  case "interface":
5895
6060
  case "type":
5896
6061
  case "enum":
6062
+ case "enum_declaration":
5897
6063
  case "struct":
6064
+ case "struct_declaration":
5898
6065
  case "impl":
5899
6066
  case "trait":
6067
+ case "protocol_declaration":
5900
6068
  case "module":
5901
6069
  return 0.1;
5902
6070
  default:
@@ -6003,11 +6171,21 @@ function isImplementationChunkType(chunkType) {
6003
6171
  "function_declaration",
6004
6172
  "method",
6005
6173
  "method_definition",
6174
+ "method_declaration",
6175
+ "protocol_function_declaration",
6176
+ "init_declaration",
6177
+ "deinit_declaration",
6178
+ "subscript_declaration",
6006
6179
  "class",
6007
6180
  "class_declaration",
6181
+ "actor_declaration",
6182
+ "extension_declaration",
6008
6183
  "interface",
6184
+ "protocol_declaration",
6009
6185
  "type",
6010
6186
  "enum",
6187
+ "enum_declaration",
6188
+ "struct_declaration",
6011
6189
  "module"
6012
6190
  ].includes(chunkType);
6013
6191
  }
@@ -7010,6 +7188,29 @@ var Indexer = class {
7010
7188
  const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
7011
7189
  return `index.forceReembed.${projectHash}`;
7012
7190
  }
7191
+ getCallGraphResolutionMetadataKey() {
7192
+ if (this.config.scope !== "global") {
7193
+ return "index.callGraphResolutionVersion";
7194
+ }
7195
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
7196
+ return `index.callGraphResolutionVersion.${projectHash}`;
7197
+ }
7198
+ getSwiftParserVersionMetadataKey() {
7199
+ const key = "index.parser.swiftVersion";
7200
+ if (this.config.scope !== "global") {
7201
+ return key;
7202
+ }
7203
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
7204
+ return `${key}.${projectHash}`;
7205
+ }
7206
+ getMetalParserVersionMetadataKey() {
7207
+ const key = "index.parser.metalVersion";
7208
+ if (this.config.scope !== "global") {
7209
+ return key;
7210
+ }
7211
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
7212
+ return `${key}.${projectHash}`;
7213
+ }
7013
7214
  hasProjectForceReembedPending() {
7014
7215
  return this.config.scope === "global" && this.database?.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
7015
7216
  }
@@ -8170,6 +8371,7 @@ var Indexer = class {
8170
8371
  this.database.setMetadata("index.embeddingProvider", provider.provider);
8171
8372
  this.database.setMetadata("index.embeddingModel", provider.modelInfo.model);
8172
8373
  this.database.setMetadata("index.embeddingDimensions", provider.modelInfo.dimensions.toString());
8374
+ this.database.setMetadata(this.getCallGraphResolutionMetadataKey(), CALL_GRAPH_RESOLUTION_VERSION);
8173
8375
  if (this.config.scope === "global") {
8174
8376
  if (completeProjectEmbeddingStrategyReset) {
8175
8377
  this.database.setMetadata(this.getProjectEmbeddingStrategyMetadataKey(), EMBEDDING_STRATEGY_VERSION);
@@ -8357,6 +8559,20 @@ var Indexer = class {
8357
8559
  totalChunks: 0
8358
8560
  });
8359
8561
  this.loadFileHashCache();
8562
+ const swiftParserMetadataKey = this.getSwiftParserVersionMetadataKey();
8563
+ const reparseCachedSwiftFiles = database.getMetadata(swiftParserMetadataKey) !== SWIFT_PARSER_VERSION;
8564
+ const metalParserMetadataKey = this.getMetalParserVersionMetadataKey();
8565
+ const reparseCachedMetalFiles = database.getMetadata(metalParserMetadataKey) !== METAL_PARSER_VERSION;
8566
+ if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some(
8567
+ (filePath) => path12.extname(filePath).toLowerCase() === ".swift"
8568
+ )) {
8569
+ this.logger.info("Reindexing cached Swift files for parser support");
8570
+ }
8571
+ if (reparseCachedMetalFiles && Array.from(this.fileHashCache.keys()).some(
8572
+ (filePath) => path12.extname(filePath).toLowerCase() === ".metal"
8573
+ )) {
8574
+ this.logger.info("Reindexing cached Metal files for parser support");
8575
+ }
8360
8576
  const includePatterns = [...this.config.include, ...this.config.additionalInclude];
8361
8577
  const { files, skipped } = await collectFiles(
8362
8578
  this.projectRoot,
@@ -8376,10 +8592,17 @@ var Indexer = class {
8376
8592
  const changedFiles = [];
8377
8593
  const unchangedFilePaths = /* @__PURE__ */ new Set();
8378
8594
  const currentFileHashes = /* @__PURE__ */ new Map();
8595
+ const needsCallGraphResolutionMigration = database.getMetadata(this.getCallGraphResolutionMetadataKey()) !== CALL_GRAPH_RESOLUTION_VERSION;
8379
8596
  for (const f of files) {
8380
8597
  const currentHash = hashFile(f.path);
8381
8598
  currentFileHashes.set(f.path, currentHash);
8382
- if (this.fileHashCache.get(f.path) === currentHash) {
8599
+ const cachedHashMatches = this.fileHashCache.get(f.path) === currentHash;
8600
+ const needsCallGraphRefresh = cachedHashMatches && needsCallGraphResolutionMigration && database.getChunksByFile(f.path).some(
8601
+ (chunk) => chunk.language === "php" || chunk.language === "c" || chunk.language === "cpp"
8602
+ );
8603
+ const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path12.extname(f.path).toLowerCase() === ".swift";
8604
+ const requiresMetalParserUpgrade = reparseCachedMetalFiles && path12.extname(f.path).toLowerCase() === ".metal";
8605
+ if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade) {
8383
8606
  unchangedFilePaths.add(f.path);
8384
8607
  this.logger.recordCacheHit();
8385
8608
  } else {
@@ -8581,16 +8804,28 @@ var Indexer = class {
8581
8804
  const fileSymbols = [];
8582
8805
  for (const chunk of parsed.chunks) {
8583
8806
  if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
8584
- const symbolId = `sym_${hashContent(parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine).slice(0, 16)}`;
8807
+ const existingMetalSymbol = chunk.language === "metal" ? fileSymbols.find(
8808
+ (symbol2) => symbol2.name === chunk.name && symbol2.kind === chunk.chunkType && symbol2.startLine <= chunk.endLine && chunk.startLine <= symbol2.endLine
8809
+ ) : void 0;
8810
+ if (existingMetalSymbol) {
8811
+ existingMetalSymbol.startLine = Math.min(existingMetalSymbol.startLine, chunk.startLine);
8812
+ existingMetalSymbol.endLine = Math.max(existingMetalSymbol.endLine, chunk.endLine);
8813
+ existingMetalSymbol.startCol = Math.min(existingMetalSymbol.startCol, chunk.startCol ?? 0);
8814
+ existingMetalSymbol.endCol = Math.max(existingMetalSymbol.endCol, chunk.endCol ?? 0);
8815
+ continue;
8816
+ }
8817
+ const symbolId = `sym_${hashContent(
8818
+ parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine + ":" + (chunk.startCol ?? 0)
8819
+ ).slice(0, 16)}`;
8585
8820
  const symbol = {
8586
8821
  id: symbolId,
8587
8822
  filePath: parsed.path,
8588
8823
  name: chunk.name,
8589
8824
  kind: chunk.chunkType,
8590
8825
  startLine: chunk.startLine,
8591
- startCol: 0,
8826
+ startCol: chunk.startCol ?? 0,
8592
8827
  endLine: chunk.endLine,
8593
- endCol: 0,
8828
+ endCol: chunk.endCol ?? 0,
8594
8829
  language: chunk.language
8595
8830
  };
8596
8831
  fileSymbols.push(symbol);
@@ -8615,8 +8850,10 @@ var Indexer = class {
8615
8850
  if (callSites.length === 0) continue;
8616
8851
  const edges = [];
8617
8852
  for (const site of callSites) {
8618
- const enclosingSymbol = fileSymbols.find(
8619
- (sym) => site.line >= sym.startLine && site.line <= sym.endLine
8853
+ const enclosingSymbol = findEnclosingSymbol(
8854
+ fileSymbols,
8855
+ site.line,
8856
+ site.column
8620
8857
  );
8621
8858
  if (!enclosingSymbol) continue;
8622
8859
  const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
@@ -8635,7 +8872,21 @@ var Indexer = class {
8635
8872
  if (edges.length > 0) {
8636
8873
  database.upsertCallEdgesBatch(edges);
8637
8874
  for (const edge of edges) {
8638
- const candidates = symbolsByName.get(normalizeSymbolKey(edge.targetName));
8875
+ let candidates = symbolsByName.get(normalizeSymbolKey(edge.targetName));
8876
+ if (fileLanguage === "php" && candidates) {
8877
+ if (edge.callType === "Constructor") {
8878
+ candidates = candidates.filter(
8879
+ (candidate) => PHP_CLASS_SYMBOL_CHUNK_TYPES.has(candidate.kind)
8880
+ );
8881
+ } else if (edge.callType === "Call") {
8882
+ candidates = candidates.filter(
8883
+ (candidate) => PHP_FUNCTION_SYMBOL_CHUNK_TYPES.has(candidate.kind)
8884
+ );
8885
+ }
8886
+ }
8887
+ candidates = candidates?.filter(
8888
+ (symbol) => isCompatibleCFamilyCallTarget(fileLanguage, edge.callType, symbol.kind)
8889
+ );
8639
8890
  if (candidates && candidates.length === 1) {
8640
8891
  database.resolveCallEdge(edge.id, candidates[0].id);
8641
8892
  }
@@ -8690,6 +8941,8 @@ var Indexer = class {
8690
8941
  this.saveFileHashCache();
8691
8942
  this.saveFailedBatches([]);
8692
8943
  }
8944
+ database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
8945
+ database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
8693
8946
  this.saveIndexMetadata(configuredProviderInfo);
8694
8947
  this.indexCompatibility = { compatible: true };
8695
8948
  stats.durationMs = Date.now() - startTime;
@@ -8717,6 +8970,8 @@ var Indexer = class {
8717
8970
  this.saveFileHashCache();
8718
8971
  this.saveFailedBatches([]);
8719
8972
  }
8973
+ database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
8974
+ database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
8720
8975
  this.saveIndexMetadata(configuredProviderInfo);
8721
8976
  this.indexCompatibility = { compatible: true };
8722
8977
  stats.durationMs = Date.now() - startTime;
@@ -8987,6 +9242,8 @@ var Indexer = class {
8987
9242
  if (forceScopedReembed && failedForcedChunkIds.size === 0) {
8988
9243
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
8989
9244
  }
9245
+ database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
9246
+ database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
8990
9247
  this.saveIndexMetadata(configuredProviderInfo);
8991
9248
  this.indexCompatibility = { compatible: true };
8992
9249
  this.logger.recordIndexingEnd();
@@ -10097,251 +10354,45 @@ var Indexer = class {
10097
10354
  var import_fs11 = require("fs");
10098
10355
  var path17 = __toESM(require("path"), 1);
10099
10356
 
10100
- // src/config/merger.ts
10101
- var import_fs9 = require("fs");
10102
- var path14 = __toESM(require("path"), 1);
10103
-
10104
- // src/config/rebase.ts
10357
+ // src/tools/knowledge-base-paths.ts
10105
10358
  var path13 = __toESM(require("path"), 1);
10106
- function isWithinRoot(rootDir, targetPath) {
10107
- const relativePath = path13.relative(rootDir, targetPath);
10108
- return relativePath === "" || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath);
10109
- }
10110
- function rebasePathEntries(values, fromDir, toDir) {
10111
- if (!Array.isArray(values)) {
10112
- return [];
10113
- }
10114
- return values.filter((value) => typeof value === "string").map((value) => {
10115
- const trimmed = value.trim();
10116
- if (!trimmed || path13.isAbsolute(trimmed)) {
10117
- return trimmed;
10118
- }
10119
- return normalizePathSeparators(path13.normalize(path13.relative(toDir, path13.resolve(fromDir, trimmed))));
10120
- }).filter(Boolean);
10121
- }
10122
- function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
10123
- if (!Array.isArray(values)) {
10124
- return [];
10359
+ function resolveConfigPathValue(value, baseDir) {
10360
+ const trimmed = value.trim();
10361
+ if (!trimmed) {
10362
+ return trimmed;
10125
10363
  }
10126
- return values.filter((value) => typeof value === "string").map((value) => {
10127
- const trimmed = value.trim();
10128
- if (!trimmed) {
10129
- return trimmed;
10130
- }
10131
- if (path13.isAbsolute(trimmed)) {
10132
- if (isWithinRoot(sourceRoot, trimmed)) {
10133
- return normalizePathSeparators(path13.normalize(path13.relative(sourceRoot, trimmed) || "."));
10134
- }
10135
- return path13.normalize(trimmed);
10136
- }
10137
- const resolvedFromSource = path13.resolve(sourceRoot, trimmed);
10138
- if (isWithinRoot(sourceRoot, resolvedFromSource)) {
10139
- return normalizePathSeparators(path13.normalize(trimmed));
10140
- }
10141
- return normalizePathSeparators(path13.normalize(path13.relative(targetRoot, resolvedFromSource)));
10142
- }).filter(Boolean);
10364
+ const absolutePath = path13.isAbsolute(trimmed) ? trimmed : path13.resolve(baseDir, trimmed);
10365
+ return path13.normalize(absolutePath);
10143
10366
  }
10144
10367
 
10145
- // src/config/merger.ts
10146
- var PROJECT_OVERRIDE_KEYS = [
10147
- "embeddingProvider",
10148
- "customProvider",
10149
- "embeddingModel",
10150
- "reranker",
10151
- "include",
10152
- "exclude",
10153
- "indexing",
10154
- "search",
10155
- "debug",
10156
- "scope"
10157
- ];
10158
- var MERGE_ARRAY_KEYS = ["knowledgeBases", "additionalInclude"];
10159
- function isRecord(value) {
10160
- return typeof value === "object" && value !== null && !Array.isArray(value);
10161
- }
10162
- function isStringArray2(value) {
10163
- return Array.isArray(value) && value.every((item) => typeof item === "string");
10164
- }
10165
- function applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key) {
10166
- if (key in normalizedProjectConfig) {
10167
- merged[key] = normalizedProjectConfig[key];
10168
- return;
10169
- }
10170
- if (key in globalConfig) {
10171
- merged[key] = globalConfig[key];
10368
+ // src/tools/utils.ts
10369
+ var import_tiktoken = require("tiktoken");
10370
+ var MIN_CONTEXT_PACK_TOKEN_BUDGET = 128;
10371
+ var MAX_CONTEXT_PACK_TOKEN_BUDGET = 4e3;
10372
+ var DEFAULT_CONTEXT_PACK_TOKEN_BUDGET = 1200;
10373
+ var CONTEXT_TOKENIZER = (0, import_tiktoken.get_encoding)("cl100k_base");
10374
+ function clampContextPackTokenBudget(tokenBudget) {
10375
+ if (tokenBudget === void 0 || !Number.isFinite(tokenBudget)) {
10376
+ return DEFAULT_CONTEXT_PACK_TOKEN_BUDGET;
10172
10377
  }
10378
+ return Math.min(
10379
+ MAX_CONTEXT_PACK_TOKEN_BUDGET,
10380
+ Math.max(MIN_CONTEXT_PACK_TOKEN_BUDGET, Math.floor(tokenBudget))
10381
+ );
10173
10382
  }
10174
- function mergeUniqueStringArray(values) {
10175
- return [...new Set(values.map((value) => String(value).trim()))];
10383
+ function countContextTokens(text) {
10384
+ return CONTEXT_TOKENIZER.encode(text).length;
10176
10385
  }
10177
- function normalizeKnowledgeBasePath(value) {
10178
- let normalized = path14.normalize(String(value).trim());
10179
- const root = path14.parse(normalized).root;
10180
- while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10181
- normalized = normalized.slice(0, -1);
10182
- }
10183
- return normalized;
10184
- }
10185
- function mergeKnowledgeBasePaths(values) {
10186
- return [...new Set(values.map((value) => normalizeKnowledgeBasePath(value)).filter((value) => value.length > 0))];
10187
- }
10188
- function validateConfigLayerShape(rawConfig, filePath) {
10189
- if (!isRecord(rawConfig)) {
10190
- throw new Error(`Config file ${filePath} must contain a JSON object at the root.`);
10191
- }
10192
- if (rawConfig.knowledgeBases !== void 0 && !isStringArray2(rawConfig.knowledgeBases)) {
10193
- throw new Error(`Config file ${filePath} field 'knowledgeBases' must be an array of strings.`);
10194
- }
10195
- if (rawConfig.additionalInclude !== void 0 && !isStringArray2(rawConfig.additionalInclude)) {
10196
- throw new Error(`Config file ${filePath} field 'additionalInclude' must be an array of strings.`);
10197
- }
10198
- if (rawConfig.include !== void 0 && !isStringArray2(rawConfig.include)) {
10199
- throw new Error(`Config file ${filePath} field 'include' must be an array of strings.`);
10200
- }
10201
- if (rawConfig.exclude !== void 0 && !isStringArray2(rawConfig.exclude)) {
10202
- throw new Error(`Config file ${filePath} field 'exclude' must be an array of strings.`);
10203
- }
10204
- for (const section of ["customProvider", "indexing", "search", "debug", "reranker"]) {
10205
- const value = rawConfig[section];
10206
- if (value !== void 0 && !isRecord(value)) {
10207
- throw new Error(`Config file ${filePath} field '${section}' must be an object.`);
10208
- }
10209
- }
10210
- return rawConfig;
10211
- }
10212
- function loadJsonFile(filePath) {
10213
- if (!(0, import_fs9.existsSync)(filePath)) {
10214
- return null;
10215
- }
10216
- try {
10217
- const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
10218
- return validateConfigLayerShape(JSON.parse(content), filePath);
10219
- } catch (error) {
10220
- if (error instanceof Error && error.message.startsWith("Config file ")) {
10221
- throw error;
10222
- }
10223
- const message = error instanceof Error ? error.message : String(error);
10224
- throw new Error(`Failed to load config file ${filePath}: ${message}`);
10225
- }
10226
- }
10227
- function loadConfigFile(filePath) {
10228
- return loadJsonFile(filePath);
10229
- }
10230
- function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10231
- const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10232
- (0, import_fs9.mkdirSync)(path14.dirname(localConfigPath), { recursive: true });
10233
- (0, import_fs9.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10234
- return localConfigPath;
10235
- }
10236
- function loadProjectConfigLayer(projectRoot, host = "opencode") {
10237
- const projectConfigPath = resolveProjectConfigPath(projectRoot, host);
10238
- const projectConfig = loadJsonFile(projectConfigPath);
10239
- if (!projectConfig) {
10240
- return {};
10241
- }
10242
- const normalizedConfig = { ...projectConfig };
10243
- const projectConfigBaseDir = path14.dirname(path14.dirname(projectConfigPath));
10244
- if (Array.isArray(normalizedConfig.knowledgeBases)) {
10245
- normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10246
- normalizedConfig.knowledgeBases,
10247
- projectConfigBaseDir,
10248
- projectRoot
10249
- );
10250
- }
10251
- return normalizedConfig;
10252
- }
10253
- function loadMergedConfig(projectRoot, host = "opencode") {
10254
- const globalConfigPath = resolveGlobalConfigPath(host);
10255
- const projectConfigPath = resolveProjectConfigPath(projectRoot, host);
10256
- let globalConfig = null;
10257
- let globalConfigError = null;
10258
- try {
10259
- globalConfig = loadJsonFile(globalConfigPath);
10260
- } catch (error) {
10261
- globalConfigError = error instanceof Error ? error : new Error(String(error));
10262
- }
10263
- const projectConfig = loadJsonFile(projectConfigPath);
10264
- const normalizedProjectConfig = loadProjectConfigLayer(projectRoot, host);
10265
- if (globalConfigError) {
10266
- if (!projectConfig) {
10267
- throw globalConfigError;
10268
- }
10269
- globalConfig = null;
10270
- }
10271
- if (!globalConfig && !projectConfig) {
10272
- return {};
10273
- }
10274
- if (!projectConfig && globalConfig) {
10275
- return globalConfig;
10276
- }
10277
- if (!globalConfig && projectConfig) {
10278
- return normalizedProjectConfig;
10279
- }
10280
- if (!globalConfig || !projectConfig) {
10281
- return globalConfig ?? normalizedProjectConfig;
10282
- }
10283
- const merged = { ...globalConfig };
10284
- for (const key of PROJECT_OVERRIDE_KEYS) {
10285
- applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key);
10286
- }
10287
- if (projectConfig) {
10288
- for (const key of Object.keys(projectConfig)) {
10289
- if (PROJECT_OVERRIDE_KEYS.includes(key) || MERGE_ARRAY_KEYS.includes(key)) {
10290
- continue;
10291
- }
10292
- merged[key] = normalizedProjectConfig[key];
10293
- }
10294
- }
10295
- const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
10296
- const projectKbs = projectConfig ? Array.isArray(normalizedProjectConfig.knowledgeBases) ? normalizedProjectConfig.knowledgeBases : [] : [];
10297
- const allKbs = [...globalKbs, ...projectKbs];
10298
- merged.knowledgeBases = mergeKnowledgeBasePaths(allKbs);
10299
- const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
10300
- const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
10301
- const allAdditional = [...globalAdditional, ...projectAdditional];
10302
- merged.additionalInclude = mergeUniqueStringArray(allAdditional);
10303
- return merged;
10304
- }
10305
-
10306
- // src/tools/knowledge-base-paths.ts
10307
- var path15 = __toESM(require("path"), 1);
10308
- function resolveConfigPathValue(value, baseDir) {
10309
- const trimmed = value.trim();
10310
- if (!trimmed) {
10311
- return trimmed;
10312
- }
10313
- const absolutePath = path15.isAbsolute(trimmed) ? trimmed : path15.resolve(baseDir, trimmed);
10314
- return path15.normalize(absolutePath);
10315
- }
10316
-
10317
- // src/tools/utils.ts
10318
- var import_tiktoken = require("tiktoken");
10319
- var MIN_CONTEXT_PACK_TOKEN_BUDGET = 128;
10320
- var MAX_CONTEXT_PACK_TOKEN_BUDGET = 4e3;
10321
- var DEFAULT_CONTEXT_PACK_TOKEN_BUDGET = 1200;
10322
- var CONTEXT_TOKENIZER = (0, import_tiktoken.get_encoding)("cl100k_base");
10323
- function clampContextPackTokenBudget(tokenBudget) {
10324
- if (tokenBudget === void 0 || !Number.isFinite(tokenBudget)) {
10325
- return DEFAULT_CONTEXT_PACK_TOKEN_BUDGET;
10326
- }
10327
- return Math.min(
10328
- MAX_CONTEXT_PACK_TOKEN_BUDGET,
10329
- Math.max(MIN_CONTEXT_PACK_TOKEN_BUDGET, Math.floor(tokenBudget))
10330
- );
10331
- }
10332
- function countContextTokens(text) {
10333
- return CONTEXT_TOKENIZER.encode(text).length;
10334
- }
10335
- function fitTextToContextBudget(text, tokenBudget) {
10336
- const normalizedBudget = clampContextPackTokenBudget(tokenBudget);
10337
- const tokenEstimate = countContextTokens(text);
10338
- if (tokenEstimate <= normalizedBudget) {
10339
- return {
10340
- text,
10341
- tokenBudget: normalizedBudget,
10342
- tokenEstimate,
10343
- truncated: false
10344
- };
10386
+ function fitTextToContextBudget(text, tokenBudget) {
10387
+ const normalizedBudget = clampContextPackTokenBudget(tokenBudget);
10388
+ const tokenEstimate = countContextTokens(text);
10389
+ if (tokenEstimate <= normalizedBudget) {
10390
+ return {
10391
+ text,
10392
+ tokenBudget: normalizedBudget,
10393
+ tokenEstimate,
10394
+ truncated: false
10395
+ };
10345
10396
  }
10346
10397
  const suffix = "\n...[truncated to context token budget]";
10347
10398
  const codePoints = Array.from(text);
@@ -10723,6 +10774,208 @@ ${truncateContent(r.content)}
10723
10774
  // src/tools/config-state.ts
10724
10775
  var import_fs10 = require("fs");
10725
10776
  var path16 = __toESM(require("path"), 1);
10777
+
10778
+ // src/config/merger.ts
10779
+ var import_fs9 = require("fs");
10780
+ var path15 = __toESM(require("path"), 1);
10781
+
10782
+ // src/config/rebase.ts
10783
+ var path14 = __toESM(require("path"), 1);
10784
+ function isWithinRoot(rootDir, targetPath) {
10785
+ const relativePath = path14.relative(rootDir, targetPath);
10786
+ return relativePath === "" || !relativePath.startsWith("..") && !path14.isAbsolute(relativePath);
10787
+ }
10788
+ function rebasePathEntries(values, fromDir, toDir) {
10789
+ if (!Array.isArray(values)) {
10790
+ return [];
10791
+ }
10792
+ return values.filter((value) => typeof value === "string").map((value) => {
10793
+ const trimmed = value.trim();
10794
+ if (!trimmed || path14.isAbsolute(trimmed)) {
10795
+ return trimmed;
10796
+ }
10797
+ return normalizePathSeparators(path14.normalize(path14.relative(toDir, path14.resolve(fromDir, trimmed))));
10798
+ }).filter(Boolean);
10799
+ }
10800
+ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
10801
+ if (!Array.isArray(values)) {
10802
+ return [];
10803
+ }
10804
+ return values.filter((value) => typeof value === "string").map((value) => {
10805
+ const trimmed = value.trim();
10806
+ if (!trimmed) {
10807
+ return trimmed;
10808
+ }
10809
+ if (path14.isAbsolute(trimmed)) {
10810
+ if (isWithinRoot(sourceRoot, trimmed)) {
10811
+ return normalizePathSeparators(path14.normalize(path14.relative(sourceRoot, trimmed) || "."));
10812
+ }
10813
+ return path14.normalize(trimmed);
10814
+ }
10815
+ const resolvedFromSource = path14.resolve(sourceRoot, trimmed);
10816
+ if (isWithinRoot(sourceRoot, resolvedFromSource)) {
10817
+ return normalizePathSeparators(path14.normalize(trimmed));
10818
+ }
10819
+ return normalizePathSeparators(path14.normalize(path14.relative(targetRoot, resolvedFromSource)));
10820
+ }).filter(Boolean);
10821
+ }
10822
+
10823
+ // src/config/merger.ts
10824
+ var PROJECT_OVERRIDE_KEYS = [
10825
+ "embeddingProvider",
10826
+ "customProvider",
10827
+ "embeddingModel",
10828
+ "reranker",
10829
+ "include",
10830
+ "exclude",
10831
+ "indexing",
10832
+ "search",
10833
+ "debug",
10834
+ "scope"
10835
+ ];
10836
+ var MERGE_ARRAY_KEYS = ["knowledgeBases", "additionalInclude"];
10837
+ function isRecord(value) {
10838
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10839
+ }
10840
+ function isStringArray2(value) {
10841
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
10842
+ }
10843
+ function applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key) {
10844
+ if (key in normalizedProjectConfig) {
10845
+ merged[key] = normalizedProjectConfig[key];
10846
+ return;
10847
+ }
10848
+ if (key in globalConfig) {
10849
+ merged[key] = globalConfig[key];
10850
+ }
10851
+ }
10852
+ function mergeUniqueStringArray(values) {
10853
+ return [...new Set(values.map((value) => String(value).trim()))];
10854
+ }
10855
+ function normalizeKnowledgeBasePath(value) {
10856
+ let normalized = path15.normalize(String(value).trim());
10857
+ const root = path15.parse(normalized).root;
10858
+ while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10859
+ normalized = normalized.slice(0, -1);
10860
+ }
10861
+ return normalized;
10862
+ }
10863
+ function mergeKnowledgeBasePaths(values) {
10864
+ return [...new Set(values.map((value) => normalizeKnowledgeBasePath(value)).filter((value) => value.length > 0))];
10865
+ }
10866
+ function validateConfigLayerShape(rawConfig, filePath) {
10867
+ if (!isRecord(rawConfig)) {
10868
+ throw new Error(`Config file ${filePath} must contain a JSON object at the root.`);
10869
+ }
10870
+ if (rawConfig.knowledgeBases !== void 0 && !isStringArray2(rawConfig.knowledgeBases)) {
10871
+ throw new Error(`Config file ${filePath} field 'knowledgeBases' must be an array of strings.`);
10872
+ }
10873
+ if (rawConfig.additionalInclude !== void 0 && !isStringArray2(rawConfig.additionalInclude)) {
10874
+ throw new Error(`Config file ${filePath} field 'additionalInclude' must be an array of strings.`);
10875
+ }
10876
+ if (rawConfig.include !== void 0 && !isStringArray2(rawConfig.include)) {
10877
+ throw new Error(`Config file ${filePath} field 'include' must be an array of strings.`);
10878
+ }
10879
+ if (rawConfig.exclude !== void 0 && !isStringArray2(rawConfig.exclude)) {
10880
+ throw new Error(`Config file ${filePath} field 'exclude' must be an array of strings.`);
10881
+ }
10882
+ for (const section of ["customProvider", "indexing", "search", "debug", "reranker"]) {
10883
+ const value = rawConfig[section];
10884
+ if (value !== void 0 && !isRecord(value)) {
10885
+ throw new Error(`Config file ${filePath} field '${section}' must be an object.`);
10886
+ }
10887
+ }
10888
+ return rawConfig;
10889
+ }
10890
+ function loadJsonFile(filePath) {
10891
+ if (!(0, import_fs9.existsSync)(filePath)) {
10892
+ return null;
10893
+ }
10894
+ try {
10895
+ const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
10896
+ return validateConfigLayerShape(JSON.parse(content), filePath);
10897
+ } catch (error) {
10898
+ if (error instanceof Error && error.message.startsWith("Config file ")) {
10899
+ throw error;
10900
+ }
10901
+ const message = error instanceof Error ? error.message : String(error);
10902
+ throw new Error(`Failed to load config file ${filePath}: ${message}`);
10903
+ }
10904
+ }
10905
+ function loadConfigFile(filePath) {
10906
+ return loadJsonFile(filePath);
10907
+ }
10908
+ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10909
+ const projectConfigPath = resolveProjectConfigPath(projectRoot, host);
10910
+ const projectConfig = loadJsonFile(projectConfigPath);
10911
+ if (!projectConfig) {
10912
+ return {};
10913
+ }
10914
+ const normalizedConfig = { ...projectConfig };
10915
+ const projectConfigBaseDir = path15.dirname(path15.dirname(projectConfigPath));
10916
+ if (Array.isArray(normalizedConfig.knowledgeBases)) {
10917
+ normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10918
+ normalizedConfig.knowledgeBases,
10919
+ projectConfigBaseDir,
10920
+ projectRoot
10921
+ );
10922
+ }
10923
+ return normalizedConfig;
10924
+ }
10925
+ function loadMergedConfig(projectRoot, host = "opencode") {
10926
+ const globalConfigPath = resolveGlobalConfigPath(host);
10927
+ const projectConfigPath = resolveProjectConfigPath(projectRoot, host);
10928
+ let globalConfig = null;
10929
+ let globalConfigError = null;
10930
+ try {
10931
+ globalConfig = loadJsonFile(globalConfigPath);
10932
+ } catch (error) {
10933
+ globalConfigError = error instanceof Error ? error : new Error(String(error));
10934
+ }
10935
+ const projectConfig = loadJsonFile(projectConfigPath);
10936
+ const normalizedProjectConfig = loadProjectConfigLayer(projectRoot, host);
10937
+ if (globalConfigError) {
10938
+ if (!projectConfig) {
10939
+ throw globalConfigError;
10940
+ }
10941
+ globalConfig = null;
10942
+ }
10943
+ if (!globalConfig && !projectConfig) {
10944
+ return {};
10945
+ }
10946
+ if (!projectConfig && globalConfig) {
10947
+ return globalConfig;
10948
+ }
10949
+ if (!globalConfig && projectConfig) {
10950
+ return normalizedProjectConfig;
10951
+ }
10952
+ if (!globalConfig || !projectConfig) {
10953
+ return globalConfig ?? normalizedProjectConfig;
10954
+ }
10955
+ const merged = { ...globalConfig };
10956
+ for (const key of PROJECT_OVERRIDE_KEYS) {
10957
+ applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key);
10958
+ }
10959
+ if (projectConfig) {
10960
+ for (const key of Object.keys(projectConfig)) {
10961
+ if (PROJECT_OVERRIDE_KEYS.includes(key) || MERGE_ARRAY_KEYS.includes(key)) {
10962
+ continue;
10963
+ }
10964
+ merged[key] = normalizedProjectConfig[key];
10965
+ }
10966
+ }
10967
+ const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
10968
+ const projectKbs = projectConfig ? Array.isArray(normalizedProjectConfig.knowledgeBases) ? normalizedProjectConfig.knowledgeBases : [] : [];
10969
+ const allKbs = [...globalKbs, ...projectKbs];
10970
+ merged.knowledgeBases = mergeKnowledgeBasePaths(allKbs);
10971
+ const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
10972
+ const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
10973
+ const allAdditional = [...globalAdditional, ...projectAdditional];
10974
+ merged.additionalInclude = mergeUniqueStringArray(allAdditional);
10975
+ return merged;
10976
+ }
10977
+
10978
+ // src/tools/config-state.ts
10726
10979
  function normalizeKnowledgeBasePaths(config, projectRoot) {
10727
10980
  const normalized = { ...config };
10728
10981
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -10806,15 +11059,6 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
10806
11059
  indexerCache.set(key, indexer);
10807
11060
  configCache.set(key, config);
10808
11061
  }
10809
- function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10810
- const root = getProjectRoot(projectRoot, host);
10811
- const localIndexPath = path17.join(root, getHostProjectIndexRelativePath(host));
10812
- if ((0, import_fs11.existsSync)(localIndexPath)) {
10813
- return false;
10814
- }
10815
- const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
10816
- return inheritedIndexPath !== null;
10817
- }
10818
11062
  async function searchCodebase(projectRoot, host, query, options = {}) {
10819
11063
  const indexer = getIndexerForProject(projectRoot, host);
10820
11064
  return indexer.search(query, options.limit, {
@@ -10864,9 +11108,7 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10864
11108
  }
10865
11109
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
10866
11110
  const root = getProjectRoot(projectRoot, host);
10867
- const key = getIndexerCacheKey(root, host);
10868
- let indexer = getIndexerForProject(root, host);
10869
- const runtimeConfig = configCache.get(key);
11111
+ const indexer = getIndexerForProject(root, host);
10870
11112
  try {
10871
11113
  if (args.estimateOnly) {
10872
11114
  return { kind: "estimate", estimate: await indexer.estimateCost() };
@@ -10887,19 +11129,7 @@ async function runIndexCodebase(projectRoot, host, args, onProgress) {
10887
11129
  return Promise.resolve();
10888
11130
  });
10889
11131
  };
10890
- let stats;
10891
- if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
10892
- const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
10893
- stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
10894
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10895
- refreshIndexerForDirectory(root, host, runtimeConfig);
10896
- indexer = getIndexerForProject(root, host);
10897
- return runIndex(indexer);
10898
- }, { completeRecoveries: false });
10899
- } else {
10900
- stats = await runIndex(indexer);
10901
- }
10902
- return { kind: "stats", stats };
11132
+ return { kind: "stats", stats: await runIndex(indexer) };
10903
11133
  } catch (error) {
10904
11134
  const busyResult = getIndexBusyResult(error);
10905
11135
  if (!busyResult) throw error;