codesearch-mcp 0.0.17-alpha.1 → 0.0.18-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 (83) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +20 -0
  3. package/dist/app.js +34 -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 +22 -7
  7. package/dist/src/commands/McpCommand.js +4 -3
  8. package/dist/src/commands/WebCommand.d.ts +8 -0
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +43 -0
  11. package/dist/src/config-store.js +92 -27
  12. package/dist/src/config.js +16 -17
  13. package/dist/src/constants.d.ts +2 -1
  14. package/dist/src/constants.js +6 -4
  15. package/dist/src/container.d.ts +10 -6
  16. package/dist/src/container.js +6 -3
  17. package/dist/src/graph.d.ts +60 -0
  18. package/dist/src/graph.js +239 -120
  19. package/dist/src/help.js +15 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +6 -3
  21. package/dist/src/interfaces/IDatabase.d.ts +13 -0
  22. package/dist/src/interfaces/IDatabase.js +2 -0
  23. package/dist/src/interfaces/IEmbedder.d.ts +1 -0
  24. package/dist/src/interfaces/ILogger.d.ts +10 -0
  25. package/dist/src/interfaces/ILogger.js +2 -0
  26. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  27. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  28. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  29. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  30. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  31. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  32. package/dist/src/interfaces/ISearcher.d.ts +36 -0
  33. package/dist/src/interfaces/ISearcher.js +2 -0
  34. package/dist/src/interfaces/index.d.ts +7 -0
  35. package/dist/src/interfaces/index.js +2 -0
  36. package/dist/src/project-selector.d.ts +1 -1
  37. package/dist/src/services/AstParserService.d.ts +4 -6
  38. package/dist/src/services/AstParserService.js +29 -13
  39. package/dist/src/services/DatabaseService.d.ts +2 -1
  40. package/dist/src/services/IndexerService.d.ts +6 -8
  41. package/dist/src/services/IndexerService.js +5 -8
  42. package/dist/src/services/LoggerService.d.ts +2 -1
  43. package/dist/src/services/McpServer.d.ts +16 -7
  44. package/dist/src/services/McpServer.js +54 -367
  45. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  46. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  47. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  48. package/dist/src/services/NxMonorepoResolver.js +308 -0
  49. package/dist/src/services/OllamaEmbedder.d.ts +5 -2
  50. package/dist/src/services/OllamaEmbedder.js +9 -2
  51. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  52. package/dist/src/services/ProjectDiscoveryService.js +52 -0
  53. package/dist/src/services/SearcherService.d.ts +39 -6
  54. package/dist/src/services/SearcherService.js +151 -40
  55. package/dist/src/services/WebServerService.d.ts +72 -0
  56. package/dist/src/services/WebServerService.js +247 -0
  57. package/dist/src/setup.js +7 -7
  58. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  59. package/dist/src/tools/FindSymbolTool.js +45 -0
  60. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  61. package/dist/src/tools/GetCallersTool.js +42 -0
  62. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  63. package/dist/src/tools/GetDependenciesTool.js +38 -0
  64. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  65. package/dist/src/tools/GetDependentsTool.js +57 -0
  66. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  67. package/dist/src/tools/ListFilesTool.js +39 -0
  68. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  69. package/dist/src/tools/ListProjectsTool.js +44 -0
  70. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  71. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  72. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  73. package/dist/src/tools/ReadFileTool.js +42 -0
  74. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  75. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  76. package/dist/src/tools/format-helpers.d.ts +8 -0
  77. package/dist/src/tools/format-helpers.js +57 -0
  78. package/dist/src/tools/index.d.ts +9 -0
  79. package/dist/src/tools/index.js +21 -0
  80. package/dist/src/web/dashboard.d.ts +9 -0
  81. package/dist/src/web/dashboard.js +337 -0
  82. package/docs/web-dashboard.md +187 -0
  83. package/package.json +5 -2
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MonorepoAwareDiscoveryService = void 0;
4
+ exports.toLeafProjects = toLeafProjects;
5
+ const ProjectDiscoveryService_1 = require("./ProjectDiscoveryService");
6
+ /**
7
+ * Expand discovery units into the leaf projects to index: a nested monorepo
8
+ * unit (with `children`) becomes its sub-projects; an ordinary project stays
9
+ * itself. Shared by IndexCommand (indexing loop) and SearcherService (name→path
10
+ * resolution) so both agree on the leaf set.
11
+ */
12
+ function toLeafProjects(projects) {
13
+ return projects.flatMap(p => (p.children && p.children.length > 0 ? p.children : [p]));
14
+ }
15
+ /**
16
+ * Project discovery that is monorepo-aware.
17
+ *
18
+ * When a monorepo is detected (via the resolver), it uses the resolver
19
+ * to discover projects. Otherwise, it falls back to the original
20
+ * flat-workspace discovery (directories with package.json). Cross-project
21
+ * import resolution relies on the tsconfig `paths` attached via
22
+ * `rootTsConfigPath` — no separate scope map is built.
23
+ *
24
+ * Single Responsibility: only discovers projects and workspace metadata.
25
+ * Does not index or search.
26
+ *
27
+ * Implements {@link IProjectDiscovery} so it can be wired as the default
28
+ * discovery in the container. `discover()` returns one entry per top-level
29
+ * project: when the root itself is a monorepo, each entry carries the
30
+ * workspace-wide `rootTsConfigPath`; under a flat root, an ordinary project
31
+ * gets `rootTsConfigPath: null`, while a top-level directory that is itself a
32
+ * nested monorepo is returned as a single unit with expanded `children` (see
33
+ * `toLeafProjects`). `analyzeWorkspace()` exposes the full workspace metadata
34
+ * (monorepo flag, root tsconfig, project list).
35
+ */
36
+ class MonorepoAwareDiscoveryService {
37
+ resolvers;
38
+ logger;
39
+ flatDiscovery;
40
+ constructor(resolvers, logger,
41
+ /** Discovery used when no monorepo is detected. Defaults to flat package.json scan. */
42
+ flatDiscovery = new ProjectDiscoveryService_1.ProjectDiscoveryService()) {
43
+ this.resolvers = resolvers;
44
+ this.logger = logger;
45
+ this.flatDiscovery = flatDiscovery;
46
+ }
47
+ /**
48
+ * IProjectDiscovery contract: enumerate projects in the workspace.
49
+ * Attaches the workspace-wide root tsconfig to every project so the
50
+ * indexer can resolve cross-project path aliases.
51
+ */
52
+ discover(rootDir) {
53
+ const workspace = this.analyzeWorkspace(rootDir);
54
+ // Case A: workspaceRoot itself is a monorepo — attach the workspace-wide
55
+ // tsconfig to every project (original behavior).
56
+ if (workspace.isMonorepo) {
57
+ return workspace.projects.map(p => ({ ...p, rootTsConfigPath: workspace.rootTsConfigPath }));
58
+ }
59
+ // Case B: flat workspaceRoot — a top-level project directory may itself be a
60
+ // nested monorepo (e.g. an Nx repo). Expand such a directory into its
61
+ // sub-projects as `children`, keeping the repo a single selectable unit.
62
+ return workspace.projects.map(p => this.expandIfNestedMonorepo(p));
63
+ }
64
+ /**
65
+ * If `project`'s directory is itself a monorepo (resolver detects it),
66
+ * return it with expanded `children` named `<repo>/<sub-project>`, each
67
+ * carrying the repo's workspace-wide tsconfig. Otherwise return it unchanged.
68
+ */
69
+ expandIfNestedMonorepo(project) {
70
+ // Ordinary flat project — preserve the original contract (null root tsconfig).
71
+ const flat = { ...project, rootTsConfigPath: null };
72
+ const resolver = this.resolvers.find(r => r.isMonorepo(project.path));
73
+ if (!resolver)
74
+ return flat;
75
+ const rootTsConfigPath = resolver.getRootTsConfigPath(project.path);
76
+ const children = resolver.discoverProjects(project.path).map(c => ({
77
+ name: `${project.name}/${c.name}`,
78
+ path: c.path,
79
+ packageName: c.packageName,
80
+ type: c.type,
81
+ rootTsConfigPath,
82
+ }));
83
+ if (children.length === 0)
84
+ return flat;
85
+ this.logger.info('MonorepoAwareDiscovery: nested monorepo expanded', {
86
+ repo: project.name,
87
+ projects: children.length,
88
+ rootTsConfigPath,
89
+ });
90
+ return { ...project, children };
91
+ }
92
+ /**
93
+ * Analyze the workspace root and return full workspace information:
94
+ * monorepo detection flag, root tsconfig path, and project list.
95
+ */
96
+ analyzeWorkspace(rootDir) {
97
+ for (const resolver of this.resolvers) {
98
+ if (resolver.isMonorepo(rootDir)) {
99
+ this.logger.info('MonorepoAwareDiscovery: monorepo detected', { rootDir });
100
+ const monoProjects = resolver.discoverProjects(rootDir);
101
+ const rootTsConfigPath = resolver.getRootTsConfigPath(rootDir);
102
+ const projects = monoProjects.map(p => ({
103
+ name: p.name,
104
+ path: p.path,
105
+ packageName: p.packageName,
106
+ type: p.type,
107
+ }));
108
+ this.logger.debug('MonorepoAwareDiscovery: workspace analyzed', {
109
+ projects: projects.length,
110
+ rootTsConfigPath,
111
+ });
112
+ return { isMonorepo: true, rootTsConfigPath, projects };
113
+ }
114
+ }
115
+ // Fallback: flat workspace (original behavior) — delegated to the default discovery.
116
+ this.logger.debug('MonorepoAwareDiscovery: no monorepo detected, using flat discovery', { rootDir });
117
+ const projects = this.flatDiscovery.discover(rootDir);
118
+ return {
119
+ isMonorepo: false,
120
+ rootTsConfigPath: null,
121
+ projects,
122
+ };
123
+ }
124
+ }
125
+ exports.MonorepoAwareDiscoveryService = MonorepoAwareDiscoveryService;
@@ -0,0 +1,45 @@
1
+ import type { IMonorepoResolver, MonorepoProject } from '../interfaces/IMonorepoResolver';
2
+ import type { ILogger } from '../interfaces';
3
+ /**
4
+ * Nx monorepo resolver.
5
+ *
6
+ * Detection: presence of `nx.json` at the workspace root.
7
+ *
8
+ * Project discovery strategies (results deduplicated by absolute path):
9
+ * 1. Read `workspace.json` or `angular.json` → projects map
10
+ * 2. Scan standard Nx directories (`apps/`, `libs/`, `packages/`) for
11
+ * `project.json`, then `package.json` (only when strategy 1 found nothing)
12
+ * 3. `tsconfig.base.json` `paths` — root-level libs referenced only by path
13
+ * alias (e.g. `contracts/`, `interfaces/`) that the directory scan misses
14
+ * 4. Root-level fallback: treat the workspace root itself as a project
15
+ * (standalone Nx setup) when nothing else matched
16
+ *
17
+ * tsconfig resolution:
18
+ * - Returns `tsconfig.base.json` at workspace root (Nx convention)
19
+ * - Falls back to `tsconfig.json` at workspace root
20
+ *
21
+ * Cross-project import resolution is handled by the AST parser via the
22
+ * workspace-wide tsconfig `paths` (tsconfig.base.json), so no scope map is built here.
23
+ */
24
+ export declare class NxMonorepoResolver implements IMonorepoResolver {
25
+ private logger;
26
+ constructor(logger: ILogger);
27
+ isMonorepo(rootDir: string): boolean;
28
+ discoverProjects(rootDir: string): MonorepoProject[];
29
+ /**
30
+ * Discover projects from `tsconfig.base.json` `compilerOptions.paths`. Each
31
+ * alias (e.g. `@scope/interfaces` → `interfaces/src/index.ts`) resolves to a
32
+ * lib root (the path before `/src/`), which for real Nx libs contains a
33
+ * project.json. Aliases pointing at non-projects (no project.json/package.json,
34
+ * e.g. `@env` → `environments/environment.ts`) are skipped.
35
+ */
36
+ private discoverFromTsconfigPaths;
37
+ /** Parse JSON that may contain comments / trailing commas (tsconfig style). */
38
+ private parseJsonc;
39
+ getRootTsConfigPath(rootDir: string): string | null;
40
+ private readWorkspaceJson;
41
+ private scanDirectory;
42
+ private tryReadProject;
43
+ private readPackageName;
44
+ private inferProjectType;
45
+ }
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NxMonorepoResolver = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const file_utils_1 = require("../file-utils");
40
+ /**
41
+ * Nx monorepo resolver.
42
+ *
43
+ * Detection: presence of `nx.json` at the workspace root.
44
+ *
45
+ * Project discovery strategies (results deduplicated by absolute path):
46
+ * 1. Read `workspace.json` or `angular.json` → projects map
47
+ * 2. Scan standard Nx directories (`apps/`, `libs/`, `packages/`) for
48
+ * `project.json`, then `package.json` (only when strategy 1 found nothing)
49
+ * 3. `tsconfig.base.json` `paths` — root-level libs referenced only by path
50
+ * alias (e.g. `contracts/`, `interfaces/`) that the directory scan misses
51
+ * 4. Root-level fallback: treat the workspace root itself as a project
52
+ * (standalone Nx setup) when nothing else matched
53
+ *
54
+ * tsconfig resolution:
55
+ * - Returns `tsconfig.base.json` at workspace root (Nx convention)
56
+ * - Falls back to `tsconfig.json` at workspace root
57
+ *
58
+ * Cross-project import resolution is handled by the AST parser via the
59
+ * workspace-wide tsconfig `paths` (tsconfig.base.json), so no scope map is built here.
60
+ */
61
+ class NxMonorepoResolver {
62
+ logger;
63
+ constructor(logger) {
64
+ this.logger = logger;
65
+ }
66
+ isMonorepo(rootDir) {
67
+ return fs.existsSync(path.join(rootDir, 'nx.json'));
68
+ }
69
+ discoverProjects(rootDir) {
70
+ // Deduplicate across strategies by absolute project path.
71
+ const byPath = new Map();
72
+ const add = (p) => { if (p && !byPath.has(p.path))
73
+ byPath.set(p.path, p); };
74
+ // Strategy 1: workspace.json / angular.json (authoritative when present)
75
+ const workspaceProjects = this.readWorkspaceJson(rootDir);
76
+ for (const p of workspaceProjects)
77
+ add(p);
78
+ // Strategy 2 & 3: scan standard directories (only when no workspace.json map)
79
+ if (byPath.size === 0) {
80
+ for (const dir of ['apps', 'libs', 'packages']) {
81
+ const fullDir = path.join(rootDir, dir);
82
+ if (!fs.existsSync(fullDir))
83
+ continue;
84
+ const type = dir === 'apps' ? 'app' : 'lib';
85
+ for (const p of this.scanDirectory(fullDir, type, rootDir))
86
+ add(p);
87
+ }
88
+ }
89
+ // Strategy 4: tsconfig.base.json `paths` — captures libs that live at the
90
+ // repo root (e.g. `contracts/`, `interfaces/`) referenced by path aliases,
91
+ // which the apps/libs/packages scan misses. Only real Nx projects (with a
92
+ // project.json/package.json) are added; dedup handles overlap with the scan.
93
+ for (const p of this.discoverFromTsconfigPaths(rootDir))
94
+ add(p);
95
+ // Root-level fallback (standalone Nx setup)
96
+ if (byPath.size === 0) {
97
+ const rootProject = this.tryReadProject(rootDir, rootDir);
98
+ if (rootProject)
99
+ add(rootProject);
100
+ }
101
+ const projects = [...byPath.values()];
102
+ this.logger.debug('NxMonorepoResolver: discovered projects', { count: projects.length });
103
+ return projects;
104
+ }
105
+ /**
106
+ * Discover projects from `tsconfig.base.json` `compilerOptions.paths`. Each
107
+ * alias (e.g. `@scope/interfaces` → `interfaces/src/index.ts`) resolves to a
108
+ * lib root (the path before `/src/`), which for real Nx libs contains a
109
+ * project.json. Aliases pointing at non-projects (no project.json/package.json,
110
+ * e.g. `@env` → `environments/environment.ts`) are skipped.
111
+ */
112
+ discoverFromTsconfigPaths(rootDir) {
113
+ const tsconfigPath = this.getRootTsConfigPath(rootDir);
114
+ if (!tsconfigPath)
115
+ return [];
116
+ let json;
117
+ try {
118
+ json = this.parseJsonc(fs.readFileSync(tsconfigPath, 'utf-8'));
119
+ }
120
+ catch (err) {
121
+ this.logger.warn('NxMonorepoResolver: failed to parse tsconfig paths', { file: tsconfigPath, reason: err.message });
122
+ return [];
123
+ }
124
+ const co = json?.compilerOptions ?? {};
125
+ const baseUrl = typeof co.baseUrl === 'string' ? co.baseUrl : '.';
126
+ const paths = co.paths;
127
+ if (!paths || typeof paths !== 'object')
128
+ return [];
129
+ const projects = [];
130
+ for (const targets of Object.values(paths)) {
131
+ const target = Array.isArray(targets) ? targets[0] : targets;
132
+ if (typeof target !== 'string')
133
+ continue;
134
+ const srcIdx = target.indexOf('/src/');
135
+ const relRoot = srcIdx >= 0 ? target.slice(0, srcIdx) : path.dirname(target);
136
+ const libRoot = path.resolve(rootDir, baseUrl, relRoot);
137
+ const project = this.tryReadProject(libRoot, rootDir); // null unless a real project marker exists
138
+ if (project)
139
+ projects.push(project);
140
+ }
141
+ return projects;
142
+ }
143
+ /** Parse JSON that may contain comments / trailing commas (tsconfig style). */
144
+ parseJsonc(text) {
145
+ try {
146
+ return JSON.parse(text);
147
+ }
148
+ catch {
149
+ const stripped = text
150
+ .replace(/\/\*[\s\S]*?\*\//g, '')
151
+ .replace(/(^|[^:])\/\/.*$/gm, '$1')
152
+ .replace(/,(\s*[}\]])/g, '$1');
153
+ return JSON.parse(stripped);
154
+ }
155
+ }
156
+ getRootTsConfigPath(rootDir) {
157
+ // Nx convention: tsconfig.base.json at workspace root
158
+ const basePath = path.join(rootDir, 'tsconfig.base.json');
159
+ if (fs.existsSync(basePath))
160
+ return basePath;
161
+ // Fallback: tsconfig.json at workspace root
162
+ const rootPath = path.join(rootDir, 'tsconfig.json');
163
+ if (fs.existsSync(rootPath))
164
+ return rootPath;
165
+ return null;
166
+ }
167
+ // ── Private helpers ─────────────────────────────────────────────────────────
168
+ readWorkspaceJson(rootDir) {
169
+ const projects = [];
170
+ for (const filename of ['workspace.json', 'angular.json']) {
171
+ const filePath = path.join(rootDir, filename);
172
+ if (!fs.existsSync(filePath))
173
+ continue;
174
+ try {
175
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
176
+ const projectsMap = content.projects ?? {};
177
+ for (const [name, value] of Object.entries(projectsMap)) {
178
+ // value can be a string (path) or an object with a `root` property.
179
+ // Guard per-entry: a malformed entry (object without `root`) must not
180
+ // abort discovery of the remaining projects in this file.
181
+ const rawRoot = typeof value === 'string'
182
+ ? value
183
+ : value?.root;
184
+ if (typeof rawRoot !== 'string' || rawRoot.length === 0) {
185
+ this.logger.warn('NxMonorepoResolver: skipping workspace entry without valid root', { file: filePath, name });
186
+ continue;
187
+ }
188
+ const projectPath = path.resolve(rootDir, rawRoot);
189
+ if (!fs.existsSync(projectPath))
190
+ continue;
191
+ const type = this.inferProjectType(projectPath, name);
192
+ const packageName = this.readPackageName(projectPath);
193
+ projects.push({ name, path: projectPath, packageName, type });
194
+ }
195
+ }
196
+ catch (err) {
197
+ this.logger.warn('NxMonorepoResolver: failed to parse workspace file', {
198
+ file: filePath,
199
+ reason: err.message,
200
+ });
201
+ }
202
+ }
203
+ return projects;
204
+ }
205
+ scanDirectory(dir, defaultType, rootDir) {
206
+ const projects = [];
207
+ let entries;
208
+ try {
209
+ entries = fs.readdirSync(dir, { withFileTypes: true });
210
+ }
211
+ catch {
212
+ return projects;
213
+ }
214
+ for (const entry of entries) {
215
+ if (!entry.isDirectory())
216
+ continue;
217
+ if (file_utils_1.IGNORE_DIRS.has(entry.name))
218
+ continue;
219
+ const projectPath = path.join(dir, entry.name);
220
+ const project = this.tryReadProject(projectPath, rootDir, defaultType);
221
+ if (project) {
222
+ projects.push(project);
223
+ continue;
224
+ }
225
+ // Check for nested grouping directories (e.g. libs/shared/utils)
226
+ const nested = this.scanDirectory(projectPath, defaultType, rootDir);
227
+ projects.push(...nested);
228
+ }
229
+ return projects;
230
+ }
231
+ tryReadProject(projectPath, rootDir, defaultType = 'lib') {
232
+ // Check for project.json (Nx project marker)
233
+ const projectJsonPath = path.join(projectPath, 'project.json');
234
+ const packageJsonPath = path.join(projectPath, 'package.json');
235
+ const hasProjectJson = fs.existsSync(projectJsonPath);
236
+ const hasPackageJson = fs.existsSync(packageJsonPath);
237
+ if (!hasProjectJson && !hasPackageJson)
238
+ return null;
239
+ let name = path.relative(rootDir, projectPath).replace(/\\/g, '/');
240
+ let type = defaultType;
241
+ let packageName = null;
242
+ // Read project.json for name and type
243
+ if (hasProjectJson) {
244
+ try {
245
+ const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
246
+ if (projectJson.name)
247
+ name = projectJson.name;
248
+ if (projectJson.projectType === 'application')
249
+ type = 'app';
250
+ else if (projectJson.projectType === 'library')
251
+ type = 'lib';
252
+ }
253
+ catch (err) {
254
+ this.logger.warn('NxMonorepoResolver: failed to parse project.json', {
255
+ file: projectJsonPath,
256
+ reason: err.message,
257
+ });
258
+ }
259
+ }
260
+ // Read package.json for npm scope
261
+ if (hasPackageJson) {
262
+ packageName = this.readPackageName(projectPath);
263
+ // If no project.json name, use package name without scope
264
+ if (!hasProjectJson && packageName) {
265
+ const shortName = packageName.replace(/^@[^/]+\//, '');
266
+ name = shortName;
267
+ }
268
+ }
269
+ return { name, path: projectPath, packageName, type };
270
+ }
271
+ readPackageName(projectPath) {
272
+ const packageJsonPath = path.join(projectPath, 'package.json');
273
+ if (!fs.existsSync(packageJsonPath))
274
+ return null;
275
+ try {
276
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
277
+ return typeof pkg.name === 'string' ? pkg.name : null;
278
+ }
279
+ catch {
280
+ return null;
281
+ }
282
+ }
283
+ inferProjectType(projectPath, name) {
284
+ // Check project.json
285
+ const projectJsonPath = path.join(projectPath, 'project.json');
286
+ if (fs.existsSync(projectJsonPath)) {
287
+ try {
288
+ const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
289
+ if (projectJson.projectType === 'application')
290
+ return 'app';
291
+ if (projectJson.projectType === 'library')
292
+ return 'lib';
293
+ }
294
+ catch { /* fall through */ }
295
+ }
296
+ // Infer from path
297
+ const rel = projectPath.replace(/\\/g, '/');
298
+ if (rel.includes('/apps/') || rel.includes('/app/'))
299
+ return 'app';
300
+ if (rel.includes('/libs/') || rel.includes('/lib/') || rel.includes('/packages/'))
301
+ return 'lib';
302
+ // Infer from name
303
+ if (name.includes('lib') || name.includes('shared') || name.includes('util'))
304
+ return 'lib';
305
+ return 'app';
306
+ }
307
+ }
308
+ exports.NxMonorepoResolver = NxMonorepoResolver;
@@ -1,11 +1,14 @@
1
1
  import type { Config } from '../config';
