@vohongtho.infotech/code-intel 1.0.1 → 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,
@@ -528,7 +535,7 @@ var init_logger = __esm({
528
535
  const isProduction = process.env.NODE_ENV === "production";
529
536
  const logLevel = process.env.LOG_LEVEL ?? "info";
530
537
  const transports = [];
531
- transports.push(new winston.transports.Console());
538
+ transports.push(new winston.transports.Console({ stderrLevels: ["error", "warn", "info", "http", "verbose", "debug", "silly"] }));
532
539
  if (!isProduction) {
533
540
  try {
534
541
  if (!fs26.existsSync(_Logger.LOG_DIR)) {
@@ -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,138 @@ 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
+ };
2750
+ }
2751
+ };
2752
+ }
2753
+ });
2754
+
2755
+ // src/llm/providers/custom.ts
2756
+ var custom_exports = {};
2757
+ __export(custom_exports, {
2758
+ CustomProvider: () => CustomProvider
2759
+ });
2760
+ var KNOWN_CONTEXT_WINDOWS, CustomProvider;
2761
+ var init_custom = __esm({
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
+ };
2782
+ CustomProvider = class {
2783
+ modelName;
2784
+ endpoint;
2785
+ baseUrl;
2786
+ apiKey;
2787
+ constructor(baseUrl, model, apiKey = "") {
2788
+ this.baseUrl = baseUrl.replace(/\/$/, "");
2789
+ this.modelName = model;
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];
2808
+ }
2809
+ async summarize(prompt) {
2810
+ const base = this.baseUrl.endsWith("/v1") ? this.baseUrl : `${this.baseUrl}/v1`;
2811
+ const url = `${base}/chat/completions`;
2812
+ const headers = {
2813
+ "Content-Type": "application/json"
2814
+ };
2815
+ if (this.apiKey) {
2816
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
2817
+ }
2818
+ const body = JSON.stringify({
2819
+ model: this.modelName,
2820
+ messages: [{ role: "user", content: prompt }],
2821
+ max_tokens: 500,
2822
+ temperature: 0.3
2823
+ });
2824
+ const res = await fetch(url, { method: "POST", headers, body });
2825
+ if (!res.ok) {
2826
+ const text2 = await res.text().catch(() => res.statusText);
2827
+ throw new Error(`Custom LLM API error ${res.status}: ${text2}`);
2828
+ }
2829
+ const data = await res.json();
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
+ };
2633
2837
  }
2634
2838
  };
2635
2839
  }
@@ -2645,10 +2849,31 @@ var init_ollama = __esm({
2645
2849
  "src/llm/providers/ollama.ts"() {
2646
2850
  OllamaProvider = class {
2647
2851
  modelName;
2852
+ endpoint;
2648
2853
  baseUrl;
2649
2854
  constructor(model, baseUrl) {
2650
2855
  this.modelName = model ?? "llama3";
2651
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
+ }
2652
2877
  }
2653
2878
  async summarize(prompt) {
2654
2879
  const url = `${this.baseUrl}/api/generate`;
@@ -2666,7 +2891,11 @@ var init_ollama = __esm({
2666
2891
  throw new Error(`Ollama request failed: ${res.status} ${res.statusText}`);
2667
2892
  }
2668
2893
  const json = await res.json();
2669
- 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
+ };
2670
2899
  }
2671
2900
  };
2672
2901
  }
@@ -2678,20 +2907,27 @@ __export(factory_exports, {
2678
2907
  createLLMProvider: () => createLLMProvider
2679
2908
  });
2680
2909
  async function createLLMProvider(config = {}) {
2681
- const { provider = "ollama", model } = config;
2910
+ const { provider = "ollama", model, baseUrl, apiKey } = config;
2682
2911
  switch (provider) {
2683
2912
  case "openai": {
2684
2913
  const { OpenAIProvider: OpenAIProvider2 } = await Promise.resolve().then(() => (init_openai(), openai_exports));
2685
- return new OpenAIProvider2(model);
2914
+ return new OpenAIProvider2(model, baseUrl, apiKey);
2686
2915
  }
2687
2916
  case "anthropic": {
2688
2917
  const { AnthropicProvider: AnthropicProvider2 } = await Promise.resolve().then(() => (init_anthropic(), anthropic_exports));
2689
- return new AnthropicProvider2(model);
2918
+ return new AnthropicProvider2(model, baseUrl, apiKey);
2919
+ }
2920
+ case "custom": {
2921
+ const { CustomProvider: CustomProvider2 } = await Promise.resolve().then(() => (init_custom(), custom_exports));
2922
+ const url = baseUrl ?? "http://localhost:1234/v1";
2923
+ const mdl = model ?? "default";
2924
+ const key = apiKey ?? "";
2925
+ return new CustomProvider2(url, mdl, key);
2690
2926
  }
2691
2927
  case "ollama":
2692
2928
  default: {
2693
2929
  const { OllamaProvider: OllamaProvider2 } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
2694
- return new OllamaProvider2(model);
2930
+ return new OllamaProvider2(model, baseUrl);
2695
2931
  }
2696
2932
  }
2697
2933
  }
@@ -2762,168 +2998,6 @@ var init_embedder = __esm({
2762
2998
  }
2763
2999
  });
