codesearch-mcp 0.0.12-alpha → 0.0.14-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/README.md CHANGED
@@ -34,7 +34,7 @@ rag-setup
34
34
  Walks you through:
35
35
  - **Local Docker** — copies `docker-compose.yml`, starts PostgreSQL + Ollama, pulls `bge-m3` automatically
36
36
  - **Remote DB** — enter connection details for Supabase, Railway, or self-hosted; connection is verified before saving
37
- - **Logging** — choose log level and file path (defaults: `error` level, `~/.config/codesearch-mcp/rag.log`)
37
+ - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
38
38
 
39
39
  Config is saved to `~/.config/codesearch-mcp/.env`.
40
40
 
@@ -102,18 +102,18 @@ src/generated/
102
102
 
103
103
  ## Logging
104
104
 
105
- Logs are written to `~/.config/codesearch-mcp/rag.log` by default. Only errors are logged unless configured otherwise.
105
+ Logs are written to `~/.config/codesearch-mcp/logs/` by default (separate subdirectories for indexer and MCP). Only errors are logged unless configured otherwise.
106
106
 
107
107
  ```bash
108
108
  # Read logs
109
- tail -f ~/.config/codesearch-mcp/rag.log
110
- npx pino-pretty < ~/.config/codesearch-mcp/rag.log
109
+ tail -f ~/.config/codesearch-mcp/logs/indexer/current.log
110
+ npx pino-pretty < ~/.config/codesearch-mcp/logs/indexer/current.log
111
111
 
112
112
  # Detailed indexing diagnostics (timing per file: deps / embed / db)
113
113
  LOG_LEVEL=debug rag-index /path/to/workspace
114
114
 
115
115
  # Disable file logging
116
- LOG_FILE=false rag-index /path/to/workspace
116
+ LOG_DIR=false rag-index /path/to/workspace
117
117
  ```
118
118
 
119
119
  ## Environment variables
@@ -134,7 +134,7 @@ All set by `rag-setup`. Override any of them in `~/.config/codesearch-mcp/.env`.
134
134
  | `AUTO_TRANSLATE` | `false` | Translate non-English queries to English before embedding |
135
135
  | `TRANSLATE_MODEL` | `llama3.2` | Ollama model for translation (requires `ollama pull llama3.2`) |
136
136
  | `LOG_LEVEL` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
137
- | `LOG_FILE` | `~/.config/codesearch-mcp/rag.log` | Log file path, or `false` to disable |
137
+ | `LOG_DIR` | `~/.config/codesearch-mcp/logs` | Base log directory, or `false` to disable |
138
138
 
139
139
  ## License
140
140
 
package/dist/app.js CHANGED
@@ -53,7 +53,8 @@ async function main() {
53
53
  (0, help_1.printHelp)(argv1 !== 'app.js' ? argv1 : mode);
54
54
  process.exit(0);
55
55
  }
