@vohongtho.infotech/code-intel 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'url';
14
14
  import { Parser, Language, Query } from 'web-tree-sitter';
15
15
  import { Database, Connection } from '@ladybugdb/core';
16
16
  import { execSync } from 'child_process';
17
- import Database2 from 'better-sqlite3';
17
+ import Database3 from 'better-sqlite3';
18
18
  import bcrypt from 'bcrypt';
19
19
  import crypto5 from 'crypto';
20
20
  import { v4 } from 'uuid';
@@ -198,6 +198,13 @@ var init_id_generator = __esm({
198
198
  });
199
199
 
200
200
  // src/storage/schema.ts
201
+ var schema_exports = {};
202
+ __export(schema_exports, {
203
+ ALL_NODE_TABLES: () => ALL_NODE_TABLES,
204
+ NODE_TABLE_MAP: () => NODE_TABLE_MAP,
205
+ getCreateEdgeTableDDL: () => getCreateEdgeTableDDL,
206
+ getCreateNodeTableDDL: () => getCreateNodeTableDDL
207
+ });
201
208
  function getCreateNodeTableDDL(tableName) {
202
209
  return `CREATE NODE TABLE IF NOT EXISTS ${tableName} (
203
210
  id STRING,
@@ -2580,6 +2587,65 @@ var init_resolve_phase = __esm({
2580
2587
  }
2581
2588
  });
2582
2589
 
2590
+ // src/storage/db-manager.ts
2591
+ var db_manager_exports = {};
2592
+ __export(db_manager_exports, {
2593
+ DbManager: () => DbManager
2594
+ });
2595
+ var DbManager;
2596
+ var init_db_manager = __esm({
2597
+ "src/storage/db-manager.ts"() {
2598
+ DbManager = class {
2599
+ db = null;
2600
+ conn = null;
2601
+ dbPath;
2602
+ readOnly;
2603
+ constructor(dbPath, readOnly = false) {
2604
+ this.dbPath = dbPath;
2605
+ this.readOnly = readOnly;
2606
+ }
2607
+ async init() {
2608
+ if (!this.readOnly) {
2609
+ fs26.mkdirSync(path32.dirname(this.dbPath), { recursive: true });
2610
+ }
2611
+ this.db = new Database(this.dbPath, 0, true, this.readOnly);
2612
+ await this.db.init();
2613
+ this.conn = new Connection(this.db);
2614
+ await this.conn.init();
2615
+ }
2616
+ async query(cypher) {
2617
+ if (!this.conn) throw new Error("Database not initialized");
2618
+ const result = await this.conn.query(cypher);
2619
+ const qr = Array.isArray(result) ? result[0] : result;
2620
+ const rows = await qr.getAll();
2621
+ qr.close();
2622
+ return rows;
2623
+ }
2624
+ async execute(cypher) {
2625
+ if (!this.conn) throw new Error("Database not initialized");
2626
+ const result = await this.conn.query(cypher);
2627
+ const qr = Array.isArray(result) ? result[0] : result;
2628
+ qr.close();
2629
+ }
2630
+ close() {
2631
+ try {
2632
+ this.conn?.closeSync();
2633
+ } catch {
2634
+ }
2635
+ try {
2636
+ this.db?.closeSync();
2637
+ } catch {
2638
+ }
2639
+ this.conn = null;
2640
+ this.db = null;
2641
+ }
2642
+ get isOpen() {
2643
+ return this.conn !== null;
2644
+ }
2645
+ };
2646
+ }
2647
+ });
2648
+
2583
2649
  // src/llm/providers/openai.ts
2584
2650
  var openai_exports = {};
2585
2651
  __export(openai_exports, {
@@ -2590,18 +2656,42 @@ var init_openai = __esm({
2590
2656
  "src/llm/providers/openai.ts"() {
2591
2657
  OpenAIProvider = class {
2592
2658
  modelName;
2593
- constructor(model) {
2659
+ endpoint;
2660
+ apiKey;
2661
+ constructor(model, baseUrl, apiKey) {
2594
2662
  this.modelName = model ?? "gpt-4o-mini";
2663
+ this.endpoint = (baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
2664
+ this.apiKey = apiKey ?? "";
2665
+ }
2666
+ resolvedKey() {
2667
+ return this.apiKey || process.env["OPENAI_API_KEY"] || "";
2668
+ }
2669
+ async getContextWindow() {
2670
+ try {
2671
+ const res = await fetch(`${this.endpoint}/models/${encodeURIComponent(this.modelName)}`, {
2672
+ headers: { "Authorization": `Bearer ${this.resolvedKey()}` },
2673
+ signal: AbortSignal.timeout(5e3)
2674
+ });
2675
+ if (!res.ok) return void 0;
2676
+ const json = await res.json();
2677
+ return json.context_window;
2678
+ } catch {
2679
+ return void 0;
2680
+ }
2595
2681
  }
2596
2682
  async summarize(prompt) {
2597
2683
  const { default: OpenAI } = await import('openai');
2598
- const client = new OpenAI({ apiKey: process.env["OPENAI_API_KEY"] });
2684
+ const client = new OpenAI({ apiKey: this.resolvedKey(), baseURL: this.endpoint });
2599
2685
  const res = await client.chat.completions.create({
2600
2686
  model: this.modelName,
2601
2687
  messages: [{ role: "user", content: prompt }],
2602
- max_tokens: 200
2688
+ max_tokens: 500
2603
2689
  });
2604
- return res.choices?.[0]?.message?.content?.trim() ?? "";
2690
+ return {
2691
+ text: res.choices?.[0]?.message?.content?.trim() ?? "",
2692
+ promptTokens: res.usage?.prompt_tokens ?? 0,
2693
+ completionTokens: res.usage?.completion_tokens ?? 0
2694
+ };
2605
2695
  }
2606
2696
  };
2607
2697
  }
@@ -2612,24 +2702,51 @@ var anthropic_exports = {};
2612
2702
  __export(anthropic_exports, {
2613
2703
  AnthropicProvider: () => AnthropicProvider
2614
2704
  });
2615
- var AnthropicProvider;
2705
+ var ANTHROPIC_CONTEXT, AnthropicProvider;
2616
2706
  var init_anthropic = __esm({
2617
2707
  "src/llm/providers/anthropic.ts"() {
2708
+ ANTHROPIC_CONTEXT = {
2709
+ "claude-3-5-sonnet-20241022": 2e5,
2710
+ "claude-3-5-haiku-20241022": 2e5,
2711
+ "claude-3-opus-20240229": 2e5,
2712
+ "claude-3-sonnet-20240229": 2e5,
2713
+ "claude-3-haiku-20240307": 2e5,
2714
+ "claude-haiku-4-5": 2e5,
2715
+ "claude-sonnet-4-5": 2e5,
2716
+ "claude-opus-4-5": 2e5
2717
+ };
2618
2718
  AnthropicProvider = class {
2619
2719
  modelName;
2620
- constructor(model) {
2720
+ endpoint;
2721
+ apiKey;
2722
+ constructor(model, baseUrl, apiKey) {
2621
2723
  this.modelName = model ?? "claude-haiku-4-5";
2724
+ this.endpoint = (baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
2725
+ this.apiKey = apiKey ?? "";
2726
+ }
2727
+ resolvedKey() {
2728
+ return this.apiKey || process.env["ANTHROPIC_API_KEY"] || "";
2729
+ }
2730
+ async getContextWindow() {
2731
+ return ANTHROPIC_CONTEXT[this.modelName] ?? 2e5;
2622
2732
  }
2623
2733
  async summarize(prompt) {
2624
2734
  const Anthropic = (await import('@anthropic-ai/sdk')).default;
2625
- const client = new Anthropic({ apiKey: process.env["ANTHROPIC_API_KEY"] });
2735
+ const client = new Anthropic({
2736
+ apiKey: this.resolvedKey(),
2737
+ baseURL: this.endpoint
2738
+ });
2626
2739
  const res = await client.messages.create({
2627
2740
  model: this.modelName,
2628
- max_tokens: 200,
2741
+ max_tokens: 500,
2629
2742
  messages: [{ role: "user", content: prompt }]
2630
2743
  });
2631
2744
  const block = res.content?.[0];
2632
- return block && block.type === "text" ? block.text.trim() : "";
2745
+ return {
2746
+ text: block && block.type === "text" ? block.text.trim() : "",
2747
+ promptTokens: res.usage?.input_tokens ?? 0,
2748
+ completionTokens: res.usage?.output_tokens ?? 0
2749
+ };
2633
2750
  }
2634
2751
  };
2635
2752
  }
@@ -2640,20 +2757,58 @@ var custom_exports = {};
2640
2757
  __export(custom_exports, {
2641
2758
  CustomProvider: () => CustomProvider
2642
2759
  });
2643
- var CustomProvider;
2760
+ var KNOWN_CONTEXT_WINDOWS, CustomProvider;
2644
2761
  var init_custom = __esm({
2645
2762
  "src/llm/providers/custom.ts"() {
2763
+ KNOWN_CONTEXT_WINDOWS = {
2764
+ // DeepSeek
2765
+ "deepseek-v4-flash": 64e3,
2766
+ "deepseek-v4-pro": 64e3,
2767
+ "deepseek-chat": 64e3,
2768
+ "deepseek-coder": 16384,
2769
+ "deepseek-v2": 128e3,
2770
+ "deepseek-v3": 64e3,
2771
+ // Groq
2772
+ "llama3-8b-8192": 8192,
2773
+ "llama3-70b-8192": 8192,
2774
+ "mixtral-8x7b-32768": 32768,
2775
+ "gemma-7b-it": 8192,
2776
+ // Together AI
2777
+ "mistralai/Mistral-7B-v0.1": 8192,
2778
+ "meta-llama/Llama-3-8b-chat": 8192,
2779
+ // LM Studio / vLLM defaults
2780
+ "default": 4096
2781
+ };
2646
2782
  CustomProvider = class {
2647
2783
  modelName;
2784
+ endpoint;
2648
2785
  baseUrl;
2649
2786
  apiKey;
2650
2787
  constructor(baseUrl, model, apiKey = "") {
2651
2788
  this.baseUrl = baseUrl.replace(/\/$/, "");
2652
2789
  this.modelName = model;
2653
2790
  this.apiKey = apiKey;
2791
+ this.endpoint = this.baseUrl;
2792
+ }
2793
+ async getContextWindow() {
2794
+ const modelsUrl = this.baseUrl.endsWith("/v1") ? `${this.baseUrl}/models` : `${this.baseUrl}/v1/models`;
2795
+ try {
2796
+ const res = await fetch(modelsUrl, {
2797
+ headers: this.apiKey ? { "Authorization": `Bearer ${this.apiKey}` } : {},
2798
+ signal: AbortSignal.timeout(5e3)
2799
+ });
2800
+ if (res.ok) {
2801
+ const json = await res.json();
2802
+ const model = json.data?.find((m) => m.id === this.modelName);
2803
+ if (model?.context_window) return model.context_window;
2804
+ }
2805
+ } catch {
2806
+ }
2807
+ return KNOWN_CONTEXT_WINDOWS[this.modelName];
2654
2808
  }
2655
2809
  async summarize(prompt) {
2656
- const url = `${this.baseUrl}/chat/completions`;
2810
+ const base = this.baseUrl.endsWith("/v1") ? this.baseUrl : `${this.baseUrl}/v1`;
2811
+ const url = `${base}/chat/completions`;
2657
2812
  const headers = {
2658
2813
  "Content-Type": "application/json"
2659
2814
  };
@@ -2663,16 +2818,22 @@ var init_custom = __esm({
2663
2818
  const body = JSON.stringify({
2664
2819
  model: this.modelName,
2665
2820
  messages: [{ role: "user", content: prompt }],
2666
- max_tokens: 200,
2821
+ max_tokens: 500,
2667
2822
  temperature: 0.3
2668
2823
  });
2669
2824
  const res = await fetch(url, { method: "POST", headers, body });
2670
2825
  if (!res.ok) {
2671
- const text = await res.text().catch(() => res.statusText);
2672
- throw new Error(`Custom LLM API error ${res.status}: ${text}`);
2826
+ const text2 = await res.text().catch(() => res.statusText);
2827
+ throw new Error(`Custom LLM API error ${res.status}: ${text2}`);
2673
2828
  }
2674
2829
  const data = await res.json();
2675
- return data.choices?.[0]?.message?.content?.trim() ?? "";
2830
+ const msg = data.choices?.[0]?.message;
2831
+ const text = (msg?.content?.trim() || msg?.reasoning_content?.trim()) ?? "";
2832
+ return {
2833
+ text,
2834
+ promptTokens: data.usage?.prompt_tokens ?? 0,
2835
+ completionTokens: data.usage?.completion_tokens ?? 0
2836
+ };
2676
2837
  }
2677
2838
  };
2678
2839
  }
@@ -2688,10 +2849,31 @@ var init_ollama = __esm({
2688
2849
  "src/llm/providers/ollama.ts"() {
2689
2850
  OllamaProvider = class {
2690
2851
  modelName;
2852
+ endpoint;
2691
2853
  baseUrl;
2692
2854
  constructor(model, baseUrl) {
2693
2855
  this.modelName = model ?? "llama3";
2694
2856
  this.baseUrl = baseUrl ?? "http://localhost:11434";
2857
+ this.endpoint = this.baseUrl;
2858
+ }
2859
+ async getContextWindow() {
2860
+ try {
2861
+ const res = await fetch(`${this.baseUrl}/api/show`, {
2862
+ method: "POST",
2863
+ headers: { "Content-Type": "application/json" },
2864
+ body: JSON.stringify({ model: this.modelName }),
2865
+ signal: AbortSignal.timeout(5e3)
2866
+ });
2867
+ if (!res.ok) return void 0;
2868
+ const json = await res.json();
2869
+ const info = json.model_info ?? {};
2870
+ for (const [k, v] of Object.entries(info)) {
2871
+ if (k.endsWith(".context_length") && typeof v === "number") return v;
2872
+ }
2873
+ return void 0;
2874
+ } catch {
2875
+ return void 0;
2876
+ }
2695
2877
  }
2696
2878
  async summarize(prompt) {
2697
2879
  const url = `${this.baseUrl}/api/generate`;
@@ -2709,7 +2891,11 @@ var init_ollama = __esm({
2709
2891
  throw new Error(`Ollama request failed: ${res.status} ${res.statusText}`);
2710
2892
  }
2711
2893
  const json = await res.json();
2712
- return (json.response ?? "").trim();
2894
+ return {
2895
+ text: (json.response ?? "").trim(),
2896
+ promptTokens: json.prompt_eval_count ?? 0,
2897
+ completionTokens: json.eval_count ?? 0
2898
+ };
2713
2899
  }
2714
2900
  };
2715
2901
  }
@@ -2725,11 +2911,11 @@ async function createLLMProvider(config = {}) {
2725
2911
  switch (provider) {
2726
2912
  case "openai": {
2727
2913
  const { OpenAIProvider: OpenAIProvider2 } = await Promise.resolve().then(() => (init_openai(), openai_exports));
2728
- return new OpenAIProvider2(model);
2914
+ return new OpenAIProvider2(model, baseUrl, apiKey);
2729
2915
  }
2730
2916
  case "anthropic": {
2731
2917
  const { AnthropicProvider: AnthropicProvider2 } = await Promise.resolve().then(() => (init_anthropic(), anthropic_exports));
2732
- return new AnthropicProvider2(model);
2918
+ return new AnthropicProvider2(model, baseUrl, apiKey);
2733
2919
  }
2734
2920
  case "custom": {
2735
2921
  const { CustomProvider: CustomProvider2 } = await Promise.resolve().then(() => (init_custom(), custom_exports));
@@ -2741,7 +2927,7 @@ async function createLLMProvider(config = {}) {
2741
2927
  case "ollama":
2742
2928
  default: {
2743
2929
  const { OllamaProvider: OllamaProvider2 } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
2744
- return new OllamaProvider2(model);
2930
+ return new OllamaProvider2(model, baseUrl);
2745
2931
  }
2746
2932
  }
2747
2933
  }
@@ -2812,65 +2998,6 @@ var init_embedder = __esm({
2812
2998
  }
2813
2999
  });
2814
3000
 
2815
- // src/storage/db-manager.ts
2816
- var db_manager_exports = {};
2817
- __export(db_manager_exports, {
2818
- DbManager: () => DbManager
2819
- });
2820
- var DbManager;
2821
- var init_db_manager = __esm({
2822
- "src/storage/db-manager.ts"() {
2823
- DbManager = class {
2824
- db = null;
2825
- conn = null;
2826
- dbPath;
2827
- readOnly;
2828
- constructor(dbPath, readOnly = false) {
2829
- this.dbPath = dbPath;
2830
- this.readOnly = readOnly;
2831
- }
2832
- async init() {
2833
- if (!this.readOnly) {
2834
- fs26.mkdirSync(path32.dirname(this.dbPath), { recursive: true });
2835
- }
2836
- this.db = new Database(this.dbPath, 0, true, this.readOnly);
2837
- await this.db.init();
2838
- this.conn = new Connection(this.db);
2839
- await this.conn.init();
2840
- }
2841
- async query(cypher) {
2842
- if (!this.conn) throw new Error("Database not initialized");
2843
- const result = await this.conn.query(cypher);
2844
- const qr = Array.isArray(result) ? result[0] : result;
2845
- const rows = await qr.getAll();
2846
- qr.close();
2847
- return rows;
2848
- }
2849
- async execute(cypher) {
2850
- if (!this.conn) throw new Error("Database not initialized");
2851
- const result = await this.conn.query(cypher);
2852
- const qr = Array.isArray(result) ? result[0] : result;
2853
- qr.close();
2854
- }
2855
- close() {
2856
- try {
2857
- this.conn?.closeSync();
2858
- } catch {
2859
- }
2860
- try {
2861
- this.db?.closeSync();
2862
- } catch {
2863
- }
2864
- this.conn = null;
2865
- this.db = null;
2866
- }
2867
- get isOpen() {
2868
- return this.conn !== null;
2869
- }
2870
- };
2871
- }
2872
- });
2873
-
2874
3001
  // src/multi-repo/graph-from-db.ts
2875
3002
  var graph_from_db_exports = {};
2876
3003
  __export(graph_from_db_exports, {
@@ -3760,19 +3887,26 @@ function executeFIND(stmt, graph) {
3760
3887
  totalCount
3761
3888
  };
3762
3889
  }
3890
+ function findBestNode(graph, name) {
3891
+ const matches = [];
3892
+ for (const node of graph.allNodes()) {
3893
+ if (node.name === name) matches.push(node);
3894
+ }
3895
+ if (matches.length === 0) return void 0;
3896
+ if (matches.length === 1) return matches[0];
3897
+ return matches.sort((a, b) => {
3898
+ const aEdges = [...graph.findEdgesFrom(a.id)].length + [...graph.findEdgesTo(a.id)].length;
3899
+ const bEdges = [...graph.findEdgesFrom(b.id)].length + [...graph.findEdgesTo(b.id)].length;
3900
+ return bEdges - aEdges;
3901
+ })[0];
3902
+ }
3763
3903
  function executeTRAVERSE(stmt, graph) {
3764
3904
  const start = Date.now();
3765
3905
  const maxDepth = stmt.depth ?? 5;
3766
3906
  const edgeKind = stmt.edgeKind;
3767
3907
  const direction = stmt.direction ?? "OUTGOING";
3768
3908
  const deadline = start + EXECUTION_TIMEOUT_MS;
3769
- let startNode;
3770
- for (const node of graph.allNodes()) {
3771
- if (node.name === stmt.from) {
3772
- startNode = node;
3773
- break;
3774
- }
3775
- }
3909
+ const startNode = findBestNode(graph, stmt.from);
3776
3910
  if (!startNode) {
3777
3911
  return {
3778
3912
  nodes: [],
@@ -3836,13 +3970,8 @@ function executeTRAVERSE(stmt, graph) {
3836
3970
  function executePATH(stmt, graph) {
3837
3971
  const start = Date.now();
3838
3972
  const deadline = start + EXECUTION_TIMEOUT_MS;
3839
- let startNode;
3840
- let endNode;
3841
- for (const node of graph.allNodes()) {
3842
- if (node.name === stmt.from) startNode = node;
3843
- if (node.name === stmt.to) endNode = node;
3844
- if (startNode && endNode) break;
3845
- }
3973
+ const startNode = findBestNode(graph, stmt.from);
3974
+ const endNode = findBestNode(graph, stmt.to);
3846
3975
  if (!startNode || !endNode) {
3847
3976
  return {
3848
3977
  path: null,
@@ -4615,7 +4744,7 @@ var init_users_db = __esm({
4615
4744
  constructor(dbPath) {
4616
4745
  const dir = path32.dirname(dbPath);
4617
4746
  secureMkdir(dir);
4618
- this.db = new Database2(dbPath);
4747
+ this.db = new Database3(dbPath);
4619
4748
  this.db.pragma("journal_mode = WAL");
4620
4749
  this.db.pragma("foreign_keys = ON");
4621
4750
  this.createTables();
@@ -6770,12 +6899,16 @@ function textSearch(graph, query, limit = 20) {
6770
6899
  let score = 0;
6771
6900
  const nameLC = node.name.toLowerCase();
6772
6901
  const pathLC = node.filePath.toLowerCase();
6902
+ const fileBaseName = (node.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, "") ?? "").toLowerCase();
6903
+ const contentLC = node.content?.toLowerCase() ?? "";
6773
6904
  for (const term of terms) {
6774
6905
  if (nameLC === term) score += 10;
6775
6906
  else if (nameLC.startsWith(term)) score += 7;
6776
6907
  else if (nameLC.includes(term)) score += 5;
6777
- if (pathLC.includes(term)) score += 2;
6778
- if (node.content?.toLowerCase().includes(term)) score += 3;
6908
+ if (fileBaseName === term) score += 8;
6909
+ else if (fileBaseName.startsWith(term)) score += 5;
6910
+ else if (pathLC.includes(term)) score += 2;
6911
+ if (contentLC.includes(term)) score += 3;
6779
6912
  }
6780
6913
  if (score > 0) {
6781
6914
  if (isDistPath(node.filePath)) score -= 8;
@@ -6830,7 +6963,7 @@ var VectorIndex = class {
6830
6963
  this.sqlitePath = sqlitePath;
6831
6964
  }
6832
6965
  async init() {
6833
- this.db = new Database2(this.sqlitePath);
6966
+ this.db = new Database3(this.sqlitePath);
6834
6967
  this.db.pragma("journal_mode = WAL");
6835
6968
  this.db.exec(`
6836
6969
  CREATE TABLE IF NOT EXISTS ${EMBED_TABLE} (
@@ -6960,24 +7093,92 @@ function siftDown(arr, i, score) {
6960
7093
  }
6961
7094
  }
6962
7095
  init_embedder();
7096
+
7097
+ // src/search/reranker.ts
7098
+ function tokenizeForRerank(text) {
7099
+ return text.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s\-_./:(){}[\]<>,"'`~!@#$%^&*+=|;?\\]+/).filter((t) => t.length >= 2);
7100
+ }
7101
+ var DEFAULT_KIND_WEIGHTS = {
7102
+ class: 1.2,
7103
+ interface: 1.15,
7104
+ function: 1.1,
7105
+ method: 1.08,
7106
+ type_alias: 1.03,
7107
+ enum: 1.02,
7108
+ constant: 0.98,
7109
+ variable: 0.9,
7110
+ file: 0.85
7111
+ };
7112
+ var PATH_MULTIPLIER_TEST = 0.4;
7113
+ var PATH_MULTIPLIER_DIST = 0.25;
7114
+ function rerank(query, results, options = {}) {
7115
+ if (results.length === 0) return results;
7116
+ const {
7117
+ nameWeight = 0.4,
7118
+ snippetWeight = 0.25,
7119
+ kindWeights = {}
7120
+ } = options;
7121
+ const effectiveKindWeights = {
7122
+ ...DEFAULT_KIND_WEIGHTS,
7123
+ ...kindWeights
7124
+ };
7125
+ const queryTerms = [...new Set(tokenizeForRerank(query))];
7126
+ if (queryTerms.length === 0) return results.slice();
7127
+ const queryLower = query.toLowerCase();
7128
+ const scored = results.map((r) => {
7129
+ let bonus = 0;
7130
+ const nameLower = r.name.toLowerCase();
7131
+ const nameTerms = tokenizeForRerank(r.name);
7132
+ if (nameLower === queryLower) {
7133
+ bonus += nameWeight;
7134
+ } else if (nameLower.startsWith(queryLower)) {
7135
+ bonus += nameWeight * 0.75;
7136
+ } else if (queryLower.includes(nameLower) && nameLower.length >= 3) {
7137
+ bonus += nameWeight * 0.45;
7138
+ } else {
7139
+ const matchCount = queryTerms.filter((t) => nameTerms.includes(t)).length;
7140
+ if (matchCount > 0) {
7141
+ const overlap = matchCount / queryTerms.length;
7142
+ bonus += nameWeight * overlap * 0.6;
7143
+ }
7144
+ }
7145
+ if (r.snippet && r.snippet.length > 0) {
7146
+ const snippetLower = r.snippet.toLowerCase();
7147
+ const hitCount = queryTerms.filter((t) => snippetLower.includes(t)).length;
7148
+ bonus += snippetWeight * (hitCount / queryTerms.length);
7149
+ }
7150
+ const kw = effectiveKindWeights[r.kind] ?? 1;
7151
+ const fp = r.filePath;
7152
+ const fpNorm = "/" + fp;
7153
+ const isTestPath = fpNorm.includes("/test/") || fpNorm.includes("/tests/") || fpNorm.includes("/spec/") || fpNorm.includes("/__tests__/") || fp.includes(".test.") || fp.includes(".spec.");
7154
+ const isDistPath = fpNorm.includes("/dist/") || fpNorm.includes("/build/") || fp.endsWith(".d.ts") || fpNorm.includes("/node_modules/");
7155
+ const pathMul = isDistPath ? PATH_MULTIPLIER_DIST : isTestPath ? PATH_MULTIPLIER_TEST : 1;
7156
+ const clampedBonus = Math.max(0, Math.min(nameWeight + snippetWeight, bonus));
7157
+ const finalScore = r.score * (1 + clampedBonus) * kw * pathMul;
7158
+ return { result: r, finalScore };
7159
+ });
7160
+ return scored.sort((a, b) => b.finalScore - a.finalScore).map(({ result, finalScore }) => ({ ...result, score: finalScore }));
7161
+ }
7162
+
7163
+ // src/search/hybrid-search.ts
6963
7164
  async function hybridSearch(graph, query, limit, options = {}) {
6964
7165
  const { vectorDbPath, bm25Limit = 50, vectorLimit = 50, bm25Results: precomputedBm25 } = options;
7166
+ const rerankEnabled = options.rerank?.enabled !== false;
7167
+ const rerankOptions = rerankEnabled && options.rerank && options.rerank.enabled !== false ? options.rerank : {};
6965
7168
  const bm25Promise = precomputedBm25 ? Promise.resolve(precomputedBm25) : Promise.resolve(textSearch(graph, query, bm25Limit));
6966
7169
  const hasVectorDb = Boolean(vectorDbPath && fs26.existsSync(vectorDbPath));
6967
7170
  if (!hasVectorDb) {
6968
7171
  const bm25Results2 = await bm25Promise;
6969
- return {
6970
- results: bm25Results2.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
6971
- searchMode: "bm25"
6972
- };
7172
+ const candidates2 = bm25Results2.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" }));
7173
+ const final2 = rerankEnabled ? rerank(query, candidates2, rerankOptions) : candidates2;
7174
+ return { results: final2, searchMode: "bm25" };
6973
7175
  }
6974
7176
  const vectorPromise = runVectorSearch(vectorDbPath, query, vectorLimit);
6975
7177
  const [bm25Results, vectorResults] = await Promise.all([bm25Promise, vectorPromise]);
6976
7178
  if (vectorResults === null || vectorResults.length === 0) {
6977
- return {
6978
- results: bm25Results.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
6979
- searchMode: "bm25"
6980
- };
7179
+ const candidates2 = bm25Results.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" }));
7180
+ const final2 = rerankEnabled ? rerank(query, candidates2, rerankOptions) : candidates2;
7181
+ return { results: final2, searchMode: "bm25" };
6981
7182
  }
6982
7183
  const vectorAsSearchResults = vectorResults.map((h) => ({
6983
7184
  nodeId: h.nodeId,
@@ -6988,10 +7189,9 @@ async function hybridSearch(graph, query, limit, options = {}) {
6988
7189
  snippet: graph.getNode(h.nodeId)?.content?.slice(0, 200)
6989
7190
  }));
6990
7191
  const merged = reciprocalRankFusion(bm25Results, vectorAsSearchResults);
6991
- return {
6992
- results: merged.slice(0, limit).map((r) => ({ ...r, searchMode: "hybrid" })),
6993
- searchMode: "hybrid"
6994
- };
7192
+ const candidates = merged.slice(0, limit).map((r) => ({ ...r, searchMode: "hybrid" }));
7193
+ const final = rerankEnabled ? rerank(query, candidates, rerankOptions) : candidates;
7194
+ return { results: final, searchMode: "hybrid" };
6995
7195
  }
6996
7196
  async function runVectorSearch(vectorDbPath, query, topK) {
6997
7197
  try {
@@ -7021,11 +7221,19 @@ function tokenize(text) {
7021
7221
  return text.toLowerCase().split(/[\s\-_./\\:(){}[\]<>,"'`~!@#$%^&*+=|;?]+/).filter((t) => t.length >= 2 && t.length <= 64);
7022
7222
  }
7023
7223
  function nodeToDoc(node) {
7224
+ const fileBaseName = node.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, "") ?? "";
7024
7225
  return [
7025
7226
  node.name,
7227
+ node.name,
7228
+ // repeat name to boost exact-name matches
7026
7229
  node.kind,
7027
7230
  node.filePath,
7028
- (node.content ?? "").slice(0, 1e3)
7231
+ fileBaseName,
7232
+ // filename stem (class name) — extra weight
7233
+ fileBaseName,
7234
+ // repeat again for stronger class-name boosting
7235
+ (node.content ?? "").slice(0, 1500)
7236
+ // increased from 1000
7029
7237
  ].join(" ");
7030
7238
  }
7031
7239
  function heapTopK(scores, k) {
@@ -7122,7 +7330,7 @@ var Bm25Index = class {
7122
7330
  } catch {
7123
7331
  }
7124
7332
  }
7125
- const db = new Database2(this.dbPath);
7333
+ const db = new Database3(this.dbPath);
7126
7334
  db.pragma("journal_mode = WAL");
7127
7335
  db.exec(`
7128
7336
  CREATE TABLE bm25_index (term TEXT PRIMARY KEY, postings TEXT NOT NULL);
@@ -7157,7 +7365,7 @@ var Bm25Index = class {
7157
7365
  */
7158
7366
  load() {
7159
7367
  if (!fs26.existsSync(this.dbPath)) return;
7160
- const db = new Database2(this.dbPath, { readonly: true });
7368
+ const db = new Database3(this.dbPath, { readonly: true });
7161
7369
  try {
7162
7370
  const getMeta = db.prepare("SELECT value FROM bm25_meta WHERE key = ?");
7163
7371
  this.avgdl = parseFloat(getMeta.get("avgdl")?.value ?? "1");
@@ -7250,7 +7458,7 @@ var Bm25Index = class {
7250
7458
  newTermFreqs.set(node.id, tf);
7251
7459
  }
7252
7460
  const newTermSet = new Set([...newTermFreqs.values()].flatMap((m) => [...m.keys()]));
7253
- const db = new Database2(this.dbPath);
7461
+ const db = new Database3(this.dbPath);
7254
7462
  db.pragma("journal_mode = WAL");
7255
7463
  const termsToRewrite = /* @__PURE__ */ new Map();
7256
7464
  for (const term of newTermSet) {
@@ -7512,19 +7720,24 @@ function buildNodeProps(node) {
7512
7720
  function escCypher(s) {
7513
7721
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
7514
7722
  }
7515
- var GLOBAL_DIR = path32.join(os13.homedir(), ".code-intel");
7516
- var REPOS_FILE = path32.join(GLOBAL_DIR, "repos.json");
7723
+ function getGlobalDir() {
7724
+ return path32.join(process.env["CODE_INTEL_HOME"] ?? os13.homedir(), ".code-intel");
7725
+ }
7726
+ function getReposFile() {
7727
+ return path32.join(getGlobalDir(), "repos.json");
7728
+ }
7517
7729
  function loadRegistry() {
7518
7730
  try {
7519
- const data = fs26.readFileSync(REPOS_FILE, "utf-8");
7731
+ const data = fs26.readFileSync(getReposFile(), "utf-8");
7520
7732
  return JSON.parse(data);
7521
7733
  } catch {
7522
7734
  return [];
7523
7735
  }
7524
7736
  }
7525
7737
  function saveRegistry(entries) {
7526
- fs26.mkdirSync(GLOBAL_DIR, { recursive: true });
7527
- fs26.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
7738
+ const globalDir = getGlobalDir();
7739
+ fs26.mkdirSync(globalDir, { recursive: true });
7740
+ fs26.writeFileSync(getReposFile(), JSON.stringify(entries, null, 2));
7528
7741
  }
7529
7742
  function upsertRepo(entry) {
7530
7743
  const entries = loadRegistry();
@@ -8741,7 +8954,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8741
8954
  // ── Search & inspect ─────────────────────────────────────────────────
8742
8955
  {
8743
8956
  name: "search",
8744
- description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc. Optionally scope to a specific repo or group.",
8957
+ description: `BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc. Optionally scope to a specific repo or group. TIP: To find a method in a specific class when multiple classes have the same method name, include the class/file name in the query (e.g. search("Token requestAccessToken redis save") to find Token.php's version vs JWT.php's). This is the preferred alternative to bare inspect() for ambiguous method names.`,
8745
8958
  inputSchema: {
8746
8959
  type: "object",
8747
8960
  properties: {
@@ -8757,16 +8970,31 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8757
8970
  },
8758
8971
  {
8759
8972
  name: "inspect",
8760
- description: "360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview (first 500 chars)",
8973
+ description: '360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview. When multiple files define the same symbol name, returns a disambiguation list \u2014 re-call with file_path to select the correct implementation. IMPORTANT: When you expect a symbol to be in a specific class (e.g. Token vs JWT, or CMS vs API module), always pass file_path to avoid silently resolving the wrong class. For methods that exist in multiple classes (requestAccessToken, verify, revoke, login, logout), prefer search("ClassName methodName unique-context") over bare inspect to guarantee the right class.',
8761
8974
  inputSchema: {
8762
8975
  type: "object",
8763
8976
  properties: {
8764
8977
  symbol_name: { type: "string", description: "Exact symbol name to inspect" },
8978
+ file_path: { type: "string", description: 'Optional partial file path to disambiguate when multiple symbols share the same name (e.g. "Token.php" or "src/Modules/CMS"). Required when inspect returns a disambiguation list or when you know the target class.' },
8765
8979
  ..._tokenProp
8766
8980
  },
8767
8981
  required: ["symbol_name"]
8768
8982
  }
8769
8983
  },
8984
+ {
8985
+ name: "get_source",
8986
+ description: `Read raw source lines from any file in the workspace. Use this to verify exact implementation details that inspect's content preview may not fully show. Supports partial file path matching (e.g. "Token.php"). Returns numbered lines.`,
8987
+ inputSchema: {
8988
+ type: "object",
8989
+ properties: {
8990
+ file_path: { type: "string", description: 'File path to read (partial match supported, e.g. "Token.php" or "src/Modules/CMS/AuthController.php")' },
8991
+ start_line: { type: "number", description: "First line to return (1-indexed, default: 1)" },
8992
+ end_line: { type: "number", description: "Last line to return inclusive (default: start_line + 99). Max 300 lines per call." },
8993
+ ..._tokenProp
8994
+ },
8995
+ required: ["file_path"]
8996
+ }
8997
+ },
8770
8998
  {
8771
8999
  name: "blast_radius",
8772
9000
  description: "Impact analysis: traverse the call/import graph to find all symbols that depend on or are affected by a given symbol. Returns risk level (LOW / MEDIUM / HIGH).",
@@ -9319,8 +9547,41 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolve
9319
9547
  // ── inspect ────────────────────────────────────────────────────────────
9320
9548
  case "inspect": {
9321
9549
  const symbolName = a.symbol_name;
9322
- const node = findNodeByName(graph, symbolName);
9323
- if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
9550
+ const filePathHint = a.file_path;
9551
+ const allMatchingNodes = findNodesByName(graph, symbolName);
9552
+ if (allMatchingNodes.length === 0) {
9553
+ return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
9554
+ }
9555
+ let node = allMatchingNodes[0];
9556
+ if (allMatchingNodes.length > 1) {
9557
+ if (filePathHint) {
9558
+ const filtered = allMatchingNodes.filter((n) => n.filePath.includes(filePathHint));
9559
+ if (filtered.length === 0) {
9560
+ return {
9561
+ content: [{
9562
+ type: "text",
9563
+ text: compact({
9564
+ disambiguation: true,
9565
+ message: `No match for file_path "${filePathHint}". Available definitions of "${symbolName}":`,
9566
+ candidates: allMatchingNodes.map((n) => ({ filePath: n.filePath, kind: n.kind, startLine: n.startLine }))
9567
+ })
9568
+ }]
9569
+ };
9570
+ }
9571
+ node = filtered[0];
9572
+ } else {
9573
+ return {
9574
+ content: [{
9575
+ type: "text",
9576
+ text: compact({
9577
+ disambiguation: true,
9578
+ message: `Multiple definitions of "${symbolName}" found. Re-call inspect with file_path to select one.`,
9579
+ candidates: allMatchingNodes.map((n) => ({ filePath: n.filePath, kind: n.kind, startLine: n.startLine, content: n.content?.slice(0, 120) }))
9580
+ })
9581
+ }]
9582
+ };
9583
+ }
9584
+ }
9324
9585
  const incoming = [...graph.findEdgesTo(node.id)];
9325
9586
  const outgoing = [...graph.findEdgesFrom(node.id)];
9326
9587
  const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
@@ -9367,12 +9628,70 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolve
9367
9628
  kind: graph.getNode(e.target)?.kind
9368
9629
  })),
9369
9630
  cluster,
9370
- content: node.content?.slice(0, 500),
9631
+ content: node.content?.slice(0, 1500),
9632
+ contentNote: node.content && node.content.length > 1500 ? `(truncated \u2014 use get_source with file_path="${node.filePath}" start_line=${node.startLine} end_line=${node.endLine} for full source)` : void 0,
9371
9633
  ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
9372
9634
  })
9373
9635
  }]
