codesearch-mcp 0.0.13-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);
@@ -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) {
@@ -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
  }
@@ -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,11 +50,17 @@ 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
@@ -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;
@@ -65,30 +65,22 @@ You have access to a local semantic code search server. Use these tools to navig
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.
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
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
- 8. If \`search_codebase\` returns results that seem off-topic, pass a lower \`threshold\` (e.g. \`0.35\`) to filter out weak semantic matches.
75
74
 
76
- ## When to use read_file_range vs include_neighbors
75
+ ## Dependency graph
77
76
 
78
- - **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).
79
- - **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).
80
78
 
81
- ## Handling Code Structure
82
- - **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.
83
- - **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.
84
80
 
85
81
  ## Relevance scores
86
82
 
87
- Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Pass \`threshold\` to \`search_codebase\` to override the default per-query (e.g. \`0.35\` for strict matching, \`0.55\` for broader results).
88
-
89
- ## Dependency graph
90
-
91
- \`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).
92
84
  `.trim();
93
85
  class McpServer {
94
86
  searcher;
@@ -99,7 +91,7 @@ class McpServer {
99
91
  this.config = config;
100
92
  this.logger = logger;
101
93
  }
102
- async start() {
94
+ async start(startupError) {
103
95
  const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
104
96
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
105
97
  tools: [
@@ -206,6 +198,14 @@ class McpServer {
206
198
  ],
207
199
  }));
208
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
+ }
209
209
  try {
210
210
  if (req.params.name === 'list_projects') {
211
211
  const rows = await this.searcher.listProjects();
@@ -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.13-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",