56
- const container = (0, container_1.createContainer)();
56
+ const loggerName = mode === 'index' || mode === 'backfill' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'setup' ? 'setup' : undefined;
57
+ const container = (0, container_1.createContainer)(loggerName);
57
58
  if (!container.config.workspaceRoot && mode !== 'setup') {
58
59
  const hint = argv1 === 'rag-mcp'
59
60
  ? 'Set WORKSPACE_ROOT in ~/.config/codesearch-mcp/.env or run `rag-setup`.'
@@ -64,7 +65,7 @@ async function main() {
64
65
  const commands = {
65
66
  index: new IndexCommand_1.IndexCommand(container, argv1),
66
67
  mcp: new McpCommand_1.McpCommand(container),
67
- setup: new SetupCommand_1.SetupCommand(),
68
+ setup: new SetupCommand_1.SetupCommand(container),
68
69
  backfill: new BackfillCommand_1.BackfillCommand(container),
69
70
  };
70
71
  const cmd = commands[mode];
@@ -7,9 +7,22 @@ class McpCommand {
7
7
  this.container = container;
8
8
  }
9
9
  async execute() {
10
- const { db, mcp } = this.container;
11
- await db.initDb();
12
- await mcp.start();
10
+ const { db, mcp, logger } = this.container;
11
+ let startupError;
12
+ try {
13
+ await db.initDb();
14
+ }
15
+ catch (err) {
16
+ startupError = err;
17
+ logger.error('McpCommand: database connection failed', { reason: err.message });
18
+ }
19
+ try {
20
+ await mcp.start(startupError);
21
+ }
22
+ catch (err) {
23
+ logger.error('McpCommand: MCP server failed to start', { reason: err.message });
24
+ return;
25
+ }
13
26
  // server.connect() resolves immediately after setup; keep the process alive
14
27
  // until the MCP client disconnects (stdin closes)
15
28
  await new Promise(resolve => {
@@ -1,4 +1,7 @@
1
+ import type { Container } from '../container';
1
2
  import type { ICommand } from './ICommand';
2
3
  export declare class SetupCommand implements ICommand {
4
+ private container;
5
+ constructor(container: Container);
3
6
  execute(): Promise<void>;
4
7
  }
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SetupCommand = void 0;
4
4
  const setup_1 = require("../setup");
5
5
  class SetupCommand {
6
+ container;
7
+ constructor(container) {
8
+ this.container = container;
9
+ }
6
10
  async execute() {
7
- await (0, setup_1.runSetup)();
11
+ await (0, setup_1.runSetup)(this.container.logger);
8
12
  }
9
13
  }
10
14
  exports.SetupCommand = SetupCommand;
@@ -12,4 +12,4 @@ export interface Container {
12
12
  mcp: McpServer;
13
13
  logger: LoggerService;
14
14
  }
15
- export declare function createContainer(): Container;
15
+ export declare function createContainer(loggerName?: string): Container;
@@ -9,8 +9,8 @@ const IndexerService_1 = require("./services/IndexerService");
9
9
  const SearcherService_1 = require("./services/SearcherService");
10
10
  const McpServer_1 = require("./services/McpServer");
11
11
  const LoggerService_1 = require("./services/LoggerService");
12
- function createContainer() {
13
- const logger = LoggerService_1.LoggerService.fromEnv();
12
+ function createContainer(loggerName) {
13
+ const logger = LoggerService_1.LoggerService.fromEnv(loggerName);
14
14
  const db = new DatabaseService_1.DatabaseService(config_1.config);
15
15
  const embedder = new OllamaEmbedder_1.OllamaEmbedder(config_1.config);
16
16
  const ast = new AstParserService_1.AstParserService(logger);
@@ -10,7 +10,7 @@ export interface SymbolEntry {
10
10
  project: string;
11
11
  filePath: string;
12
12
  name: string;
13
- kind: 'function' | 'class' | 'interface' | 'type' | 'variable';
13
+ kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'method';
14
14
  signature: string | null;
15
15
  line: number | null;
16
16
  col: number | null;
@@ -37,37 +37,49 @@ exports.selectProjects = selectProjects;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const prompts_1 = require("@inquirer/prompts");
40
- function loadLines(filePath) {
41
- if (!fs.existsSync(filePath))
40
+ async function loadLines(filePath) {
41
+ try {
42
+ const content = await fs.promises.readFile(filePath, 'utf-8');
43
+ return new Set(content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')));
44
+ }
45
+ catch (err) {
46
+ if (err.code !== 'ENOENT')
47
+ throw err;
42
48
  return new Set();
43
- return new Set(fs.readFileSync(filePath, 'utf-8')
44
- .split('\n')
45
- .map(l => l.trim())
46
- .filter(l => l && !l.startsWith('#')));
49
+ }
47
50
  }
48
51
  // Write the set if non-empty, otherwise delete the file so state is deterministic.
49
52
  // Previously .ragskip could be left stale (last entry removed but file not rewritten);
50
53
  // treating both files the same way avoids that drift.
51
- function saveLines(filePath, names) {
54
+ async function saveLines(filePath, names) {
52
55
  if (names.size > 0) {
53
- fs.writeFileSync(filePath, [...names].sort().join('\n') + '\n');
56
+ await fs.promises.writeFile(filePath, [...names].sort().join('\n') + '\n');
54
57
  }
55
- else if (fs.existsSync(filePath)) {
56
- fs.unlinkSync(filePath);
58
+ else {
59
+ try {
60
+ await fs.promises.unlink(filePath);
61
+ }
62
+ catch (err) {
63
+ if (err.code !== 'ENOENT')
64
+ throw err;
65
+ }
57
66
  }
58
67
  }
59
- function loadConfig(workspaceDir) {
60
- return {
61
- included: loadLines(path.join(workspaceDir, '.ragprojects')),
62
- skipped: loadLines(path.join(workspaceDir, '.ragskip')),
63
- };
68
+ async function loadConfig(workspaceDir) {
69
+ const [included, skipped] = await Promise.all([
70
+ loadLines(path.join(workspaceDir, '.ragprojects')),
71
+ loadLines(path.join(workspaceDir, '.ragskip')),
72
+ ]);
73
+ return { included, skipped };
64
74
  }
65
- function saveConfig(workspaceDir, config) {
66
- saveLines(path.join(workspaceDir, '.ragprojects'), config.included);
67
- saveLines(path.join(workspaceDir, '.ragskip'), config.skipped);
75
+ async function saveConfig(workspaceDir, config) {
76
+ await Promise.all([
77
+ saveLines(path.join(workspaceDir, '.ragprojects'), config.included),
78
+ saveLines(path.join(workspaceDir, '.ragskip'), config.skipped),
79
+ ]);
68
80
  }
69
81
  async function selectProjects(allProjects, workspaceDir, forceSelect) {
70
- const config = loadConfig(workspaceDir);
82
+ const config = await loadConfig(workspaceDir);
71
83
  const newProjects = allProjects.filter(p => !config.included.has(p.name) && !config.skipped.has(p.name));
72
84
  // Nothing to ask — return previously saved selection silently
73
85
  if (newProjects.length === 0 && !forceSelect) {
@@ -121,7 +133,7 @@ async function selectProjects(allProjects, workspaceDir, forceSelect) {
121
133
  for (const name of toSkip)
122
134
  config.skipped.add(name);
123
135
  }
124
- saveConfig(workspaceDir, config);
136
+ await saveConfig(workspaceDir, config);
125
137
  const selected = allProjects.filter(p => config.included.has(p.name));
126
138
  console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
127
139
  return selected;
@@ -43,6 +43,7 @@ const FALLBACK_OPTS = {
43
43
  skipFileDependencyResolution: false,
44
44
  compilerOptions: { allowJs: true, noEmit: true },
45
45
  };
46
+ const MORPH_CACHE_MAX = 10;
46
47
  class AstParserService {
47
48
  logger;
48
49
  morphProjects = new Map();
@@ -75,38 +76,49 @@ class AstParserService {
75
76
  const { edges } = await this.analyzeFile(filePath, serviceName, projectRoot);
76
77
  return edges;
77
78
  }
78
- // ── private: MorphProject cache ────────────────────────────────────────────
79
+ // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
79
80
  getMorphProject(projectRoot) {
80
- if (!this.morphProjects.has(projectRoot)) {
81
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
82
- const hasTsconfig = fs.existsSync(tsconfigPath);
83
- this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
84
- let project;
85
- try {
86
- const opts = hasTsconfig
87
- ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
88
- : FALLBACK_OPTS;
89
- project = new ts_morph_1.Project(opts);
90
- if (hasTsconfig) {
91
- const co = project.getCompilerOptions();
92
- const pathsMap = co.paths ?? {};
93
- const pathCount = Object.keys(pathsMap).length;
94
- this.logger.debug('AstParser: tsconfig loaded', { pathCount });
95
- if (pathCount > 0) {
96
- this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
97
- }
81
+ if (this.morphProjects.has(projectRoot)) {
82
+ // Move to end = most recently used
83
+ const cached = this.morphProjects.get(projectRoot);
84
+ this.morphProjects.delete(projectRoot);
85
+ this.morphProjects.set(projectRoot, cached);
86
+ return cached;
87
+ }
88
+ if (this.morphProjects.size >= MORPH_CACHE_MAX) {
89
+ // Evict least recently used (first entry in insertion-order Map)
90
+ const lruKey = this.morphProjects.keys().next().value;
91
+ this.morphProjects.delete(lruKey);
92
+ this.logger.debug('AstParser: evicted LRU project', { lruKey });
93
+ }
94
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
95
+ const hasTsconfig = fs.existsSync(tsconfigPath);
96
+ this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
97
+ let project;
98
+ try {
99
+ const opts = hasTsconfig
100
+ ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
101
+ : FALLBACK_OPTS;
102
+ project = new ts_morph_1.Project(opts);
103
+ if (hasTsconfig) {
104
+ const co = project.getCompilerOptions();
105
+ const pathsMap = co.paths ?? {};
106
+ const pathCount = Object.keys(pathsMap).length;
107
+ this.logger.debug('AstParser: tsconfig loaded', { pathCount });
108
+ if (pathCount > 0) {
109
+ this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
98
110
  }
99
111
  }
100
- catch (err) {
101
- this.logger.warn('AstParser: tsconfig load failed, using fallback', {
102
- tsconfigPath,
103
- reason: err.message,
104
- });
105
- project = new ts_morph_1.Project(FALLBACK_OPTS);
106
- }
107
- this.morphProjects.set(projectRoot, project);
108
112
  }
109
- return this.morphProjects.get(projectRoot);
113
+ catch (err) {
114
+ this.logger.warn('AstParser: tsconfig load failed, using fallback', {
115
+ tsconfigPath,
116
+ reason: err.message,
117
+ });
118
+ project = new ts_morph_1.Project(FALLBACK_OPTS);
119
+ }
120
+ this.morphProjects.set(projectRoot, project);
121
+ return project;
110
122
  }
111
123
  // ── private: edge extraction (imports + re-exports) ────────────────────────
112
124
  extractEdges(sourceFile, filePath, serviceName) {
@@ -116,7 +128,8 @@ class AstParserService {
116
128
  const resolved = decl.getModuleSpecifierSourceFile();
117
129
  if (!resolved) {
118
130
  const specifier = decl.getModuleSpecifierValue();
119
- if (specifier.startsWith('.')) {
131
+ const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
132
+ if (specifier.startsWith('.') && isCodeImport) {
120
133
  this.logger.debug('AstParser: unresolved import', { specifier, file: filePath });
121
134
  }
122
135
  continue;
@@ -108,7 +108,8 @@ class DatabaseService {
108
108
  await client.connect();
109
109
  const { rows } = await client.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [database]);
110
110
  if (rows.length === 0) {
111
- await client.query(`CREATE DATABASE "${database}"`);
111
+ const safeName = database.replace(/"/g, '""');
112
+ await client.query(`CREATE DATABASE "${safeName}"`);
112
113
  await client.end();
113
114
  const extClient = new pg_1.Client({ host, port, user: username, password, database });
114
115
  try {
@@ -126,7 +127,8 @@ class DatabaseService {
126
127
  }
127
128
  catch (err) {
128
129
  await client.end().catch(() => { });
129
- throw new Error(`Failed to ensure database "${database}" exists: ${err.message}`);
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ throw new Error(`Failed to ensure database "${database}" exists: ${msg}`);
130
132
  }
131
133
  }
132
134
  }
@@ -80,7 +80,7 @@ class IndexerService {
80
80
  }
81
81
  try {
82
82
  const { symbols: rawSymbols } = await this.ast.analyzeFile(absPath, serviceName, projectRoot);
83
- const symbols = rawSymbols.map(s => ({ ...s, filePath }));
83
+ const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
84
84
  await this.db.transaction(async (manager) => {
85
85
  await manager.delete(db_1.CodeSymbol, { project: serviceName, filePath });
86
86
  if (symbols.length > 0) {
@@ -223,7 +223,7 @@ class IndexerService {
223
223
  toFile: path.relative(this.config.workspaceRoot, e.toFile),
224
224
  }));
225
225
  // normalize symbols: replace absolute filePath with workspace-relative
226
- const symbols = rawSymbols.map(s => ({ ...s, filePath: relFilePath }));
226
+ const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath: relFilePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
227
227
  const depsMs = Date.now() - t0;
228
228
  const code = fs.readFileSync(filePath, 'utf-8');
229
229
  const chunks = await this.getSplitter(filePath).createDocuments([code]);
@@ -4,7 +4,7 @@ export declare class LoggerService {
4
4
  private readonly logger;
5
5
  constructor(logger: pino.Logger);
6
6
  static silent(): LoggerService;
7
- static fromEnv(): LoggerService;
7
+ static fromEnv(name?: string): LoggerService;
8
8
  debug(msg: string, data?: Record<string, unknown>): void;
9
9
  info(msg: string, data?: Record<string, unknown>): void;
10
10
  warn(msg: string, data?: Record<string, unknown>): void;
@@ -41,7 +41,7 @@ const pino_1 = __importDefault(require("pino"));
41
41
  const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const fs = __importStar(require("fs"));
44
- const DEFAULT_LOG_FILE = path.join(os.homedir(), '.config', 'codesearch-mcp', 'rag.log');
44
+ const DEFAULT_LOG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp', 'logs');
45
45
  class LoggerService {
46
46
  logger;
47
47
  constructor(logger) {
@@ -50,15 +50,35 @@ class LoggerService {
50
50
  static silent() {
51
51
  return new LoggerService((0, pino_1.default)({ level: 'silent' }));
52
52
  }
53
- static fromEnv() {
53
+ static fromEnv(name) {
54
54
  const level = (process.env.LOG_LEVEL ?? 'error');
55
- const logFileEnv = process.env.LOG_FILE;
56
- const enabled = logFileEnv !== 'false' && logFileEnv !== '0';
57
- const filePath = enabled ? (logFileEnv || DEFAULT_LOG_FILE) : null;
55
+ const logDirEnv = process.env.LOG_DIR;
56
+ const enabled = logDirEnv !== 'false' && logDirEnv !== '0';
57
+ let filePath = null;
58
+ if (enabled) {
59
+ const logDir = logDirEnv || DEFAULT_LOG_DIR;
60
+ filePath = name
61
+ ? path.join(logDir, name, `${name}.log`)
62
+ : path.join(logDir, 'rag.log');
63
+ }
58
64
  if (filePath)
59
65
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
60
66
  const transport = filePath
61
- ? pino_1.default.transport({ targets: [{ target: 'pino/file', options: { destination: filePath }, level }] })
67
+ ? pino_1.default.transport({
68
+ targets: [{
69
+ target: 'pino-roll',
70
+ options: {
71
+ file: filePath,
72
+ frequency: 'daily',
73
+ dateFormat: 'yyyy-MM-dd',
74
+ mkdir: true,
75
+ symlink: true,
76
+ limit: { count: 30 },
77
+ size: '50m',
78
+ },
79
+ level,
80
+ }],
81
+ })
62
82
  : undefined;
63
83
  const logger = transport
64
84
  ? (0, pino_1.default)({ level }, transport)
@@ -6,7 +6,7 @@ export declare class McpServer {
6
6
  private config;
7
7
  private logger;
8
8
  constructor(searcher: SearcherService, config: Config, logger?: LoggerService);
9
- start(): Promise<void>;
9
+ start(startupError?: Error): Promise<void>;
10
10
  private formatChunk;
11
11
  private formatDependencies;
12
12
  private formatDependents;
@@ -58,36 +58,29 @@ You have access to a local semantic code search server. Use these tools to navig
58
58
  - **read_file_range** — read a specific line range from any indexed file. Use this to expand context around a search result.
59
59
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
60
60
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
61
- - **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.
61
+ - **find_symbol** — find where a symbol (function, class, type, interface, variable, method) is defined by exact name. Faster than semantic search when you know the name.
62
62
  - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
63
63
  - **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
64
64
 
65
65
  ## Recommended workflow
66
66
 
67
67
  1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
68
- 2. Use \`search_codebase\` to find relevant code by meaning.
69
- 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\`.
70
- 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.
71
- 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.
68
+ 2. **Before searching**: if your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.35\`) to \`search_codebase\` to filter out weak matches. Default threshold is permissive — tighten it when semantic drift is likely.
69
+ 3. Use \`search_codebase\` to find relevant code by meaning.
70
+ 4. If a result **starts mid-function or mid-class** (e.g. you see a closing brace or a method body without the declaration), re-run the same search with \`include_neighbors: true\` — it automatically fetches the chunk before and after each match. If you need a specific line range, use \`read_file_range\` instead.
71
+ 5. Use \`find_symbol\` when you know the exact name of a function, class, or method and want its definition location immediately — faster than semantic search. Use \`kind: "method"\` to find public class methods like \`canActivate\`, \`execute\`, \`handle\`.
72
72
  6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
73
73
  7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
74
74
 
75
- ## When to use read_file_range vs include_neighbors
75
+ ## Dependency graph
76
76
 
77
- - **include_neighbors: true** — good when you already know the context will be fragmented (e.g. searching for a class definition that's likely split across chunks).
78
- - **read_file_range** — better when you want to read a precise section, or when a result looks cut off and you want to expand just that one file.
77
+ \`get_dependencies\` and \`get_dependents\` require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
79
78
 
80
- ## Handling Code Structure
81
- - **Class/Function limits**: Chunks often end abruptly. Always use \`read_file_range\` to find the closing brace or the beginning of a method if you suspect you're seeing only a fragment.
82
- - **Cross-file references**: If you see a method call like \`this.validator.validate(data)\`, use \`search_codebase\` to find the definition of that \`validator\` service.
79
+ If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you care about) — this triggers a grep fallback that finds runtime usages in DI containers, string references, and dynamic lookups that the import graph misses.
83
80
 
84
81
  ## Relevance scores
85
82
 
86
- Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically.
87
-
88
- ## Dependency graph
89
-
90
- \`get_dependencies\` and \`get_dependents\` require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
83
+ Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Pass \`threshold\` to \`search_codebase\` to override per-query (e.g. \`0.35\` for strict, \`0.55\` for broad).
91
84
  `.trim();
92
85
  class McpServer {
93
86
  searcher;
@@ -98,7 +91,7 @@ class McpServer {
98
91
  this.config = config;
99
92
  this.logger = logger;
100
93
  }
101
- async start() {
94
+ async start(startupError) {
102
95
  const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
103
96
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
104
97
  tools: [
@@ -112,6 +105,7 @@ class McpServer {
112
105
  limit: { type: 'number', description: `Max results (default ${this.config.search.defaultLimit}, max ${this.config.search.maxLimit})` },
113
106
  project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
114
107
  include_neighbors: { type: 'boolean', description: 'Also include the chunk before and after each result from the same file (Small-to-Big retrieval). Useful when results look truncated.' },
108
+ threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.config.search.similarityThreshold}. Use a lower value (e.g. 0.35) to filter out weak matches.` },
115
109
  },
116
110
  required: ['query'],
117
111
  },
@@ -164,13 +158,13 @@ class McpServer {
164
158
  },
165
159
  {
166
160
  name: 'find_symbol',
167
- 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.',
161
+ description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables, methods) by exact name. Use when you know the symbol name and want its definition location fast.',
168
162
  inputSchema: {
169
163
  type: 'object',
170
164
  properties: {
171
165
  name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
172
166
  project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
173
- kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'all'], description: 'Symbol kind filter. Default: all.' },
167
+ kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'], description: 'Symbol kind filter. Default: all.' },
174
168
  },
175
169
  required: ['name'],
176
170
  },
@@ -204,6 +198,14 @@ class McpServer {
204
198
  ],
205
199
  }));
206
200
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
201
+ if (startupError) {
202
+ return {
203
+ content: [{
204
+ type: 'text',
205
+ text: `MCP server is unavailable: ${startupError.message}\n\nCheck the log file and fix the issue, then restart the MCP server.`,
206
+ }],
207
+ };
208
+ }
207
209
  try {
208
210
  if (req.params.name === 'list_projects') {
209
211
  const rows = await this.searcher.listProjects();
@@ -232,8 +234,13 @@ class McpServer {
232
234
  const limit = Math.min(Math.max(1, Number(req.params.arguments?.limit) || this.config.search.defaultLimit), this.config.search.maxLimit);
233
235
  const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
234
236
  const includeNeighbors = Boolean(req.params.arguments?.include_neighbors);
237
+ const rawThreshold = req.params.arguments?.threshold;
238
+ const parsedThreshold = Number(rawThreshold);
239
+ const threshold = rawThreshold != null && !isNaN(parsedThreshold)
240
+ ? Math.min(1, Math.max(0, parsedThreshold))
241
+ : undefined;
235
242
  const { query, translated } = await this.searcher.prepareQuery(raw);
236
- const results = await this.searcher.searchChunks(query, limit, project);
243
+ const results = await this.searcher.searchChunks(query, limit, project, threshold);
237
244
  if (results.length === 0) {
238
245
  return { content: [{ type: 'text', text: 'No relevant results found.' }] };
239
246
  }
@@ -318,7 +325,7 @@ class McpServer {
318
325
  const name = String(req.params.arguments?.name ?? '');
319
326
  const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
320
327
  const rawKind = req.params.arguments?.kind;
321
- const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'all'];
328
+ const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
322
329
  const kind = validKinds.includes(rawKind) ? rawKind : 'all';
323
330
  const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
324
331
  if (hits.length === 0) {
@@ -45,7 +45,7 @@ export interface GrepMatch {
45
45
  filePath: string;
46
46
  line: number;
47
47
  }
48
- export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'all';
48
+ export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'all';
49
49
  export interface SymbolHit {
50
50
  filePath: string;
51
51
  line: number;
@@ -64,7 +64,7 @@ export declare class SearcherService {
64
64
  translated: boolean;
65
65
  }>;
66
66
  listProjects(): Promise<ProjectRow[]>;
67
- searchChunks(query: string, limit: number, project: string | null): Promise<SearchResult[]>;
67
+ searchChunks(query: string, limit: number, project: string | null, threshold?: number): Promise<SearchResult[]>;
68
68
  fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
69
69
  getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
70
70
  getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
@@ -59,8 +59,11 @@ class SearcherService {
59
59
  }
60
60
  async prepareQuery(raw) {
61
61
  if (this.config.search.autoTranslate && this.isNonEnglish(raw) && this.embedder.translate) {
62
- return { query: await this.embedder.translate(raw, this.config.search.translateModel), translated: true };
62
+ const query = await this.embedder.translate(raw, this.config.search.translateModel);
63
+ this.logger.debug('SearcherService: prepareQuery', { translated: true, raw: raw.slice(0, 120), query: query.slice(0, 120) });
64
+ return { query, translated: true };
63
65
  }
66
+ this.logger.debug('SearcherService: prepareQuery', { translated: false, query: raw.slice(0, 120) });
64
67
  return { query: raw, translated: false };
65
68
  }
66
69
  async listProjects() {
@@ -73,25 +76,36 @@ class SearcherService {
73
76
  return { ...r, ...(typeof meta === 'object' ? meta : EMPTY_META) };
74
77
  });
75
78
  }
76
- async searchChunks(query, limit, project) {
79
+ async searchChunks(query, limit, project, threshold) {
80
+ const effectiveThreshold = threshold ?? this.config.search.similarityThreshold;
81
+ this.logger.debug('SearcherService: searchChunks', {
82
+ query: query.slice(0, 120), limit, project, threshold: effectiveThreshold,
83
+ });
77
84
  const embedding = await this.embedder.embed(query);
78
- const params = [`[${embedding}]`, this.config.search.similarityThreshold, limit];
85
+ const params = [`[${embedding}]`, effectiveThreshold, limit];
79
86
  let projectFilter = '';
80
87
  if (project) {
81
88
  params.push(project);
82
89
  projectFilter = `AND "serviceName" = $${params.length}`;
83
90
  }
84
- return this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
91
+ const results = await this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
85
92
  ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
86
93
  FROM code_chunk
87
94
  WHERE embedding <=> $1 < $2
88
95
  ${projectFilter}
89
96
  ORDER BY embedding <=> $1
90
97
  LIMIT $3`, params);
98
+ this.logger.debug('SearcherService: searchChunks results', {
99
+ count: results.length,
100
+ scores: results.map(r => r.score),
101
+ files: results.map(r => r.filePath),
102
+ });
103
+ return results;
91
104
  }
92
105
  async fetchNeighbors(results) {
93
106
  if (results.length === 0)
94
107
  return [];
108
+ this.logger.debug('SearcherService: fetchNeighbors', { count: results.length });
95
109
  const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
96
110
  const params = results.flatMap(r => [r.filePath, r.startLine]);
97
111
  const rows = await this.db.query(`SELECT r.idx,
@@ -109,13 +123,19 @@ class SearcherService {
109
123
  ORDER BY "startLine" ASC LIMIT 1
110
124
  ) next ON true
111
125
  ORDER BY r.idx`, params);
112
- return rows.map((row, i) => ({
126
+ const neighbors = rows.map((row, i) => ({
113
127
  result: results[i],
114
128
  prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
115
129
  nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
116
130
  }));
131
+ this.logger.debug('SearcherService: fetchNeighbors results', {
132
+ withPrev: neighbors.filter(n => n.prevChunk).length,
133
+ withNext: neighbors.filter(n => n.nextChunk).length,
134
+ });
135
+ return neighbors;
117
136
  }
118
137
  async getDependencies(filePath, project, symbol = null, transitive = false) {
138
+ this.logger.debug('SearcherService: getDependencies', { filePath, project, symbol, transitive });
119
139
  const params = [filePath, project];
120
140
  let symbolFilter = '';
121
141
  if (symbol) {
@@ -127,8 +147,10 @@ class SearcherService {
127
147
  WHERE "fromFile" = $1 AND project = $2
128
148
  ${symbolFilter}
129
149
  ORDER BY "toFile"`, params);
130
- if (!transitive)
150
+ if (!transitive) {
151
+ this.logger.debug('SearcherService: getDependencies result', { direct: direct.length });
131
152
  return direct;
153
+ }
132
154
  const indirect = await this.db.query(`WITH RECURSIVE deps AS (
133
155
  SELECT "toFile", 1 AS depth, "toFile" AS via
134
156
  FROM code_dependency
@@ -151,9 +173,11 @@ class SearcherService {
151
173
  WHERE depth > 1
152
174
  GROUP BY "toFile"
153
175
  ORDER BY MIN(depth), "toFile"`, [filePath, project]);
176
+ this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length });
154
177
  return [...direct, ...indirect];
155
178
  }
156
179
  async getDependents(filePath, project, symbol = null, transitive = false) {
180
+ this.logger.debug('SearcherService: getDependents', { filePath, project, symbol, transitive });
157
181
  const params = [filePath, project];
158
182
  let symbolFilter = '';
159
183
  if (symbol) {
@@ -165,8 +189,10 @@ class SearcherService {
165
189
  WHERE "toFile" = $1 AND project = $2
166
190
  ${symbolFilter}
167
191
  ORDER BY "fromFile"`, params);
168
- if (!transitive)
192
+ if (!transitive) {
193
+ this.logger.debug('SearcherService: getDependents result', { direct: direct.length });
169
194
  return direct;
195
+ }
170
196
  const indirect = await this.db.query(`WITH RECURSIVE rdeps AS (
171
197
  SELECT "fromFile", 1 AS depth, "fromFile" AS via
172
198
  FROM code_dependency
@@ -189,9 +215,11 @@ class SearcherService {
189
215
  WHERE depth > 1
190
216
  GROUP BY "fromFile"
191
217
  ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
218
+ this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length });
192
219
  return [...direct, ...indirect];
193
220
  }
194
221
  grepSymbol(symbol, project, excludeFile) {
222
+ this.logger.debug('SearcherService: grepSymbol', { symbol, project, excludeFile });
195
223
  const projectDir = path.join(this.config.workspaceRoot, project);
196
224
  const result = (0, child_process_1.spawnSync)('grep', [
197
225
  '-rn', '-F', '-w',
@@ -216,16 +244,21 @@ class SearcherService {
216
244
  return [];
217
245
  return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
218
246
  });
219
- return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
247
+ const truncated = all.length > GREP_MAX_RESULTS;
248
+ this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated });
249
+ return { matches: all.slice(0, GREP_MAX_RESULTS), truncated };
220
250
  }
221
251
  // Sync wrapper: validates name (throws synchronously), then returns Promise.
222
- // Falls back to grep when the symbol index (code_symbol table) is unavailable.
252
+ // Methods are never indexed — always resolved via grep. Other kinds use index with grep fallback.
223
253
  findSymbol(name, project, kind) {
224
254
  if (!/^[\w$]+$/.test(name))
225
255
  throw new Error('Invalid symbol name');
256
+ if (kind === 'method')
257
+ return Promise.resolve(this.grepFindSymbol(name, project, kind));
226
258
  return this.findSymbolFromIndex(name, project, kind);
227
259
  }
228
260
  async findSymbolFromIndex(name, project, kind) {
261
+ this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'index' });
229
262
  try {
230
263
  const params = [name];
231
264
  let projectFilter = '';
@@ -252,6 +285,7 @@ class SearcherService {
252
285
  kind: row.kind,
253
286
  signature: row.signature ?? row.name,
254
287
  }));
288
+ this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated });
255
289
  return { hits, truncated };
256
290
  }
257
291
  catch {
@@ -260,6 +294,7 @@ class SearcherService {
260
294
  }
261
295
  }
262
296
  grepFindSymbol(name, project, kind) {
297
+ this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'grep' });
263
298
  const searchDir = project
264
299
  ? path.join(this.config.workspaceRoot, project)
265
300
  : this.config.workspaceRoot;
@@ -300,9 +335,11 @@ class SearcherService {
300
335
  signature: matchedLine.trim(),
301
336
  });
302
337
  }
338
+ this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: hits.length, truncated: hits.length >= FIND_SYMBOL_LIMIT });
303
339
  return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
304
340
  }
305
341
  listFiles(relativePath, project, depth, pattern) {
342
+ this.logger.debug('SearcherService: listFiles', { relativePath, project, depth, pattern });
306
343
  const projectRoot = path.join(this.config.workspaceRoot, project);
307
344
  const resolvedTarget = path.resolve(projectRoot, relativePath);
308
345
  const resolvedProject = path.resolve(projectRoot);
@@ -318,12 +355,14 @@ class SearcherService {
318
355
  const clampedDepth = Math.min(5, Math.max(1, depth));
319
356
  const lines = [];
320
357
  this.walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
358
+ this.logger.debug('SearcherService: listFiles result', { entries: lines.length });
321
359
  if (lines.length === 0) {
322
360
  return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
323
361
  }
324
362
  return { tree: lines.join('\n') };
325
363
  }
326
364
  async readFileAbsolute(absPath, maxLines) {
365
+ this.logger.debug('SearcherService: readFileAbsolute', { absPath, maxLines });
327
366
  const resolvedRoot = path.resolve(this.config.workspaceRoot);
328
367
  const resolved = path.resolve(absPath);
329
368
  if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
@@ -356,9 +395,11 @@ class SearcherService {
356
395
  throw err;
357
396
  }
358
397
  const relPath = path.relative(resolvedRoot, resolved);
398
+ this.logger.debug('SearcherService: readFileAbsolute result', { linesRead: lines.length, hasMore });
359
399
  return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
360
400
  }
361
401
  async readFileRange(filePath, startLine, endLine) {
402
+ this.logger.debug('SearcherService: readFileRange', { filePath, startLine, endLine });
362
403
  const resolvedRoot = path.resolve(this.config.workspaceRoot);
363
404
  const absPath = path.resolve(resolvedRoot, filePath);
364
405
  if (absPath !== resolvedRoot && !absPath.startsWith(resolvedRoot + path.sep)) {
@@ -388,6 +429,7 @@ class SearcherService {
388
429
  break;
389
430
  }
390
431
  }
432
+ this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore });
391
433
  return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
392
434
  }
393
435
  isNonEnglish(text) {
@@ -432,6 +474,7 @@ class SearcherService {
432
474
  type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
433
475
  interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
434
476
  variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
477
+ method: `(public[[:space:]]+|async[[:space:]]+|static[[:space:]]+|override[[:space:]]+)+(async[[:space:]]+)?${name}[[:space:]]*[(<]`,
435
478
  };
436
479
  if (kind === 'all')
437
480
  return Object.values(patterns).join('|');
@@ -447,6 +490,8 @@ class SearcherService {
447
490
  return 'interface';
448
491
  if (/\btype\s+$/.test(before))
449
492
  return 'type';
493
+ if (/\b(public|async|static|override)\s+$/.test(before))
494
+ return 'method';
450
495
  return 'variable';
451
496
  }
452
497
  }
@@ -1 +1,2 @@
1
- export declare function runSetup(): Promise<void>;
1
+ import type { LoggerService } from './services/LoggerService';
2
+ export declare function runSetup(logger?: LoggerService): Promise<void>;
package/dist/src/setup.js CHANGED
@@ -41,6 +41,7 @@ const os = __importStar(require("os"));
41
41
  const child_process_1 = require("child_process");
42
42
  const constants_1 = require("./constants");
43
43
  const CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
44
+ const DEFAULT_LOG_DIR = path.join(CONFIG_DIR, 'logs');
44
45
  const CONFIG_FILE = path.join(CONFIG_DIR, '.env');
45
46
  const DOCKER_COMPOSE_SRC = path.join(__dirname, '..', '..', 'docker', 'docker-compose.yml');
46
47
  const DOCKER_COMPOSE_DST = path.join(CONFIG_DIR, 'docker-compose.yml');
@@ -64,7 +65,7 @@ function writeEnv(vars) {
64
65
  fs.writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
65
66
  console.log(`\nConfig saved to ${CONFIG_FILE}`);
66
67
  }
67
- async function waitForDb(host, port, user, pass, db, maxWaitSec = 60) {
68
+ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
68
69
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
69
70
  const deadline = Date.now() + maxWaitSec * 1000;
70
71
  process.stdout.write('Waiting for PostgreSQL to be ready');
@@ -81,10 +82,12 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60) {
81
82
  await new Promise(r => setTimeout(r, 2000));
82
83
  }
83
84
  }
84
- console.error('\nPostgreSQL did not become ready in time.');
85
+ const reason = 'PostgreSQL did not become ready in time.';
86
+ console.error(`\n${reason}`);
87
+ logger?.error('waitForDb: timeout', { reason, steps });
85
88
  process.exit(1);
86
89
  }
87
- async function waitForOllama(host, maxWaitSec = 60) {
90
+ async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
88
91
  const deadline = Date.now() + maxWaitSec * 1000;
89
92
  process.stdout.write('Waiting for Ollama to be ready');
90
93
  while (Date.now() < deadline) {
@@ -106,10 +109,12 @@ async function waitForOllama(host, maxWaitSec = 60) {
106
109
  await new Promise(r => setTimeout(r, 2000));
107
110
  }
108
111
  }
109
- console.error('\nOllama did not become ready in time.');
112
+ const reason = 'Ollama did not become ready in time.';
113
+ console.error(`\n${reason}`);
114
+ logger?.error('waitForOllama: timeout', { reason, steps });
110
115
  process.exit(1);
111
116
  }
112
- async function checkRemoteDb(host, port, user, pass, db) {
117
+ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
113
118
  try {
114
119
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
115
120
  const client = new Client({ host, port: parseInt(port), user, password: pass, database: db, connectionTimeoutMillis: 5000 });
@@ -118,50 +123,62 @@ async function checkRemoteDb(host, port, user, pass, db) {
118
123
  return true;
119
124
  }
120
125
  catch (err) {
121
- console.error(` Connection failed: ${err.message}`);
126
+ const msg = err instanceof Error ? err.message : String(err);
127
+ console.error(` Connection failed: ${msg}`);
128
+ logger?.error('checkRemoteDb: connection failed', { reason: msg, steps });
122
129
  return false;
123
130
  }
124
131
  }
125
- async function setupLocalDocker(ollamaHost) {
126
- console.log('\nCopying docker-compose.yml to', DOCKER_COMPOSE_DST);
132
+ async function setupLocalDocker(ollamaHost, dbPort, dbUser, dbPass, logger, steps) {
133
+ console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
134
+ const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
135
+ let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
136
+ composeContent = composeContent
137
+ .replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
138
+ .replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
139
+ .replace('"5433:5432"', `"${dbPort}:5432"`);
127
140
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
128
- fs.copyFileSync(DOCKER_COMPOSE_SRC, DOCKER_COMPOSE_DST);
141
+ fs.writeFileSync(DOCKER_COMPOSE_DST, composeContent, 'utf-8');
129
142
  console.log('\nStarting Docker containers...');
130
143
  const upResult = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d'], { stdio: 'inherit' });
131
144
  if (upResult.status !== 0) {
132
- console.error('Failed to start Docker containers. Make sure Docker is running.');
145
+ const reason = 'Failed to start Docker containers. Make sure Docker is running.';
146
+ console.error(reason);
147
+ logger?.error('setupLocalDocker: docker startup failed', { reason, steps });
133
148
  process.exit(1);
134
149
  }
135
- await waitForDb('localhost', 5433, 'dev', 'dev', 'postgres');
136
- await waitForOllama(ollamaHost);
150
+ await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
151
+ await waitForOllama(ollamaHost, 60, logger, steps);
137
152
  // Resolve the ollama container ID via compose — compose project name (and thus the
138
153
  // default container name) depends on the parent directory, so hardcoding it breaks when
139
154
  // users install via npm -g or move the compose file around.
140
155
  const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
141
156
  .stdout?.trim();
142
157
  if (!ollamaId) {
143
- console.error('Could not resolve ollama container ID from compose. Pull the model manually:');
158
+ const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
159
+ console.error(reason);
144
160
  console.error(` docker exec <ollama-container> ollama pull ${constants_1.DEFAULT_EMBED_MODEL}`);
161
+ logger?.error('setupLocalDocker: ollama container not found', { reason, steps });
145
162
  process.exit(1);
146
163
  }
147
164
  console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
148
165
  (0, child_process_1.spawnSync)('docker', ['exec', ollamaId, 'ollama', 'pull', constants_1.DEFAULT_EMBED_MODEL], { stdio: 'inherit' });
149
- return {
150
- DB_HOST: 'localhost',
151
- DB_PORT: '5433',
152
- DB_USER: 'dev',
153
- DB_PASS: 'dev',
154
- };
166
+ return { DB_HOST: 'localhost', DB_PORT: dbPort, DB_USER: dbUser, DB_PASS: dbPass };
155
167
  }
156
- async function setupRemoteDb(rl) {
168
+ async function setupRemoteDb(rl, logger, steps, logStep) {
169
+ const lp = async (label, def) => {
170
+ const answer = await prompt(rl, label, def);
171
+ logStep?.(label, answer);
172
+ return answer;
173
+ };
157
174
  let vars = {};
158
175
  while (true) {
159
- vars.DB_HOST = await prompt(rl, 'DB host', 'localhost');
160
- vars.DB_PORT = await prompt(rl, 'DB port', '5432');
161
- vars.DB_USER = await prompt(rl, 'DB user', 'postgres');
162
- vars.DB_PASS = await prompt(rl, 'DB password', '');
176
+ vars.DB_HOST = await lp('DB host', 'localhost');
177
+ vars.DB_PORT = await lp('DB port', '5432');
178
+ vars.DB_USER = await lp('DB user', 'postgres');
179
+ vars.DB_PASS = await lp('DB password', '');
163
180
  console.log('\nTesting connection...');
164
- const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, 'postgres');
181
+ const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, 'postgres', logger, steps);
165
182
  if (ok) {
166
183
  console.log('Connection successful.');
167
184
  break;
@@ -172,18 +189,47 @@ async function setupRemoteDb(rl) {
172
189
  }
173
190
  return vars;
174
191
  }
175
- async function runSetup() {
192
+ async function runSetup(logger) {
176
193
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
177
- console.log('=== codesearch-mcp setup ===\n');
178
- 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): ');
194
+ const steps = [];
195
+ function logStep(q, raw) {
196
+ const a = /password/i.test(q) ? '***' : raw.trim();
197
+ logger?.debug('setup: prompt', { q, a });
198
+ steps.push({ q, a });
199
+ }
200
+ const loggedAsk = async (question) => {
201
+ const answer = await ask(rl, question);
202
+ const key = question.split('\n').find(l => l.trim())?.trim() ?? question.trim();
203
+ logStep(key, answer);
204
+ return answer;
205
+ };
206
+ const loggedPrompt = async (label, def) => {
207
+ const answer = await prompt(rl, label, def);
208
+ logStep(label, answer || def);
209
+ return answer;
210
+ };
211
+ console.log('=== codesearch-mcp setup ===');
212
+ console.log('Press Enter to keep the default value shown in parentheses.\n');
213
+ const dbChoice = await loggedAsk('Infrastructure:\n 1. Local Docker (auto-start PostgreSQL + Ollama)\n 2. Remote (Supabase, Railway, self-hosted DB + local Ollama)\n\nEnter choice (1): ');
179
214
  const useDocker = (dbChoice.trim() || '1') === '1';
180
- const defaultWorkspace = path.join(os.homedir(), 'Projects');
181
- const workspaceInput = await ask(rl, `\nWorkspace root — directory that contains all your projects\n Example: ${os.homedir()}/WebstormProjects\n Path (${defaultWorkspace}): `);
182
- const workspaceRoot = expandHome(workspaceInput.trim() || defaultWorkspace);
183
- const ollamaHost = await prompt(rl, 'Ollama API URL', 'http://localhost:11434');
215
+ const workspaceExample = (() => {
216
+ const candidates = ['Projects', 'projects', 'workspace', 'code', 'Code', 'dev', 'Development'];
217
+ const found = candidates.find(d => fs.existsSync(path.join(os.homedir(), d)));
218
+ return path.join(os.homedir(), found ?? 'projects');
219
+ })();
220
+ console.log(`\nWorkspace root — full path to the directory that contains all your projects`);
221
+ console.log(` (e.g. ${workspaceExample})`);
222
+ let workspaceInput;
223
+ do {
224
+ workspaceInput = await loggedAsk('Path: ');
225
+ if (!workspaceInput.trim())
226
+ console.log(' Path is required.');
227
+ } while (!workspaceInput.trim());
228
+ const workspaceRoot = expandHome(workspaceInput.trim());
229
+ const ollamaHost = await loggedPrompt('Ollama API URL', 'http://localhost:11434');
184
230
  const dbVars = useDocker
185
- ? await setupLocalDocker(ollamaHost)
186
- : await setupRemoteDb(rl);
231
+ ? await setupLocalDocker(ollamaHost, await loggedPrompt('DB port', '5433'), await loggedPrompt('DB user', 'dev'), await loggedPrompt('DB password', 'dev'), logger, steps)
232
+ : await setupRemoteDb(rl, logger, steps, logStep);
187
233
  console.log('\n--- Logging ---');
188
234
  console.log(' error — failures only (recommended)');
189
235
  console.log(' warn — + recoverable issues');
@@ -191,20 +237,21 @@ async function runSetup() {
191
237
  const validLevels = ['error', 'warn', 'info', 'debug'];
192
238
  let logLevel;
193
239
  do {
194
- logLevel = await prompt(rl, 'Log level (error/warn/info/debug)', 'error');
240
+ logLevel = await loggedPrompt('Log level (error/warn/info/debug)', 'error');
195
241
  if (!validLevels.includes(logLevel))
196
242
  console.log(` Invalid level. Choose: ${validLevels.join(', ')}`);
197
243
  } while (!validLevels.includes(logLevel));
198
- const defaultLogFile = path.join(os.homedir(), '.config', 'codesearch-mcp', 'rag.log');
199
- const logFileInput = await ask(rl, `Log file (${defaultLogFile}, or "false" to disable): `);
200
- const logFile = logFileInput.trim() || defaultLogFile;
201
- writeEnv({
244
+ const logDirInput = await loggedAsk(`Log dir (leave blank for default: ${DEFAULT_LOG_DIR}, or "false" to disable): `);
245
+ const logDir = logDirInput.trim();
246
+ const envVars = {
202
247
  ...dbVars,
203
248
  WORKSPACE_ROOT: workspaceRoot,
204
249
  OLLAMA_HOST: ollamaHost,
205
250
  LOG_LEVEL: logLevel,
206
- LOG_FILE: logFile,
207
- });
251
+ };
252
+ if (logDir)
253
+ envVars['LOG_DIR'] = logDir;
254
+ writeEnv(envVars);
208
255
  rl.close();
209
256
  console.log('\nInitializing database schema...');
210
257
  const dotenv = await Promise.resolve().then(() => __importStar(require('dotenv')));
@@ -212,8 +259,21 @@ async function runSetup() {
212
259
  const { config } = await Promise.resolve().then(() => __importStar(require('./config')));
213
260
  const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
214
261
  const dbService = new DatabaseService(config);
215
- await dbService.initDbWithSchema();
216
- await dbService.destroy();
262
+ let initError = null;
263
+ try {
264
+ await dbService.initDbWithSchema();
265
+ }
266
+ catch (err) {
267
+ initError = err instanceof Error ? err : new Error(String(err));
268
+ }
269
+ finally {
270
+ await dbService.destroy();
271
+ }
272
+ if (initError) {
273
+ console.error(`Database schema init failed: ${initError.message}`);
274
+ logger?.error('runSetup: database schema init failed', { reason: initError.message, steps });
275
+ process.exit(1);
276
+ }
217
277
  console.log('\nSetup complete!');
218
278
  console.log('\nNext steps:');
219
279
  console.log(` rag-index ${workspaceRoot}`);
@@ -6,7 +6,7 @@ services:
6
6
  environment:
7
7
  POSTGRES_USER: dev
8
8
  POSTGRES_PASSWORD: dev
9
- POSTGRES_DB: rag
9
+ POSTGRES_DB: postgres
10
10
  ports:
11
11
  - "5433:5432"
12
12
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.12-alpha",
3
+ "version": "0.0.14-alpha",
4
4
  "description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
5
5
  "license": "MIT",
6
6
  "main": "dist/app.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@inquirer/prompts": "^8.4.1",
32
- "@langchain/textsplitters": "^0.1.0",
32
+ "@langchain/textsplitters": "^1.0.1",
33
33
  "@modelcontextprotocol/sdk": "^1.0.0",
34
34
  "cli-progress": "^3.12.0",
35
35
  "dotenv": "^17.4.2",
@@ -37,6 +37,7 @@
37
37
  "ollama": "^0.5.0",
38
38
  "pg": "^8.13.0",
39
39
  "pino": "^10.3.1",
40
+ "pino-roll": "^4.0.0",
40
41
  "reflect-metadata": "^0.2.2",
41
42
  "ts-morph": "^28.0.0",
42
43
  "typeorm": "^0.3.20",