2
- import type { IEmbedder } from '../interfaces/IEmbedder';
2
+ import type { IEmbedder } from '../interfaces';
3
+ import type { ILogger } from '../interfaces';
3
4
  export declare class OllamaEmbedder implements IEmbedder {
4
5
  private config;
6
+ private logger;
5
7
  private client;
6
8
  rerank?: (query: string, documents: string[], topN: number) => Promise<number[]>;
7
- constructor(config: Config);
9
+ constructor(config: Config, logger?: ILogger);
8
10
  embed(text: string): Promise<number[]>;
11
+ listModels(): Promise<string[]>;
9
12
  translate(text: string, model: string): Promise<string>;
10
13
  private _rerank;
11
14
  }
@@ -2,19 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OllamaEmbedder = void 0;
4
4
  const ollama_1 = require("ollama");
5
+ const LoggerService_1 = require("./LoggerService");
5
6
  class OllamaEmbedder {
6
7
  config;
8
+ logger;
7
9
  client;
8
10
  rerank;
9
- constructor(config) {
11
+ constructor(config, logger = LoggerService_1.LoggerService.silent()) {
10
12
  this.config = config;
13
+ this.logger = logger;
11
14
  this.client = new ollama_1.Ollama({ host: config.ollama.host });
12
15
  if (config.search.rerankerEnabled) {
13
16
  if (config.ollama.rerankerModel) {
14
17
  this.rerank = this._rerank.bind(this);
15
18
  }
16
19
  else {
17
- console.warn('[OllamaEmbedder] RERANKER_ENABLED=true but RERANKER_MODEL is not set — reranking disabled');
20
+ this.logger.warn('OllamaEmbedder: rerankerEnabled=true but rerankerModel is not set — reranking disabled');
18
21
  }
19
22
  }
20
23
  }
@@ -25,6 +28,10 @@ class OllamaEmbedder {
25
28
  });
26
29
  return embedding;
27
30
  }
