codesearch-mcp 0.0.1-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 (39) hide show
  1. package/README.md +129 -0
  2. package/dist/app.d.ts +1 -0
  3. package/dist/app.js +124 -0
  4. package/dist/src/ast.d.ts +2 -0
  5. package/dist/src/ast.js +60 -0
  6. package/dist/src/config.d.ts +26 -0
  7. package/dist/src/config.js +60 -0
  8. package/dist/src/db/data-source.d.ts +2 -0
  9. package/dist/src/db/data-source.js +13 -0
  10. package/dist/src/db/entities/code-chunk.entity.d.ts +9 -0
  11. package/dist/src/db/entities/code-chunk.entity.js +54 -0
  12. package/dist/src/db/entities/code-dependency.entity.d.ts +9 -0
  13. package/dist/src/db/entities/code-dependency.entity.js +54 -0
  14. package/dist/src/db/index.d.ts +4 -0
  15. package/dist/src/db/index.js +12 -0
  16. package/dist/src/db/init.d.ts +2 -0
  17. package/dist/src/db/init.js +50 -0
  18. package/dist/src/env-loader.d.ts +1 -0
  19. package/dist/src/env-loader.js +41 -0
  20. package/dist/src/file-utils.d.ts +12 -0
  21. package/dist/src/file-utils.js +97 -0
  22. package/dist/src/graph.d.ts +4 -0
  23. package/dist/src/graph.js +259 -0
  24. package/dist/src/indexer.d.ts +13 -0
  25. package/dist/src/indexer.js +217 -0
  26. package/dist/src/interrupt.d.ts +3 -0
  27. package/dist/src/interrupt.js +27 -0
  28. package/dist/src/mcp.d.ts +1 -0
  29. package/dist/src/mcp.js +248 -0
  30. package/dist/src/project-selector.d.ts +7 -0
  31. package/dist/src/project-selector.js +116 -0
  32. package/dist/src/searcher.d.ts +50 -0
  33. package/dist/src/searcher.js +205 -0
  34. package/dist/src/setup.d.ts +1 -0
  35. package/dist/src/setup.js +145 -0
  36. package/dist/vitest.config.d.ts +2 -0
  37. package/dist/vitest.config.js +9 -0
  38. package/docker/docker-compose.yml +19 -0
  39. package/package.json +49 -0