2764
3000
 
2765
- // src/storage/db-manager.ts
2766
- var db_manager_exports = {};
2767
- __export(db_manager_exports, {
2768
- DbManager: () => DbManager
2769
- });
2770
- var DbManager;
2771
- var init_db_manager = __esm({
2772
- "src/storage/db-manager.ts"() {
2773
- DbManager = class {
2774
- db = null;
2775
- conn = null;
2776
- dbPath;
2777
- readOnly;
2778
- constructor(dbPath, readOnly = false) {
2779
- this.dbPath = dbPath;
2780
- this.readOnly = readOnly;
2781
- }
2782
- async init() {
2783
- if (!this.readOnly) {
2784
- fs26.mkdirSync(path32.dirname(this.dbPath), { recursive: true });
2785
- }
2786
- this.db = new Database(this.dbPath, 0, true, this.readOnly);
2787
- await this.db.init();
2788
- this.conn = new Connection(this.db);
2789
- await this.conn.init();
2790
- }
2791
- async query(cypher) {
2792
- if (!this.conn) throw new Error("Database not initialized");
2793
- const result = await this.conn.query(cypher);
2794
- const qr = Array.isArray(result) ? result[0] : result;
2795
- const rows = await qr.getAll();
2796
- qr.close();
2797
- return rows;
2798
- }
2799
- async execute(cypher) {
2800
- if (!this.conn) throw new Error("Database not initialized");
2801
- const result = await this.conn.query(cypher);
2802
- const qr = Array.isArray(result) ? result[0] : result;
2803
- qr.close();
2804
- }
2805
- close() {
2806
- try {
2807
- this.conn?.closeSync();
2808
- } catch {
2809
- }
2810
- try {
2811
- this.db?.closeSync();
2812
- } catch {
2813
- }
2814
- this.conn = null;
2815
- this.db = null;
2816
- }
2817
- get isOpen() {
2818
- return this.conn !== null;
2819
- }
2820
- };
2821
- }
2822
- });
2823
-
2824
- // src/multi-repo/group-registry.ts
2825
- var group_registry_exports = {};
2826
- __export(group_registry_exports, {
2827
- addMember: () => addMember,
2828
- deleteGroup: () => deleteGroup,
2829
- groupExists: () => groupExists,
2830
- listGroups: () => listGroups,
2831
- loadGroup: () => loadGroup,
2832
- loadSyncResult: () => loadSyncResult,
2833
- removeMember: () => removeMember,
2834
- saveGroup: () => saveGroup,
2835
- saveSyncResult: () => saveSyncResult
2836
- });
2837
- function groupFile(name) {
2838
- return path32.join(GROUPS_DIR, `${name}.json`);
2839
- }
2840
- function loadGroup(name) {
2841
- try {
2842
- return JSON.parse(fs26.readFileSync(groupFile(name), "utf-8"));
2843
- } catch {
2844
- return null;
2845
- }
2846
- }
2847
- function saveGroup(group) {
2848
- fs26.mkdirSync(GROUPS_DIR, { recursive: true });
2849
- fs26.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
2850
- }
2851
- function listGroups() {
2852
- const groups = [];
2853
- try {
2854
- for (const file of fs26.readdirSync(GROUPS_DIR)) {
2855
- if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
2856
- try {
2857
- const g = JSON.parse(
2858
- fs26.readFileSync(path32.join(GROUPS_DIR, file), "utf-8")
2859
- );
2860
- groups.push(g);
2861
- } catch {
2862
- }
2863
- }
2864
- } catch {
2865
- }
2866
- return groups;
2867
- }
2868
- function deleteGroup(name) {
2869
- try {
2870
- fs26.unlinkSync(groupFile(name));
2871
- } catch {
2872
- }
2873
- try {
2874
- fs26.unlinkSync(path32.join(GROUPS_DIR, `${name}.sync.json`));
2875
- } catch {
2876
- }
2877
- }
2878
- function groupExists(name) {
2879
- return fs26.existsSync(groupFile(name));
2880
- }
2881
- function addMember(groupName, member) {
2882
- const group = loadGroup(groupName);
2883
- if (!group) throw new Error(`Group "${groupName}" not found.`);
2884
- const idx = group.members.findIndex((m) => m.groupPath === member.groupPath);
2885
- if (idx >= 0) {
2886
- group.members[idx] = member;
2887
- } else {
2888
- group.members.push(member);
2889
- }
2890
- saveGroup(group);
2891
- return group;
2892
- }
2893
- function removeMember(groupName, groupPath) {
2894
- const group = loadGroup(groupName);
2895
- if (!group) throw new Error(`Group "${groupName}" not found.`);
2896
- const before = group.members.length;
2897
- group.members = group.members.filter((m) => m.groupPath !== groupPath);
2898
- if (group.members.length === before) {
2899
- throw new Error(`No member at path "${groupPath}" in group "${groupName}".`);
2900
- }
2901
- saveGroup(group);
2902
- return group;
2903
- }
2904
- function saveSyncResult(result) {
2905
- fs26.mkdirSync(GROUPS_DIR, { recursive: true });
2906
- fs26.writeFileSync(
2907
- path32.join(GROUPS_DIR, `${result.groupName}.sync.json`),
2908
- JSON.stringify(result, null, 2) + "\n"
2909
- );
2910
- }
2911
- function loadSyncResult(groupName) {
2912
- try {
2913
- return JSON.parse(
2914
- fs26.readFileSync(path32.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
2915
- );
2916
- } catch {
2917
- return null;
2918
- }
2919
- }
2920
- var GROUPS_DIR;
2921
- var init_group_registry = __esm({
2922
- "src/multi-repo/group-registry.ts"() {
2923
- GROUPS_DIR = path32.join(os13.homedir(), ".code-intel", "groups");
2924
- }
2925
- });
2926
-
2927
3001
  // src/multi-repo/graph-from-db.ts
2928
3002
  var graph_from_db_exports = {};
2929
3003
  __export(graph_from_db_exports, {
@@ -3813,19 +3887,26 @@ function executeFIND(stmt, graph) {
3813
3887
  totalCount
3814
3888
  };
3815
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
+ }
3816
3903
  function executeTRAVERSE(stmt, graph) {
3817
3904
  const start = Date.now();
3818
3905
  const maxDepth = stmt.depth ?? 5;
3819
3906
  const edgeKind = stmt.edgeKind;
3820
3907
  const direction = stmt.direction ?? "OUTGOING";
3821
3908
  const deadline = start + EXECUTION_TIMEOUT_MS;
3822
- let startNode;
3823
- for (const node of graph.allNodes()) {
3824
- if (node.name === stmt.from) {
3825
- startNode = node;
3826
- break;
3827
- }
3828
- }
3909
+ const startNode = findBestNode(graph, stmt.from);
3829
3910
  if (!startNode) {
3830
3911
  return {
3831
3912
  nodes: [],
@@ -3889,13 +3970,8 @@ function executeTRAVERSE(stmt, graph) {
3889
3970
  function executePATH(stmt, graph) {
3890
3971
  const start = Date.now();
3891
3972
  const deadline = start + EXECUTION_TIMEOUT_MS;
3892
- let startNode;
3893
- let endNode;
3894
- for (const node of graph.allNodes()) {
3895
- if (node.name === stmt.from) startNode = node;
3896
- if (node.name === stmt.to) endNode = node;
3897
- if (startNode && endNode) break;
3898
- }
3973
+ const startNode = findBestNode(graph, stmt.from);
3974
+ const endNode = findBestNode(graph, stmt.to);
3899
3975
  if (!startNode || !endNode) {
3900
3976
  return {
3901
3977
  path: null,
@@ -4585,6 +4661,7 @@ var init_codes = __esm({
4585
4661
  RATE_LIMIT_EXCEEDED: "CI-1100",
4586
4662
  PAYLOAD_TOO_LARGE: "CI-1101",
4587
4663
  INVALID_REQUEST: "CI-1200",
4664
+ CONFLICT: "CI-1409",
4588
4665
  // Config (CI-2xxx)
4589
4666
  CONFIG_INVALID: "CI-2000",
4590
4667
  CONFIG_NOT_FOUND: "CI-2001",
@@ -4667,7 +4744,7 @@ var init_users_db = __esm({
4667
4744
  constructor(dbPath) {
4668
4745
  const dir = path32.dirname(dbPath);
4669
4746
  secureMkdir(dir);
4670
- this.db = new Database2(dbPath);
4747
+ this.db = new Database3(dbPath);
4671
4748
  this.db.pragma("journal_mode = WAL");
4672
4749
  this.db.pragma("foreign_keys = ON");
4673
4750
  this.createTables();
@@ -6822,12 +6899,16 @@ function textSearch(graph, query, limit = 20) {
6822
6899
  let score = 0;
6823
6900
  const nameLC = node.name.toLowerCase();
6824
6901
  const pathLC = node.filePath.toLowerCase();
6902
+ const fileBaseName = (node.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, "") ?? "").toLowerCase();
6903
+ const contentLC = node.content?.toLowerCase() ?? "";
6825
6904
  for (const term of terms) {
6826
6905
  if (nameLC === term) score += 10;
6827
6906
  else if (nameLC.startsWith(term)) score += 7;
6828
6907
  else if (nameLC.includes(term)) score += 5;
6829
- if (pathLC.includes(term)) score += 2;
6830
- 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;
6831
6912
  }
6832
6913
  if (score > 0) {
6833
6914
  if (isDistPath(node.filePath)) score -= 8;
@@ -6882,7 +6963,7 @@ var VectorIndex = class {
6882
6963
  this.sqlitePath = sqlitePath;
6883
6964
  }
6884
6965
  async init() {
6885
- this.db = new Database2(this.sqlitePath);
6966
+ this.db = new Database3(this.sqlitePath);
6886
6967
  this.db.pragma("journal_mode = WAL");
6887
6968
  this.db.exec(`
6888
6969
  CREATE TABLE IF NOT EXISTS ${EMBED_TABLE} (
@@ -7012,24 +7093,92 @@ function siftDown(arr, i, score) {
7012
7093
  }
7013
7094
  }
7014
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
7015
7164
  async function hybridSearch(graph, query, limit, options = {}) {
7016
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 : {};
7017
7168
  const bm25Promise = precomputedBm25 ? Promise.resolve(precomputedBm25) : Promise.resolve(textSearch(graph, query, bm25Limit));
7018
7169
  const hasVectorDb = Boolean(vectorDbPath && fs26.existsSync(vectorDbPath));
7019
7170
  if (!hasVectorDb) {
7020
7171
  const bm25Results2 = await bm25Promise;
7021
- return {
7022
- results: bm25Results2.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
7023
- searchMode: "bm25"
7024
- };
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" };
7025
7175
  }
7026
7176
  const vectorPromise = runVectorSearch(vectorDbPath, query, vectorLimit);
7027
7177
  const [bm25Results, vectorResults] = await Promise.all([bm25Promise, vectorPromise]);
7028
7178
  if (vectorResults === null || vectorResults.length === 0) {
7029
- return {
7030
- results: bm25Results.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
7031
- searchMode: "bm25"
7032
- };
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" };
7033
7182
  }
7034
7183
  const vectorAsSearchResults = vectorResults.map((h) => ({
7035
7184
  nodeId: h.nodeId,
@@ -7040,10 +7189,9 @@ async function hybridSearch(graph, query, limit, options = {}) {
7040
7189
  snippet: graph.getNode(h.nodeId)?.content?.slice(0, 200)
7041
7190
  }));
7042
7191
  const merged = reciprocalRankFusion(bm25Results, vectorAsSearchResults);
7043
- return {
7044
- results: merged.slice(0, limit).map((r) => ({ ...r, searchMode: "hybrid" })),
7045
- searchMode: "hybrid"
7046
- };
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" };
7047
7195
  }
7048
7196
  async function runVectorSearch(vectorDbPath, query, topK) {
7049
7197
  try {
@@ -7073,11 +7221,19 @@ function tokenize(text) {
7073
7221
  return text.toLowerCase().split(/[\s\-_./\\:(){}[\]<>,"'`~!@#$%^&*+=|;?]+/).filter((t) => t.length >= 2 && t.length <= 64);
7074
7222
  }
7075
7223
  function nodeToDoc(node) {
7224
+ const fileBaseName = node.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, "") ?? "";
7076
7225
  return [
7077
7226
  node.name,
7227
+ node.name,
7228
+ // repeat name to boost exact-name matches
7078
7229
  node.kind,
7079
7230
  node.filePath,
7080
- (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
7081
7237
  ].join(" ");
7082
7238
  }
7083
7239
  function heapTopK(scores, k) {
@@ -7174,7 +7330,7 @@ var Bm25Index = class {
7174
7330
  } catch {
7175
7331
  }
7176
7332
  }
7177
- const db = new Database2(this.dbPath);
7333
+ const db = new Database3(this.dbPath);
7178
7334
  db.pragma("journal_mode = WAL");
7179
7335
  db.exec(`
7180
7336
  CREATE TABLE bm25_index (term TEXT PRIMARY KEY, postings TEXT NOT NULL);
@@ -7209,7 +7365,7 @@ var Bm25Index = class {
7209
7365
  */
7210
7366
  load() {
7211
7367
  if (!fs26.existsSync(this.dbPath)) return;
7212
- const db = new Database2(this.dbPath, { readonly: true });
7368
+ const db = new Database3(this.dbPath, { readonly: true });
7213
7369
  try {
7214
7370
  const getMeta = db.prepare("SELECT value FROM bm25_meta WHERE key = ?");
7215
7371
  this.avgdl = parseFloat(getMeta.get("avgdl")?.value ?? "1");
@@ -7302,7 +7458,7 @@ var Bm25Index = class {
7302
7458
  newTermFreqs.set(node.id, tf);
7303
7459
  }
7304
7460
  const newTermSet = new Set([...newTermFreqs.values()].flatMap((m) => [...m.keys()]));
7305
- const db = new Database2(this.dbPath);
7461
+ const db = new Database3(this.dbPath);
7306
7462
  db.pragma("journal_mode = WAL");
7307
7463
  const termsToRewrite = /* @__PURE__ */ new Map();
7308
7464
  for (const term of newTermSet) {
@@ -7564,19 +7720,24 @@ function buildNodeProps(node) {
7564
7720
  function escCypher(s) {
7565
7721
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
7566
7722
  }
7567
- var GLOBAL_DIR = path32.join(os13.homedir(), ".code-intel");
7568
- 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
+ }
7569
7729
  function loadRegistry() {
7570
7730
  try {
7571
- const data = fs26.readFileSync(REPOS_FILE, "utf-8");
7731
+ const data = fs26.readFileSync(getReposFile(), "utf-8");
7572
7732
  return JSON.parse(data);
7573
7733
  } catch {
7574
7734
  return [];
7575
7735
  }
7576
7736
  }
7577
7737
  function saveRegistry(entries) {
7578
- fs26.mkdirSync(GLOBAL_DIR, { recursive: true });
7579
- 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));
7580
7741
  }
7581
7742
  function upsertRepo(entry) {
7582
7743
  const entries = loadRegistry();
@@ -7611,9 +7772,90 @@ function getDbPath(repoDir) {
7611
7772
  function getVectorDbPath(repoDir) {
7612
7773
  return path32.join(repoDir, ".code-intel", "vector.db");
7613
7774
  }
7614
-
7615
- // src/mcp-server/server.ts
7616
- init_group_registry();
7775
+ var GROUPS_DIR = path32.join(os13.homedir(), ".code-intel", "groups");
7776
+ function groupFile(name) {
7777
+ return path32.join(GROUPS_DIR, `${name}.json`);
7778
+ }
7779
+ function loadGroup(name) {
7780
+ try {
7781
+ return JSON.parse(fs26.readFileSync(groupFile(name), "utf-8"));
7782
+ } catch {
7783
+ return null;
7784
+ }
7785
+ }
7786
+ function saveGroup(group) {
7787
+ fs26.mkdirSync(GROUPS_DIR, { recursive: true });
7788
+ fs26.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
7789
+ }
7790
+ function listGroups() {
7791
+ const groups = [];
7792
+ try {
7793
+ for (const file of fs26.readdirSync(GROUPS_DIR)) {
7794
+ if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
7795
+ try {
7796
+ const g = JSON.parse(
7797
+ fs26.readFileSync(path32.join(GROUPS_DIR, file), "utf-8")
7798
+ );
7799
+ groups.push(g);
7800
+ } catch {
7801
+ }
7802
+ }
7803
+ } catch {
7804
+ }
7805
+ return groups;
7806
+ }
7807
+ function deleteGroup(name) {
7808
+ try {
7809
+ fs26.unlinkSync(groupFile(name));
7810
+ } catch {
7811
+ }
7812
+ try {
7813
+ fs26.unlinkSync(path32.join(GROUPS_DIR, `${name}.sync.json`));
7814
+ } catch {
7815
+ }
7816
+ }
7817
+ function groupExists(name) {
7818
+ return fs26.existsSync(groupFile(name));
7819
+ }
7820
+ function addMember(groupName, member) {
7821
+ const group = loadGroup(groupName);
7822
+ if (!group) throw new Error(`Group "${groupName}" not found.`);
7823
+ const idx = group.members.findIndex((m) => m.groupPath === member.groupPath);
7824
+ if (idx >= 0) {
7825
+ group.members[idx] = member;
7826
+ } else {
7827
+ group.members.push(member);
7828
+ }
7829
+ saveGroup(group);
7830
+ return group;
7831
+ }
7832
+ function removeMember(groupName, groupPath) {
7833
+ const group = loadGroup(groupName);
7834
+ if (!group) throw new Error(`Group "${groupName}" not found.`);
7835
+ const before = group.members.length;
7836
+ group.members = group.members.filter((m) => m.groupPath !== groupPath);
7837
+ if (group.members.length === before) {
7838
+ throw new Error(`No member at path "${groupPath}" in group "${groupName}".`);
7839
+ }
7840
+ saveGroup(group);
7841
+ return group;
7842
+ }
7843
+ function saveSyncResult(result) {
7844
+ fs26.mkdirSync(GROUPS_DIR, { recursive: true });
7845
+ fs26.writeFileSync(
7846
+ path32.join(GROUPS_DIR, `${result.groupName}.sync.json`),
7847
+ JSON.stringify(result, null, 2) + "\n"
7848
+ );
7849
+ }
7850
+ function loadSyncResult(groupName) {
7851
+ try {
7852
+ return JSON.parse(
7853
+ fs26.readFileSync(path32.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
7854
+ );
7855
+ } catch {
7856
+ return null;
7857
+ }
7858
+ }
7617
7859
  init_db_manager();
7618
7860
  init_knowledge_graph();
7619
7861
  init_graph_from_db();
@@ -8712,7 +8954,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8712
8954
  // ── Search & inspect ─────────────────────────────────────────────────
8713
8955
  {
8714
8956
  name: "search",
8715
- 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.`,
8716
8958
  inputSchema: {
8717
8959
  type: "object",
8718
8960
  properties: {
@@ -8728,16 +8970,31 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8728
8970
  },
8729
8971
  {
8730
8972
  name: "inspect",
8731
- 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.',
8732
8974
  inputSchema: {
8733
8975
  type: "object",
8734
8976
  properties: {
8735
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.' },
8736
8979
  ..._tokenProp
8737
8980
  },
8738
8981
  required: ["symbol_name"]
8739
8982
  }
8740
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
+ },
8741
8998
  {
8742
8999
  name: "blast_radius",
8743
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).",
@@ -9290,8 +9547,41 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolve
9290
9547
  // ── inspect ────────────────────────────────────────────────────────────
9291
9548
  case "inspect": {
9292
9549
  const symbolName = a.symbol_name;
9293
- const node = findNodeByName(graph, symbolName);
9294
- 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
+ }
9295
9585
  const incoming = [...graph.findEdgesTo(node.id)];
9296
9586
  const outgoing = [...graph.findEdgesFrom(node.id)];
9297
9587
  const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
@@ -9338,12 +9628,70 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolve
9338
9628
  kind: graph.getNode(e.target)?.kind
9339
9629
  })),
9340
9630
  cluster,
9341
- 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,
9342
9633
  ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
9343
9634
  })
9344
9635
  }]
9345
9636
  };
9346
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
+ }
9347
9695
  // ── blast_radius ───────────────────────────────────────────────────────
9348
9696
  case "blast_radius": {
9349
9697
  const target = a.target;
@@ -9476,12 +9824,12 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolve
9476
9824
  allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
9477
9825
  }
9478
9826
  const total = allExports.length;
9479
- const exports$1 = allExports.slice(offset, offset + effectiveLimit);
9827
+ const exports = allExports.slice(offset, offset + effectiveLimit);
9480
9828
  const hasMore = offset + effectiveLimit < total;
9481
9829
  return {
9482
9830
  content: [{
9483
9831
  type: "text",
9484
- text: compact({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore })
9832
+ text: compact({ exports, total, offset, limit: effectiveLimit, hasMore })
9485
9833
  }]
9486
9834
  };
9487
9835
  }
@@ -9962,6 +10310,13 @@ function findNodeByName(graph, name) {
9962
10310
  }
9963
10311
  return void 0;
9964
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
+ }
9965
10320
  function parseDiff(diffText) {
9966
10321
  const result = [];
9967
10322
  let currentFile = null;
@@ -9992,7 +10347,6 @@ function parseDiff(diffText) {
9992
10347
  }
9993
10348
  return result;
9994
10349
  }
9995
- init_group_registry();
9996
10350
  init_knowledge_graph();
9997
10351
  init_graph_from_db();
9998
10352
  init_logger();
@@ -10006,7 +10360,7 @@ var JobsDB = class {
10006
10360
  db;
10007
10361
  constructor(dbPath) {
10008
10362
  fs26.mkdirSync(path32.dirname(dbPath), { recursive: true });
10009
- this.db = new Database2(dbPath);
10363
+ this.db = new Database3(dbPath);
10010
10364
  this.db.pragma("journal_mode = WAL");
10011
10365
  this.db.pragma("foreign_keys = ON");
10012
10366
  this.createTables();
@@ -12289,6 +12643,97 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12289
12643
  }
12290
12644
  res.json(result);
12291
12645
  });
12646
+ app.post("/api/v1/groups", requireAuth, requireRole("analyst"), (req, res) => {
12647
+ const { name } = req.body;
12648
+ if (!name || !name.trim()) {
12649
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "Group name is required" } });
12650
+ return;
12651
+ }
12652
+ const trimmed = name.trim();
12653
+ if (groupExists(trimmed)) {
12654
+ res.status(409).json({ error: { code: ErrorCodes.CONFLICT, message: `Group "${trimmed}" already exists` } });
12655
+ return;
12656
+ }
12657
+ const group = { name: trimmed, createdAt: (/* @__PURE__ */ new Date()).toISOString(), members: [] };
12658
+ saveGroup(group);
12659
+ res.status(201).json(group);
12660
+ });
12661
+ app.delete("/api/v1/groups/:name", requireAuth, requireRole("analyst"), (req, res) => {
12662
+ const groupName = req.params["name"];
12663
+ if (!groupExists(groupName)) {
12664
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12665
+ return;
12666
+ }
12667
+ deleteGroup(groupName);
12668
+ res.status(204).end();
12669
+ });
12670
+ app.patch("/api/v1/groups/:name", requireAuth, requireRole("analyst"), (req, res) => {
12671
+ const groupName = req.params["name"];
12672
+ const group = loadGroup(groupName);
12673
+ if (!group) {
12674
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12675
+ return;
12676
+ }
12677
+ const { name } = req.body;
12678
+ if (!name || !name.trim()) {
12679
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "New name is required" } });
12680
+ return;
12681
+ }
12682
+ const newName = name.trim();
12683
+ if (newName !== group.name && groupExists(newName)) {
12684
+ res.status(409).json({ error: { code: ErrorCodes.CONFLICT, message: `Group "${newName}" already exists` } });
12685
+ return;
12686
+ }
12687
+ if (newName !== group.name) {
12688
+ deleteGroup(group.name);
12689
+ group.name = newName;
12690
+ }
12691
+ saveGroup(group);
12692
+ res.json(group);
12693
+ });
12694
+ app.post("/api/v1/groups/:name/members", requireAuth, requireRole("analyst"), (req, res) => {
12695
+ const groupName = req.params["name"];
12696
+ const group = loadGroup(groupName);
12697
+ if (!group) {
12698
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12699
+ return;
12700
+ }
12701
+ const { groupPath, registryName } = req.body;
12702
+ if (!groupPath || !registryName) {
12703
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "groupPath and registryName are required" } });
12704
+ return;
12705
+ }
12706
+ const registry = loadRegistry();
12707
+ if (!registry.find((r) => r.name === registryName)) {
12708
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: `Repo "${registryName}" not found in registry. Run code-intel analyze first.` } });
12709
+ return;
12710
+ }
12711
+ try {
12712
+ const updated = addMember(groupName, { groupPath, registryName });
12713
+ res.json(updated);
12714
+ } catch (err) {
12715
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: err instanceof Error ? err.message : String(err) } });
12716
+ }
12717
+ });
12718
+ app.delete("/api/v1/groups/:name/members", requireAuth, requireRole("analyst"), (req, res) => {
12719
+ const groupName = req.params["name"];
12720
+ const group = loadGroup(groupName);
12721
+ if (!group) {
12722
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12723
+ return;
12724
+ }
12725
+ const { groupPath } = req.body;
12726
+ if (!groupPath) {
12727
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "groupPath is required" } });
12728
+ return;
12729
+ }
12730
+ try {
12731
+ const updated = removeMember(groupName, groupPath);
12732
+ res.json(updated);
12733
+ } catch (err) {
12734
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: err instanceof Error ? err.message : String(err) } });
12735
+ }
12736
+ });
12292
12737
  app.post("/api/v1/groups/:name/sync", async (req, res) => {
12293
12738
  const group = loadGroup(req.params.name);
12294
12739
  if (!group) {
@@ -12299,8 +12744,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12299
12744
  const result = await syncGroup(group);
12300
12745
  saveSyncResult(result);
12301
12746
  group.lastSync = result.syncedAt;
12302
- const { saveGroup: saveGroup2 } = await Promise.resolve().then(() => (init_group_registry(), group_registry_exports));
12303
- saveGroup2(group);
12747
+ saveGroup(group);
12304
12748
  res.json(result);
12305
12749
  } catch (err) {
12306
12750
  res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err) } });
@@ -12800,7 +13244,6 @@ async function startHttpServer(graph, repoName, port = 4747, workspaceRoot, watc
12800
13244
  }
12801
13245
 
12802
13246
  // src/multi-repo/index.ts
12803
- init_group_registry();
12804
13247
  init_graph_from_db();
12805
13248
 
12806
13249
  // src/multi-repo/cross-repo-search.ts