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.
- package/CHANGELOG.md +48 -0
- package/README.md +95 -6
- package/dist/app.js +70 -10
- package/dist/src/commands/ConfigCommand.d.ts +1 -0
- package/dist/src/commands/ConfigCommand.js +84 -4
- package/dist/src/commands/IndexCommand.js +35 -7
- package/dist/src/commands/McpCommand.js +9 -4
- package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
- package/dist/src/commands/WebCommand.js +56 -0
- package/dist/src/config-store.d.ts +85 -4
- package/dist/src/config-store.js +157 -32
- package/dist/src/config.d.ts +32 -8
- package/dist/src/config.js +93 -30
- package/dist/src/constants.d.ts +35 -0
- package/dist/src/constants.js +57 -4
- package/dist/src/container.d.ts +10 -6
- package/dist/src/container.js +10 -5
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +111 -0
- package/dist/src/db/schema.js +15 -1
- package/dist/src/graph.d.ts +60 -0
- package/dist/src/graph.js +239 -120
- package/dist/src/help.js +20 -5
- package/dist/src/interfaces/IAstParser.d.ts +20 -3
- package/dist/src/interfaces/IChunker.d.ts +24 -0
- package/dist/src/interfaces/IChunker.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +27 -0
- package/dist/src/interfaces/IDatabase.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +15 -2
- package/dist/src/interfaces/ILogger.d.ts +10 -0
- package/dist/src/interfaces/ILogger.js +2 -0
- package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
- package/dist/src/interfaces/IMcpToolHandler.js +2 -0
- package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
- package/dist/src/interfaces/IMonorepoResolver.js +2 -0
- package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
- package/dist/src/interfaces/IProjectDiscovery.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +37 -0
- package/dist/src/interfaces/ISearcher.js +2 -0
- package/dist/src/interfaces/index.d.ts +8 -0
- package/dist/src/interfaces/index.js +2 -0
- package/dist/src/owner-path.d.ts +10 -0
- package/dist/src/owner-path.js +27 -0
- package/dist/src/project-selector.d.ts +1 -1
- package/dist/src/services/AstChunker.d.ts +31 -0
- package/dist/src/services/AstChunker.js +114 -0
- package/dist/src/services/AstParserService.d.ts +10 -6
- package/dist/src/services/AstParserService.js +60 -15
- package/dist/src/services/DatabaseService.d.ts +14 -1
- package/dist/src/services/DatabaseService.js +70 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +29 -0
- package/dist/src/services/IndexerService.d.ts +22 -12
- package/dist/src/services/IndexerService.js +106 -46
- package/dist/src/services/InfinityEmbedder.d.ts +79 -0
- package/dist/src/services/InfinityEmbedder.js +199 -0
- package/dist/src/services/LoggerService.d.ts +2 -1
- package/dist/src/services/McpServer.d.ts +16 -7
- package/dist/src/services/McpServer.js +54 -367
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
- package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
- package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
- package/dist/src/services/NxMonorepoResolver.js +308 -0
- package/dist/src/services/OllamaEmbedder.d.ts +19 -5
- package/dist/src/services/OllamaEmbedder.js +91 -36
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
- package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
- package/dist/src/services/SearcherService.d.ts +46 -7
- package/dist/src/services/SearcherService.js +183 -53
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +77 -0
- package/dist/src/services/WebServerService.js +267 -0
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- package/dist/src/services/reranker.d.ts +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +6 -0
- package/dist/src/setup.js +249 -31
- package/dist/src/tools/FindSymbolTool.d.ts +8 -0
- package/dist/src/tools/FindSymbolTool.js +45 -0
- package/dist/src/tools/GetCallersTool.d.ts +8 -0
- package/dist/src/tools/GetCallersTool.js +42 -0
- package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
- package/dist/src/tools/GetDependenciesTool.js +38 -0
- package/dist/src/tools/GetDependentsTool.d.ts +8 -0
- package/dist/src/tools/GetDependentsTool.js +57 -0
- package/dist/src/tools/ListFilesTool.d.ts +8 -0
- package/dist/src/tools/ListFilesTool.js +39 -0
- package/dist/src/tools/ListProjectsTool.d.ts +8 -0
- package/dist/src/tools/ListProjectsTool.js +44 -0
- package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
- package/dist/src/tools/ReadFileRangeTool.js +44 -0
- package/dist/src/tools/ReadFileTool.d.ts +8 -0
- package/dist/src/tools/ReadFileTool.js +42 -0
- package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
- package/dist/src/tools/SearchCodebaseTool.js +81 -0
- package/dist/src/tools/format-helpers.d.ts +8 -0
- package/dist/src/tools/format-helpers.js +58 -0
- package/dist/src/tools/index.d.ts +9 -0
- package/dist/src/tools/index.js +21 -0
- package/dist/src/web/dashboard.d.ts +9 -0
- package/dist/src/web/dashboard.js +342 -0
- package/docker/docker-compose.infinity-gpu.yml +19 -0
- package/docker/docker-compose.yml +25 -0
- package/docs/web-dashboard.md +187 -0
- package/package.json +25 -3
- package/dist/src/ast.d.ts +0 -2
- package/dist/src/ast.js +0 -110
- package/dist/src/commands/SetupCommand.js +0 -14
- package/dist/src/db/data-source.d.ts +0 -2
- package/dist/src/db/data-source.js +0 -13
- package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
- package/dist/src/db/entities/code-chunk.entity.js +0 -54
- package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
- package/dist/src/db/entities/code-dependency.entity.js +0 -54
- package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
- package/dist/src/db/entities/code-symbol.entity.js +0 -59
- package/dist/src/db/init.d.ts +0 -2
- package/dist/src/db/init.js +0 -62
- package/dist/src/env-loader.d.ts +0 -1
- package/dist/src/indexer.d.ts +0 -17
- package/dist/src/indexer.js +0 -242
- package/dist/src/mcp.d.ts +0 -1
- package/dist/src/mcp.js +0 -403
- package/dist/src/ollama.d.ts +0 -2
- package/dist/src/ollama.js +0 -6
- package/dist/src/searcher.d.ts +0 -74
- package/dist/src/searcher.js +0 -446
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
package/dist/src/searcher.d.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { type ProjectMeta } from './file-utils';
|
|
2
|
-
export interface ChunkRow {
|
|
3
|
-
serviceName: string;
|
|
4
|
-
filePath: string;
|
|
5
|
-
startLine: number;
|
|
6
|
-
content: string;
|
|
7
|
-
}
|
|
8
|
-
export interface SearchResult extends ChunkRow {
|
|
9
|
-
score: number;
|
|
10
|
-
}
|
|
11
|
-
export interface NeighborResult {
|
|
12
|
-
result: SearchResult;
|
|
13
|
-
prevChunk: ChunkRow | null;
|
|
14
|
-
nextChunk: ChunkRow | null;
|
|
15
|
-
}
|
|
16
|
-
export type FileRangeResult = {
|
|
17
|
-
ok: true;
|
|
18
|
-
filePath: string;
|
|
19
|
-
startLine: number;
|
|
20
|
-
endLine: number;
|
|
21
|
-
lines: string[];
|
|
22
|
-
hasMore: boolean;
|
|
23
|
-
} | {
|
|
24
|
-
ok: false;
|
|
25
|
-
reason: 'not_indexed' | 'not_on_disk' | 'invalid_path';
|
|
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
|
-
export interface ProjectRow extends ProjectMeta {
|
|
41
|
-
serviceName: string;
|
|
42
|
-
chunks: number;
|
|
43
|
-
}
|
|
44
|
-
export declare function listProjects(): Promise<ProjectRow[]>;
|
|
45
|
-
export declare function searchChunks(query: string, limit: number, project: string | null): Promise<SearchResult[]>;
|
|
46
|
-
export declare function fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
|
|
47
|
-
export declare function getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
48
|
-
export declare function getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
49
|
-
export interface GrepMatch {
|
|
50
|
-
filePath: string;
|
|
51
|
-
line: number;
|
|
52
|
-
}
|
|
53
|
-
export declare function grepSymbol(symbol: string, project: string, excludeFile: string): {
|
|
54
|
-
matches: GrepMatch[];
|
|
55
|
-
truncated: boolean;
|
|
56
|
-
};
|
|
57
|
-
export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'all';
|
|
58
|
-
export interface SymbolHit {
|
|
59
|
-
filePath: string;
|
|
60
|
-
line: number;
|
|
61
|
-
column: number;
|
|
62
|
-
kind: Exclude<SymbolKind, 'all'>;
|
|
63
|
-
signature: string;
|
|
64
|
-
}
|
|
65
|
-
export declare function findSymbol(name: string, project: string | null, kind: SymbolKind): {
|
|
66
|
-
hits: SymbolHit[];
|
|
67
|
-
truncated: boolean;
|
|
68
|
-
};
|
|
69
|
-
export declare function listFiles(relativePath: string, project: string, depth: number, pattern: string | null): {
|
|
70
|
-
tree: string;
|
|
71
|
-
error?: string;
|
|
72
|
-
};
|
|
73
|
-
export declare function readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
|
|
74
|
-
export declare function readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
|
package/dist/src/searcher.js
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
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.grepSymbol = grepSymbol;
|
|
43
|
-
exports.findSymbol = findSymbol;
|
|
44
|
-
exports.listFiles = listFiles;
|
|
45
|
-
exports.readFileAbsolute = readFileAbsolute;
|
|
46
|
-
exports.readFileRange = readFileRange;
|
|
47
|
-
const fs = __importStar(require("fs"));
|
|
48
|
-
const path = __importStar(require("path"));
|
|
49
|
-
const readline = __importStar(require("readline"));
|
|
50
|
-
const child_process_1 = require("child_process");
|
|
51
|
-
const db_1 = require("./db");
|
|
52
|
-
const config_1 = require("./config");
|
|
53
|
-
const ollama_1 = require("./ollama");
|
|
54
|
-
const file_utils_1 = require("./file-utils");
|
|
55
|
-
// Detect non-English input by presence of non-ASCII characters (Cyrillic, etc.)
|
|
56
|
-
function isNonEnglish(text) {
|
|
57
|
-
return /[^\x00-\x7F]/.test(text);
|
|
58
|
-
}
|
|
59
|
-
// Translate query to English using a local Ollama model
|
|
60
|
-
async function translateToEnglish(text) {
|
|
61
|
-
const response = await ollama_1.ollama.chat({
|
|
62
|
-
model: config_1.config.search.translateModel,
|
|
63
|
-
messages: [{ role: 'user', content: `Translate the following text to English. Return only the translation, no explanations:\n${text}` }],
|
|
64
|
-
});
|
|
65
|
-
return response.message.content.trim();
|
|
66
|
-
}
|
|
67
|
-
// Translate if AUTO_TRANSLATE is enabled and input is non-English
|
|
68
|
-
async function prepareQuery(raw) {
|
|
69
|
-
if (config_1.config.search.autoTranslate && isNonEnglish(raw)) {
|
|
70
|
-
return { query: await translateToEnglish(raw), translated: true };
|
|
71
|
-
}
|
|
72
|
-
return { query: raw, translated: false };
|
|
73
|
-
}
|
|
74
|
-
const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
|
|
75
|
-
async function listProjects() {
|
|
76
|
-
const rows = await db_1.db.query(`SELECT "serviceName", COUNT(*)::int AS chunks
|
|
77
|
-
FROM code_chunk
|
|
78
|
-
GROUP BY "serviceName"
|
|
79
|
-
ORDER BY "serviceName"`);
|
|
80
|
-
return rows.map(r => ({
|
|
81
|
-
...r,
|
|
82
|
-
...((0, file_utils_1.readRagMeta)(path.join(config_1.config.workspaceRoot, r.serviceName)) ?? EMPTY_META),
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
// Embed query and run vector similarity search
|
|
86
|
-
async function searchChunks(query, limit, project) {
|
|
87
|
-
const { embedding } = await ollama_1.ollama.embeddings({ model: config_1.config.ollama.model, prompt: query });
|
|
88
|
-
const params = [`[${embedding}]`, config_1.config.search.similarityThreshold, limit];
|
|
89
|
-
let projectFilter = '';
|
|
90
|
-
if (project) {
|
|
91
|
-
params.push(project);
|
|
92
|
-
projectFilter = `AND "serviceName" = $${params.length}`;
|
|
93
|
-
}
|
|
94
|
-
// Cast score to float8 so TypeORM returns a number, not a string
|
|
95
|
-
return db_1.db.query(`SELECT "serviceName", "filePath", "startLine", content,
|
|
96
|
-
ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
|
|
97
|
-
FROM code_chunk
|
|
98
|
-
WHERE embedding <=> $1 < $2
|
|
99
|
-
${projectFilter}
|
|
100
|
-
ORDER BY embedding <=> $1
|
|
101
|
-
LIMIT $3`, params);
|
|
102
|
-
}
|
|
103
|
-
// Fetch the chunk immediately before and after each result in the same file (Small-to-Big).
|
|
104
|
-
// Single query with LATERAL joins — avoids 2N round-trips.
|
|
105
|
-
async function fetchNeighbors(results) {
|
|
106
|
-
if (results.length === 0)
|
|
107
|
-
return [];
|
|
108
|
-
const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
|
|
109
|
-
const params = results.flatMap(r => [r.filePath, r.startLine]);
|
|
110
|
-
const rows = await db_1.db.query(`SELECT r.idx,
|
|
111
|
-
prev."serviceName" AS prev_sn, prev."filePath" AS prev_fp, prev."startLine" AS prev_sl, prev.content AS prev_c,
|
|
112
|
-
next."serviceName" AS next_sn, next."filePath" AS next_fp, next."startLine" AS next_sl, next.content AS next_c
|
|
113
|
-
FROM (VALUES ${values}) r(idx, fp, sl)
|
|
114
|
-
LEFT JOIN LATERAL (
|
|
115
|
-
SELECT "serviceName", "filePath", "startLine", content
|
|
116
|
-
FROM code_chunk WHERE "filePath" = r.fp AND "startLine" < r.sl
|
|
117
|
-
ORDER BY "startLine" DESC LIMIT 1
|
|
118
|
-
) prev ON true
|
|
119
|
-
LEFT JOIN LATERAL (
|
|
120
|
-
SELECT "serviceName", "filePath", "startLine", content
|
|
121
|
-
FROM code_chunk WHERE "filePath" = r.fp AND "startLine" > r.sl
|
|
122
|
-
ORDER BY "startLine" ASC LIMIT 1
|
|
123
|
-
) next ON true
|
|
124
|
-
ORDER BY r.idx`, params);
|
|
125
|
-
return rows.map((row, i) => ({
|
|
126
|
-
result: results[i],
|
|
127
|
-
prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
|
|
128
|
-
nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
|
|
129
|
-
}));
|
|
130
|
-
}
|
|
131
|
-
// Return all files imported by filePath (direct or transitive)
|
|
132
|
-
async function getDependencies(filePath, project, symbol = null, transitive = false) {
|
|
133
|
-
const params = [filePath, project];
|
|
134
|
-
let symbolFilter = '';
|
|
135
|
-
if (symbol) {
|
|
136
|
-
params.push(symbol);
|
|
137
|
-
symbolFilter = `AND "toSymbol" = $${params.length}`;
|
|
138
|
-
}
|
|
139
|
-
const direct = await db_1.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
|
|
140
|
-
FROM code_dependency
|
|
141
|
-
WHERE "fromFile" = $1 AND project = $2
|
|
142
|
-
${symbolFilter}
|
|
143
|
-
ORDER BY "toFile"`, params);
|
|
144
|
-
if (!transitive)
|
|
145
|
-
return direct;
|
|
146
|
-
// Indirect: depth>1 only, deduplicated by toFile (symbols dropped for clarity)
|
|
147
|
-
const indirect = await db_1.db.query(`WITH RECURSIVE deps AS (
|
|
148
|
-
SELECT "toFile", 1 AS depth, "toFile" AS via
|
|
149
|
-
FROM code_dependency
|
|
150
|
-
WHERE "fromFile" = $1 AND project = $2
|
|
151
|
-
|
|
152
|
-
UNION
|
|
153
|
-
|
|
154
|
-
SELECT d."toFile", deps.depth + 1, deps."toFile" AS via
|
|
155
|
-
FROM code_dependency d
|
|
156
|
-
JOIN deps ON deps."toFile" = d."fromFile"
|
|
157
|
-
WHERE d.project = $2
|
|
158
|
-
AND d."toFile" <> $1
|
|
159
|
-
AND deps.depth < 20
|
|
160
|
-
)
|
|
161
|
-
SELECT "toFile", MIN(depth) AS depth,
|
|
162
|
-
(array_agg(via ORDER BY depth))[1] AS via,
|
|
163
|
-
NULL::text AS "fromFile", NULL::text AS "fromSymbol",
|
|
164
|
-
NULL::text AS "toSymbol", NULL::text AS "depType"
|
|
165
|
-
FROM deps
|
|
166
|
-
WHERE depth > 1
|
|
167
|
-
GROUP BY "toFile"
|
|
168
|
-
ORDER BY MIN(depth), "toFile"`, [filePath, project]);
|
|
169
|
-
return [...direct, ...indirect];
|
|
170
|
-
}
|
|
171
|
-
// Return all files that import filePath (direct or transitive)
|
|
172
|
-
async function getDependents(filePath, project, symbol = null, transitive = false) {
|
|
173
|
-
const params = [filePath, project];
|
|
174
|
-
let symbolFilter = '';
|
|
175
|
-
if (symbol) {
|
|
176
|
-
params.push(symbol);
|
|
177
|
-
symbolFilter = `AND "toSymbol" = $${params.length}`;
|
|
178
|
-
}
|
|
179
|
-
const direct = await db_1.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
|
|
180
|
-
FROM code_dependency
|
|
181
|
-
WHERE "toFile" = $1 AND project = $2
|
|
182
|
-
${symbolFilter}
|
|
183
|
-
ORDER BY "fromFile"`, params);
|
|
184
|
-
if (!transitive)
|
|
185
|
-
return direct;
|
|
186
|
-
// Indirect: depth>1 only, deduplicated by fromFile
|
|
187
|
-
const indirect = await db_1.db.query(`WITH RECURSIVE rdeps AS (
|
|
188
|
-
SELECT "fromFile", 1 AS depth, "fromFile" AS via
|
|
189
|
-
FROM code_dependency
|
|
190
|
-
WHERE "toFile" = $1 AND project = $2
|
|
191
|
-
|
|
192
|
-
UNION
|
|
193
|
-
|
|
194
|
-
SELECT d."fromFile", rdeps.depth + 1, rdeps."fromFile" AS via
|
|
195
|
-
FROM code_dependency d
|
|
196
|
-
JOIN rdeps ON rdeps."fromFile" = d."toFile"
|
|
197
|
-
WHERE d.project = $2
|
|
198
|
-
AND d."fromFile" <> $1
|
|
199
|
-
AND rdeps.depth < 20
|
|
200
|
-
)
|
|
201
|
-
SELECT "fromFile", MIN(depth) AS depth,
|
|
202
|
-
(array_agg(via ORDER BY depth))[1] AS via,
|
|
203
|
-
NULL::text AS "toFile", NULL::text AS "fromSymbol",
|
|
204
|
-
NULL::text AS "toSymbol", NULL::text AS "depType"
|
|
205
|
-
FROM rdeps
|
|
206
|
-
WHERE depth > 1
|
|
207
|
-
GROUP BY "fromFile"
|
|
208
|
-
ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
|
|
209
|
-
return [...direct, ...indirect];
|
|
210
|
-
}
|
|
211
|
-
const GREP_MAX_RESULTS = 200;
|
|
212
|
-
// Grep stops after this many raw lines; generous multiple of GREP_MAX_RESULTS to account for
|
|
213
|
-
// the declaring file's own matches being excluded from the final count.
|
|
214
|
-
const GREP_RAW_LIMIT = GREP_MAX_RESULTS * 5;
|
|
215
|
-
const GREP_EXCLUDE_DIRS = [...file_utils_1.IGNORE_DIRS].map(d => `--exclude-dir=${d}`);
|
|
216
|
-
// Text search for a symbol across all TS/JS files in the project directory.
|
|
217
|
-
// Uses grep -F -w (literal whole-word match) via spawnSync to avoid shell injection.
|
|
218
|
-
// Excludes the declaring file itself. Uses IGNORE_DIRS for consistency with the indexer.
|
|
219
|
-
function grepSymbol(symbol, project, excludeFile) {
|
|
220
|
-
const projectDir = path.join(config_1.config.workspaceRoot, project);
|
|
221
|
-
const result = (0, child_process_1.spawnSync)('grep', [
|
|
222
|
-
'-rn', '-F', '-w',
|
|
223
|
-
'-m', String(GREP_RAW_LIMIT),
|
|
224
|
-
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
225
|
-
...GREP_EXCLUDE_DIRS,
|
|
226
|
-
symbol,
|
|
227
|
-
projectDir,
|
|
228
|
-
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
229
|
-
if (result.error || !result.stdout) {
|
|
230
|
-
if (result.error)
|
|
231
|
-
console.error(`[grepSymbol] grep failed: ${result.error.message}`);
|
|
232
|
-
return { matches: [], truncated: false };
|
|
233
|
-
}
|
|
234
|
-
const excludeAbsPath = path.join(config_1.config.workspaceRoot, excludeFile);
|
|
235
|
-
const all = result.stdout.split('\n').flatMap(line => {
|
|
236
|
-
const match = line.match(/^(.+?):(\d+):/);
|
|
237
|
-
if (!match)
|
|
238
|
-
return [];
|
|
239
|
-
const [, absFile, lineNum] = match;
|
|
240
|
-
if (absFile === excludeAbsPath)
|
|
241
|
-
return [];
|
|
242
|
-
return [{ filePath: path.relative(config_1.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
|
|
243
|
-
});
|
|
244
|
-
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
|
|
245
|
-
}
|
|
246
|
-
const FIND_SYMBOL_LIMIT = 50;
|
|
247
|
-
const FIND_SYMBOL_RAW_LIMIT = FIND_SYMBOL_LIMIT * 10;
|
|
248
|
-
function buildFindSymbolPattern(name, kind) {
|
|
249
|
-
const patterns = {
|
|
250
|
-
function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${name}[[:space:](<]`,
|
|
251
|
-
class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${name}[[:space:]{<]`,
|
|
252
|
-
type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
|
|
253
|
-
interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
|
|
254
|
-
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
|
|
255
|
-
};
|
|
256
|
-
if (kind === 'all')
|
|
257
|
-
return Object.values(patterns).join('|');
|
|
258
|
-
return patterns[kind];
|
|
259
|
-
}
|
|
260
|
-
function inferKind(line, name) {
|
|
261
|
-
const before = line.slice(0, line.indexOf(name));
|
|
262
|
-
if (/\bfunction\s+$/.test(before))
|
|
263
|
-
return 'function';
|
|
264
|
-
if (/\bclass\s+$/.test(before))
|
|
265
|
-
return 'class';
|
|
266
|
-
if (/\binterface\s+$/.test(before))
|
|
267
|
-
return 'interface';
|
|
268
|
-
if (/\btype\s+$/.test(before))
|
|
269
|
-
return 'type';
|
|
270
|
-
return 'variable';
|
|
271
|
-
}
|
|
272
|
-
function findSymbol(name, project, kind) {
|
|
273
|
-
if (!/^[\w$]+$/.test(name))
|
|
274
|
-
throw new Error('Invalid symbol name');
|
|
275
|
-
const searchDir = project
|
|
276
|
-
? path.join(config_1.config.workspaceRoot, project)
|
|
277
|
-
: config_1.config.workspaceRoot;
|
|
278
|
-
const pattern = buildFindSymbolPattern(name, kind);
|
|
279
|
-
const result = (0, child_process_1.spawnSync)('grep', [
|
|
280
|
-
'-rn', '-E',
|
|
281
|
-
'-m', String(FIND_SYMBOL_RAW_LIMIT),
|
|
282
|
-
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
283
|
-
...GREP_EXCLUDE_DIRS,
|
|
284
|
-
pattern,
|
|
285
|
-
searchDir,
|
|
286
|
-
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
287
|
-
if (result.error) {
|
|
288
|
-
console.error(`[findSymbol] grep failed: ${result.error.message}`);
|
|
289
|
-
return { hits: [], truncated: false };
|
|
290
|
-
}
|
|
291
|
-
if (!result.stdout)
|
|
292
|
-
return { hits: [], truncated: false };
|
|
293
|
-
const hits = [];
|
|
294
|
-
for (const raw of result.stdout.split('\n')) {
|
|
295
|
-
if (hits.length >= FIND_SYMBOL_LIMIT)
|
|
296
|
-
break;
|
|
297
|
-
// grep -n format: "/abs/path/file.ts:42: export function Foo(...)"
|
|
298
|
-
// File paths can contain colons on some systems, so split carefully:
|
|
299
|
-
// find first colon after a path separator, use lastIndexOf on the line number part
|
|
300
|
-
const m = raw.match(/^(.+?):(\d+):(.*)$/);
|
|
301
|
-
if (!m)
|
|
302
|
-
continue;
|
|
303
|
-
const [, absFile, lineNumStr, matchedLine] = m;
|
|
304
|
-
const lineNum = parseInt(lineNumStr, 10);
|
|
305
|
-
if (isNaN(lineNum))
|
|
306
|
-
continue;
|
|
307
|
-
const col = matchedLine.indexOf(name);
|
|
308
|
-
if (col === -1)
|
|
309
|
-
continue;
|
|
310
|
-
hits.push({
|
|
311
|
-
filePath: path.relative(config_1.config.workspaceRoot, absFile),
|
|
312
|
-
line: lineNum,
|
|
313
|
-
column: col + 1,
|
|
314
|
-
kind: inferKind(matchedLine, name),
|
|
315
|
-
signature: matchedLine.trim(),
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
|
|
319
|
-
}
|
|
320
|
-
// ── list_files ────────────────────────────────────────────────────────────────
|
|
321
|
-
function walkDir(absDir, currentDepth, maxDepth, pattern, lines) {
|
|
322
|
-
let entries;
|
|
323
|
-
try {
|
|
324
|
-
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
entries.sort((a, b) => {
|
|
330
|
-
const ad = a.isDirectory() ? 0 : 1;
|
|
331
|
-
const bd = b.isDirectory() ? 0 : 1;
|
|
332
|
-
if (ad !== bd)
|
|
333
|
-
return ad - bd;
|
|
334
|
-
return a.name.localeCompare(b.name);
|
|
335
|
-
});
|
|
336
|
-
for (const entry of entries) {
|
|
337
|
-
if (file_utils_1.IGNORE_DIRS.has(entry.name))
|
|
338
|
-
continue;
|
|
339
|
-
const indent = ' '.repeat(currentDepth);
|
|
340
|
-
if (entry.isDirectory()) {
|
|
341
|
-
lines.push(`${indent}${entry.name}/`);
|
|
342
|
-
if (currentDepth < maxDepth) {
|
|
343
|
-
walkDir(path.join(absDir, entry.name), currentDepth + 1, maxDepth, pattern, lines);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
if (pattern && !entry.name.includes(pattern))
|
|
348
|
-
continue;
|
|
349
|
-
lines.push(`${indent}${entry.name}`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function listFiles(relativePath, project, depth, pattern) {
|
|
354
|
-
const projectRoot = path.join(config_1.config.workspaceRoot, project);
|
|
355
|
-
const resolvedTarget = path.resolve(projectRoot, relativePath);
|
|
356
|
-
const resolvedProject = path.resolve(projectRoot);
|
|
357
|
-
if (resolvedTarget !== resolvedProject && !resolvedTarget.startsWith(resolvedProject + path.sep)) {
|
|
358
|
-
return { tree: '', error: 'Path escapes project root' };
|
|
359
|
-
}
|
|
360
|
-
if (!fs.existsSync(resolvedTarget)) {
|
|
361
|
-
return { tree: '', error: `Path not found: ${relativePath}` };
|
|
362
|
-
}
|
|
363
|
-
if (!fs.statSync(resolvedTarget).isDirectory()) {
|
|
364
|
-
return { tree: '', error: `Not a directory: ${relativePath}` };
|
|
365
|
-
}
|
|
366
|
-
const clampedDepth = Math.min(5, Math.max(1, depth));
|
|
367
|
-
const lines = [];
|
|
368
|
-
walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
|
|
369
|
-
if (lines.length === 0) {
|
|
370
|
-
return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
|
|
371
|
-
}
|
|
372
|
-
return { tree: lines.join('\n') };
|
|
373
|
-
}
|
|
374
|
-
// ── readFileAbsolute ──────────────────────────────────────────────────────────
|
|
375
|
-
// Variant of readFileRange that accepts an absolute path and skips the DB index check.
|
|
376
|
-
// Used by the read_file MCP tool, which receives absolute paths from search_codebase.
|
|
377
|
-
async function readFileAbsolute(absPath, maxLines) {
|
|
378
|
-
const resolvedRoot = path.resolve(config_1.config.workspaceRoot);
|
|
379
|
-
const resolved = path.resolve(absPath);
|
|
380
|
-
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
|
|
381
|
-
return { ok: false, reason: 'invalid_path' };
|
|
382
|
-
}
|
|
383
|
-
if (!fs.existsSync(resolved))
|
|
384
|
-
return { ok: false, reason: 'not_on_disk' };
|
|
385
|
-
const lines = [];
|
|
386
|
-
const stream = fs.createReadStream(resolved, { encoding: 'utf-8' });
|
|
387
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
388
|
-
let lineNo = 0;
|
|
389
|
-
let hasMore = false;
|
|
390
|
-
try {
|
|
391
|
-
for await (const line of rl) {
|
|
392
|
-
lineNo++;
|
|
393
|
-
if (lineNo <= maxLines)
|
|
394
|
-
lines.push(line);
|
|
395
|
-
if (lineNo >= maxLines) {
|
|
396
|
-
hasMore = true;
|
|
397
|
-
rl.close();
|
|
398
|
-
stream.destroy();
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
catch (err) {
|
|
404
|
-
rl.close();
|
|
405
|
-
stream.destroy();
|
|
406
|
-
throw err;
|
|
407
|
-
}
|
|
408
|
-
const relPath = path.relative(resolvedRoot, resolved);
|
|
409
|
-
return { ok: true, filePath: relPath, startLine: 1, endLine: lineNo, lines, hasMore };
|
|
410
|
-
}
|
|
411
|
-
// Read a line range from a file that is present in the index
|
|
412
|
-
async function readFileRange(filePath, startLine, endLine) {
|
|
413
|
-
// Defensive: reject paths that escape the workspace root (e.g. "../../etc/passwd")
|
|
414
|
-
const resolvedRoot = path.resolve(config_1.config.workspaceRoot);
|
|
415
|
-
const absPath = path.resolve(resolvedRoot, filePath);
|
|
416
|
-
if (absPath !== resolvedRoot && !absPath.startsWith(resolvedRoot + path.sep)) {
|
|
417
|
-
return { ok: false, reason: 'invalid_path' };
|
|
418
|
-
}
|
|
419
|
-
const [{ exists }] = await db_1.db.query(`SELECT EXISTS (SELECT 1 FROM code_chunk WHERE "filePath" = $1) AS exists`, [filePath]);
|
|
420
|
-
if (!exists)
|
|
421
|
-
return { ok: false, reason: 'not_indexed' };
|
|
422
|
-
if (!fs.existsSync(absPath))
|
|
423
|
-
return { ok: false, reason: 'not_on_disk' };
|
|
424
|
-
// Stream line-by-line — avoids loading entire file into memory.
|
|
425
|
-
// Stops reading as soon as we pass endLine.
|
|
426
|
-
const lines = [];
|
|
427
|
-
const stream = fs.createReadStream(absPath, { encoding: 'utf-8' });
|
|
428
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
429
|
-
let lineNo = 0;
|
|
430
|
-
let lastRead = 0;
|
|
431
|
-
let hasMore = false;
|
|
432
|
-
for await (const line of rl) {
|
|
433
|
-
lineNo++;
|
|
434
|
-
lastRead = lineNo;
|
|
435
|
-
if (lineNo >= startLine && lineNo <= endLine)
|
|
436
|
-
lines.push(line);
|
|
437
|
-
if (lineNo >= endLine) {
|
|
438
|
-
hasMore = true;
|
|
439
|
-
rl.close();
|
|
440
|
-
stream.destroy();
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// If we broke out early, lastRead is endLine. If file was shorter, lastRead is file length.
|
|
445
|
-
return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
|
|
446
|
-
}
|
package/dist/vitest.config.d.ts
DELETED
package/dist/vitest.config.js
DELETED