codesearch-mcp 0.0.7-alpha → 0.0.9-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/dist/app.js +66 -36
- package/dist/src/ast.js +8 -12
- package/dist/src/config.js +2 -1
- package/dist/src/constants.d.ts +1 -0
- package/dist/src/constants.js +7 -0
- package/dist/src/db/init.js +16 -4
- package/dist/src/help.d.ts +1 -0
- package/dist/src/help.js +52 -0
- package/dist/src/indexer.d.ts +5 -1
- package/dist/src/indexer.js +58 -33
- package/dist/src/mcp.js +151 -14
- package/dist/src/ollama.d.ts +2 -0
- package/dist/src/ollama.js +6 -0
- package/dist/src/project-selector.js +10 -3
- package/dist/src/searcher.d.ts +20 -4
- package/dist/src/searcher.js +225 -21
- package/dist/src/setup.js +14 -5
- package/docker/docker-compose.yml +2 -0
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -36,6 +36,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
require("./src/env-loader");
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const help_1 = require("./src/help");
|
|
40
|
+
function formatEta(sec) {
|
|
41
|
+
if (sec < 60)
|
|
42
|
+
return `${sec}s`;
|
|
43
|
+
const m = Math.floor(sec / 60);
|
|
44
|
+
const s = sec % 60;
|
|
45
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
46
|
+
}
|
|
39
47
|
function printSummary(allStats) {
|
|
40
48
|
const cols = { name: 30, num: 8 };
|
|
41
49
|
const line = '─'.repeat(cols.name + cols.num * 5 + 5);
|
|
@@ -54,6 +62,16 @@ function printSummary(allStats) {
|
|
|
54
62
|
elapsedSec: acc.elapsedSec + s.elapsedSec,
|
|
55
63
|
}), { indexed: 0, skipped: 0, failed: 0, chunks: 0, elapsedSec: 0 });
|
|
56
64
|
console.log('TOTAL'.padEnd(cols.name), String(total.indexed).padStart(cols.num), String(total.skipped).padStart(cols.num), String(total.failed).padStart(cols.num), String(total.chunks).padStart(cols.num), String(total.elapsedSec.toFixed(1)).padStart(cols.num));
|
|
65
|
+
// Reprint warnings at the end — progress bar activity scrolls per-project warnings out of
|
|
66
|
+
// view, so we collect them on IndexStats and surface them here as an explicit section.
|
|
67
|
+
// const projectsWithWarnings = allStats.filter(s => s.warnings.length > 0);
|
|
68
|
+
// if (projectsWithWarnings.length > 0) {
|
|
69
|
+
// console.log('\n=== Warnings ===');
|
|
70
|
+
// for (const s of projectsWithWarnings) {
|
|
71
|
+
// console.log(`\n${s.serviceName}:`);
|
|
72
|
+
// for (const w of s.warnings) console.log(` ⚠ ${w}`);
|
|
73
|
+
// }
|
|
74
|
+
// }
|
|
57
75
|
}
|
|
58
76
|
async function main() {
|
|
59
77
|
const argv1 = path.basename(process.argv[1] ?? '');
|
|
@@ -62,6 +80,10 @@ async function main() {
|
|
|
62
80
|
const isSetup = argv1 === 'rag-setup' || process.argv[2] === 'setup';
|
|
63
81
|
const isBackfill = argv1 === 'rag-index' && process.argv[2] === 'backfill';
|
|
64
82
|
const mode = isBackfill ? 'backfill' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : (process.argv[2] ?? '');
|
|
83
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
84
|
+
(0, help_1.printHelp)(argv1 !== 'app.js' ? argv1 : mode);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
65
87
|
if (mode === 'index') {
|
|
66
88
|
const { initDbWithSchema } = await Promise.resolve().then(() => __importStar(require('./src/db')));
|
|
67
89
|
const { discoverProjects, scanAndIndex } = await Promise.resolve().then(() => __importStar(require('./src/indexer')));
|
|
@@ -82,27 +104,41 @@ async function main() {
|
|
|
82
104
|
process.env.WORKSPACE_ROOT ??= workspacePath;
|
|
83
105
|
await initDbWithSchema();
|
|
84
106
|
setupInterrupt();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(
|
|
94
|
-
|
|
107
|
+
try {
|
|
108
|
+
const { collectFiles, loadIgnore } = await Promise.resolve().then(() => __importStar(require('./src/file-utils')));
|
|
109
|
+
const discovered = discoverProjects(workspacePath);
|
|
110
|
+
const projects = await selectProjects(discovered, workspacePath, forceSelect);
|
|
111
|
+
if (projects.length === 0)
|
|
112
|
+
process.exit(0);
|
|
113
|
+
const filesPerProject = projects.map(p => collectFiles(p.path, loadIgnore(p.path), p.path));
|
|
114
|
+
const totalFiles = filesPerProject.reduce((sum, f) => sum + f.length, 0);
|
|
115
|
+
console.log(`\n${projects.length} projects — ${totalFiles} files total`);
|
|
116
|
+
if (force)
|
|
117
|
+
console.log('Reindex mode: all files will be re-indexed.');
|
|
118
|
+
const allStats = [];
|
|
119
|
+
for (let i = 0; i < projects.length; i++) {
|
|
120
|
+
if (signal.aborted)
|
|
121
|
+
break;
|
|
122
|
+
const stats = await scanAndIndex(projects[i].path, projects[i].name, force, signal, filesPerProject[i]);
|
|
123
|
+
allStats.push(stats);
|
|
124
|
+
const remaining = projects.length - allStats.length;
|
|
125
|
+
if (remaining > 0 && !signal.aborted) {
|
|
126
|
+
const doneFiles = allStats.reduce((s, r) => s + r.indexed + r.skipped + r.failed, 0);
|
|
127
|
+
const elapsedSec = allStats.reduce((s, r) => s + r.elapsedSec, 0);
|
|
128
|
+
const leftFiles = filesPerProject.slice(allStats.length).reduce((sum, f) => sum + f.length, 0);
|
|
129
|
+
const eta = doneFiles > 0 ? formatEta(Math.round(elapsedSec / doneFiles * leftFiles)) : '?';
|
|
130
|
+
console.log(` ${allStats.length}/${projects.length} projects done — ${remaining} left (~${leftFiles} files) — ETA ~${eta}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
95
133
|
if (signal.aborted)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
console.log('\nIndexing interrupted.');
|
|
135
|
+
printSummary(allStats);
|
|
136
|
+
if (!signal.aborted)
|
|
137
|
+
generateProjectGraph(projects, workspacePath);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
teardownInterrupt();
|
|
99
141
|
}
|
|
100
|
-
teardownInterrupt();
|
|
101
|
-
if (signal.aborted)
|
|
102
|
-
console.log('\nIndexing interrupted.');
|
|
103
|
-
printSummary(allStats);
|
|
104
|
-
if (!signal.aborted)
|
|
105
|
-
generateProjectGraph(projects, workspacePath);
|
|
106
142
|
process.exit(0);
|
|
107
143
|
}
|
|
108
144
|
else if (mode === 'mcp') {
|
|
@@ -125,8 +161,8 @@ async function main() {
|
|
|
125
161
|
await runSetup();
|
|
126
162
|
}
|
|
127
163
|
else if (mode === 'backfill') {
|
|
128
|
-
const { initDbWithSchema, db
|
|
129
|
-
const {
|
|
164
|
+
const { initDbWithSchema, db } = await Promise.resolve().then(() => __importStar(require('./src/db')));
|
|
165
|
+
const { collectDepEdges, writeDepEdges } = await Promise.resolve().then(() => __importStar(require('./src/indexer')));
|
|
130
166
|
const { config } = await Promise.resolve().then(() => __importStar(require('./src/config')));
|
|
131
167
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
132
168
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
@@ -141,16 +177,8 @@ async function main() {
|
|
|
141
177
|
skipped++;
|
|
142
178
|
continue;
|
|
143
179
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
.filter(e => e.toFile.startsWith(config.workspaceRoot))
|
|
147
|
-
.map(e => ({ ...e, fromFile: path.relative(config.workspaceRoot, e.fromFile), toFile: path.relative(config.workspaceRoot, e.toFile) }));
|
|
148
|
-
await db.transaction(async (manager) => {
|
|
149
|
-
await manager.delete(CodeDependency, { project: serviceName, fromFile: filePath });
|
|
150
|
-
if (edges.length > 0) {
|
|
151
|
-
await manager.createQueryBuilder().insert().into(CodeDependency).values(edges).execute();
|
|
152
|
-
}
|
|
153
|
-
});
|
|
180
|
+
const edges = await collectDepEdges(absPath, serviceName, projectRoot);
|
|
181
|
+
await writeDepEdges(serviceName, filePath, edges);
|
|
154
182
|
totalEdges += edges.length;
|
|
155
183
|
processed++;
|
|
156
184
|
if (processed % 50 === 0)
|
|
@@ -161,11 +189,13 @@ async function main() {
|
|
|
161
189
|
process.exit(0);
|
|
162
190
|
}
|
|
163
191
|
else {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
// No command → show help and exit cleanly. Unknown command → error + help + exit 1.
|
|
193
|
+
const hasUnknownArg = mode !== '';
|
|
194
|
+
const out = hasUnknownArg ? console.error : console.log;
|
|
195
|
+
if (hasUnknownArg)
|
|
196
|
+
out(`Unknown command: ${mode}\n`);
|
|
197
|
+
(0, help_1.printHelp)('default');
|
|
198
|
+
process.exit(hasUnknownArg ? 1 : 0);
|
|
169
199
|
}
|
|
170
200
|
}
|
|
171
201
|
main().catch(err => { console.error(err); process.exit(1); });
|
package/dist/src/ast.js
CHANGED
|
@@ -37,23 +37,19 @@ exports.extractImportEdges = extractImportEdges;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const ts_morph_1 = require("ts-morph");
|
|
40
|
-
// Per-project ts-morph instances keyed by
|
|
40
|
+
// Per-project ts-morph instances keyed by projectRoot. Each project gets its own instance so that
|
|
41
|
+
// projects without a tsconfig (key previously shared as '') don't leak files into each other.
|
|
41
42
|
// Loading the tsconfig gives ts-morph the compilerOptions.paths needed to resolve aliases like @core/*.
|
|
42
43
|
const morphProjects = new Map();
|
|
43
|
-
// Secondary cache: projectRoot → tsconfig key. Avoids one fs.existsSync per file after first lookup.
|
|
44
|
-
const projectRootKeys = new Map();
|
|
45
44
|
const FALLBACK_OPTS = { skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false, compilerOptions: { allowJs: true, noEmit: true } };
|
|
46
45
|
function getMorphProject(projectRoot) {
|
|
47
|
-
if (!
|
|
46
|
+
if (!morphProjects.has(projectRoot)) {
|
|
48
47
|
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
const key = projectRootKeys.get(projectRoot);
|
|
52
|
-
if (!morphProjects.has(key)) {
|
|
48
|
+
const hasTsconfig = fs.existsSync(tsconfigPath);
|
|
53
49
|
let project;
|
|
54
50
|
try {
|
|
55
|
-
const opts =
|
|
56
|
-
? { tsConfigFilePath:
|
|
51
|
+
const opts = hasTsconfig
|
|
52
|
+
? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
|
|
57
53
|
: FALLBACK_OPTS;
|
|
58
54
|
project = new ts_morph_1.Project(opts);
|
|
59
55
|
}
|
|
@@ -61,9 +57,9 @@ function getMorphProject(projectRoot) {
|
|
|
61
57
|
// tsconfig is invalid — fall back so other files in the project can still be processed
|
|
62
58
|
project = new ts_morph_1.Project(FALLBACK_OPTS);
|
|
63
59
|
}
|
|
64
|
-
morphProjects.set(
|
|
60
|
+
morphProjects.set(projectRoot, project);
|
|
65
61
|
}
|
|
66
|
-
return morphProjects.get(
|
|
62
|
+
return morphProjects.get(projectRoot);
|
|
67
63
|
}
|
|
68
64
|
// Extract import edges from a TS/JS file using ts-morph.
|
|
69
65
|
// projectRoot must be the project root (where tsconfig.json lives) for alias resolution to work.
|
package/dist/src/config.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.config = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
+
const constants_1 = require("./constants");
|
|
5
6
|
const schema = zod_1.z.object({
|
|
6
7
|
// required — no defaults, process exits if missing
|
|
7
8
|
DB_HOST: zod_1.z.string(),
|
|
@@ -11,7 +12,7 @@ const schema = zod_1.z.object({
|
|
|
11
12
|
DB_NAME: zod_1.z.string(),
|
|
12
13
|
// optional with defaults
|
|
13
14
|
OLLAMA_HOST: zod_1.z.string().url().default('http://localhost:11434'),
|
|
14
|
-
OLLAMA_MODEL: zod_1.z.string().default(
|
|
15
|
+
OLLAMA_MODEL: zod_1.z.string().default(constants_1.DEFAULT_EMBED_MODEL),
|
|
15
16
|
CHUNK_SIZE: zod_1.z.coerce.number().default(1500),
|
|
16
17
|
CHUNK_OVERLAP: zod_1.z.coerce.number().default(200),
|
|
17
18
|
EMBED_BATCH_SIZE: zod_1.z.coerce.number().default(5),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_EMBED_MODEL = "bge-m3";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_EMBED_MODEL = void 0;
|
|
4
|
+
// Embedding model is tied to the vector column dimension (bge-m3 → 1024).
|
|
5
|
+
// Change both together: update this constant, drop/recreate the `embedding` column,
|
|
6
|
+
// and re-run indexing.
|
|
7
|
+
exports.DEFAULT_EMBED_MODEL = 'bge-m3';
|
package/dist/src/db/init.js
CHANGED
|
@@ -7,7 +7,8 @@ const data_source_1 = require("./data-source");
|
|
|
7
7
|
async function initDb() {
|
|
8
8
|
await data_source_1.db.initialize();
|
|
9
9
|
}
|
|
10
|
-
// For indexer — connect + create schema only if tables don't exist yet
|
|
10
|
+
// For indexer — connect + create schema only if tables don't exist yet.
|
|
11
|
+
// All secondary indexes use IF NOT EXISTS so existing installs pick up new indexes on re-run.
|
|
11
12
|
async function initDbWithSchema() {
|
|
12
13
|
await data_source_1.db.initialize();
|
|
13
14
|
// code_chunk table
|
|
@@ -24,6 +25,9 @@ async function initDbWithSchema() {
|
|
|
24
25
|
ON code_chunk USING hnsw (embedding vector_cosine_ops)
|
|
25
26
|
`);
|
|
26
27
|
}
|
|
28
|
+
// Composite btree for all per-service lookups (scanAndIndex stale-sweep, ingest upsert, search filter).
|
|
29
|
+
// Applied idempotently so existing installs pick it up on next indexing run.
|
|
30
|
+
await data_source_1.db.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
|
|
27
31
|
// code_dependency table
|
|
28
32
|
const [{ existsDep }] = await data_source_1.db.query(`
|
|
29
33
|
SELECT EXISTS (
|
|
@@ -43,8 +47,16 @@ async function initDbWithSchema() {
|
|
|
43
47
|
"depType" TEXT NOT NULL
|
|
44
48
|
)
|
|
45
49
|
`);
|
|
46
|
-
await data_source_1.db.query(`CREATE INDEX idx_dep_from ON code_dependency ("fromFile")`);
|
|
47
|
-
await data_source_1.db.query(`CREATE INDEX idx_dep_to ON code_dependency ("toFile")`);
|
|
48
|
-
await data_source_1.db.query(`CREATE INDEX idx_dep_project ON code_dependency (project)`);
|
|
49
50
|
}
|
|
51
|
+
// Composite indexes for the two query shapes used everywhere:
|
|
52
|
+
// WHERE project = $1 AND "fromFile" = $2 (get_dependencies, ingest delete-by-from)
|
|
53
|
+
// WHERE project = $1 AND "toFile" = $2 (get_dependents, stale-file sweep)
|
|
54
|
+
// IF NOT EXISTS lets existing installs upgrade without a manual migration.
|
|
55
|
+
await data_source_1.db.query(`CREATE INDEX IF NOT EXISTS idx_dep_project_from ON code_dependency (project, "fromFile")`);
|
|
56
|
+
await data_source_1.db.query(`CREATE INDEX IF NOT EXISTS idx_dep_project_to ON code_dependency (project, "toFile")`);
|
|
57
|
+
// Drop redundant legacy single-column indexes (covered by the composites above).
|
|
58
|
+
// Safe on fresh installs too — DROP IF EXISTS is a no-op when absent.
|
|
59
|
+
await data_source_1.db.query(`DROP INDEX IF EXISTS idx_dep_from`);
|
|
60
|
+
await data_source_1.db.query(`DROP INDEX IF EXISTS idx_dep_to`);
|
|
61
|
+
await data_source_1.db.query(`DROP INDEX IF EXISTS idx_dep_project`);
|
|
50
62
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function printHelp(command: string): void;
|
package/dist/src/help.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printHelp = printHelp;
|
|
4
|
+
function printHelp(command) {
|
|
5
|
+
switch (command) {
|
|
6
|
+
case 'rag-index':
|
|
7
|
+
case 'index':
|
|
8
|
+
console.log('Usage: rag-index [path] [options]');
|
|
9
|
+
console.log(' rag-index backfill');
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log('Index a workspace of JS/TS projects into the vector database.');
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log('Arguments:');
|
|
14
|
+
console.log(' path Path to workspace (defaults to WORKSPACE_ROOT env var)');
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log('Options:');
|
|
17
|
+
console.log(' --reindex Force re-index all files (ignores cached mtimes)');
|
|
18
|
+
console.log(' --select Interactively select which projects to index');
|
|
19
|
+
console.log(' -h, --help Show this help message');
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log('Subcommands:');
|
|
22
|
+
console.log(' backfill Populate dependency graph for already-indexed files');
|
|
23
|
+
break;
|
|
24
|
+
case 'rag-mcp':
|
|
25
|
+
case 'mcp':
|
|
26
|
+
console.log('Usage: rag-mcp');
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log('Start the MCP server for semantic code search.');
|
|
29
|
+
console.log('Requires WORKSPACE_ROOT to be set (run rag-setup first).');
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('Options:');
|
|
32
|
+
console.log(' -h, --help Show this help message');
|
|
33
|
+
break;
|
|
34
|
+
case 'rag-setup':
|
|
35
|
+
case 'setup':
|
|
36
|
+
console.log('Usage: rag-setup');
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log('Interactive setup: configure DB, Ollama, and WORKSPACE_ROOT.');
|
|
39
|
+
console.log('Run once before using rag-index or rag-mcp.');
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log('Options:');
|
|
42
|
+
console.log(' -h, --help Show this help message');
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
console.log('Usage: rag-index [path] [--reindex] [--select]');
|
|
46
|
+
console.log(' rag-index backfill');
|
|
47
|
+
console.log(' rag-mcp');
|
|
48
|
+
console.log(' rag-setup');
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Run any command with -h or --help for details.');
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/src/indexer.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CodeDependency } from './db';
|
|
1
2
|
export interface IndexStats {
|
|
2
3
|
serviceName: string;
|
|
3
4
|
indexed: number;
|
|
@@ -5,9 +6,12 @@ export interface IndexStats {
|
|
|
5
6
|
failed: number;
|
|
6
7
|
chunks: number;
|
|
7
8
|
elapsedSec: number;
|
|
9
|
+
warnings: string[];
|
|
8
10
|
}
|
|
9
11
|
export declare function discoverProjects(rootDir: string): {
|
|
10
12
|
name: string;
|
|
11
13
|
path: string;
|
|
12
14
|
}[];
|
|
13
|
-
export declare function
|
|
15
|
+
export declare function collectDepEdges(absFilePath: string, serviceName: string, projectRoot: string): Promise<Omit<CodeDependency, 'id'>[]>;
|
|
16
|
+
export declare function writeDepEdges(serviceName: string, relFilePath: string, edges: Omit<CodeDependency, 'id'>[], manager?: import('typeorm').EntityManager): Promise<void>;
|
|
17
|
+
export declare function scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[]): Promise<IndexStats>;
|
package/dist/src/indexer.js
CHANGED
|
@@ -34,17 +34,18 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.discoverProjects = discoverProjects;
|
|
37
|
+
exports.collectDepEdges = collectDepEdges;
|
|
38
|
+
exports.writeDepEdges = writeDepEdges;
|
|
37
39
|
exports.scanAndIndex = scanAndIndex;
|
|
38
40
|
const fs = __importStar(require("fs"));
|
|
39
41
|
const path = __importStar(require("path"));
|
|
40
42
|
const textsplitters_1 = require("@langchain/textsplitters");
|
|
41
|
-
const ollama_1 = require("ollama");
|
|
42
43
|
const cli_progress_1 = require("cli-progress");
|
|
43
44
|
const db_1 = require("./db");
|
|
44
45
|
const ast_1 = require("./ast");
|
|
45
46
|
const config_1 = require("./config");
|
|
47
|
+
const ollama_1 = require("./ollama");
|
|
46
48
|
const file_utils_1 = require("./file-utils");
|
|
47
|
-
const ollama = new ollama_1.Ollama({ host: config_1.config.ollama.host });
|
|
48
49
|
// JS/TS splitter uses language-aware separators (functions, classes, blocks)
|
|
49
50
|
const jsSplitter = textsplitters_1.RecursiveCharacterTextSplitter.fromLanguage('js', {
|
|
50
51
|
chunkSize: config_1.config.indexer.chunkSize,
|
|
@@ -69,12 +70,39 @@ function discoverProjects(rootDir) {
|
|
|
69
70
|
}
|
|
70
71
|
// Embed a pre-formatted prompt (caller is responsible for prefix and context)
|
|
71
72
|
async function embedText(prompt) {
|
|
72
|
-
const { embedding } = await ollama.embeddings({
|
|
73
|
+
const { embedding } = await ollama_1.ollama.embeddings({
|
|
73
74
|
model: config_1.config.ollama.model,
|
|
74
75
|
prompt,
|
|
75
76
|
});
|
|
76
77
|
return embedding;
|
|
77
78
|
}
|
|
79
|
+
// Shared helpers for dependency edge writes — used by ingestFile (incremental) and backfill CLI.
|
|
80
|
+
// Extract edges via ts-morph (disk I/O) and normalize to workspace-relative paths.
|
|
81
|
+
// Call this OUTSIDE any DB transaction — disk/AST work must not hold a DB connection open.
|
|
82
|
+
async function collectDepEdges(absFilePath, serviceName, projectRoot) {
|
|
83
|
+
const rawEdges = await (0, ast_1.extractImportEdges)(absFilePath, serviceName, projectRoot);
|
|
84
|
+
return rawEdges
|
|
85
|
+
.filter(e => e.toFile.startsWith(config_1.config.workspaceRoot))
|
|
86
|
+
.map(e => ({
|
|
87
|
+
...e,
|
|
88
|
+
fromFile: path.relative(config_1.config.workspaceRoot, e.fromFile),
|
|
89
|
+
toFile: path.relative(config_1.config.workspaceRoot, e.toFile),
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
// Replace edge rows for a file. When `manager` is provided, runs inside that transaction;
|
|
93
|
+
// otherwise opens its own. Keeps delete + insert atomic either way.
|
|
94
|
+
async function writeDepEdges(serviceName, relFilePath, edges, manager) {
|
|
95
|
+
const run = async (mgr) => {
|
|
96
|
+
await mgr.delete(db_1.CodeDependency, { project: serviceName, fromFile: relFilePath });
|
|
97
|
+
if (edges.length > 0) {
|
|
98
|
+
await mgr.createQueryBuilder().insert().into(db_1.CodeDependency).values(edges).execute();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
if (manager)
|
|
102
|
+
await run(manager);
|
|
103
|
+
else
|
|
104
|
+
await db_1.db.transaction(run);
|
|
105
|
+
}
|
|
78
106
|
// Ingest a single file: skip if unchanged, otherwise delete old chunks and re-index.
|
|
79
107
|
// Returns status and chunk count. Never throws — failures are caught and reported.
|
|
80
108
|
async function ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
|
|
@@ -90,14 +118,7 @@ async function ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
|
|
|
90
118
|
return { status: 'skipped', chunks: 0 };
|
|
91
119
|
}
|
|
92
120
|
// Extract import edges before the transaction — disk I/O must not hold a DB connection open
|
|
93
|
-
const
|
|
94
|
-
const edges = rawEdges
|
|
95
|
-
.filter(e => e.toFile.startsWith(config_1.config.workspaceRoot))
|
|
96
|
-
.map(e => ({
|
|
97
|
-
...e,
|
|
98
|
-
fromFile: path.relative(config_1.config.workspaceRoot, e.fromFile),
|
|
99
|
-
toFile: path.relative(config_1.config.workspaceRoot, e.toFile),
|
|
100
|
-
}));
|
|
121
|
+
const edges = await collectDepEdges(filePath, serviceName, projectRoot);
|
|
101
122
|
// Read and embed outside the transaction — Ollama calls must not hold a DB transaction open
|
|
102
123
|
const code = fs.readFileSync(filePath, 'utf-8');
|
|
103
124
|
const chunks = await getSplitter(filePath).createDocuments([code]);
|
|
@@ -129,14 +150,7 @@ async function ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
|
|
|
129
150
|
.values(records.slice(i, i + config_1.config.indexer.batchSize))
|
|
130
151
|
.execute();
|
|
131
152
|
}
|
|
132
|
-
await
|
|
133
|
-
if (edges.length > 0) {
|
|
134
|
-
await manager.createQueryBuilder()
|
|
135
|
-
.insert()
|
|
136
|
-
.into(db_1.CodeDependency)
|
|
137
|
-
.values(edges)
|
|
138
|
-
.execute();
|
|
139
|
-
}
|
|
153
|
+
await writeDepEdges(serviceName, relFilePath, edges, manager);
|
|
140
154
|
});
|
|
141
155
|
return { status: 'indexed', chunks: chunks.length };
|
|
142
156
|
}
|
|
@@ -149,48 +163,58 @@ async function ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
|
|
|
149
163
|
// Removes chunks for deleted files, skips unchanged files, re-indexes modified ones.
|
|
150
164
|
// Pass force=true to invalidate all cached mtimes and force full re-index (e.g. after embedding strategy change).
|
|
151
165
|
// Pass signal to support graceful interruption — stops between files, never mid-transaction.
|
|
152
|
-
async function scanAndIndex(dir, serviceName, force = false, signal) {
|
|
166
|
+
async function scanAndIndex(dir, serviceName, force = false, signal, preCollectedFiles) {
|
|
153
167
|
const startTime = Date.now();
|
|
154
168
|
const repo = db_1.db.getRepository(db_1.CodeChunk);
|
|
155
|
-
//
|
|
169
|
+
// Force mode: wipe lastModified so all files are treated as changed
|
|
170
|
+
if (force) {
|
|
171
|
+
await db_1.db.query(`UPDATE code_chunk SET "lastModified" = NULL WHERE "serviceName" = $1`, [serviceName]);
|
|
172
|
+
}
|
|
173
|
+
const files = preCollectedFiles ?? (0, file_utils_1.collectFiles)(dir, (0, file_utils_1.loadIgnore)(dir), dir);
|
|
174
|
+
// Clean up chunks for files no longer in the collected set — covers both deleted files and
|
|
175
|
+
// files newly excluded by .ragignore (they exist on disk but should no longer be indexed).
|
|
176
|
+
const collectedRelPaths = new Set(files.map(f => path.relative(config_1.config.workspaceRoot, f)));
|
|
156
177
|
const dbPaths = await db_1.db.query(`SELECT DISTINCT "filePath" FROM code_chunk WHERE "serviceName" = $1`, [serviceName]);
|
|
157
178
|
for (const { filePath } of dbPaths) {
|
|
158
|
-
if (!
|
|
179
|
+
if (!collectedRelPaths.has(filePath)) {
|
|
159
180
|
await repo.delete({ serviceName, filePath });
|
|
160
|
-
// Remove dependency edges on both sides — this file no longer exists as importer or target
|
|
161
181
|
await db_1.db.query(`DELETE FROM code_dependency WHERE project = $1 AND "fromFile" = $2`, [serviceName, filePath]);
|
|
162
182
|
await db_1.db.query(`DELETE FROM code_dependency WHERE project = $1 AND "toFile" = $2`, [serviceName, filePath]);
|
|
163
183
|
console.log(` Removed stale: ${path.basename(filePath)}`);
|
|
164
184
|
}
|
|
165
185
|
}
|
|
166
|
-
// Force mode: wipe lastModified so all files are treated as changed
|
|
167
|
-
if (force) {
|
|
168
|
-
await db_1.db.query(`UPDATE code_chunk SET "lastModified" = NULL WHERE "serviceName" = $1`, [serviceName]);
|
|
169
|
-
}
|
|
170
|
-
const ig = (0, file_utils_1.loadIgnore)(dir);
|
|
171
|
-
const files = (0, file_utils_1.collectFiles)(dir, ig, dir);
|
|
172
186
|
const bar = new cli_progress_1.SingleBar({
|
|
173
|
-
format: `{serviceName} {bar} {percentage}% | {value}/{total} | {filename}`,
|
|
187
|
+
format: `{serviceName} {bar} {percentage}% | {value}/{total} | ETA {eta_formatted} | {filename}`,
|
|
174
188
|
barCompleteChar: '█',
|
|
175
189
|
barIncompleteChar: '░',
|
|
176
190
|
hideCursor: true,
|
|
177
191
|
clearOnComplete: false,
|
|
178
192
|
}, cli_progress_1.Presets.shades_grey);
|
|
193
|
+
const warnings = [];
|
|
179
194
|
console.log(`\nIndexing ${serviceName}: ${files.length} files`);
|
|
180
195
|
const meta = (0, file_utils_1.readRagMeta)(dir);
|
|
181
196
|
if (meta === null && fs.existsSync(path.join(dir, '.ragmeta.json'))) {
|
|
182
|
-
|
|
197
|
+
const msg = `.ragmeta.json found but invalid JSON`;
|
|
198
|
+
console.log(` ⚠ ${msg}`);
|
|
199
|
+
warnings.push(msg);
|
|
183
200
|
}
|
|
184
201
|
else if (meta) {
|
|
185
202
|
const parts = [meta.description, meta.stack && `[${meta.stack}]`].filter(Boolean).join(' ');
|
|
186
203
|
console.log(` ✓ .ragmeta.json: ${parts || '(no fields)'}`);
|
|
187
204
|
}
|
|
188
205
|
else {
|
|
189
|
-
|
|
206
|
+
const msg = `No .ragmeta.json — add {"description":"...","stack":"...","notes":"..."} so list_projects can describe this project`;
|
|
207
|
+
console.log(` ⚠ ${msg}`);
|
|
208
|
+
warnings.push(msg);
|
|
209
|
+
}
|
|
210
|
+
if (!fs.existsSync(path.join(dir, '.ragignore'))) {
|
|
211
|
+
const msg = `No .ragignore — create one to exclude generated/codegen files (e.g. src/gql/graphql.ts, **/__snapshots__/**)`;
|
|
212
|
+
console.log(` ⚠ ${msg}`);
|
|
213
|
+
warnings.push(msg);
|
|
190
214
|
}
|
|
191
215
|
bar.start(files.length, 0, { serviceName, filename: '' });
|
|
192
216
|
const stats = {
|
|
193
|
-
serviceName, indexed: 0, skipped: 0, failed: 0, chunks: 0, elapsedSec: 0,
|
|
217
|
+
serviceName, indexed: 0, skipped: 0, failed: 0, chunks: 0, elapsedSec: 0, warnings,
|
|
194
218
|
};
|
|
195
219
|
for (const filePath of files) {
|
|
196
220
|
if (signal?.aborted) {
|
|
@@ -212,6 +236,7 @@ async function scanAndIndex(dir, serviceName, force = false, signal) {
|
|
|
212
236
|
bar.increment();
|
|
213
237
|
}
|
|
214
238
|
bar.stop();
|
|
239
|
+
process.stdout.write('\x1B[J'); // erase any blank lines cli-progress left below the bar
|
|
215
240
|
stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
|
|
216
241
|
return stats;
|
|
217
242
|
}
|
package/dist/src/mcp.js
CHANGED
|
@@ -1,11 +1,54 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.startMCP = startMCP;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
4
39
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
40
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
41
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
42
|
const config_1 = require("./config");
|
|
8
43
|
const searcher_1 = require("./searcher");
|
|
44
|
+
const PKG_VERSION = (() => {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')).version ?? '0.0.0';
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return '0.0.0';
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
9
52
|
const SERVER_INSTRUCTIONS = `
|
|
10
53
|
You have access to a local semantic code search server. Use these tools to navigate unfamiliar codebases.
|
|
11
54
|
|
|
@@ -14,13 +57,18 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
14
57
|
- **list_projects** — call this first to see what projects are indexed and their exact names.
|
|
15
58
|
- **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk.
|
|
16
59
|
- **read_file_range** — read a specific line range from any indexed file. Use this to expand context around a search result.
|
|
60
|
+
- **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
|
|
61
|
+
- **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
|
|
17
62
|
|
|
18
63
|
## Recommended workflow
|
|
19
64
|
|
|
20
65
|
1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
|
|
21
66
|
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
|
|
67
|
+
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 \`start_line\` set to the **Line** number shown in the result. Example: if the result shows **Line:** 150, pass \`start_line: 150\`.
|
|
23
68
|
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.
|
|
69
|
+
5. Use \`find_symbol\` when you know the exact name of a function or class and want its definition location immediately — faster than semantic search.
|
|
70
|
+
6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
|
|
71
|
+
7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
|
|
24
72
|
|
|
25
73
|
## When to use read_file_range vs include_neighbors
|
|
26
74
|
|
|
@@ -39,6 +87,9 @@ Lower score = better match (cosine distance). Results above the configured thres
|
|
|
39
87
|
|
|
40
88
|
- **get_dependencies** — show what files a given file imports (direct or transitive). Use when you need to understand what a file depends on.
|
|
41
89
|
- **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes.
|
|
90
|
+
- **find_symbol** — find where a symbol (function, class, type, interface, variable) is defined by exact name. Faster than semantic search when you know the name.
|
|
91
|
+
- **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
|
|
92
|
+
- **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
|
|
42
93
|
|
|
43
94
|
Both tools require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain.
|
|
44
95
|
`.trim();
|
|
@@ -79,7 +130,7 @@ function formatChunk(r, label) {
|
|
|
79
130
|
return `${label ? `*(${label})*\n` : ''}**File:** ${r.filePath}\n**Project:** ${r.serviceName}\n**Line:** ${r.startLine}\n\`\`\`\n${r.content}\n\`\`\``;
|
|
80
131
|
}
|
|
81
132
|
async function startMCP() {
|
|
82
|
-
const server = new index_js_1.Server({ name: 'local-rag', version:
|
|
133
|
+
const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
|
|
83
134
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
84
135
|
tools: [
|
|
85
136
|
{
|
|
@@ -108,8 +159,8 @@ async function startMCP() {
|
|
|
108
159
|
type: 'object',
|
|
109
160
|
properties: {
|
|
110
161
|
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
111
|
-
start_line: { type: 'number', description: 'First line to read (1-based
|
|
112
|
-
end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99' },
|
|
162
|
+
start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
|
|
163
|
+
end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
|
|
113
164
|
},
|
|
114
165
|
required: ['file_path'],
|
|
115
166
|
},
|
|
@@ -136,12 +187,51 @@ async function startMCP() {
|
|
|
136
187
|
properties: {
|
|
137
188
|
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
138
189
|
project: { type: 'string', description: 'Project name (use list_projects to find it)' },
|
|
139
|
-
symbol: { type: 'string', description: '
|
|
190
|
+
symbol: { type: 'string', description: 'Exported symbol name. Two behaviors: (1) if graph dependents exist, narrows them to imports of this symbol; (2) if the import graph returns zero results, triggers a whole-word text-search fallback (grep -F -w) across all .ts/.tsx/.js/.jsx files in the project to find runtime usages — DI containers, singletons, dynamic lookups, string references. Capped at 200 matches.' },
|
|
140
191
|
transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
|
|
141
192
|
},
|
|
142
193
|
required: ['file_path', 'project'],
|
|
143
194
|
},
|
|
144
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'find_symbol',
|
|
198
|
+
description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables) by exact name. Use when you know the symbol name and want its definition location fast.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
|
|
203
|
+
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
204
|
+
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
205
|
+
},
|
|
206
|
+
required: ['name'],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'list_files',
|
|
211
|
+
description: 'List files and directories in a project path as an indented tree. Use to understand project structure before reading files.',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
path: { type: 'string', description: 'Directory path relative to the project root (e.g. "src/components" or "." for root)' },
|
|
216
|
+
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
217
|
+
depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
|
|
218
|
+
pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
|
|
219
|
+
},
|
|
220
|
+
required: ['path', 'project'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'read_file',
|
|
225
|
+
description: 'Read a file from the workspace by absolute path (as returned by search_codebase). Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
file_path: { type: 'string', description: 'Absolute path to the file (as returned in filePath by search_codebase)' },
|
|
230
|
+
max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
|
|
231
|
+
},
|
|
232
|
+
required: ['file_path'],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
145
235
|
],
|
|
146
236
|
}));
|
|
147
237
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
@@ -203,18 +293,17 @@ async function startMCP() {
|
|
|
203
293
|
const endLine = Number(req.params.arguments?.end_line) || startLine + 99;
|
|
204
294
|
const range = await (0, searcher_1.readFileRange)(filePath, startLine, endLine);
|
|
205
295
|
if (!range.ok) {
|
|
206
|
-
const message = range.reason === '
|
|
207
|
-
? `File is not indexed: ${filePath}. Run indexing first.`
|
|
208
|
-
|
|
296
|
+
const message = range.reason === 'invalid_path' ? `Invalid file path (outside workspace): ${filePath}` :
|
|
297
|
+
range.reason === 'not_indexed' ? `File is not indexed: ${filePath}. Run indexing first.` :
|
|
298
|
+
`File was deleted from disk after indexing: ${filePath}. Re-run indexing to update.`;
|
|
209
299
|
return { content: [{ type: 'text', text: message }] };
|
|
210
300
|
}
|
|
211
301
|
const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
302
|
+
let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
303
|
+
if (range.hasMore) {
|
|
304
|
+
text += `\n\n[file has more lines — call read_file_range again with start_line: ${range.endLine + 1}]`;
|
|
305
|
+
}
|
|
306
|
+
return { content: [{ type: 'text', text }] };
|
|
218
307
|
}
|
|
219
308
|
if (req.params.name === 'get_dependencies') {
|
|
220
309
|
const filePath = String(req.params.arguments?.file_path);
|
|
@@ -256,6 +345,54 @@ async function startMCP() {
|
|
|
256
345
|
}
|
|
257
346
|
return { content: [{ type: 'text', text: header + fallbackLines.join('\n') }] };
|
|
258
347
|
}
|
|
348
|
+
if (req.params.name === 'find_symbol') {
|
|
349
|
+
const name = String(req.params.arguments?.name ?? '');
|
|
350
|
+
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
351
|
+
const rawKind = req.params.arguments?.kind;
|
|
352
|
+
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'all'];
|
|
353
|
+
const kind = validKinds.includes(rawKind) ? rawKind : 'all';
|
|
354
|
+
const { hits, truncated } = (0, searcher_1.findSymbol)(name, project, kind);
|
|
355
|
+
if (hits.length === 0) {
|
|
356
|
+
return { content: [{ type: 'text', text: `No definitions found for symbol: ${name}` }] };
|
|
357
|
+
}
|
|
358
|
+
const lines = [];
|
|
359
|
+
if (truncated)
|
|
360
|
+
lines.push(`*(truncated to ${hits.length} results)*\n`);
|
|
361
|
+
for (const h of hits) {
|
|
362
|
+
lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
|
|
363
|
+
lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
|
|
364
|
+
}
|
|
365
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
366
|
+
}
|
|
367
|
+
if (req.params.name === 'list_files') {
|
|
368
|
+
const relPath = String(req.params.arguments?.path ?? '.');
|
|
369
|
+
const project = String(req.params.arguments?.project ?? '');
|
|
370
|
+
const rawDepth = Number(req.params.arguments?.depth);
|
|
371
|
+
const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
|
|
372
|
+
const pattern = req.params.arguments?.pattern ? String(req.params.arguments.pattern) : null;
|
|
373
|
+
const { tree, error } = (0, searcher_1.listFiles)(relPath, project, depth, pattern);
|
|
374
|
+
if (error) {
|
|
375
|
+
return { content: [{ type: 'text', text: `list_files error: ${error}` }] };
|
|
376
|
+
}
|
|
377
|
+
return { content: [{ type: 'text', text: `**${project}/${relPath}**\n\`\`\`\n${tree}\n\`\`\`` }] };
|
|
378
|
+
}
|
|
379
|
+
if (req.params.name === 'read_file') {
|
|
380
|
+
const filePath = String(req.params.arguments?.file_path ?? '');
|
|
381
|
+
const rawMax = Number(req.params.arguments?.max_lines);
|
|
382
|
+
const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
|
|
383
|
+
const result = await (0, searcher_1.readFileAbsolute)(filePath, maxLines);
|
|
384
|
+
if (!result.ok) {
|
|
385
|
+
const message = result.reason === 'invalid_path' ? `Invalid path (outside workspace): ${filePath}` :
|
|
386
|
+
`File not found on disk: ${filePath}`;
|
|
387
|
+
return { content: [{ type: 'text', text: message }] };
|
|
388
|
+
}
|
|
389
|
+
const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
|
|
390
|
+
let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
391
|
+
if (result.hasMore) {
|
|
392
|
+
text += `\n\n[truncated: file has more lines — use read_file_range with file_path: "${result.filePath}", start_line: ${result.endLine + 1} to continue]`;
|
|
393
|
+
}
|
|
394
|
+
return { content: [{ type: 'text', text }] };
|
|
395
|
+
}
|
|
259
396
|
throw new Error(`Unknown tool: ${req.params.name}`);
|
|
260
397
|
}
|
|
261
398
|
catch (err) {
|
|
@@ -45,8 +45,16 @@ function loadLines(filePath) {
|
|
|
45
45
|
.map(l => l.trim())
|
|
46
46
|
.filter(l => l && !l.startsWith('#')));
|
|
47
47
|
}
|
|
48
|
+
// Write the set if non-empty, otherwise delete the file so state is deterministic.
|
|
49
|
+
// Previously .ragskip could be left stale (last entry removed but file not rewritten);
|
|
50
|
+
// treating both files the same way avoids that drift.
|
|
48
51
|
function saveLines(filePath, names) {
|
|
49
|
-
|
|
52
|
+
if (names.size > 0) {
|
|
53
|
+
fs.writeFileSync(filePath, [...names].sort().join('\n') + '\n');
|
|
54
|
+
}
|
|
55
|
+
else if (fs.existsSync(filePath)) {
|
|
56
|
+
fs.unlinkSync(filePath);
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
function loadConfig(workspaceDir) {
|
|
52
60
|
return {
|
|
@@ -56,8 +64,7 @@ function loadConfig(workspaceDir) {
|
|
|
56
64
|
}
|
|
57
65
|
function saveConfig(workspaceDir, config) {
|
|
58
66
|
saveLines(path.join(workspaceDir, '.ragprojects'), config.included);
|
|
59
|
-
|
|
60
|
-
saveLines(path.join(workspaceDir, '.ragskip'), config.skipped);
|
|
67
|
+
saveLines(path.join(workspaceDir, '.ragskip'), config.skipped);
|
|
61
68
|
}
|
|
62
69
|
async function selectProjects(allProjects, workspaceDir, forceSelect) {
|
|
63
70
|
const config = loadConfig(workspaceDir);
|
package/dist/src/searcher.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export type { ProjectMeta } from './file-utils';
|
|
1
|
+
import { type ProjectMeta } from './file-utils';
|
|
3
2
|
export interface ChunkRow {
|
|
4
3
|
serviceName: string;
|
|
5
4
|
filePath: string;
|
|
@@ -20,9 +19,10 @@ export type FileRangeResult = {
|
|
|
20
19
|
startLine: number;
|
|
21
20
|
endLine: number;
|
|
22
21
|
lines: string[];
|
|
22
|
+
hasMore: boolean;
|
|
23
23
|
} | {
|
|
24
24
|
ok: false;
|
|
25
|
-
reason: 'not_indexed' | 'not_on_disk';
|
|
25
|
+
reason: 'not_indexed' | 'not_on_disk' | 'invalid_path';
|
|
26
26
|
};
|
|
27
27
|
export interface DependencyRow {
|
|
28
28
|
fromFile: string;
|
|
@@ -37,7 +37,6 @@ export declare function prepareQuery(raw: string): Promise<{
|
|
|
37
37
|
query: string;
|
|
38
38
|
translated: boolean;
|
|
39
39
|
}>;
|
|
40
|
-
import type { ProjectMeta } from './file-utils';
|
|
41
40
|
export interface ProjectRow extends ProjectMeta {
|
|
42
41
|
serviceName: string;
|
|
43
42
|
chunks: number;
|
|
@@ -55,4 +54,21 @@ export declare function grepSymbol(symbol: string, project: string, excludeFile:
|
|
|
55
54
|
matches: GrepMatch[];
|
|
56
55
|
truncated: boolean;
|
|
57
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>;
|
|
58
74
|
export declare function readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
|
package/dist/src/searcher.js
CHANGED
|
@@ -40,22 +40,25 @@ exports.fetchNeighbors = fetchNeighbors;
|
|
|
40
40
|
exports.getDependencies = getDependencies;
|
|
41
41
|
exports.getDependents = getDependents;
|
|
42
42
|
exports.grepSymbol = grepSymbol;
|
|
43
|
+
exports.findSymbol = findSymbol;
|
|
44
|
+
exports.listFiles = listFiles;
|
|
45
|
+
exports.readFileAbsolute = readFileAbsolute;
|
|
43
46
|
exports.readFileRange = readFileRange;
|
|
44
47
|
const fs = __importStar(require("fs"));
|
|
45
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const readline = __importStar(require("readline"));
|
|
46
50
|
const child_process_1 = require("child_process");
|
|
47
|
-
const ollama_1 = require("ollama");
|
|
48
51
|
const db_1 = require("./db");
|
|
49
52
|
const config_1 = require("./config");
|
|
53
|
+
const ollama_1 = require("./ollama");
|
|
50
54
|
const file_utils_1 = require("./file-utils");
|
|
51
|
-
const ollama = new ollama_1.Ollama({ host: config_1.config.ollama.host });
|
|
52
55
|
// Detect non-English input by presence of non-ASCII characters (Cyrillic, etc.)
|
|
53
56
|
function isNonEnglish(text) {
|
|
54
57
|
return /[^\x00-\x7F]/.test(text);
|
|
55
58
|
}
|
|
56
59
|
// Translate query to English using a local Ollama model
|
|
57
60
|
async function translateToEnglish(text) {
|
|
58
|
-
const response = await ollama.chat({
|
|
61
|
+
const response = await ollama_1.ollama.chat({
|
|
59
62
|
model: config_1.config.search.translateModel,
|
|
60
63
|
messages: [{ role: 'user', content: `Translate the following text to English. Return only the translation, no explanations:\n${text}` }],
|
|
61
64
|
});
|
|
@@ -81,7 +84,7 @@ async function listProjects() {
|
|
|
81
84
|
}
|
|
82
85
|
// Embed query and run vector similarity search
|
|
83
86
|
async function searchChunks(query, limit, project) {
|
|
84
|
-
const { embedding } = await ollama.embeddings({ model: config_1.config.ollama.model, prompt: query });
|
|
87
|
+
const { embedding } = await ollama_1.ollama.embeddings({ model: config_1.config.ollama.model, prompt: query });
|
|
85
88
|
const params = [`[${embedding}]`, config_1.config.search.similarityThreshold, limit];
|
|
86
89
|
let projectFilter = '';
|
|
87
90
|
if (project) {
|
|
@@ -97,20 +100,32 @@ async function searchChunks(query, limit, project) {
|
|
|
97
100
|
ORDER BY embedding <=> $1
|
|
98
101
|
LIMIT $3`, params);
|
|
99
102
|
}
|
|
100
|
-
// Fetch the chunk immediately before and after each result in the same file (Small-to-Big)
|
|
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.
|
|
101
105
|
async function fetchNeighbors(results) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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,
|
|
114
129
|
}));
|
|
115
130
|
}
|
|
116
131
|
// Return all files imported by filePath (direct or transitive)
|
|
@@ -228,15 +243,204 @@ function grepSymbol(symbol, project, excludeFile) {
|
|
|
228
243
|
});
|
|
229
244
|
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
|
|
230
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
|
+
}
|
|
231
411
|
// Read a line range from a file that is present in the index
|
|
232
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
|
+
}
|
|
233
419
|
const [{ exists }] = await db_1.db.query(`SELECT EXISTS (SELECT 1 FROM code_chunk WHERE "filePath" = $1) AS exists`, [filePath]);
|
|
234
420
|
if (!exists)
|
|
235
421
|
return { ok: false, reason: 'not_indexed' };
|
|
236
|
-
const absPath = path.join(config_1.config.workspaceRoot, filePath);
|
|
237
422
|
if (!fs.existsSync(absPath))
|
|
238
423
|
return { ok: false, reason: 'not_on_disk' };
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 };
|
|
242
446
|
}
|
package/dist/src/setup.js
CHANGED
|
@@ -39,6 +39,7 @@ const fs = __importStar(require("fs"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
41
|
const child_process_1 = require("child_process");
|
|
42
|
+
const constants_1 = require("./constants");
|
|
42
43
|
const CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
|
|
43
44
|
const CONFIG_FILE = path.join(CONFIG_DIR, '.env');
|
|
44
45
|
const DOCKER_COMPOSE_SRC = path.join(__dirname, '..', '..', 'docker', 'docker-compose.yml');
|
|
@@ -133,8 +134,18 @@ async function setupLocalDocker(ollamaHost) {
|
|
|
133
134
|
}
|
|
134
135
|
await waitForDb('localhost', 5433, 'dev', 'dev', 'rag');
|
|
135
136
|
await waitForOllama(ollamaHost);
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
// Resolve the ollama container ID via compose — compose project name (and thus the
|
|
138
|
+
// default container name) depends on the parent directory, so hardcoding it breaks when
|
|
139
|
+
// users install via npm -g or move the compose file around.
|
|
140
|
+
const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
|
|
141
|
+
.stdout?.trim();
|
|
142
|
+
if (!ollamaId) {
|
|
143
|
+
console.error('Could not resolve ollama container ID from compose. Pull the model manually:');
|
|
144
|
+
console.error(` docker exec <ollama-container> ollama pull ${constants_1.DEFAULT_EMBED_MODEL}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
|
|
148
|
+
(0, child_process_1.spawnSync)('docker', ['exec', ollamaId, 'ollama', 'pull', constants_1.DEFAULT_EMBED_MODEL], { stdio: 'inherit' });
|
|
138
149
|
return {
|
|
139
150
|
DB_HOST: 'localhost',
|
|
140
151
|
DB_PORT: '5433',
|
|
@@ -166,13 +177,12 @@ async function setupRemoteDb(rl) {
|
|
|
166
177
|
async function runSetup() {
|
|
167
178
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
168
179
|
console.log('=== codesearch-mcp setup ===\n');
|
|
169
|
-
const dbChoice = await ask(rl, '
|
|
180
|
+
const dbChoice = await ask(rl, 'Infrastructure:\n 1. Local Docker (auto-start PostgreSQL + Ollama)\n 2. Remote (Supabase, Railway, self-hosted DB + local Ollama)\n\nEnter choice (1): ');
|
|
170
181
|
const useDocker = (dbChoice.trim() || '1') === '1';
|
|
171
182
|
const defaultWorkspace = path.join(os.homedir(), 'Projects');
|
|
172
183
|
const workspaceInput = await ask(rl, `\nWorkspace root — directory that contains all your projects\n Example: ${os.homedir()}/WebstormProjects\n Path (${defaultWorkspace}): `);
|
|
173
184
|
const workspaceRoot = expandHome(workspaceInput.trim() || defaultWorkspace);
|
|
174
185
|
const ollamaHost = await prompt(rl, 'Ollama API URL', 'http://localhost:11434');
|
|
175
|
-
const ollamaModel = await prompt(rl, 'Embedding model', 'bge-m3');
|
|
176
186
|
const dbVars = useDocker
|
|
177
187
|
? await setupLocalDocker(ollamaHost)
|
|
178
188
|
: await setupRemoteDb(rl);
|
|
@@ -180,7 +190,6 @@ async function runSetup() {
|
|
|
180
190
|
...dbVars,
|
|
181
191
|
WORKSPACE_ROOT: workspaceRoot,
|
|
182
192
|
OLLAMA_HOST: ollamaHost,
|
|
183
|
-
OLLAMA_MODEL: ollamaModel,
|
|
184
193
|
});
|
|
185
194
|
rl.close();
|
|
186
195
|
console.log('\nInitializing database schema...');
|
package/package.json
CHANGED