9374
9636
  };
9375
9637
  }
9638
+ // ── get_source ─────────────────────────────────────────────────────────
9639
+ case "get_source": {
9640
+ const filePathQuery = a.file_path;
9641
+ const startLine = Math.max(1, a.start_line ?? 1);
9642
+ const maxLines = 300;
9643
+ const endLine = Math.min(a.end_line ?? startLine + 99, startLine + maxLines - 1);
9644
+ if (!workspaceRoot) {
9645
+ return { content: [{ type: "text", text: "workspaceRoot not set \u2014 cannot read files." }] };
9646
+ }
9647
+ const matchedFiles = /* @__PURE__ */ new Set();
9648
+ for (const node of graph.allNodes()) {
9649
+ if (node.filePath && node.filePath.includes(filePathQuery)) {
9650
+ matchedFiles.add(node.filePath);
9651
+ }
9652
+ }
9653
+ if (matchedFiles.size === 0) {
9654
+ return { content: [{ type: "text", text: `No indexed file matching "${filePathQuery}". Try a different partial path or use file_symbols to browse.` }] };
9655
+ }
9656
+ if (matchedFiles.size > 1) {
9657
+ return {
9658
+ content: [{
9659
+ type: "text",
9660
+ text: compact({
9661
+ disambiguation: true,
9662
+ message: `Multiple files match "${filePathQuery}". Re-call with a more specific file_path.`,
9663
+ candidates: [...matchedFiles]
9664
+ })
9665
+ }]
9666
+ };
9667
+ }
9668
+ const [resolvedPath] = [...matchedFiles];
9669
+ let fileContent;
9670
+ try {
9671
+ fileContent = fs26.readFileSync(resolvedPath, "utf-8");
9672
+ } catch {
9673
+ return { content: [{ type: "text", text: `Could not read file: ${resolvedPath}` }] };
9674
+ }
9675
+ const lines = fileContent.split("\n");
9676
+ const totalLines = lines.length;
9677
+ const sliceStart = startLine - 1;
9678
+ const sliceEnd = Math.min(endLine, totalLines);
9679
+ const selectedLines = lines.slice(sliceStart, sliceEnd);
9680
+ const numbered = selectedLines.map((l, i) => `${sliceStart + i + 1}: ${l}`).join("\n");
9681
+ return {
9682
+ content: [{
9683
+ type: "text",
9684
+ text: compact({
9685
+ filePath: resolvedPath,
9686
+ startLine,
9687
+ endLine: sliceEnd,
9688
+ totalLines,
9689
+ hasMore: sliceEnd < totalLines,
9690
+ source: numbered
9691
+ })
9692
+ }]
9693
+ };
9694
+ }
9376
9695
  // ── blast_radius ───────────────────────────────────────────────────────
