codesearch-mcp 0.0.17-alpha.1 → 0.1.0-alpha

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +95 -6
  3. package/dist/app.js +70 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +35 -7
  7. package/dist/src/commands/McpCommand.js +9 -4
  8. package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +85 -4
  11. package/dist/src/config-store.js +157 -32
  12. package/dist/src/config.d.ts +32 -8
  13. package/dist/src/config.js +93 -30
  14. package/dist/src/constants.d.ts +35 -0
  15. package/dist/src/constants.js +57 -4
  16. package/dist/src/container.d.ts +10 -6
  17. package/dist/src/container.js +10 -5
  18. package/dist/src/db/index.d.ts +1 -1
  19. package/dist/src/db/index.js +2 -1
  20. package/dist/src/db/schema.d.ts +111 -0
  21. package/dist/src/db/schema.js +15 -1
  22. package/dist/src/graph.d.ts +60 -0
  23. package/dist/src/graph.js +239 -120
  24. package/dist/src/help.js +20 -5
  25. package/dist/src/interfaces/IAstParser.d.ts +20 -3
  26. package/dist/src/interfaces/IChunker.d.ts +24 -0
  27. package/dist/src/interfaces/IChunker.js +2 -0
  28. package/dist/src/interfaces/IDatabase.d.ts +27 -0
  29. package/dist/src/interfaces/IDatabase.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +15 -2
  31. package/dist/src/interfaces/ILogger.d.ts +10 -0
  32. package/dist/src/interfaces/ILogger.js +2 -0
  33. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  34. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  35. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  36. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  37. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  38. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  39. package/dist/src/interfaces/ISearcher.d.ts +37 -0
  40. package/dist/src/interfaces/ISearcher.js +2 -0
  41. package/dist/src/interfaces/index.d.ts +8 -0
  42. package/dist/src/interfaces/index.js +2 -0
  43. package/dist/src/owner-path.d.ts +10 -0
  44. package/dist/src/owner-path.js +27 -0
  45. package/dist/src/project-selector.d.ts +1 -1
  46. package/dist/src/services/AstChunker.d.ts +31 -0
  47. package/dist/src/services/AstChunker.js +114 -0
  48. package/dist/src/services/AstParserService.d.ts +10 -6
  49. package/dist/src/services/AstParserService.js +60 -15
  50. package/dist/src/services/DatabaseService.d.ts +14 -1
  51. package/dist/src/services/DatabaseService.js +70 -25
  52. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  53. package/dist/src/services/EmbedderFactory.js +29 -0
  54. package/dist/src/services/IndexerService.d.ts +22 -12
  55. package/dist/src/services/IndexerService.js +106 -46
  56. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  57. package/dist/src/services/InfinityEmbedder.js +199 -0
  58. package/dist/src/services/LoggerService.d.ts +2 -1
  59. package/dist/src/services/McpServer.d.ts +16 -7
  60. package/dist/src/services/McpServer.js +54 -367
  61. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  62. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  63. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  64. package/dist/src/services/NxMonorepoResolver.js +308 -0
  65. package/dist/src/services/OllamaEmbedder.d.ts +19 -5
  66. package/dist/src/services/OllamaEmbedder.js +91 -36
  67. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  68. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  69. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  70. package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
  71. package/dist/src/services/SearcherService.d.ts +46 -7
  72. package/dist/src/services/SearcherService.js +183 -53
  73. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  74. package/dist/src/services/VoyageEmbedder.js +72 -0
  75. package/dist/src/services/WebServerService.d.ts +77 -0
  76. package/dist/src/services/WebServerService.js +267 -0
  77. package/dist/src/services/http-retry.d.ts +19 -0
  78. package/dist/src/services/http-retry.js +54 -0
  79. package/dist/src/services/reranker.d.ts +15 -0
  80. package/dist/src/services/reranker.js +44 -0
  81. package/dist/src/setup.d.ts +6 -0
  82. package/dist/src/setup.js +249 -31
  83. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  84. package/dist/src/tools/FindSymbolTool.js +45 -0
  85. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  86. package/dist/src/tools/GetCallersTool.js +42 -0
  87. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  88. package/dist/src/tools/GetDependenciesTool.js +38 -0
  89. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  90. package/dist/src/tools/GetDependentsTool.js +57 -0
  91. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  92. package/dist/src/tools/ListFilesTool.js +39 -0
  93. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  94. package/dist/src/tools/ListProjectsTool.js +44 -0
  95. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  96. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  97. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  98. package/dist/src/tools/ReadFileTool.js +42 -0
  99. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  100. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  101. package/dist/src/tools/format-helpers.d.ts +8 -0
  102. package/dist/src/tools/format-helpers.js +58 -0
  103. package/dist/src/tools/index.d.ts +9 -0
  104. package/dist/src/tools/index.js +21 -0
  105. package/dist/src/web/dashboard.d.ts +9 -0
  106. package/dist/src/web/dashboard.js +342 -0
  107. package/docker/docker-compose.infinity-gpu.yml +19 -0
  108. package/docker/docker-compose.yml +25 -0
  109. package/docs/web-dashboard.md +187 -0
  110. package/package.json +25 -3
  111. package/dist/src/ast.d.ts +0 -2
  112. package/dist/src/ast.js +0 -110
  113. package/dist/src/commands/SetupCommand.js +0 -14
  114. package/dist/src/db/data-source.d.ts +0 -2
  115. package/dist/src/db/data-source.js +0 -13
  116. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  117. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  118. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  119. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  120. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  121. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  122. package/dist/src/db/init.d.ts +0 -2
  123. package/dist/src/db/init.js +0 -62
  124. package/dist/src/env-loader.d.ts +0 -1
  125. package/dist/src/indexer.d.ts +0 -17
  126. package/dist/src/indexer.js +0 -242
  127. package/dist/src/mcp.d.ts +0 -1
  128. package/dist/src/mcp.js +0 -403
  129. package/dist/src/ollama.d.ts +0 -2
  130. package/dist/src/ollama.js +0 -6
  131. package/dist/src/searcher.d.ts +0 -74
  132. package/dist/src/searcher.js +0 -446
  133. package/dist/vitest.config.d.ts +0 -2
  134. package/dist/vitest.config.js +0 -9
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Represents a discovered project within a monorepo workspace.
3
+ */
4
+ export interface MonorepoProject {
5
+ /** Project name (e.g. "my-app", "shared-utils") */
6
+ name: string;
7
+ /** Absolute path to the project root */
8
+ path: string;
9
+ /** npm scope + package name (e.g. "@scope/my-app") — null if no package.json or no name */
10
+ packageName: string | null;
11
+ /** "app" | "lib" — derived from project.json or directory convention */
12
+ type: 'app' | 'lib';
13
+ }
14
+ /**
15
+ * Monorepo resolver — detects and resolves workspace structure.
16
+ * Implementations handle specific monorepo tooling (Nx, Turborepo, etc.)
17
+ */
18
+ export interface IMonorepoResolver {
19
+ /**
20
+ * Detect whether the given root directory is a monorepo workspace.
21
+ */
22
+ isMonorepo(rootDir: string): boolean;
23
+ /**
24
+ * Discover all projects (apps + libs) in the monorepo.
25
+ */
26
+ discoverProjects(rootDir: string): MonorepoProject[];
27
+ /**
28
+ * Get the root tsconfig path that contains workspace-wide path aliases.
29
+ * For Nx this is typically tsconfig.base.json at the workspace root.
30
+ * The AST parser uses this to resolve cross-project imports (@scope/*).
31
+ */
32
+ getRootTsConfigPath(rootDir: string): string | null;
33
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,28 @@
1
+ export interface DiscoveredProject {
2
+ name: string;
3
+ path: string;
4
+ /** npm scope + package name (e.g. "@scope/my-app") — only set for monorepo projects */
5
+ packageName?: string | null;
6
+ /** "app" | "lib" — only set for monorepo projects */
7
+ type?: 'app' | 'lib';
8
+ /**
9
+ * Path to the workspace-wide tsconfig (e.g. tsconfig.base.json) that holds
10
+ * cross-project path aliases. Set for monorepo projects; undefined for flat
11
+ * workspaces (where each project uses its own local tsconfig.json).
12
+ */
13
+ rootTsConfigPath?: string | null;
14
+ /**
15
+ * When a top-level directory is itself a nested monorepo (e.g. an Nx repo
16
+ * under a flat workspaceRoot), this holds its expanded sub-projects. The
17
+ * repo stays a single selectable unit; `toLeafProjects` expands `children`
18
+ * into the actual projects to index. Absent/undefined for ordinary projects.
19
+ */
20
+ children?: DiscoveredProject[];
21
+ }
22
+ /**
23
+ * Project discovery abstraction — decouples project enumeration
24
+ * from the indexing service (Single Responsibility Principle).
25
+ */
26
+ export interface IProjectDiscovery {
27
+ discover(rootDir: string): DiscoveredProject[];
28
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,37 @@
1
+ import type { SearchResult, NeighborResult, GraphContext, DependencyRow, ProjectRow, GrepMatch, SymbolKind, SymbolHit, CallerRow, FileRangeResult } from '../services/SearcherService';
2
+ /**
3
+ * Searcher abstraction — McpServer depends on this interface
4
+ * rather than the concrete SearcherService.
5
+ */
6
+ export interface ISearcher {
7
+ prepareQuery(raw: string): Promise<{
8
+ query: string;
9
+ translated: boolean;
10
+ }>;
11
+ assertIndexMatches(): Promise<void>;
12
+ listProjects(): Promise<ProjectRow[]>;
13
+ getProjectEdges(): Promise<{
14
+ from: string;
15
+ to: string;
16
+ }[]>;
17
+ searchChunks(query: string, limit: number, project: string | null, threshold?: number): Promise<SearchResult[]>;
18
+ fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
19
+ fetchGraphContext(results: SearchResult[]): Promise<Map<string, GraphContext>>;
20
+ getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
21
+ getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
22
+ getCallers(symbol: string, project: string, calleeFile?: string | null): Promise<CallerRow[]>;
23
+ grepSymbol(symbol: string, project: string, excludeFile: string): Promise<{
24
+ matches: GrepMatch[];
25
+ truncated: boolean;
26
+ }>;
27
+ findSymbol(name: string, project: string | null, kind: SymbolKind): Promise<{
28
+ hits: SymbolHit[];
29
+ truncated: boolean;
30
+ }>;
31
+ listFiles(relativePath: string, project: string, depth: number, pattern: string | null): {
32
+ tree: string;
33
+ error?: string;
34
+ };
35
+ readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
36
+ readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
37
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ export type { IEmbedder, InputType } from './IEmbedder';
2
+ export type { IAstParser, DepEdge, SymbolEntry, CallEdge, FileAnalysis } from './IAstParser';
3
+ export type { IChunker, ChunkPiece, ChunkOpts } from './IChunker';
4
+ export type { ILogger } from './ILogger';
5
+ export type { IDatabase } from './IDatabase';
6
+ export type { ISearcher } from './ISearcher';
7
+ export type { IProjectDiscovery, DiscoveredProject } from './IProjectDiscovery';
8
+ export type { IMcpToolHandler, ToolDefinition, ToolResponse } from './IMcpToolHandler';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ import type { OwnerRange } from './interfaces/IAstParser';
2
+ /**
3
+ * Resolve the owning declaration for a chunk given its 1-based start line and a
4
+ * flat list of candidate ranges (top-level symbols + method ranges). The
5
+ * deepest range containing the start line wins (smallest `endLine - line` span),
6
+ * so a method beats its enclosing class. Returns `Container.name` for methods
7
+ * (via `container`) or `name` for top-level declarations, and `null` when the
8
+ * start line lies outside every range (imports / module-scope gaps).
9
+ */
10
+ export declare function resolveOwnerPath(startLine: number, ranges: OwnerRange[]): string | null;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveOwnerPath = resolveOwnerPath;
4
+ /**
5
+ * Resolve the owning declaration for a chunk given its 1-based start line and a
6
+ * flat list of candidate ranges (top-level symbols + method ranges). The
7
+ * deepest range containing the start line wins (smallest `endLine - line` span),
8
+ * so a method beats its enclosing class. Returns `Container.name` for methods
9
+ * (via `container`) or `name` for top-level declarations, and `null` when the
10
+ * start line lies outside every range (imports / module-scope gaps).
11
+ */
12
+ function resolveOwnerPath(startLine, ranges) {
13
+ let best = null;
14
+ let bestSpan = Infinity;
15
+ for (const r of ranges) {
16
+ if (r.line <= startLine && startLine <= r.endLine) {
17
+ const span = r.endLine - r.line;
18
+ if (span < bestSpan) {
19
+ bestSpan = span;
20
+ best = r;
21
+ }
22
+ }
23
+ }
24
+ if (!best)
25
+ return null;
26
+ return best.container ? `${best.container}.${best.name}` : best.name;
27
+ }
@@ -7,7 +7,7 @@ export declare class ProjectSelector {
7
7
  private included;
8
8
  private skipped;
9
9
  constructor(workspaceDir: string);
10
- select(allProjects: Project[], forceSelect: boolean): Promise<Project[]>;
10
+ select<T extends Project>(allProjects: T[], forceSelect: boolean): Promise<T[]>;
11
11
  private printAlreadySelected;
12
12
  private promptForSelection;
13
13
  private promptForSkip;
@@ -0,0 +1,31 @@
1
+ import type { Config } from '../config';
2
+ import type { IChunker, ChunkPiece, ChunkOpts } from '../interfaces/IChunker';
3
+ /**
4
+ * Splits TS/JS source into embeddable pieces aligned to top-level declaration
5
+ * ranges (AST strategy). Falls back to the character-based recursive splitter
6
+ * for schema files, files without symbol analysis, or when strategy is
7
+ * 'character' — that path is byte-for-byte the previous behavior.
8
+ */
9
+ export declare class AstChunker implements IChunker {
10
+ private config;
11
+ private readonly jsSplitter;
12
+ private readonly schemaSplitter;
13
+ private readonly chunkSize;
14
+ constructor(config: Config);
15
+ chunk(code: string, opts: ChunkOpts): Promise<ChunkPiece[]>;
16
+ /** Character-splitter path for a whole file; line numbers from splitter metadata. */
17
+ private splitWholeFile;
18
+ private chunkByAst;
19
+ /**
20
+ * Build disjoint, sorted line intervals from symbol ranges. Overlapping /
21
+ * nested ranges merge into the outermost (a merged region covers any symbol
22
+ * inside it). Ranges are clamped to the file's line count.
23
+ */
24
+ private static buildIntervals;
25
+ /**
26
+ * Emit pieces for a 1-based inclusive line region. Whitespace-only gaps are
27
+ * dropped; regions longer than chunkSize are sub-split with the char splitter,
28
+ * with sub-piece line numbers offset back to absolute file lines.
29
+ */
30
+ private regionPieces;
31
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AstChunker = void 0;
4
+ const textsplitters_1 = require("@langchain/textsplitters");
5
+ /**
6
+ * Splits TS/JS source into embeddable pieces aligned to top-level declaration
7
+ * ranges (AST strategy). Falls back to the character-based recursive splitter
8
+ * for schema files, files without symbol analysis, or when strategy is
9
+ * 'character' — that path is byte-for-byte the previous behavior.
10
+ */
11
+ class AstChunker {
12
+ config;
13
+ jsSplitter;
14
+ schemaSplitter;
15
+ chunkSize;
16
+ constructor(config) {
17
+ this.config = config;
18
+ this.chunkSize = config.indexer.chunkSize;
19
+ this.jsSplitter = textsplitters_1.RecursiveCharacterTextSplitter.fromLanguage('js', {
20
+ chunkSize: config.indexer.chunkSize,
21
+ chunkOverlap: config.indexer.chunkOverlap,
22
+ });
23
+ this.schemaSplitter = new textsplitters_1.RecursiveCharacterTextSplitter({
24
+ chunkSize: config.indexer.chunkSize,
25
+ chunkOverlap: config.indexer.chunkOverlap,
26
+ separators: ['\n\n', '\n', ' '],
27
+ });
28
+ }
29
+ async chunk(code, opts) {
30
+ if (opts.isSchemaFile) {
31
+ return this.splitWholeFile(code, this.schemaSplitter);
32
+ }
33
+ if (opts.strategy === 'character' || opts.symbols.length === 0) {
34
+ return this.splitWholeFile(code, this.jsSplitter);
35
+ }
36
+ return this.chunkByAst(code, opts.symbols);
37
+ }
38
+ /** Character-splitter path for a whole file; line numbers from splitter metadata. */
39
+ async splitWholeFile(code, splitter) {
40
+ const docs = await splitter.createDocuments([code]);
41
+ return docs.map((d) => {
42
+ const from = (d.metadata?.loc?.lines?.from ?? 1);
43
+ const to = (d.metadata?.loc?.lines?.to ?? from);
44
+ return { content: d.pageContent, startLine: from, endLine: to };
45
+ });
46
+ }
47
+ async chunkByAst(code, symbols) {
48
+ const lines = code.split('\n');
49
+ const total = lines.length;
50
+ const intervals = AstChunker.buildIntervals(symbols, total);
51
+ const pieces = [];
52
+ let cursor = 1;
53
+ for (const iv of intervals) {
54
+ if (cursor < iv.start) {
55
+ pieces.push(...await this.regionPieces(lines, cursor, iv.start - 1, true));
56
+ }
57
+ pieces.push(...await this.regionPieces(lines, iv.start, iv.end, false));
58
+ cursor = iv.end + 1;
59
+ }
60
+ if (cursor <= total) {
61
+ pieces.push(...await this.regionPieces(lines, cursor, total, true));
62
+ }
63
+ return pieces;
64
+ }
65
+ /**
66
+ * Build disjoint, sorted line intervals from symbol ranges. Overlapping /
67
+ * nested ranges merge into the outermost (a merged region covers any symbol
68
+ * inside it). Ranges are clamped to the file's line count.
69
+ */
70
+ static buildIntervals(symbols, total) {
71
+ const raw = symbols
72
+ .filter(s => s.line != null && s.line >= 1 && s.line <= total)
73
+ .map(s => ({
74
+ start: s.line,
75
+ end: Math.min(Math.max(s.endLine ?? s.line, s.line), total),
76
+ }))
77
+ .sort((a, b) => a.start - b.start || b.end - a.end);
78
+ const merged = [];
79
+ for (const iv of raw) {
80
+ const last = merged[merged.length - 1];
81
+ if (last && iv.start <= last.end) {
82
+ last.end = Math.max(last.end, iv.end);
83
+ }
84
+ else {
85
+ merged.push({ ...iv });
86
+ }
87
+ }
88
+ return merged;
89
+ }
90
+ /**
91
+ * Emit pieces for a 1-based inclusive line region. Whitespace-only gaps are
92
+ * dropped; regions longer than chunkSize are sub-split with the char splitter,
93
+ * with sub-piece line numbers offset back to absolute file lines.
94
+ */
95
+ async regionPieces(lines, start, end, isGap) {
96
+ const text = lines.slice(start - 1, end).join('\n');
97
+ if (isGap && text.trim() === '')
98
+ return [];
99
+ if (text.length <= this.chunkSize) {
100
+ return [{ content: text, startLine: start, endLine: end }];
101
+ }
102
+ const docs = await this.jsSplitter.createDocuments([text]);
103
+ return docs.map((d) => {
104
+ const from = (d.metadata?.loc?.lines?.from ?? 1);
105
+ const to = (d.metadata?.loc?.lines?.to ?? from);
106
+ return {
107
+ content: d.pageContent,
108
+ startLine: start + from - 1,
109
+ endLine: start + to - 1,
110
+ };
111
+ });
112
+ }
113
+ }
114
+ exports.AstChunker = AstChunker;
@@ -1,12 +1,10 @@
1
- import type { IAstParser, DepEdge, FileAnalysis } from '../interfaces/IAstParser';
2
- import { LoggerService } from './LoggerService';
1
+ import type { IAstParser, DepEdge, FileAnalysis, AnalyzeProjectOpts } from '../interfaces/IAstParser';
2
+ import type { ILogger } from '../interfaces';
3
3
  export declare class AstParserService implements IAstParser {
4
4
  private logger;
5
5
  private morphProjects;
6
- constructor(logger?: LoggerService);
7
- analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: {
8
- skipDependencyResolution?: boolean;
9
- }): Promise<Map<string, FileAnalysis>>;
6
+ constructor(logger?: ILogger);
7
+ analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: AnalyzeProjectOpts): Promise<Map<string, FileAnalysis>>;
10
8
  clearCache(projectRoot?: string): void;
11
9
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
12
10
  extractImportEdges(filePath: string, serviceName: string, projectRoot?: string): Promise<DepEdge[]>;
@@ -16,6 +14,12 @@ export declare class AstParserService implements IAstParser {
16
14
  private resolveFromSymbol;
17
15
  private buildUsageMap;
18
16
  private extractSymbols;
17
+ /**
18
+ * Method / constructor / accessor line ranges with their enclosing class, for
19
+ * owner-path resolution only. Deliberately NOT fed into `code_symbol` (methods
20
+ * stay grep-based in find_symbol), so this is separate from extractSymbols.
21
+ */
22
+ private extractOwnerRanges;
19
23
  private extractCalls;
20
24
  private getContainerName;
21
25
  }
@@ -54,9 +54,10 @@ class AstParserService {
54
54
  async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
55
55
  const t0 = Date.now();
56
56
  const skipDependencyResolution = opts.skipDependencyResolution ?? false;
57
- this.logger.debug('AstParser: analyzeProject start', { serviceName, projectRoot, files: filePaths.length, skipDependencyResolution });
57
+ const rootTsConfigPath = opts.rootTsConfigPath;
58
+ this.logger.debug('AstParser: analyzeProject start', { serviceName, projectRoot, files: filePaths.length, skipDependencyResolution, rootTsConfigPath });
58
59
  const analysisMap = new Map();
59
- const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution);
60
+ const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
60
61
  for (const filePath of filePaths) {
61
62
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
62
63
  analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
@@ -76,7 +77,8 @@ class AstParserService {
76
77
  const edges = this.extractEdges(sourceFile, filePath, serviceName);
77
78
  const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
78
79
  const calls = this.extractCalls(sourceFile, filePath, serviceName);
79
- analysisMap.set(filePath, { edges, symbols, calls });
80
+ const ownerRanges = this.extractOwnerRanges(sourceFile);
81
+ analysisMap.set(filePath, { edges, symbols, calls, ownerRanges });
80
82
  }
81
83
  catch (err) {
82
84
  this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
@@ -125,9 +127,10 @@ class AstParserService {
125
127
  const edges = this.extractEdges(sourceFile, filePath, serviceName);
126
128
  const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
127
129
  const calls = this.extractCalls(sourceFile, filePath, serviceName);
130
+ const ownerRanges = this.extractOwnerRanges(sourceFile);
128
131
  morphProject.removeSourceFile(sourceFile);
129
132
  this.logger.debug('AstParser: analyzeFile done', { filePath, edges: edges.length, symbols: symbols.length, calls: calls.length, ms: Date.now() - t0 });
130
- return { edges, symbols, calls };
133
+ return { edges, symbols, calls, ownerRanges };
131
134
  }
132
135
  // Kept for test compatibility (extract-import-edges.test.ts).
133
136
  async extractImportEdges(filePath, serviceName, projectRoot) {
@@ -135,22 +138,37 @@ class AstParserService {
135
138
  return edges;
136
139
  }
137
140
  // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
138
- createMorphProject(projectRoot, skipFileDependencyResolution = false) {
141
+ createMorphProject(projectRoot, skipFileDependencyResolution = false, rootTsConfigPath) {
139
142
  const t0 = Date.now();
140
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
141
- const hasTsconfig = fs.existsSync(tsconfigPath);
142
- this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig, skipFileDependencyResolution });
143
+ // Determine which tsconfig to use:
144
+ // 1. Project-local tsconfig.json
145
+ // 2. Root tsconfig (e.g. tsconfig.base.json for Nx monorepos)
146
+ // 3. Fallback options
147
+ const localTsconfigPath = path.join(projectRoot, 'tsconfig.json');
148
+ const hasLocalTsconfig = fs.existsSync(localTsconfigPath);
149
+ const hasRootTsconfig = rootTsConfigPath ? fs.existsSync(rootTsConfigPath) : false;
150
+ // For monorepos: prefer the root tsconfig.base.json which contains
151
+ // workspace-wide path aliases (e.g. @scope/* → libs/*/src)
152
+ const effectiveTsconfig = hasRootTsconfig ? rootTsConfigPath : (hasLocalTsconfig ? localTsconfigPath : null);
153
+ this.logger.debug('AstParser: tsconfig lookup', {
154
+ projectRoot,
155
+ localFound: hasLocalTsconfig,
156
+ rootTsConfigPath: rootTsConfigPath ?? null,
157
+ rootFound: hasRootTsconfig,
158
+ effective: effectiveTsconfig,
159
+ skipFileDependencyResolution,
160
+ });
143
161
  let project;
144
162
  try {
145
- const opts = hasTsconfig
146
- ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution }
163
+ const opts = effectiveTsconfig
164
+ ? { tsConfigFilePath: effectiveTsconfig, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution }
147
165
  : { ...FALLBACK_OPTS, skipFileDependencyResolution };
148
166
  project = new ts_morph_1.Project(opts);
149
- if (hasTsconfig) {
167
+ if (effectiveTsconfig) {
150
168
  const co = project.getCompilerOptions();
151
169
  const pathsMap = co.paths ?? {};
152
170
  const pathCount = Object.keys(pathsMap).length;
153
- this.logger.debug('AstParser: tsconfig loaded', { pathCount, ms: Date.now() - t0 });
171
+ this.logger.debug('AstParser: tsconfig loaded', { tsconfig: effectiveTsconfig, pathCount, ms: Date.now() - t0 });
154
172
  if (pathCount > 0) {
155
173
  this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
156
174
  }
@@ -161,7 +179,7 @@ class AstParserService {
161
179
  }
162
180
  catch (err) {
163
181
  this.logger.warn('AstParser: tsconfig load failed, using fallback', {
164
- tsconfigPath,
182
+ tsconfigPath: effectiveTsconfig ?? localTsconfigPath,
165
183
  reason: err.message,
166
184
  ms: Date.now() - t0,
167
185
  });
@@ -353,6 +371,33 @@ class AstParserService {
353
371
  }
354
372
  return symbols;
355
373
  }
374
+ /**
375
+ * Method / constructor / accessor line ranges with their enclosing class, for
376
+ * owner-path resolution only. Deliberately NOT fed into `code_symbol` (methods
377
+ * stay grep-based in find_symbol), so this is separate from extractSymbols.
378
+ */
379
+ extractOwnerRanges(sourceFile) {
380
+ const ranges = [];
381
+ for (const cls of sourceFile.getClasses()) {
382
+ const container = cls.getName();
383
+ if (!container)
384
+ continue;
385
+ const members = [
386
+ ...cls.getMethods().map(n => ({ name: n.getName(), node: n })),
387
+ ...cls.getGetAccessors().map(n => ({ name: n.getName(), node: n })),
388
+ ...cls.getSetAccessors().map(n => ({ name: n.getName(), node: n })),
389
+ ...cls.getConstructors().map(n => ({ name: 'constructor', node: n })),
390
+ ];
391
+ for (const { name, node } of members) {
392
+ ranges.push({
393
+ name, container,
394
+ line: node.getStartLineNumber(),
395
+ endLine: node.getEndLineNumber(),
396
+ });
397
+ }
398
+ }
399
+ return ranges;
400
+ }
356
401
  // ── private: call graph extraction ────────────────────────────────────────
357
402
  extractCalls(sourceFile, filePath, serviceName) {
358
403
  const calls = [];
@@ -367,7 +412,7 @@ class AstParserService {
367
412
  const sym = expr.getNameNode().getSymbol();
368
413
  const decl = sym?.getDeclarations()?.[0];
369
414
  const declPath = decl?.getSourceFile().getFilePath();
370
- if (declPath && !declPath.includes('/node_modules/') && declPath !== filePath) {
415
+ if (declPath && !declPath.includes('/node_modules/')) {
371
416
  calleeFile = declPath;
372
417
  calleeSymbol = calleeName;
373
418
  }
@@ -383,7 +428,7 @@ class AstParserService {
383
428
  : sym;
384
429
  const decl = resolved.getDeclarations()?.[0];
385
430
  const declPath = decl?.getSourceFile().getFilePath();
386
- if (declPath && !declPath.includes('/node_modules/') && declPath !== filePath) {
431
+ if (declPath && !declPath.includes('/node_modules/')) {
387
432
  calleeFile = declPath;
388
433
  calleeSymbol = calleeName;
389
434
  }
@@ -1,11 +1,12 @@
1
1
  import type { NodePgDatabase, NodePgTransaction } from 'drizzle-orm/node-postgres';
2
2
  import type { ExtractTablesWithRelations } from 'drizzle-orm/relations';
3
3
  import type { Config } from '../config';
4
+ import type { IDatabase, IndexMeta } from '../interfaces/IDatabase';
4
5
  import * as schema from '../db/schema';
5
6
  type Schema = typeof schema;
6
7
  export type DrizzleDb = NodePgDatabase<Schema>;
7
8
  export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
8
- export declare class DatabaseService {
9
+ export declare class DatabaseService implements IDatabase {
9
10
  private config;
10
11
  private pool;
11
12
  private _db;
@@ -16,6 +17,18 @@ export declare class DatabaseService {
16
17
  destroy(): Promise<void>;
17
18
  initDb(): Promise<void>;
18
19
  initDbWithSchema(): Promise<void>;
20
+ /**
21
+ * Create `code_chunk` at `dimensions` if it does not exist, plus its HNSW and
22
+ * (serviceName, filePath) indexes and the additive `contentHash`/`ownerPath`
23
+ * columns for pre-existing tables. Idempotent — safe to call repeatedly.
24
+ */
25
+ ensureChunkTable(dimensions: number): Promise<void>;
26
+ /** Drop and recreate `code_chunk` at `dimensions` — used on a dimension change. */
27
+ recreateChunkTable(dimensions: number): Promise<void>;
28
+ readIndexMeta(): Promise<IndexMeta | null>;
29
+ writeIndexMeta(meta: IndexMeta): Promise<void>;
30
+ /** Guard against SQL injection — the dimension is interpolated into DDL. */
31
+ private assertDim;
19
32
  private ensureDatabaseExists;
20
33
  }
21
34
  export {};
@@ -66,33 +66,20 @@ class DatabaseService {
66
66
  async initDbWithSchema() {
67
67
  await this.ensureDatabaseExists();
68
68
  await this.initDb();
69
- // code_chunk table
70
- const [{ exists }] = await this.query(`
71
- SELECT EXISTS (
72
- SELECT 1 FROM information_schema.tables
73
- WHERE table_schema = 'public' AND table_name = 'code_chunk'
69
+ // code_chunk table — created at the legacy default width (1024) so search
70
+ // tools always have a table. IndexerService recreates it at the resolved
71
+ // width before indexing when the active model's dimension differs.
72
+ await this.ensureChunkTable(1024);
73
+ // index_meta table — source of truth for the model that built the index.
74
+ await this.query(`
75
+ CREATE TABLE IF NOT EXISTS index_meta (
76
+ id BOOLEAN PRIMARY KEY DEFAULT true,
77
+ provider TEXT NOT NULL,
78
+ model TEXT NOT NULL,
79
+ dimensions INT NOT NULL,
80
+ "indexedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
74
81
  )
75
82
  `);
76
- if (!exists) {
77
- await this.query(`
78
- CREATE TABLE code_chunk (
79
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
80
- "serviceName" TEXT NOT NULL,
81
- "filePath" TEXT NOT NULL,
82
- "startLine" INT,
83
- content TEXT NOT NULL,
84
- embedding vector(1024) NOT NULL,
85
- "lastModified" DOUBLE PRECISION,
86
- "contentHash" VARCHAR(64)
87
- )
88
- `);
89
- await this.query(`
90
- CREATE INDEX idx_code_chunk_embedding
91
- ON code_chunk USING hnsw (embedding vector_cosine_ops)
92
- `);
93
- }
94
- await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
95
- await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "contentHash" varchar(64) NULL`);
96
83
  // code_dependency table
97
84
  const [{ existsDep }] = await this.query(`
98
85
  SELECT EXISTS (
@@ -160,6 +147,64 @@ class DatabaseService {
160
147
  await this.query(`CREATE INDEX IF NOT EXISTS idx_call_callee_file ON code_call (project, "calleeFile")`);
161
148
  await this.query(`CREATE INDEX IF NOT EXISTS idx_call_from ON code_call (project, "fromFile")`);
162
149
  }
150
+ /**
151
+ * Create `code_chunk` at `dimensions` if it does not exist, plus its HNSW and
152
+ * (serviceName, filePath) indexes and the additive `contentHash`/`ownerPath`
153
+ * columns for pre-existing tables. Idempotent — safe to call repeatedly.
154
+ */
155
+ async ensureChunkTable(dimensions) {
156
+ const dim = this.assertDim(dimensions);
157
+ await this.query(`
158
+ CREATE TABLE IF NOT EXISTS code_chunk (
159
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
160
+ "serviceName" TEXT NOT NULL,
161
+ "filePath" TEXT NOT NULL,
162
+ "startLine" INT,
163
+ content TEXT NOT NULL,
164
+ embedding vector(${dim}) NOT NULL,
165
+ "lastModified" DOUBLE PRECISION,
166
+ "contentHash" VARCHAR(64),
167
+ "ownerPath" VARCHAR
168
+ )
169
+ `);
170
+ await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "contentHash" varchar(64) NULL`);
171
+ await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "ownerPath" varchar NULL`);
172
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_embedding ON code_chunk USING hnsw (embedding vector_cosine_ops)`);
173
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
174
+ }
175
+ /** Drop and recreate `code_chunk` at `dimensions` — used on a dimension change. */
176
+ async recreateChunkTable(dimensions) {
177
+ const dim = this.assertDim(dimensions);
178
+ await this.query(`DROP TABLE IF EXISTS code_chunk`);
179
+ await this.ensureChunkTable(dim);
180
+ }
181
+ async readIndexMeta() {
182
+ try {
183
+ const rows = await this.query(`SELECT provider, model, dimensions FROM index_meta LIMIT 1`);
184
+ const row = rows[0];
185
+ return row ? { provider: row.provider, model: row.model, dimensions: Number(row.dimensions) } : null;
186
+ }
187
+ catch {
188
+ // Table absent (un-migrated DB) — treat as "no metadata".
189
+ return null;
190
+ }
191
+ }
192
+ async writeIndexMeta(meta) {
193
+ await this.query(`INSERT INTO index_meta (id, provider, model, dimensions, "indexedAt")
194
+ VALUES (true, $1, $2, $3, now())
195
+ ON CONFLICT (id) DO UPDATE
196
+ SET provider = EXCLUDED.provider,
197
+ model = EXCLUDED.model,
198
+ dimensions = EXCLUDED.dimensions,
199
+ "indexedAt" = now()`, [meta.provider, meta.model, meta.dimensions]);
200
+ }
201
+ /** Guard against SQL injection — the dimension is interpolated into DDL. */
202
+ assertDim(dimensions) {
203
+ if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {
204
+ throw new Error(`Invalid vector dimension: ${dimensions} (must be an integer 1–2000)`);
205
+ }
206
+ return dimensions;
207
+ }
163
208
  async ensureDatabaseExists() {
164
209
  const { host, port, username, password, database } = this.config.db;
165
210
  const client = new pg_1.Client({ host, port, user: username, password, database: 'postgres' });
@@ -0,0 +1,9 @@
1
+ import type { Config } from '../config';
2
+ import type { IEmbedder } from '../interfaces/IEmbedder';
3
+ import type { ILogger } from '../interfaces/ILogger';
4
+ import { type EmbedderDeps } from './OpenAICompatibleEmbedder';
5
+ /**
6
+ * Construct the `IEmbedder` implementation selected by `config.embedding.provider`.
7
+ * The default (`ollama`) keeps existing installs working unchanged.
8
+ */
9
+ export declare function createEmbedder(config: Config, logger?: ILogger, deps?: EmbedderDeps): IEmbedder;