@esankhan3/code-search-mcp 0.1.0 → 0.2.0

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.
@@ -0,0 +1,372 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ function defaultDataDir() {
5
+ return process.env.CODE_SEARCH_DATA_DIR ?? join(homedir(), ".code-search", "index");
6
+ }
7
+ const DEFAULTS = {
8
+ server: {
9
+ transport: "stdio",
10
+ port: 3100,
11
+ host: "127.0.0.1"
12
+ },
13
+ auth: {
14
+ mode: "none",
15
+ apiKeys: [],
16
+ jwtSecret: void 0,
17
+ jwtIssuer: "code-search-mcp",
18
+ rateLimitPerMinute: 100
19
+ },
20
+ storage: {
21
+ dataDir: defaultDataDir()
22
+ },
23
+ embedding: {
24
+ provider: "auto",
25
+ dimensions: 1024
26
+ },
27
+ reranker: {
28
+ provider: "ollama"
29
+ },
30
+ retrieval: {
31
+ maxChunks: 8,
32
+ maxTokens: 12e3,
33
+ hybridWeights: { vector: 0.5, bm25: 0.3, graph: 0.2 }
34
+ },
35
+ indexing: {
36
+ autoIndex: true,
37
+ reindexIntervalMs: 0,
38
+ chunking: { maxTokens: 500, contextEnrichment: "structural" },
39
+ respectGitignore: true,
40
+ debounceMs: 500,
41
+ ignorePatterns: ["node_modules", ".git", "dist", "build", ".next", "__pycache__", ".venv", "vendor", "target"]
42
+ },
43
+ llm: {
44
+ mode: "none",
45
+ provider: "anthropic",
46
+ model: "sonnet",
47
+ apiKey: void 0,
48
+ baseUrl: void 0,
49
+ claudeBin: "claude"
50
+ },
51
+ github: {
52
+ token: void 0
53
+ },
54
+ telemetry: {
55
+ otelEndpoint: void 0,
56
+ otelHeaders: void 0,
57
+ metricsEnabled: false,
58
+ structuredLogs: false,
59
+ recordQueries: false
60
+ },
61
+ daemon: {
62
+ autoSpawn: false,
63
+ disabled: false
64
+ }
65
+ };
66
+ function isPlainObject(v) {
67
+ return typeof v === "object" && v !== null && !Array.isArray(v);
68
+ }
69
+ function deepMerge(base, ...patches) {
70
+ const out = { ...base };
71
+ for (const p of patches) {
72
+ if (!p) continue;
73
+ for (const [k, v] of Object.entries(p)) {
74
+ if (v === void 0) continue;
75
+ if (isPlainObject(v) && isPlainObject(out[k])) {
76
+ out[k] = deepMerge(out[k], v);
77
+ } else {
78
+ out[k] = v;
79
+ }
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ function parseYaml(src) {
85
+ const lines = src.split(/\r?\n/);
86
+ const root = {};
87
+ const stack = [{ obj: root, indent: -1 }];
88
+ for (const rawLine of lines) {
89
+ const line = rawLine.replace(/#.*$/, "").replace(/\s+$/, "");
90
+ if (!line.trim()) continue;
91
+ const indent = line.length - line.trimStart().length;
92
+ const trimmed = line.trim();
93
+ const colon = trimmed.indexOf(":");
94
+ if (colon < 0) continue;
95
+ const key = trimmed.slice(0, colon).trim();
96
+ const valRaw = trimmed.slice(colon + 1).trim();
97
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
98
+ const parent = stack[stack.length - 1].obj;
99
+ if (!valRaw) {
100
+ const child = {};
101
+ parent[key] = child;
102
+ stack.push({ obj: child, indent });
103
+ continue;
104
+ }
105
+ parent[key] = coerce(valRaw);
106
+ }
107
+ return root;
108
+ }
109
+ function coerce(raw) {
110
+ let v = raw;
111
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
112
+ return v.slice(1, -1);
113
+ }
114
+ if (v === "true") return true;
115
+ if (v === "false") return false;
116
+ if (v === "null" || v === "~") return null;
117
+ if (/^-?\d+$/.test(v)) return parseInt(v, 10);
118
+ if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
119
+ if (v.startsWith("[") && v.endsWith("]")) {
120
+ return v.slice(1, -1).split(",").map((s) => coerce(s.trim()));
121
+ }
122
+ if (v.startsWith("{") && v.endsWith("}")) {
123
+ const o = {};
124
+ for (const part of v.slice(1, -1).split(",")) {
125
+ const [k, x] = part.split(":");
126
+ if (k && x !== void 0) o[k.trim()] = coerce(x.trim());
127
+ }
128
+ return o;
129
+ }
130
+ if (v.startsWith("~")) return v.replace(/^~/, homedir());
131
+ return v;
132
+ }
133
+ function snakeToCamel(obj) {
134
+ if (Array.isArray(obj)) return obj.map(snakeToCamel);
135
+ if (!isPlainObject(obj)) return obj;
136
+ const out = {};
137
+ for (const [k, v] of Object.entries(obj)) {
138
+ const camel = k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
139
+ out[camel] = snakeToCamel(v);
140
+ }
141
+ return out;
142
+ }
143
+ function loadYamlFile(path) {
144
+ if (!existsSync(path)) return null;
145
+ try {
146
+ const src = readFileSync(path, "utf-8");
147
+ const raw = parseYaml(src);
148
+ return snakeToCamel(raw);
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ function parseHeaders(s) {
154
+ if (!s) return void 0;
155
+ const out = {};
156
+ for (const pair of s.split(",")) {
157
+ const eq = pair.indexOf("=");
158
+ if (eq > 0) out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
159
+ }
160
+ return Object.keys(out).length ? out : void 0;
161
+ }
162
+ function parseReindexMs(raw) {
163
+ if (!raw || raw === "0" || raw === "none") return 0;
164
+ const m = raw.match(/^(\d+)(m|h)$/);
165
+ if (!m) {
166
+ const n = parseInt(raw, 10);
167
+ return Number.isFinite(n) ? n : 0;
168
+ }
169
+ return m[2] === "h" ? parseInt(m[1], 10) * 60 * 6e4 : parseInt(m[1], 10) * 6e4;
170
+ }
171
+ function envLayer() {
172
+ const env = (k) => process.env[`CODE_SEARCH_${k}`];
173
+ const authMode = env("AUTH_MODE");
174
+ const transport = env("TRANSPORT");
175
+ const port = env("PORT");
176
+ const rateLimit = env("RATE_LIMIT_PER_MINUTE");
177
+ const maxChunks = env("RETRIEVAL_MAX_CHUNKS");
178
+ const maxTokens = env("RETRIEVAL_MAX_TOKENS");
179
+ const chunkMaxTokens = env("CHUNKING_MAX_TOKENS");
180
+ const debounce = env("INDEXING_DEBOUNCE_MS");
181
+ const embedDims = env("EMBEDDING_DIMENSIONS");
182
+ return {
183
+ server: {
184
+ transport,
185
+ port: port ? parseInt(port, 10) : void 0,
186
+ host: env("HOST") ?? (authMode === "none" || !authMode ? void 0 : "0.0.0.0")
187
+ },
188
+ auth: {
189
+ mode: authMode,
190
+ apiKeys: env("AUTH_API_KEYS")?.split(",").map((s) => s.trim()).filter(Boolean),
191
+ jwtSecret: env("AUTH_JWT_SECRET"),
192
+ jwtIssuer: env("AUTH_JWT_ISSUER"),
193
+ rateLimitPerMinute: rateLimit ? parseInt(rateLimit, 10) : void 0
194
+ },
195
+ storage: {
196
+ dataDir: env("DATA_DIR")
197
+ },
198
+ embedding: {
199
+ provider: env("EMBEDDING_PROVIDER"),
200
+ model: env("EMBEDDING_MODEL"),
201
+ dimensions: embedDims ? parseInt(embedDims, 10) : void 0,
202
+ apiKey: env("EMBEDDING_API_KEY"),
203
+ baseUrl: env("EMBEDDING_BASE_URL"),
204
+ ollamaHost: env("OLLAMA_HOST") ?? process.env.OLLAMA_HOST
205
+ },
206
+ reranker: {
207
+ provider: env("RERANKER_PROVIDER"),
208
+ model: env("RERANKER_MODEL"),
209
+ apiKey: env("RERANKER_API_KEY"),
210
+ baseUrl: env("RERANKER_BASE_URL")
211
+ },
212
+ retrieval: {
213
+ maxChunks: maxChunks ? parseInt(maxChunks, 10) : void 0,
214
+ maxTokens: maxTokens ? parseInt(maxTokens, 10) : void 0
215
+ },
216
+ indexing: {
217
+ autoIndex: env("AUTO_INDEX") !== void 0 ? env("AUTO_INDEX") !== "false" && env("AUTO_INDEX") !== "0" : void 0,
218
+ reindexIntervalMs: env("REINDEX_INTERVAL") !== void 0 ? parseReindexMs(env("REINDEX_INTERVAL")) : void 0,
219
+ chunking: {
220
+ maxTokens: chunkMaxTokens ? parseInt(chunkMaxTokens, 10) : void 0
221
+ },
222
+ debounceMs: debounce ? parseInt(debounce, 10) : void 0
223
+ },
224
+ llm: {
225
+ mode: env("LLM_MODE"),
226
+ provider: env("LLM_PROVIDER"),
227
+ model: env("LLM_MODEL"),
228
+ apiKey: env("LLM_API_KEY") ?? process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
229
+ baseUrl: env("LLM_BASE_URL"),
230
+ claudeBin: env("CLAUDE_BIN") ?? process.env.CLAUDE_BIN
231
+ },
232
+ github: {
233
+ token: env("GITHUB_TOKEN") ?? process.env.GITHUB_TOKEN
234
+ },
235
+ telemetry: {
236
+ otelEndpoint: env("OTEL_ENDPOINT") ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
237
+ otelHeaders: parseHeaders(env("OTEL_HEADERS") ?? process.env.OTEL_EXPORTER_OTLP_HEADERS),
238
+ metricsEnabled: env("METRICS_ENABLED") === "1" || env("METRICS_ENABLED") === "true" ? true : void 0,
239
+ structuredLogs: env("STRUCTURED_LOGS") === "1" || env("STRUCTURED_LOGS") === "true" ? true : void 0,
240
+ recordQueries: env("TELEMETRY_RECORD_QUERIES") === "1" || env("TELEMETRY_RECORD_QUERIES") === "true" ? true : void 0
241
+ },
242
+ daemon: {
243
+ autoSpawn: env("DAEMON_AUTO_SPAWN") === "1" || env("DAEMON_AUTO_SPAWN") === "true" ? true : void 0,
244
+ disabled: env("DAEMON_DISABLED") === "1" || env("DAEMON_DISABLED") === "true" ? true : void 0
245
+ }
246
+ };
247
+ }
248
+ const RESERVED_FLAGS = /* @__PURE__ */ new Set([
249
+ "--local",
250
+ "--remote",
251
+ "--serve",
252
+ "--help",
253
+ "-h",
254
+ "--print-config",
255
+ "--api-key",
256
+ "--project",
257
+ "--token",
258
+ "--force",
259
+ "--workspace",
260
+ "--no-daemon",
261
+ "--auto-spawn-daemon",
262
+ "--config",
263
+ "--format",
264
+ "--top-k",
265
+ "--mode",
266
+ "--repo",
267
+ "--language"
268
+ ]);
269
+ function parseCliFlags(argv) {
270
+ const patch = {};
271
+ const rest = [];
272
+ for (let i = 0; i < argv.length; i++) {
273
+ const arg = argv[i];
274
+ if (!arg.startsWith("--") || RESERVED_FLAGS.has(arg)) {
275
+ rest.push(arg);
276
+ continue;
277
+ }
278
+ let key = arg.slice(2);
279
+ let negate = false;
280
+ if (key.startsWith("no-")) {
281
+ negate = true;
282
+ key = key.slice(3);
283
+ }
284
+ const path = key.split(".").map((seg) => seg.replace(/-([a-z])/g, (_, c) => c.toUpperCase()));
285
+ let val;
286
+ if (negate) {
287
+ val = false;
288
+ } else if (argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) {
289
+ val = coerce(argv[++i]);
290
+ } else {
291
+ val = true;
292
+ }
293
+ setDeep(patch, path, val);
294
+ }
295
+ return { patch, rest };
296
+ }
297
+ function setDeep(obj, path, value) {
298
+ let cur = obj;
299
+ for (let i = 0; i < path.length - 1; i++) {
300
+ const k = path[i];
301
+ if (!isPlainObject(cur[k])) cur[k] = {};
302
+ cur = cur[k];
303
+ }
304
+ cur[path[path.length - 1]] = value;
305
+ }
306
+ function resolveCodeSearchConfig(opts = {}) {
307
+ const globalConfigPath = opts.configPath ?? join(homedir(), ".code-search", "config.yaml");
308
+ const yaml1 = loadYamlFile(globalConfigPath) ?? {};
309
+ const yaml2 = opts.workspaceDir ? loadYamlFile(join(opts.workspaceDir, ".code-search.yaml")) ?? {} : {};
310
+ const env = envLayer();
311
+ const cli = opts.cli ?? {};
312
+ const cfg = deepMerge(DEFAULTS, yaml1, yaml2, env, cli);
313
+ bridgeLegacyEnvVars(cfg);
314
+ return cfg;
315
+ }
316
+ function bridgeLegacyEnvVars(cfg) {
317
+ process.env.ANVIL_LLM_MODE ??= process.env.CODE_SEARCH_LLM_MODE ?? cfg.llm.mode;
318
+ process.env.ANVIL_LLM_PROVIDER ??= process.env.CODE_SEARCH_LLM_PROVIDER ?? cfg.llm.provider;
319
+ process.env.ANVIL_LLM_MODEL ??= process.env.CODE_SEARCH_LLM_MODEL ?? cfg.llm.model;
320
+ if (cfg.llm.apiKey) process.env.ANVIL_LLM_API_KEY ??= cfg.llm.apiKey;
321
+ if (cfg.llm.baseUrl) process.env.ANVIL_LLM_BASE_URL ??= cfg.llm.baseUrl;
322
+ if (cfg.llm.claudeBin) process.env.ANVIL_CLAUDE_BIN ??= cfg.llm.claudeBin;
323
+ }
324
+ function toKnowledgeConfig(c) {
325
+ return {
326
+ embedding: { ...c.embedding },
327
+ chunking: { ...c.indexing.chunking },
328
+ retrieval: {
329
+ maxChunks: c.retrieval.maxChunks,
330
+ maxTokens: c.retrieval.maxTokens,
331
+ hybridWeights: { ...c.retrieval.hybridWeights },
332
+ reranker: { ...c.reranker }
333
+ },
334
+ autoIndex: c.indexing.autoIndex
335
+ };
336
+ }
337
+ const SECRET_KEYS = /* @__PURE__ */ new Set([
338
+ "apiKey",
339
+ "jwtSecret",
340
+ "token",
341
+ "githubToken"
342
+ ]);
343
+ function redactSecrets(value, parentKey = "") {
344
+ if (Array.isArray(value)) {
345
+ return value.map((v) => redactSecrets(v, parentKey));
346
+ }
347
+ if (isPlainObject(value)) {
348
+ const out = {};
349
+ for (const [k, v] of Object.entries(value)) {
350
+ if (SECRET_KEYS.has(k) && typeof v === "string" && v.length > 0) {
351
+ out[k] = `***redacted (${v.length} chars)`;
352
+ } else if (k === "apiKeys" && Array.isArray(v)) {
353
+ out[k] = v.map((s) => `***redacted (${s.length} chars)`);
354
+ } else {
355
+ out[k] = redactSecrets(v, k);
356
+ }
357
+ }
358
+ return out;
359
+ }
360
+ return value;
361
+ }
362
+ function printConfig(c) {
363
+ return JSON.stringify(redactSecrets(c), null, 2);
364
+ }
365
+ export {
366
+ DEFAULTS,
367
+ parseCliFlags,
368
+ printConfig,
369
+ redactSecrets,
370
+ resolveCodeSearchConfig,
371
+ toKnowledgeConfig
372
+ };
@@ -1,20 +1,23 @@
1
1
  import { execSync as _execSync } from "node:child_process";
2
+ import {
3
+ resolveCodeSearchConfig,
4
+ parseCliFlags
5
+ } from "./config.js";
6
+ import { normalizeRerankerConfig } from "@esankhan3/anvil-knowledge-core";
2
7
  let _config = null;
3
- function resolveLlmMode(explicit, apiKey) {
4
- if (explicit) {
5
- const mode = explicit;
6
- if (mode === "api" && !apiKey) {
8
+ function resolveLlmMode(explicit, apiKey, claudeBin) {
9
+ if (explicit && explicit !== "none") {
10
+ if (explicit === "api" && !apiKey) {
7
11
  console.error(
8
12
  `[code-search-mcp] WARNING: LLM_MODE=api but no API key found. Set CODE_SEARCH_LLM_API_KEY or ANTHROPIC_API_KEY. Falling back to LLM_MODE=none.`
9
13
  );
10
14
  return "none";
11
15
  }
12
- return mode;
16
+ return explicit;
13
17
  }
14
18
  if (apiKey) return "api";
15
19
  try {
16
- const bin = process.env.ANVIL_AGENT_CMD ?? process.env.FF_AGENT_CMD ?? process.env.CLAUDE_BIN ?? "claude";
17
- _execSync(`which ${bin}`, { stdio: "pipe", timeout: 3e3 });
20
+ _execSync(`which ${claudeBin}`, { stdio: "pipe", timeout: 3e3 });
18
21
  return "cli";
19
22
  } catch {
20
23
  }
@@ -23,38 +26,41 @@ function resolveLlmMode(explicit, apiKey) {
23
26
  );
24
27
  return "none";
25
28
  }
26
- function loadServerConfig() {
29
+ function loadServerConfig(opts) {
27
30
  if (_config) return _config;
28
- const env = (key) => process.env[`CODE_SEARCH_${key}`];
29
- const authMode = env("AUTH_MODE") ?? "none";
31
+ const cli = opts?.argv ? parseCliFlags(opts.argv).patch : void 0;
32
+ const c = resolveCodeSearchConfig({ cli, workspaceDir: opts?.workspaceDir });
33
+ const rerankerStruct = normalizeRerankerConfig(c.reranker);
34
+ const llmMode = resolveLlmMode(c.llm.mode, c.llm.apiKey, c.llm.claudeBin);
30
35
  _config = {
31
- port: parseInt(env("PORT") ?? "3100", 10),
32
- host: env("HOST") ?? (authMode === "none" ? "127.0.0.1" : "0.0.0.0"),
33
- transport: env("TRANSPORT") ?? "stdio",
34
- dataDir: env("DATA_DIR") ?? "",
35
- embeddingProvider: env("EMBEDDING_PROVIDER") ?? "auto",
36
- embeddingModel: env("EMBEDDING_MODEL"),
37
- embeddingDimensions: parseInt(env("EMBEDDING_DIMENSIONS") ?? "1024", 10),
38
- embeddingApiKey: env("EMBEDDING_API_KEY"),
39
- embeddingBaseUrl: env("EMBEDDING_BASE_URL"),
40
- ollamaHost: env("OLLAMA_HOST") ?? process.env.OLLAMA_HOST ?? "http://localhost:11434",
41
- rerankerProvider: env("RERANKER_PROVIDER") ?? "ollama",
42
- rerankerModel: env("RERANKER_MODEL"),
43
- rerankerApiKey: env("RERANKER_API_KEY"),
44
- rerankerBaseUrl: env("RERANKER_BASE_URL"),
45
- githubToken: env("GITHUB_TOKEN") ?? process.env.GITHUB_TOKEN,
46
- authEnabled: authMode !== "none",
47
- authMode,
48
- authApiKeys: (env("AUTH_API_KEYS") ?? "").split(",").map((k) => k.trim()).filter(Boolean),
49
- authJwtSecret: env("AUTH_JWT_SECRET"),
50
- authJwtIssuer: env("AUTH_JWT_ISSUER") ?? "code-search-mcp",
51
- rateLimitPerMinute: parseInt(env("RATE_LIMIT_PER_MINUTE") ?? "100", 10),
52
- llmMode: resolveLlmMode(env("LLM_MODE"), env("LLM_API_KEY") ?? process.env.ANTHROPIC_API_KEY),
53
- llmProvider: env("LLM_PROVIDER") ?? "anthropic",
54
- llmModel: env("LLM_MODEL") ?? "sonnet",
55
- llmApiKey: env("LLM_API_KEY") ?? process.env.ANTHROPIC_API_KEY,
56
- llmBaseUrl: env("LLM_BASE_URL"),
57
- claudeBin: env("CLAUDE_BIN") ?? process.env.CLAUDE_BIN ?? "claude"
36
+ port: c.server.port,
37
+ host: c.server.host,
38
+ transport: c.server.transport,
39
+ dataDir: c.storage.dataDir,
40
+ embeddingProvider: c.embedding.provider,
41
+ embeddingModel: c.embedding.model,
42
+ embeddingDimensions: c.embedding.dimensions ?? 1024,
43
+ embeddingApiKey: c.embedding.apiKey,
44
+ embeddingBaseUrl: c.embedding.baseUrl,
45
+ ollamaHost: c.embedding.ollamaHost ?? "http://localhost:11434",
46
+ rerankerProvider: rerankerStruct.provider,
47
+ rerankerModel: rerankerStruct.model,
48
+ rerankerApiKey: rerankerStruct.apiKey,
49
+ rerankerBaseUrl: rerankerStruct.baseUrl,
50
+ githubToken: c.github.token,
51
+ authEnabled: c.auth.mode !== "none",
52
+ authMode: c.auth.mode,
53
+ authApiKeys: c.auth.apiKeys,
54
+ authJwtSecret: c.auth.jwtSecret,
55
+ authJwtIssuer: c.auth.jwtIssuer,
56
+ rateLimitPerMinute: c.auth.rateLimitPerMinute,
57
+ llmMode,
58
+ llmProvider: c.llm.provider,
59
+ llmModel: c.llm.model,
60
+ llmApiKey: c.llm.apiKey,
61
+ llmBaseUrl: c.llm.baseUrl,
62
+ claudeBin: c.llm.claudeBin,
63
+ __unified: c
58
64
  };
59
65
  if (_config.embeddingApiKey) {
60
66
  const provider = _config.embeddingProvider;
@@ -67,12 +73,8 @@ function loadServerConfig() {
67
73
  }
68
74
  }
69
75
  if (_config.rerankerApiKey) {
70
- const reranker = _config.rerankerProvider;
71
- if (reranker === "cohere") {
72
- process.env.COHERE_API_KEY ??= _config.rerankerApiKey;
73
- } else if (reranker === "voyage") {
74
- process.env.VOYAGE_API_KEY ??= _config.rerankerApiKey;
75
- }
76
+ if (_config.rerankerProvider === "cohere") process.env.COHERE_API_KEY ??= _config.rerankerApiKey;
77
+ if (_config.rerankerProvider === "voyage") process.env.VOYAGE_API_KEY ??= _config.rerankerApiKey;
76
78
  }
77
79
  process.env.OLLAMA_HOST ??= _config.ollamaHost;
78
80
  return _config;
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { resolveCodeSearchConfig, parseCliFlags, toKnowledgeConfig } from "../core/config.js";
5
+ import { InProcessBackend } from "../backends/in-process.js";
6
+ import { daemonSocketPath } from "../backends/index.js";
7
+ import { RpcServer } from "./rpc-server.js";
8
+ import { Watcher } from "./watcher.js";
9
+ async function main() {
10
+ const argv = process.argv.slice(2);
11
+ const { patch, rest } = parseCliFlags(argv);
12
+ let workspaceDir = null;
13
+ for (let i = 0; i < rest.length; i++) {
14
+ if (rest[i] === "--workspace" && rest[i + 1]) workspaceDir = rest[++i];
15
+ }
16
+ if (!workspaceDir) {
17
+ process.stderr.write("code-search-daemon: missing --workspace <path>\n");
18
+ process.exit(2);
19
+ }
20
+ if (!existsSync(workspaceDir)) {
21
+ process.stderr.write(`code-search-daemon: workspace does not exist: ${workspaceDir}
22
+ `);
23
+ process.exit(2);
24
+ }
25
+ const cfg = resolveCodeSearchConfig({ cli: patch, workspaceDir });
26
+ const project = workspaceDir.split("/").filter(Boolean).pop() || "project";
27
+ const knowledge = toKnowledgeConfig(cfg);
28
+ const backend = new InProcessBackend({
29
+ project,
30
+ workspaceDir,
31
+ knowledge,
32
+ preferDaemon: false
33
+ });
34
+ process.stderr.write(`[code-search-daemon] starting for project="${project}", workspace=${workspaceDir}
35
+ `);
36
+ process.stderr.write("[code-search-daemon] building initial index...\n");
37
+ await backend.forceIndex();
38
+ process.stderr.write("[code-search-daemon] initial index ready.\n");
39
+ const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
40
+ const pidFile = join(cfg.storage.dataDir, "daemon", `${project}.pid`);
41
+ let queueDepth = 0;
42
+ let indexing = false;
43
+ const triggerReindex = async () => {
44
+ if (indexing) {
45
+ queueDepth++;
46
+ return;
47
+ }
48
+ indexing = true;
49
+ try {
50
+ while (queueDepth >= 0) {
51
+ await backend.forceIndex();
52
+ if (queueDepth === 0) break;
53
+ queueDepth--;
54
+ }
55
+ } catch (err) {
56
+ process.stderr.write(`[code-search-daemon] reindex failed: ${err instanceof Error ? err.message : err}
57
+ `);
58
+ } finally {
59
+ indexing = false;
60
+ }
61
+ };
62
+ const watcher = new Watcher({
63
+ workspaceDir,
64
+ ignorePatterns: cfg.indexing.ignorePatterns,
65
+ debounceMs: cfg.indexing.debounceMs
66
+ });
67
+ watcher.on("error", (err) => {
68
+ process.stderr.write(`[code-search-daemon] watcher error: ${err instanceof Error ? err.message : err}
69
+ `);
70
+ });
71
+ watcher.onBatch((batch) => {
72
+ process.stderr.write(`[code-search-daemon] reindex queued (${batch.changed.size} changed, ${batch.removed.size} removed)
73
+ `);
74
+ queueDepth++;
75
+ void triggerReindex();
76
+ });
77
+ watcher.start();
78
+ const startedAt = Date.now();
79
+ const rpc = new RpcServer({
80
+ socketPath,
81
+ handlers: {
82
+ "search.code": (p) => backend.search(p.query, { mode: p.mode ?? "hybrid", maxResults: p.maxResults, repos: p.repos }),
83
+ "index.status": async () => {
84
+ const s = await backend.status();
85
+ return { ...s, watching: true, queueDepth };
86
+ },
87
+ "index.force": async (p) => {
88
+ await backend.forceIndex({ force: p?.force });
89
+ return backend.status();
90
+ },
91
+ "index.invalidate": async (p) => {
92
+ await backend.invalidate(p.paths ?? []);
93
+ return { ok: true };
94
+ },
95
+ "health": () => ({ ok: true, uptime: Math.floor((Date.now() - startedAt) / 1e3) })
96
+ }
97
+ });
98
+ await rpc.start();
99
+ process.stderr.write(`[code-search-daemon] RPC listening on ${socketPath}
100
+ `);
101
+ try {
102
+ writeFileSync(pidFile, String(process.pid), "utf-8");
103
+ } catch {
104
+ }
105
+ const shutdown = async (signal) => {
106
+ process.stderr.write(`[code-search-daemon] ${signal} \u2014 draining...
107
+ `);
108
+ watcher.stop();
109
+ await rpc.stop();
110
+ process.exit(0);
111
+ };
112
+ process.on("SIGINT", () => void shutdown("SIGINT"));
113
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
114
+ }
115
+ main().catch((err) => {
116
+ process.stderr.write(`code-search-daemon fatal: ${err instanceof Error ? err.stack : err}
117
+ `);
118
+ process.exit(1);
119
+ });