9377
9696
  case "blast_radius": {
9378
9697
  const target = a.target;
@@ -9505,12 +9824,12 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolve
9505
9824
  allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
9506
9825
  }
9507
9826
  const total = allExports.length;
9508
- const exports$1 = allExports.slice(offset, offset + effectiveLimit);
9827
+ const exports = allExports.slice(offset, offset + effectiveLimit);
9509
9828
  const hasMore = offset + effectiveLimit < total;
9510
9829
  return {
9511
9830
  content: [{
9512
9831
  type: "text",
9513
- text: compact({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore })
9832
+ text: compact({ exports, total, offset, limit: effectiveLimit, hasMore })
9514
9833
  }]
9515
9834
  };
9516
9835
  }
@@ -9991,6 +10310,13 @@ function findNodeByName(graph, name) {
9991
10310
  }
9992
10311
  return void 0;
9993
10312
  }
10313
+ function findNodesByName(graph, name) {
10314
+ const results = [];
10315
+ for (const node of graph.allNodes()) {
10316
+ if (node.name === name) results.push(node);
10317
+ }
10318
+ return results;
10319
+ }
9994
10320
  function parseDiff(diffText) {
9995
10321
  const result = [];
9996
10322
  let currentFile = null;
@@ -10034,7 +10360,7 @@ var JobsDB = class {
10034
10360
  db;
10035
10361
  constructor(dbPath) {
10036
10362
  fs26.mkdirSync(path32.dirname(dbPath), { recursive: true });
10037
- this.db = new Database2(dbPath);
10363
+ this.db = new Database3(dbPath);
10038
10364
  this.db.pragma("journal_mode = WAL");
10039
10365
  this.db.pragma("foreign_keys = ON");
10040
10366
  this.createTables();