@esankhan3/code-search-mcp 0.0.2 → 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.
Files changed (50) hide show
  1. package/README.md +343 -248
  2. package/dist/__tests__/config-resolver.test.js +153 -0
  3. package/dist/__tests__/daemon-rpc.test.js +100 -0
  4. package/dist/__tests__/observability.test.js +69 -0
  5. package/dist/__tests__/projects.test.js +141 -0
  6. package/dist/backends/daemon-client.js +87 -0
  7. package/dist/backends/in-process.js +104 -0
  8. package/dist/backends/index.js +29 -0
  9. package/dist/cli/index.js +231 -0
  10. package/dist/core/config.js +362 -49
  11. package/dist/core/env-config.js +45 -43
  12. package/dist/daemon/index.js +119 -0
  13. package/dist/daemon/rpc-server.js +99 -0
  14. package/dist/daemon/watcher.js +105 -0
  15. package/dist/index.js +7 -0
  16. package/dist/observability/logger.js +66 -0
  17. package/dist/observability/metrics.js +138 -0
  18. package/dist/projects/registry.js +146 -0
  19. package/dist/resources/resources.js +3 -3
  20. package/dist/server.js +6 -2
  21. package/dist/tools/graph.js +1 -1
  22. package/dist/tools/index-tools.js +1 -1
  23. package/dist/tools/profile.js +2 -2
  24. package/dist/tools/search.js +1 -1
  25. package/dist/transports/http-transport.js +38 -0
  26. package/package.json +31 -12
  27. package/scripts/smoke/run-smoke.sh +167 -0
  28. package/dist/core/ast-graph-builder.js +0 -800
  29. package/dist/core/chunker.js +0 -368
  30. package/dist/core/claude-runner.js +0 -173
  31. package/dist/core/cross-repo-detector.js +0 -887
  32. package/dist/core/embedder.js +0 -334
  33. package/dist/core/file-walker.js +0 -186
  34. package/dist/core/git-diff.js +0 -160
  35. package/dist/core/graph-metrics.js +0 -233
  36. package/dist/core/indexer.js +0 -552
  37. package/dist/core/project-graph-builder.js +0 -202
  38. package/dist/core/query-classifier.js +0 -144
  39. package/dist/core/query-router.js +0 -210
  40. package/dist/core/rag-evaluator.js +0 -213
  41. package/dist/core/repo-profiler.js +0 -383
  42. package/dist/core/reranker.js +0 -218
  43. package/dist/core/retriever.js +0 -242
  44. package/dist/core/semantic-edge-detector.js +0 -147
  45. package/dist/core/service-mesh-inferrer.js +0 -241
  46. package/dist/core/structural-hasher.js +0 -316
  47. package/dist/core/tree-sitter-parser.js +0 -539
  48. package/dist/core/vector-store.js +0 -259
  49. package/dist/core/workspace-detector.js +0 -481
  50. /package/dist/{core → backends}/types.js +0 -0