@@ -0,0 +1 @@
1
+ export declare function startMCP(): Promise<void>;
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startMCP = startMCP;
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const config_1 = require("./config");
8
+ const searcher_1 = require("./searcher");
9
+ const SERVER_INSTRUCTIONS = `
10
+ You have access to a local semantic code search server. Use these tools to navigate unfamiliar codebases.
11
+
12
+ ## Tools
13
+
14
+ - **list_projects** — call this first to see what projects are indexed and their exact names.
15
+ - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk.
16
+ - **read_file_range** — read a specific line range from any indexed file. Use this to expand context around a search result.
17
+
18
+ ## Recommended workflow
19
+
20
+ 1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
21
+ 2. Use \`search_codebase\` to find relevant code by meaning.
22
+ 3. If a result looks **truncated or incomplete** (e.g. you see only the middle of a function), call \`read_file_range\` with the returned \`file_path\` and lines around \`startLine\` to get the full picture.
23
+ 4. For exploratory searches where you expect results to span chunk boundaries, pass \`include_neighbors: true\` to automatically include the chunk before and after each match.
24
+
25
+ ## When to use read_file_range vs include_neighbors
26
+
27
+ - **include_neighbors: true** — good when you already know the context will be fragmented (e.g. searching for a class definition that's likely split across chunks).
28
+ - **read_file_range** — better when you want to read a precise section, or when a result looks cut off and you want to expand just that one file.
29
+
30
+ ## Handling Code Structure
31
+ - **Class/Function limits**: Chunks often end abruptly. Always use \`read_file_range\` to find the closing brace or the beginning of a method if you suspect you're seeing only a fragment.
32
+ - **Cross-file references**: If you see a method call like \`this.validator.validate(data)\`, use \`search_codebase\` to find the definition of that \`validator\` service.
33
+
34
+ ## Relevance scores
35
+
36
+ Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically.
37
+
38
+ ## Dependency graph tools
39
+
40
+ - **get_dependencies** — show what files a given file imports (direct or transitive). Use when you need to understand what a file depends on.
41
+ - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes.
42
+
43
+ Both tools require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain.
44
+ `.trim();
45
+ function formatDependencies(filePath, rows, transitive) {
46
+ const direct = rows.filter(r => !transitive || (r.depth ?? 1) === 1);
47
+ const indirect = transitive ? rows.filter(r => (r.depth ?? 1) > 1) : [];
48
+ const lines = [`**Dependencies of** \`${filePath}\`\n`, '**Direct:**'];
49
+ for (const r of direct) {
50
+ const sym = r.toSymbol ? ` (${r.depType} \`${r.toSymbol}\`)` : ` (${r.depType})`;
51
+ lines.push(` → \`${r.toFile}\`${sym}`);
52
+ }
53
+ if (indirect.length > 0) {
54
+ lines.push('\n**Transitive:**');
55
+ for (const r of indirect) {
56
+ const sym = r.toSymbol ? ` (\`${r.toSymbol}\`)` : '';
57
+ lines.push(` → \`${r.toFile}\`${sym} *(via ${r.via}, depth ${r.depth})*`);
58
+ }
59
+ }
60
+ return lines.join('\n');
61
+ }
62
+ function formatDependents(filePath, rows, transitive) {
63
+ const direct = rows.filter(r => !transitive || (r.depth ?? 1) === 1);
64
+ const indirect = transitive ? rows.filter(r => (r.depth ?? 1) > 1) : [];
65
+ const lines = [`**Dependents of** \`${filePath}\`\n`, '**Direct:**'];
66
+ for (const r of direct) {
67
+ const sym = r.toSymbol ? ` (imports \`${r.toSymbol}\`)` : '';
68
+ lines.push(` ← \`${r.fromFile}\`${sym}`);
69
+ }
70
+ if (indirect.length > 0) {
71
+ lines.push('\n**Transitive:**');
72
+ for (const r of indirect) {
73
+ lines.push(` ← \`${r.fromFile}\` *(via ${r.via}, depth ${r.depth})*`);
74
+ }
75
+ }
76
+ return lines.join('\n');
77
+ }
78
+ function formatChunk(r, label) {
79
+ return `${label ? `*(${label})*\n` : ''}**File:** ${r.filePath}\n**Project:** ${r.serviceName}\n**Line:** ${r.startLine}\n\`\`\`\n${r.content}\n\`\`\``;
80
+ }
81
+ async function startMCP() {
82
+ const server = new index_js_1.Server({ name: 'local-rag', version: '1.0.0' }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
83
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
84
+ tools: [
85
+ {
86
+ name: 'search_codebase',
87
+ description: 'Search local project codebase by semantic similarity',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ query: { type: 'string' },
92
+ limit: { type: 'number', description: `Max results (default ${config_1.config.search.defaultLimit}, max ${config_1.config.search.maxLimit})` },
93
+ project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
94
+ include_neighbors: { type: 'boolean', description: 'Also include the chunk before and after each result from the same file (Small-to-Big retrieval). Useful when results look truncated.' },
95
+ },
96
+ required: ['query'],
97
+ },
98
+ },
99
+ {
100
+ name: 'list_projects',
101
+ description: 'List all indexed projects with their chunk counts',
102
+ inputSchema: { type: 'object', properties: {} },
103
+ },
104
+ {
105
+ name: 'read_file_range',
106
+ description: 'Read a specific line range from an indexed source file. Use this when a search result looks incomplete — read the surrounding lines to get full context.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
111
+ start_line: { type: 'number', description: 'First line to read (1-based, inclusive). Default: 1' },
112
+ end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99' },
113
+ },
114
+ required: ['file_path'],
115
+ },
116
+ },
117
+ {
118
+ name: 'get_dependencies',
119
+ description: 'Show what files a given file imports (direct or transitive). Use to understand what a file depends on before modifying it.',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
124
+ project: { type: 'string', description: 'Project name (use list_projects to find it)' },
125
+ symbol: { type: 'string', description: 'Filter to a specific imported symbol name (optional)' },
126
+ transitive: { type: 'boolean', description: 'Include transitive (indirect) dependencies. Default: false.' },
127
+ },
128
+ required: ['file_path', 'project'],
129
+ },
130
+ },
131
+ {
132
+ name: 'get_dependents',
133
+ description: 'Show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
138
+ project: { type: 'string', description: 'Project name (use list_projects to find it)' },
139
+ symbol: { type: 'string', description: 'Filter to a specific exported symbol name (optional)' },
140
+ transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
141
+ },
142
+ required: ['file_path', 'project'],
143
+ },
144
+ },
145
+ ],
146
+ }));
147
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
148
+ try {
149
+ if (req.params.name === 'list_projects') {
150
+ const rows = await (0, searcher_1.listProjects)();
151
+ if (rows.length === 0) {
152
+ return { content: [{ type: 'text', text: 'No indexed projects found.' }] };
153
+ }
154
+ return { content: [{ type: 'text', text: rows.map(r => {
155
+ const lines = [`${r.serviceName}: ${r.chunks} chunks`];
156
+ if (r.description)
157
+ lines.push(` description: ${r.description}`);
158
+ if (r.stack)
159
+ lines.push(` stack: ${r.stack}`);
160
+ if (r.notes)
161
+ lines.push(` notes: ${r.notes}`);
162
+ if (r.relations) {
163
+ for (const [key, val] of Object.entries(r.relations)) {
164
+ const list = Array.isArray(val) ? val.join(', ') : val;
165
+ lines.push(` ${key}: ${list}`);
166
+ }
167
+ }
168
+ return lines.join('\n');
169
+ }).join('\n\n') }] };
170
+ }
171
+ if (req.params.name === 'search_codebase') {
172
+ const raw = String(req.params.arguments?.query);
173
+ const limit = Math.min(Math.max(1, Number(req.params.arguments?.limit) || config_1.config.search.defaultLimit), config_1.config.search.maxLimit);
174
+ const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
175
+ const includeNeighbors = Boolean(req.params.arguments?.include_neighbors);
176
+ const { query, translated } = await (0, searcher_1.prepareQuery)(raw);
177
+ const results = await (0, searcher_1.searchChunks)(query, limit, project);
178
+ if (results.length === 0) {
179
+ return { content: [{ type: 'text', text: 'No relevant results found.' }] };
180
+ }
181
+ const header = translated ? `> Query translated: "${raw}" → "${query}"\n\n` : '';
182
+ if (includeNeighbors) {
183
+ const withNeighbors = await (0, searcher_1.fetchNeighbors)(results);
184
+ const blocks = withNeighbors.map(({ result, prevChunk, nextChunk }) => {
185
+ const parts = [];
186
+ if (prevChunk)
187
+ parts.push(formatChunk(prevChunk, 'prev chunk'));
188
+ parts.push(`${formatChunk(result)}\n**Relevance:** ${result.score}`);
189
+ if (nextChunk)
190
+ parts.push(formatChunk(nextChunk, 'next chunk'));
191
+ return parts.join('\n\n');
192
+ });
193
+ return { content: [{ type: 'text', text: header + blocks.join('\n\n---\n\n') }] };
194
+ }
195
+ const text = results
196
+ .map(r => `${formatChunk(r)}\n**Relevance:** ${r.score}`)
197
+ .join('\n\n---\n\n');
198
+ return { content: [{ type: 'text', text: header + text }] };
199
+ }
200
+ if (req.params.name === 'read_file_range') {
201
+ const filePath = String(req.params.arguments?.file_path);
202
+ const startLine = Math.max(1, Number(req.params.arguments?.start_line) || 1);
203
+ const endLine = Number(req.params.arguments?.end_line) || startLine + 99;
204
+ const range = await (0, searcher_1.readFileRange)(filePath, startLine, endLine);
205
+ if (!range.ok) {
206
+ const message = range.reason === 'not_indexed'
207
+ ? `File is not indexed: ${filePath}. Run indexing first.`
208
+ : `File was deleted from disk after indexing: ${filePath}. Re-run indexing to update.`;
209
+ return { content: [{ type: 'text', text: message }] };
210
+ }
211
+ const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
212
+ return {
213
+ content: [{
214
+ type: 'text',
215
+ text: `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``,
216
+ }],
217
+ };
218
+ }
219
+ if (req.params.name === 'get_dependencies') {
220
+ const filePath = String(req.params.arguments?.file_path);
221
+ const project = String(req.params.arguments?.project);
222
+ const symbol = req.params.arguments?.symbol ? String(req.params.arguments.symbol) : null;
223
+ const transitive = Boolean(req.params.arguments?.transitive);
224
+ const rows = await (0, searcher_1.getDependencies)(filePath, project, symbol, transitive);
225
+ if (rows.length === 0) {
226
+ return { content: [{ type: 'text', text: `No dependencies found for: ${filePath}` }] };
227
+ }
228
+ return { content: [{ type: 'text', text: formatDependencies(filePath, rows, transitive) }] };
229
+ }
230
+ if (req.params.name === 'get_dependents') {
231
+ const filePath = String(req.params.arguments?.file_path);
232
+ const project = String(req.params.arguments?.project);
233
+ const symbol = req.params.arguments?.symbol ? String(req.params.arguments.symbol) : null;
234
+ const transitive = Boolean(req.params.arguments?.transitive);
235
+ const rows = await (0, searcher_1.getDependents)(filePath, project, symbol, transitive);
236
+ if (rows.length === 0) {
237
+ return { content: [{ type: 'text', text: `No dependents found for: ${filePath}` }] };
238
+ }
239
+ return { content: [{ type: 'text', text: formatDependents(filePath, rows, transitive) }] };
240
+ }
241
+ throw new Error(`Unknown tool: ${req.params.name}`);
242
+ }
243
+ catch (err) {
244
+ return { content: [{ type: 'text', text: `Tool error: ${err.message}` }] };
245
+ }
246
+ });
247
+ await server.connect(new stdio_js_1.StdioServerTransport());
248
+ }
@@ -0,0 +1,7 @@
1
+ export declare function selectProjects(allProjects: {
2
+ name: string;
3
+ path: string;
4
+ }[], workspaceDir: string, forceSelect: boolean): Promise<{
5
+ name: string;
6
+ path: string;
7
+ }[]>;
@@ -0,0 +1,116 @@
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.selectProjects = selectProjects;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const prompts_1 = require("@inquirer/prompts");
40
+ function loadLines(filePath) {
41
+ if (!fs.existsSync(filePath))
42
+ return new Set();
43
+ return new Set(fs.readFileSync(filePath, 'utf-8')
44
+ .split('\n')
45
+ .map(l => l.trim())
46
+ .filter(l => l && !l.startsWith('#')));
47
+ }
48
+ function saveLines(filePath, names) {
49
+ fs.writeFileSync(filePath, [...names].sort().join('\n') + '\n');
50
+ }
51
+ function loadConfig(workspaceDir) {
52
+ return {
53
+ included: loadLines(path.join(workspaceDir, '.ragprojects')),
54
+ skipped: loadLines(path.join(workspaceDir, '.ragskip')),
55
+ };
56
+ }
57
+ function saveConfig(workspaceDir, config) {
58
+ saveLines(path.join(workspaceDir, '.ragprojects'), config.included);
59
+ if (config.skipped.size > 0)
60
+ saveLines(path.join(workspaceDir, '.ragskip'), config.skipped);
61
+ }
62
+ async function selectProjects(allProjects, workspaceDir, forceSelect) {
63
+ const config = loadConfig(workspaceDir);
64
+ const newProjects = allProjects.filter(p => !config.included.has(p.name) && !config.skipped.has(p.name));
65
+ // Nothing to ask — return previously saved selection silently
66
+ if (newProjects.length === 0 && !forceSelect) {
67
+ const selected = allProjects.filter(p => config.included.has(p.name));
68
+ if (selected.length === 0) {
69
+ console.log('No projects selected. Run with --select to choose projects.');
70
+ }
71
+ return selected;
72
+ }
73
+ // In --select mode show all non-skipped projects; otherwise only new ones
74
+ const toShow = forceSelect
75
+ ? allProjects.filter(p => !config.skipped.has(p.name))
76
+ : newProjects;
77
+ const message = forceSelect
78
+ ? 'Select projects to index'
79
+ : `Found ${newProjects.length} new project(s) — select which to index`;
80
+ // Checkbox: pre-check previously included projects
81
+ const chosen = await (0, prompts_1.checkbox)({
82
+ message,
83
+ choices: toShow.map(p => ({
84
+ name: p.name,
85
+ value: p.name,
86
+ checked: config.included.has(p.name),
87
+ })),
88
+ pageSize: 15,
89
+ });
90
+ const chosenSet = new Set(chosen);
91
+ // Update included
92
+ for (const p of toShow) {
93
+ if (chosenSet.has(p.name)) {
94
+ config.included.add(p.name);
95
+ config.skipped.delete(p.name);
96
+ }
97
+ else {
98
+ config.included.delete(p.name);
99
+ }
100
+ }
101
+ // Ask which unchecked projects to always skip
102
+ const unchecked = toShow.filter(p => !chosenSet.has(p.name) && !config.skipped.has(p.name));
103
+ if (unchecked.length > 0) {
104
+ const toSkip = await (0, prompts_1.checkbox)({
105
+ message: 'Always skip (never ask again)?',
106
+ choices: unchecked.map(p => ({ name: p.name, value: p.name, checked: false })),
107
+ pageSize: 15,
108
+ });
109
+ for (const name of toSkip)
110
+ config.skipped.add(name);
111
+ }
112
+ saveConfig(workspaceDir, config);
113
+ const selected = allProjects.filter(p => config.included.has(p.name));
114
+ console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
115
+ return selected;
116
+ }
@@ -0,0 +1,50 @@
1
+ export type { Relations } from './file-utils';
2
+ export type { ProjectMeta } from './file-utils';
3
+ export interface ChunkRow {
4
+ serviceName: string;
5
+ filePath: string;
6
+ startLine: number;
7
+ content: string;
8
+ }
9
+ export interface SearchResult extends ChunkRow {
10
+ score: number;
11
+ }
12
+ export interface NeighborResult {
13
+ result: SearchResult;
14
+ prevChunk: ChunkRow | null;
15
+ nextChunk: ChunkRow | null;
16
+ }
17
+ export type FileRangeResult = {
18
+ ok: true;
19
+ filePath: string;
20
+ startLine: number;
21
+ endLine: number;
22
+ lines: string[];
23
+ } | {
24
+ ok: false;
25
+ reason: 'not_indexed' | 'not_on_disk';
26
+ };
27
+ export interface DependencyRow {
28
+ fromFile: string;
29
+ fromSymbol: string | null;
30
+ toFile: string;
31
+ toSymbol: string | null;
32
+ depType: string;
33
+ depth?: number;
34
+ via?: string;
35
+ }
36
+ export declare function prepareQuery(raw: string): Promise<{
37
+ query: string;
38
+ translated: boolean;
39
+ }>;
40
+ import type { ProjectMeta } from './file-utils';
41
+ export interface ProjectRow extends ProjectMeta {
42
+ serviceName: string;
43
+ chunks: number;
44
+ }
45
+ export declare function listProjects(): Promise<ProjectRow[]>;
46
+ export declare function searchChunks(query: string, limit: number, project: string | null): Promise<SearchResult[]>;
47
+ export declare function fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
48
+ export declare function getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
49
+ export declare function getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
50
+ export declare function readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
@@ -0,0 +1,205 @@
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.prepareQuery = prepareQuery;
37
+ exports.listProjects = listProjects;
38
+ exports.searchChunks = searchChunks;
39
+ exports.fetchNeighbors = fetchNeighbors;
40
+ exports.getDependencies = getDependencies;
41
+ exports.getDependents = getDependents;
42
+ exports.readFileRange = readFileRange;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const ollama_1 = require("ollama");
46
+ const db_1 = require("./db");
47
+ const config_1 = require("./config");
48
+ const file_utils_1 = require("./file-utils");
49
+ const ollama = new ollama_1.Ollama({ host: config_1.config.ollama.host });
50
+ // Detect non-English input by presence of non-ASCII characters (Cyrillic, etc.)
51
+ function isNonEnglish(text) {
52
+ return /[^\x00-\x7F]/.test(text);
53
+ }
54
+ // Translate query to English using a local Ollama model
55
+ async function translateToEnglish(text) {
56
+ const response = await ollama.chat({
57
+ model: config_1.config.search.translateModel,
58
+ messages: [{ role: 'user', content: `Translate the following text to English. Return only the translation, no explanations:\n${text}` }],
59
+ });
60
+ return response.message.content.trim();
61
+ }
62
+ // Translate if AUTO_TRANSLATE is enabled and input is non-English
63
+ async function prepareQuery(raw) {
64
+ if (config_1.config.search.autoTranslate && isNonEnglish(raw)) {
65
+ return { query: await translateToEnglish(raw), translated: true };
66
+ }
67
+ return { query: raw, translated: false };
68
+ }
69
+ const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
70
+ async function listProjects() {
71
+ const rows = await db_1.db.query(`SELECT "serviceName", COUNT(*)::int AS chunks
72
+ FROM code_chunk
73
+ GROUP BY "serviceName"
74
+ ORDER BY "serviceName"`);
75
+ return rows.map(r => ({
76
+ ...r,
77
+ ...((0, file_utils_1.readRagMeta)(path.join(config_1.config.workspaceRoot, r.serviceName)) ?? EMPTY_META),
78
+ }));
79
+ }
80
+ // Embed query and run vector similarity search
81
+ async function searchChunks(query, limit, project) {
82
+ const { embedding } = await ollama.embeddings({ model: config_1.config.ollama.model, prompt: query });
83
+ const params = [`[${embedding}]`, config_1.config.search.similarityThreshold, limit];
84
+ let projectFilter = '';
85
+ if (project) {
86
+ params.push(project);
87
+ projectFilter = `AND "serviceName" = $${params.length}`;
88
+ }
89
+ // Cast score to float8 so TypeORM returns a number, not a string
90
+ return db_1.db.query(`SELECT "serviceName", "filePath", "startLine", content,
91
+ ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
92
+ FROM code_chunk
93
+ WHERE embedding <=> $1 < $2
94
+ ${projectFilter}
95
+ ORDER BY embedding <=> $1
96
+ LIMIT $3`, params);
97
+ }
98
+ // Fetch the chunk immediately before and after each result in the same file (Small-to-Big)
99
+ async function fetchNeighbors(results) {
100
+ return Promise.all(results.map(async (result) => {
101
+ const [prevRows, nextRows] = await Promise.all([
102
+ db_1.db.query(`SELECT "serviceName", "filePath", "startLine", content
103
+ FROM code_chunk
104
+ WHERE "filePath" = $1 AND "startLine" < $2
105
+ ORDER BY "startLine" DESC LIMIT 1`, [result.filePath, result.startLine]),
106
+ db_1.db.query(`SELECT "serviceName", "filePath", "startLine", content
107
+ FROM code_chunk
108
+ WHERE "filePath" = $1 AND "startLine" > $2
109
+ ORDER BY "startLine" ASC LIMIT 1`, [result.filePath, result.startLine]),
110
+ ]);
111
+ return { result, prevChunk: prevRows[0] ?? null, nextChunk: nextRows[0] ?? null };
112
+ }));
113
+ }
114
+ // Return all files imported by filePath (direct or transitive)
115
+ async function getDependencies(filePath, project, symbol = null, transitive = false) {
116
+ const params = [filePath, project];
117
+ let symbolFilter = '';
118
+ if (symbol) {
119
+ params.push(symbol);
120
+ symbolFilter = `AND "toSymbol" = $${params.length}`;
121
+ }
122
+ const direct = await db_1.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
123
+ FROM code_dependency
124
+ WHERE "fromFile" = $1 AND project = $2
125
+ ${symbolFilter}
126
+ ORDER BY "toFile"`, params);
127
+ if (!transitive)
128
+ return direct;
129
+ // Indirect: depth>1 only, deduplicated by toFile (symbols dropped for clarity)
130
+ const indirect = await db_1.db.query(`WITH RECURSIVE deps AS (
131
+ SELECT "toFile", 1 AS depth, "toFile" AS via
132
+ FROM code_dependency
133
+ WHERE "fromFile" = $1 AND project = $2
134
+
135
+ UNION
136
+
137
+ SELECT d."toFile", deps.depth + 1, deps."toFile" AS via
138
+ FROM code_dependency d
139
+ JOIN deps ON deps."toFile" = d."fromFile"
140
+ WHERE d.project = $2
141
+ AND d."toFile" <> $1
142
+ AND deps.depth < 20
143
+ )
144
+ SELECT "toFile", MIN(depth) AS depth,
145
+ (array_agg(via ORDER BY depth))[1] AS via,
146
+ NULL::text AS "fromFile", NULL::text AS "fromSymbol",
147
+ NULL::text AS "toSymbol", NULL::text AS "depType"
148
+ FROM deps
149
+ WHERE depth > 1
150
+ GROUP BY "toFile"
151
+ ORDER BY MIN(depth), "toFile"`, [filePath, project]);
152
+ return [...direct, ...indirect];
153
+ }
154
+ // Return all files that import filePath (direct or transitive)
155
+ async function getDependents(filePath, project, symbol = null, transitive = false) {
156
+ const params = [filePath, project];
157
+ let symbolFilter = '';
158
+ if (symbol) {
159
+ params.push(symbol);
160
+ symbolFilter = `AND "toSymbol" = $${params.length}`;
161
+ }
162
+ const direct = await db_1.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
163
+ FROM code_dependency
164
+ WHERE "toFile" = $1 AND project = $2
165
+ ${symbolFilter}
166
+ ORDER BY "fromFile"`, params);
167
+ if (!transitive)
168
+ return direct;
169
+ // Indirect: depth>1 only, deduplicated by fromFile
170
+ const indirect = await db_1.db.query(`WITH RECURSIVE rdeps AS (
171
+ SELECT "fromFile", 1 AS depth, "fromFile" AS via
172
+ FROM code_dependency
173
+ WHERE "toFile" = $1 AND project = $2
174
+
175
+ UNION
176
+
177
+ SELECT d."fromFile", rdeps.depth + 1, rdeps."fromFile" AS via
178
+ FROM code_dependency d
179
+ JOIN rdeps ON rdeps."fromFile" = d."toFile"
180
+ WHERE d.project = $2
181
+ AND d."fromFile" <> $1
182
+ AND rdeps.depth < 20
183
+ )
184
+ SELECT "fromFile", MIN(depth) AS depth,
185
+ (array_agg(via ORDER BY depth))[1] AS via,
186
+ NULL::text AS "toFile", NULL::text AS "fromSymbol",
187
+ NULL::text AS "toSymbol", NULL::text AS "depType"
188
+ FROM rdeps
189
+ WHERE depth > 1
190
+ GROUP BY "fromFile"
191
+ ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
192
+ return [...direct, ...indirect];
193
+ }
194
+ // Read a line range from a file that is present in the index
195
+ async function readFileRange(filePath, startLine, endLine) {
196
+ const [{ exists }] = await db_1.db.query(`SELECT EXISTS (SELECT 1 FROM code_chunk WHERE "filePath" = $1) AS exists`, [filePath]);
197
+ if (!exists)
198
+ return { ok: false, reason: 'not_indexed' };
199
+ const absPath = path.join(config_1.config.workspaceRoot, filePath);
200
+ if (!fs.existsSync(absPath))
201
+ return { ok: false, reason: 'not_on_disk' };
202
+ const all = fs.readFileSync(absPath, 'utf-8').split('\n');
203
+ const lines = all.slice(startLine - 1, endLine);
204
+ return { ok: true, filePath, startLine, endLine: Math.min(endLine, all.length), lines };
205
+ }
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;