31
+ async listModels() {
32
+ const res = await this.client.list();
33
+ return res.models.map(m => m.name).filter(Boolean).sort();
34
+ }
28
35
  async translate(text, model) {
29
36
  const response = await this.client.chat({
30
37
  model,
@@ -0,0 +1,8 @@
1
+ import type { IProjectDiscovery, DiscoveredProject } from '../interfaces/IProjectDiscovery';
2
+ /**
3
+ * Default project discovery: finds immediate subdirectories
4
+ * of the workspace root that contain a package.json.
5
+ */
6
+ export declare class ProjectDiscoveryService implements IProjectDiscovery {
7
+ discover(rootDir: string): DiscoveredProject[];
8
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ProjectDiscoveryService = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const file_utils_1 = require("../file-utils");
40
+ /**
41
+ * Default project discovery: finds immediate subdirectories
42
+ * of the workspace root that contain a package.json.
43
+ */
44
+ class ProjectDiscoveryService {
45
+ discover(rootDir) {
46
+ return fs.readdirSync(rootDir, { withFileTypes: true })
47
+ .filter(d => d.isDirectory() && !file_utils_1.IGNORE_DIRS.has(d.name))
48
+ .map(d => ({ name: d.name, path: path.join(rootDir, d.name) }))
49
+ .filter(p => fs.existsSync(path.join(p.path, 'package.json')));
50
+ }
51
+ }
52
+ exports.ProjectDiscoveryService = ProjectDiscoveryService;
@@ -1,8 +1,9 @@
1
1
  import type { Config } from '../config';
2
- import type { IEmbedder } from '../interfaces/IEmbedder';
3
- import type { DatabaseService } from './DatabaseService';
2
+ import type { IEmbedder } from '../interfaces';
3
+ import type { IDatabase } from '../interfaces';
4
4
  import { type ProjectMeta } from '../file-utils';
5
- import { LoggerService } from './LoggerService';
5
+ import type { ILogger } from '../interfaces';
6
+ import type { IProjectDiscovery } from '../interfaces';
6
7
  export interface ChunkRow {
7
8
  serviceName: string;
8
9
  filePath: string;
@@ -44,6 +45,10 @@ export interface DependencyRow {
44
45
  export interface ProjectRow extends ProjectMeta {
45
46
  serviceName: string;
46
47
  chunks: number;
48
+ packageName?: string | null;
49
+ type?: 'app' | 'lib';
50
+ /** Real filesystem root of the project (resolved via discovery). */
51
+ path?: string;
47
52
  }
48
53
  export interface GrepMatch {
49
54
  filePath: string;
@@ -67,9 +72,32 @@ export interface SymbolHit {
67
72
  export declare class SearcherService {
68
73
  private db;
69
74
  private embedder;
75
+ private discovery;
70
76
  private config;
71
77
  private logger;
72
- constructor(db: DatabaseService, embedder: IEmbedder, config: Config, logger?: LoggerService);
78
+ constructor(db: IDatabase, embedder: IEmbedder, discovery: IProjectDiscovery, config: Config, logger?: ILogger);
79
+ /** Lazily-built, cached map of project name → real filesystem root. */
80
+ private rootMap?;
81
+ /** Cached, lazily-built map of leaf project name → real filesystem root (via discovery). */
82
+ private projectRootMap;
83
+ /**
84
+ * Resolve a project name to its real filesystem root. Uses discovery (leaf
85
+ * projects) so nested-monorepo names like `repo/app` map to their true path;
86
+ * falls back to `join(workspaceRoot, name)` when the name is unknown (e.g. a
87
+ * project since removed from disk, or a flat project not surfaced by discovery).
88
+ */
89
+ private resolveProjectRoot;
90
+ /**
91
+ * Project-level dependency edges aggregated from `code_dependency`: an edge
92
+ * `A → B` means some file in project A imports a file owned by project B.
93
+ * Used by the graph to draw links between a monorepo's sub-projects. Only
94
+ * edges whose target file belongs to an indexed project are returned (targets
95
+ * outside any indexed project — e.g. unindexed libs — are skipped).
96
+ */
97
+ getProjectEdges(): Promise<{
98
+ from: string;
99
+ to: string;
100
+ }[]>;
73
101
  prepareQuery(raw: string): Promise<{
74
102
  query: string;
75
103
  translated: boolean;
@@ -81,10 +109,10 @@ export declare class SearcherService {
81
109
  getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
82
110
  getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
83
111
  getCallers(symbol: string, project: string, calleeFile?: string | null): Promise<CallerRow[]>;
84
- grepSymbol(symbol: string, project: string, excludeFile: string): {
112
+ grepSymbol(symbol: string, project: string, excludeFile: string): Promise<{
85
113
  matches: GrepMatch[];
86
114
  truncated: boolean;
87
- };
115
+ }>;
88
116
  findSymbol(name: string, project: string | null, kind: SymbolKind): Promise<{
89
117
  hits: SymbolHit[];
90
118
  truncated: boolean;
@@ -101,4 +129,9 @@ export declare class SearcherService {
101
129
  private walkDir;
102
130
  private buildFindSymbolPattern;
103
131
  private inferKind;
132
+ /**
133
+ * Async wrapper around grep to avoid blocking the event loop.
134
+ * Returns stdout as a string, or null if grep returned no results or errored.
135
+ */
136
+ private spawnGrep;
104
137
  }