@@ -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
+ });
@@ -0,0 +1,99 @@
1
+ import { createServer } from "node:net";
2
+ import { existsSync, unlinkSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { EventEmitter } from "node:events";
5
+ class RpcServer extends EventEmitter {
6
+ socketPath;
7
+ handlers;
8
+ server = null;
9
+ clients = /* @__PURE__ */ new Set();
10
+ constructor(opts) {
11
+ super();
12
+ this.socketPath = opts.socketPath;
13
+ this.handlers = opts.handlers;
14
+ }
15
+ async start() {
16
+ if (process.platform !== "win32" && existsSync(this.socketPath)) {
17
+ try {
18
+ unlinkSync(this.socketPath);
19
+ } catch {
20
+ }
21
+ }
22
+ mkdirSync(dirname(this.socketPath), { recursive: true });
23
+ await new Promise((resolveP, rejectP) => {
24
+ this.server = createServer((sock) => this.handleClient(sock));
25
+ this.server.on("error", (err) => {
26
+ this.emit("error", err);
27
+ rejectP(err);
28
+ });
29
+ this.server.listen(this.socketPath, () => {
30
+ this.emit("listening", this.socketPath);
31
+ resolveP();
32
+ });
33
+ });
34
+ }
35
+ async stop() {
36
+ for (const c of this.clients) {
37
+ try {
38
+ c.destroy();
39
+ } catch {
40
+ }
41
+ }
42
+ this.clients.clear();
43
+ if (this.server) {
44
+ await new Promise((r) => this.server.close(() => r()));
45
+ this.server = null;
46
+ }
47
+ if (process.platform !== "win32" && existsSync(this.socketPath)) {
48
+ try {
49
+ unlinkSync(this.socketPath);
50
+ } catch {
51
+ }
52
+ }
53
+ }
54
+ handleClient(sock) {
55
+ this.clients.add(sock);
56
+ let buf = "";
57
+ sock.setNoDelay(true);
58
+ sock.on("end", () => this.clients.delete(sock));
59
+ sock.on("error", () => this.clients.delete(sock));
60
+ sock.on("data", async (chunk) => {
61
+ buf += chunk.toString("utf-8");
62
+ let idx = buf.indexOf("\n");
63
+ while (idx >= 0) {
64
+ const line = buf.slice(0, idx);
65
+ buf = buf.slice(idx + 1);
66
+ await this.handleLine(sock, line);
67
+ idx = buf.indexOf("\n");
68
+ }
69
+ });
70
+ }
71
+ async handleLine(sock, line) {
72
+ if (!line.trim()) return;
73
+ let req;
74
+ try {
75
+ req = JSON.parse(line);
76
+ } catch (err) {
77
+ sock.write(this.errorEnvelope(null, -32700, err instanceof Error ? err.message : String(err)));
78
+ return;
79
+ }
80
+ const handler = this.handlers[req.method];
81
+ if (!handler) {
82
+ sock.write(this.errorEnvelope(req.id ?? null, -32601, `Method not found: ${req.method}`));
83
+ return;
84
+ }
85
+ try {
86
+ const result = await handler(req.params);
87
+ sock.write(JSON.stringify({ jsonrpc: "2.0", id: req.id ?? null, result }) + "\n");
88
+ } catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ sock.write(this.errorEnvelope(req.id ?? null, -32e3, msg));
91
+ }
92
+ }
93
+ errorEnvelope(id, code, message) {
94
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n";
95
+ }
96
+ }
97
+ export {
98
+ RpcServer
99
+ };
@@ -0,0 +1,105 @@
1
+ import { watch, statSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { EventEmitter } from "node:events";
4
+ const HAS_RECURSIVE = process.platform === "darwin" || process.platform === "win32";
5
+ class Watcher extends EventEmitter {
6
+ workspaceDir;
7
+ ignore;
8
+ debounceMs;
9
+ watchers = [];
10
+ pending = /* @__PURE__ */ new Set();
11
+ timer = null;
12
+ started = false;
13
+ constructor(opts) {
14
+ super();
15
+ this.workspaceDir = opts.workspaceDir;
16
+ this.ignore = opts.ignorePatterns;
17
+ this.debounceMs = Math.max(50, opts.debounceMs);
18
+ }
19
+ start() {
20
+ if (this.started) return;
21
+ this.started = true;
22
+ if (HAS_RECURSIVE) {
23
+ const w = watch(this.workspaceDir, { recursive: true }, (_event, filename) => {
24
+ if (!filename) return;
25
+ this.queue(filename.toString());
26
+ });
27
+ w.on("error", (err) => this.emit("error", err));
28
+ this.watchers.push(w);
29
+ } else {
30
+ this.walkAndWatch(this.workspaceDir);
31
+ }
32
+ }
33
+ stop() {
34
+ if (!this.started) return;
35
+ this.started = false;
36
+ if (this.timer) clearTimeout(this.timer);
37
+ this.timer = null;
38
+ for (const w of this.watchers) {
39
+ try {
40
+ w.close();
41
+ } catch {
42
+ }
43
+ }
44
+ this.watchers = [];
45
+ this.pending.clear();
46
+ }
47
+ onBatch(listener) {
48
+ return this.on("batch", listener);
49
+ }
50
+ walkAndWatch(dir) {
51
+ if (this.shouldIgnore(dir)) return;
52
+ try {
53
+ const w = watch(dir, (_event, filename) => {
54
+ if (!filename) return;
55
+ const full = join(dir, filename.toString());
56
+ this.queue(relative(this.workspaceDir, full));
57
+ try {
58
+ if (statSync(full).isDirectory()) this.walkAndWatch(full);
59
+ } catch {
60
+ }
61
+ });
62
+ w.on("error", (err) => this.emit("error", err));
63
+ this.watchers.push(w);
64
+ for (const entry of readdirSync(dir)) {
65
+ const full = join(dir, entry);
66
+ try {
67
+ if (statSync(full).isDirectory()) this.walkAndWatch(full);
68
+ } catch {
69
+ }
70
+ }
71
+ } catch {
72
+ }
73
+ }
74
+ queue(rel) {
75
+ if (this.shouldIgnore(rel)) return;
76
+ this.pending.add(join(this.workspaceDir, rel));
77
+ if (this.timer) clearTimeout(this.timer);
78
+ this.timer = setTimeout(() => this.flush(), this.debounceMs);
79
+ }
80
+ flush() {
81
+ const batch = { changed: new Set(this.pending), removed: /* @__PURE__ */ new Set() };
82
+ this.pending.clear();
83
+ this.timer = null;
84
+ for (const p of batch.changed) {
85
+ try {
86
+ statSync(p);
87
+ } catch {
88
+ batch.removed.add(p);
89
+ }
90
+ }
91
+ for (const r of batch.removed) batch.changed.delete(r);
92
+ this.emit("batch", batch);
93
+ }
94
+ shouldIgnore(path) {
95
+ for (const pat of this.ignore) {
96
+ if (path.includes(`/${pat}/`) || path.endsWith(`/${pat}`) || path.startsWith(`${pat}/`) || path === pat) {
97
+ return true;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+ }
103
+ export {
104
+ Watcher
105
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
+ import { printConfig, resolveCodeSearchConfig, parseCliFlags } from "./core/config.js";
4
5
  const args = process.argv.slice(2);
6
+ if (args.includes("--print-config")) {
7
+ const { patch } = parseCliFlags(args.filter((a) => a !== "--print-config"));
8
+ const cfg = resolveCodeSearchConfig({ cli: patch });
9
+ process.stdout.write(printConfig(cfg) + "\n");
10
+ process.exit(0);
11
+ }
5
12
  let mode = "remote";
6
13
  let remoteUrl = null;
7
14
  let apiKey;
@@ -0,0 +1,66 @@
1
+ const LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
2
+ class Logger {
3
+ opts;
4
+ constructor(opts) {
5
+ this.opts = {
6
+ structured: opts.structured,
7
+ level: opts.level ?? "info",
8
+ write: opts.write ?? ((line) => process.stderr.write(line + "\n"))
9
+ };
10
+ }
11
+ debug(msg, fields) {
12
+ this.emit("debug", msg, fields);
13
+ }
14
+ info(msg, fields) {
15
+ this.emit("info", msg, fields);
16
+ }
17
+ warn(msg, fields) {
18
+ this.emit("warn", msg, fields);
19
+ }
20
+ error(msg, fields) {
21
+ this.emit("error", msg, fields);
22
+ }
23
+ /** Bind a component name so every line carries it. */
24
+ child(component) {
25
+ return new Logger({
26
+ structured: this.opts.structured,
27
+ level: this.opts.level,
28
+ write: (line) => this.opts.write(line)
29
+ }).withComponent(component);
30
+ }
31
+ /** Internal — return a logger that prefixes the component field. */
32
+ withComponent(component) {
33
+ const self = this;
34
+ return new Proxy(self, {
35
+ get(target, prop) {
36
+ if (prop === "emit") {
37
+ return (level, msg, fields) => self.emit(level, msg, { component, ...fields });
38
+ }
39
+ return target[prop];
40
+ }
41
+ });
42
+ }
43
+ emit(level, msg, fields) {
44
+ if (LEVEL_RANK[level] < LEVEL_RANK[this.opts.level]) return;
45
+ if (this.opts.structured) {
46
+ const entry = { level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), ...fields };
47
+ this.opts.write(JSON.stringify(entry));
48
+ } else {
49
+ const comp = fields?.component ? `[${fields.component}]` : "";
50
+ this.opts.write(`${level.toUpperCase()} ${comp} ${msg}`);
51
+ }
52
+ }
53
+ }
54
+ let _default = null;
55
+ function getLogger() {
56
+ if (!_default) _default = new Logger({ structured: false });
57
+ return _default;
58
+ }
59
+ function configureLogger(opts) {
60
+ _default = new Logger(opts);
61
+ }
62
+ export {
63
+ Logger,
64
+ configureLogger,
65
+ getLogger
66
+ };
@@ -0,0 +1,138 @@
1
+ function formatLabels(labels) {
2
+ if (!labels) return "";
3
+ const parts = [];
4
+ for (const [k, v] of Object.entries(labels)) {
5
+ parts.push(`${k}="${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
6
+ }
7
+ return parts.length ? `{${parts.join(",")}}` : "";
8
+ }
9
+ function labelKey(labels) {
10
+ if (!labels) return "";
11
+ const keys = Object.keys(labels).sort();
12
+ return keys.map((k) => `${k}=${labels[k]}`).join("|");
13
+ }
14
+ class Counter {
15
+ name;
16
+ help;
17
+ buckets = /* @__PURE__ */ new Map();
18
+ constructor(name, help) {
19
+ this.name = name;
20
+ this.help = help;
21
+ }
22
+ inc(labels, value = 1) {
23
+ const key = labelKey(labels);
24
+ const existing = this.buckets.get(key);
25
+ if (existing) existing.value += value;
26
+ else this.buckets.set(key, { labels, value });
27
+ }
28
+ render() {
29
+ const lines = [];
30
+ lines.push(`# HELP ${this.name} ${this.help}`);
31
+ lines.push(`# TYPE ${this.name} counter`);
32
+ for (const { labels, value } of this.buckets.values()) {
33
+ lines.push(`${this.name}${formatLabels(labels)} ${value}`);
34
+ }
35
+ return lines.join("\n");
36
+ }
37
+ }
38
+ class Gauge {
39
+ name;
40
+ help;
41
+ buckets = /* @__PURE__ */ new Map();
42
+ constructor(name, help) {
43
+ this.name = name;
44
+ this.help = help;
45
+ }
46
+ set(value, labels) {
47
+ this.buckets.set(labelKey(labels), { labels, value });
48
+ }
49
+ render() {
50
+ const lines = [];
51
+ lines.push(`# HELP ${this.name} ${this.help}`);
52
+ lines.push(`# TYPE ${this.name} gauge`);
53
+ for (const { labels, value } of this.buckets.values()) {
54
+ lines.push(`${this.name}${formatLabels(labels)} ${value}`);
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+ }
59
+ const DEFAULT_BUCKETS = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
60
+ class Histogram {
61
+ name;
62
+ help;
63
+ bucketBounds;
64
+ buckets = /* @__PURE__ */ new Map();
65
+ constructor(name, help, bucketBounds = DEFAULT_BUCKETS) {
66
+ this.name = name;
67
+ this.help = help;
68
+ this.bucketBounds = bucketBounds;
69
+ }
70
+ observe(value, labels) {
71
+ const key = labelKey(labels);
72
+ let entry = this.buckets.get(key);
73
+ if (!entry) {
74
+ entry = {
75
+ labels,
76
+ counts: new Array(this.bucketBounds.length).fill(0),
77
+ sum: 0,
78
+ total: 0
79
+ };
80
+ this.buckets.set(key, entry);
81
+ }
82
+ for (let i = 0; i < this.bucketBounds.length; i++) {
83
+ if (value <= this.bucketBounds[i]) entry.counts[i]++;
84
+ }
85
+ entry.sum += value;
86
+ entry.total++;
87
+ }
88
+ render() {
89
+ const lines = [];
90
+ lines.push(`# HELP ${this.name} ${this.help}`);
91
+ lines.push(`# TYPE ${this.name} histogram`);
92
+ for (const entry of this.buckets.values()) {
93
+ let cumulative = 0;
94
+ for (let i = 0; i < this.bucketBounds.length; i++) {
95
+ cumulative = entry.counts[i];
96
+ const labels = { ...entry.labels, le: this.bucketBounds[i] };
97
+ lines.push(`${this.name}_bucket${formatLabels(labels)} ${cumulative}`);
98
+ }
99
+ lines.push(`${this.name}_bucket${formatLabels({ ...entry.labels, le: "+Inf" })} ${entry.total}`);
100
+ lines.push(`${this.name}_sum${formatLabels(entry.labels)} ${entry.sum}`);
101
+ lines.push(`${this.name}_count${formatLabels(entry.labels)} ${entry.total}`);
102
+ }
103
+ return lines.join("\n");
104
+ }
105
+ }
106
+ class Registry {
107
+ collectors = /* @__PURE__ */ new Map();
108
+ register(metric) {
109
+ if (!this.collectors.has(metric.name)) this.collectors.set(metric.name, metric);
110
+ return this.collectors.get(metric.name);
111
+ }
112
+ render() {
113
+ const parts = [];
114
+ for (const c of this.collectors.values()) parts.push(c.render());
115
+ return parts.join("\n") + "\n";
116
+ }
117
+ reset() {
118
+ this.collectors.clear();
119
+ }
120
+ }
121
+ const registry = new Registry();
122
+ const metrics = {
123
+ queriesTotal: registry.register(new Counter("code_search_queries_total", "Total search queries by mode + repo.")),
124
+ queryDuration: registry.register(new Histogram("code_search_query_duration_seconds", "Query latency by mode.")),
125
+ indexChunks: registry.register(new Gauge("code_search_index_chunks_total", "Indexed chunks by repo.")),
126
+ indexAge: registry.register(new Gauge("code_search_index_age_seconds", "Seconds since last index by repo.")),
127
+ embeddingCalls: registry.register(new Counter("code_search_embeddings_calls_total", "Embedding API calls by provider.")),
128
+ llmCalls: registry.register(new Counter("code_search_llm_calls_total", "LLM API calls by phase.")),
129
+ errors: registry.register(new Counter("code_search_errors_total", "Errors by kind.")),
130
+ rerankerCacheHits: registry.register(new Counter("code_search_reranker_cache_hits_total", "Rerank cache lookups by outcome."))
131
+ };
132
+ export {
133
+ Counter,
134
+ Gauge,
135
+ Histogram,
136
+ metrics,
137
+ registry
138
+ };