@esankhan3/code-search-mcp 1.0.6 → 1.1.1

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 (42) hide show
  1. package/README.md +114 -54
  2. package/dist/__tests__/_harness.js +194 -0
  3. package/dist/__tests__/config-cli.test.js +110 -0
  4. package/dist/__tests__/config-resolver.test.js +1 -1
  5. package/dist/__tests__/daemon-rpc-protocol.test.js +95 -0
  6. package/dist/__tests__/daemon-runtime.test.js +145 -0
  7. package/dist/__tests__/graph-tools-store.test.js +314 -0
  8. package/dist/__tests__/graph-tools.test.js +11 -4
  9. package/dist/__tests__/graph-traversal.test.js +262 -0
  10. package/dist/__tests__/http-auth.test.js +180 -0
  11. package/dist/__tests__/http-index.test.js +125 -0
  12. package/dist/__tests__/metrics-collect.test.js +48 -0
  13. package/dist/__tests__/observability.test.js +3 -4
  14. package/dist/__tests__/remote-proxy.test.js +105 -0
  15. package/dist/__tests__/server-cli-args.test.js +146 -0
  16. package/dist/__tests__/server-lifecycle.test.js +108 -0
  17. package/dist/__tests__/tool-inputs.test.js +178 -0
  18. package/dist/backends/in-process.js +5 -2
  19. package/dist/backends/index.js +2 -2
  20. package/dist/cli/index.js +143 -76
  21. package/dist/core/config.js +106 -35
  22. package/dist/core/env-config.js +3 -5
  23. package/dist/core/project-name.js +12 -0
  24. package/dist/core/version.js +5 -0
  25. package/dist/daemon/index.js +106 -65
  26. package/dist/daemon/rpc-server.js +38 -18
  27. package/dist/index.js +113 -89
  28. package/dist/middleware/auth.js +34 -66
  29. package/dist/middleware/rate-limit.js +37 -0
  30. package/dist/observability/logger.js +10 -2
  31. package/dist/observability/metrics.js +7 -6
  32. package/dist/projects/registry.js +13 -15
  33. package/dist/resources/resources.js +17 -0
  34. package/dist/server.js +216 -129
  35. package/dist/tools/graph.js +448 -165
  36. package/dist/tools/index-tools.js +6 -2
  37. package/dist/tools/profile.js +14 -8
  38. package/dist/tools/search.js +61 -51
  39. package/dist/tools/validate.js +48 -0
  40. package/dist/transports/http-transport.js +94 -55
  41. package/dist/transports/remote-proxy.js +58 -97
  42. package/package.json +2 -2
@@ -1,63 +1,83 @@
1
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";
2
+ import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { resolveCodeSearchConfig, parseCliFlags, toKnowledgeConfig, exportDataDir } from "../core/config.js";
5
+ import { validateProjectName } from "../core/project-name.js";
5
6
  import { InProcessBackend } from "../backends/in-process.js";
6
- import { daemonSocketPath } from "../backends/index.js";
7
+ import { DaemonBackend, daemonSocketPath } from "../backends/index.js";
8
+ import { configureLogger, getComponentLogger } from "../observability/logger.js";
7
9
  import { RpcServer } from "./rpc-server.js";
8
10
  import { Watcher } from "./watcher.js";
11
+ const log = getComponentLogger("code-search-daemon");
12
+ function usageError(msg) {
13
+ process.stderr.write(`code-search-daemon: ${msg}
14
+ `);
15
+ process.exit(2);
16
+ }
9
17
  async function main() {
10
- const argv = process.argv.slice(2);
11
- const { patch, rest } = parseCliFlags(argv);
12
- let workspaceDir = null;
18
+ const { patch, rest, unknown } = parseCliFlags(process.argv.slice(2));
19
+ if (unknown.length) usageError(`unknown flag(s): ${unknown.join(", ")}`);
20
+ let workspaceArg = null;
13
21
  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);
22
+ if (rest[i] === "--workspace" && rest[i + 1]) workspaceArg = rest[++i];
23
+ else usageError(`unexpected argument: ${rest[i]} (usage: code-search-daemon --workspace <path> [--<config.path> <value>])`);
24
24
  }
25
+ if (!workspaceArg) usageError("missing --workspace <path>");
26
+ const workspaceDir = resolve(workspaceArg);
27
+ if (!existsSync(workspaceDir)) usageError(`workspace does not exist: ${workspaceDir}`);
25
28
  const cfg = resolveCodeSearchConfig({ cli: patch, workspaceDir });
26
- const project = workspaceDir.split("/").filter(Boolean).pop() || "project";
29
+ configureLogger({ structured: cfg.telemetry.structuredLogs });
30
+ exportDataDir(cfg);
31
+ let project;
32
+ try {
33
+ project = validateProjectName(basename(workspaceDir));
34
+ } catch (err) {
35
+ usageError(err.message);
36
+ }
27
37
  const knowledge = toKnowledgeConfig(cfg);
38
+ const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
39
+ const pidFile = join(cfg.storage.dataDir, "daemon", `${project}.pid`);
40
+ if (existsSync(socketPath)) {
41
+ const live = await new DaemonBackend({ project, workspaceDir, knowledge, preferDaemon: true, socketPath }).ping();
42
+ if (live) {
43
+ const pid = existsSync(pidFile) ? readFileSync(pidFile, "utf-8").trim() : "unknown";
44
+ log.error(`a daemon for project "${project}" is already running (pid ${pid}, socket ${socketPath}) \u2014 not starting another`);
45
+ process.exit(1);
46
+ }
47
+ if (process.platform !== "win32") {
48
+ try {
49
+ unlinkSync(socketPath);
50
+ } catch {
51
+ }
52
+ }
53
+ }
28
54
  const backend = new InProcessBackend({
29
55
  project,
30
56
  workspaceDir,
31
57
  knowledge,
32
58
  preferDaemon: false
33
59
  });
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`);
60
+ let tail = Promise.resolve();
41
61
  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;
62
+ let watcherRunPending = false;
63
+ const enqueue = (reason, run) => {
64
+ queueDepth++;
65
+ const job = tail.then(async () => {
66
+ log.info(`reindex started (${reason})`);
67
+ try {
68
+ const out = await run();
69
+ log.info(`reindex finished (${reason})`);
70
+ return out;
71
+ } catch (err) {
72
+ log.error(`reindex failed (${reason}): ${err instanceof Error ? err.message : err}`);
73
+ throw err;
74
+ } finally {
53
75
  queueDepth--;
54
76
  }
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
- }
77
+ });
78
+ tail = job.catch(() => {
79
+ });
80
+ return job;
61
81
  };
62
82
  const watcher = new Watcher({
63
83
  workspaceDir,
@@ -65,16 +85,18 @@ async function main() {
65
85
  debounceMs: cfg.indexing.debounceMs
66
86
  });
67
87
  watcher.on("error", (err) => {
68
- process.stderr.write(`[code-search-daemon] watcher error: ${err instanceof Error ? err.message : err}
69
- `);
88
+ log.error(`watcher error: ${err instanceof Error ? err.message : err}`);
70
89
  });
71
90
  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();
91
+ log.info(`reindex queued (${batch.changed.size} changed, ${batch.removed.size} removed)`);
92
+ if (watcherRunPending) return;
93
+ watcherRunPending = true;
94
+ enqueue("watcher", () => {
95
+ watcherRunPending = false;
96
+ return backend.forceIndex();
97
+ }).catch(() => {
98
+ });
76
99
  });
77
- watcher.start();
78
100
  const startedAt = Date.now();
79
101
  const rpc = new RpcServer({
80
102
  socketPath,
@@ -84,36 +106,55 @@ async function main() {
84
106
  const s = await backend.status();
85
107
  return { ...s, watching: true, queueDepth };
86
108
  },
87
- "index.force": async (p) => {
88
- await backend.forceIndex({ force: p?.force });
89
- return backend.status();
90
- },
109
+ "index.force": (p) => enqueue("rpc", () => backend.forceIndex({ force: p?.force })),
91
110
  "index.invalidate": async (p) => {
92
- await backend.invalidate(p.paths ?? []);
111
+ await backend.invalidate(p?.paths ?? []);
93
112
  return { ok: true };
94
113
  },
95
114
  "health": () => ({ ok: true, uptime: Math.floor((Date.now() - startedAt) / 1e3) })
96
115
  }
97
116
  });
98
- await rpc.start();
99
- process.stderr.write(`[code-search-daemon] RPC listening on ${socketPath}
100
- `);
101
117
  try {
118
+ await rpc.start();
119
+ } catch (err) {
120
+ log.error(`cannot listen on ${socketPath}: ${err instanceof Error ? err.message : err} (another daemon for "${project}" starting?)`);
121
+ process.exit(1);
122
+ }
123
+ log.info(`RPC listening on ${socketPath}`);
124
+ try {
125
+ mkdirSync(dirname(pidFile), { recursive: true });
102
126
  writeFileSync(pidFile, String(process.pid), "utf-8");
103
127
  } catch {
104
128
  }
105
- const shutdown = async (signal) => {
106
- process.stderr.write(`[code-search-daemon] ${signal} \u2014 draining...
107
- `);
129
+ let stopping = false;
130
+ const stop = async (code) => {
131
+ if (stopping) return;
132
+ stopping = true;
108
133
  watcher.stop();
109
134
  await rpc.stop();
110
- process.exit(0);
135
+ try {
136
+ if (readFileSync(pidFile, "utf-8").trim() === String(process.pid)) unlinkSync(pidFile);
137
+ } catch {
138
+ }
139
+ process.exit(code);
111
140
  };
112
- process.on("SIGINT", () => void shutdown("SIGINT"));
113
- process.on("SIGTERM", () => void shutdown("SIGTERM"));
141
+ for (const sig of ["SIGINT", "SIGTERM"]) {
142
+ process.on(sig, () => {
143
+ log.info(`${sig} \u2014 draining...`);
144
+ void stop(0);
145
+ });
146
+ }
147
+ log.info(`starting for project="${project}", workspace=${workspaceDir}`);
148
+ log.info("building initial index...");
149
+ try {
150
+ await enqueue("initial", () => backend.forceIndex());
151
+ } catch {
152
+ await stop(1);
153
+ }
154
+ log.info("initial index ready.");
155
+ watcher.start();
114
156
  }
115
157
  main().catch((err) => {
116
- process.stderr.write(`code-search-daemon fatal: ${err instanceof Error ? err.stack : err}
117
- `);
158
+ log.error(`fatal: ${err instanceof Error ? err.stack : err}`);
118
159
  process.exit(1);
119
160
  });
@@ -1,5 +1,5 @@
1
1
  import { createServer } from "node:net";
2
- import { existsSync, unlinkSync, mkdirSync } from "node:fs";
2
+ import { mkdirSync, statSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { EventEmitter } from "node:events";
5
5
  class RpcServer extends EventEmitter {
@@ -7,18 +7,16 @@ class RpcServer extends EventEmitter {
7
7
  handlers;
8
8
  server = null;
9
9
  clients = /* @__PURE__ */ new Set();
10
+ /** Inode of the socket file this server created — stop() unlinks only that. */
11
+ socketIno = null;
10
12
  constructor(opts) {
11
13
  super();
12
14
  this.socketPath = opts.socketPath;
13
15
  this.handlers = opts.handlers;
14
16
  }
17
+ /** Listen on the socket. An existing socket file is NOT removed — the
18
+ * caller decides whether it is stale; listening then fails EADDRINUSE. */
15
19
  async start() {
16
- if (process.platform !== "win32" && existsSync(this.socketPath)) {
17
- try {
18
- unlinkSync(this.socketPath);
19
- } catch {
20
- }
21
- }
22
20
  mkdirSync(dirname(this.socketPath), { recursive: true });
23
21
  await new Promise((resolveP, rejectP) => {
24
22
  this.server = createServer((sock) => this.handleClient(sock));
@@ -27,6 +25,12 @@ class RpcServer extends EventEmitter {
27
25
  rejectP(err);
28
26
  });
29
27
  this.server.listen(this.socketPath, () => {
28
+ if (process.platform !== "win32") {
29
+ try {
30
+ this.socketIno = statSync(this.socketPath).ino;
31
+ } catch {
32
+ }
33
+ }
30
34
  this.emit("listening", this.socketPath);
31
35
  resolveP();
32
36
  });
@@ -41,14 +45,20 @@ class RpcServer extends EventEmitter {
41
45
  }
42
46
  this.clients.clear();
43
47
  if (this.server) {
44
- await new Promise((r) => this.server.close(() => r()));
48
+ if (process.platform === "win32" || this.ownsSocketFile()) {
49
+ await new Promise((r) => this.server.close(() => r()));
50
+ } else {
51
+ this.server.unref();
52
+ }
45
53
  this.server = null;
46
54
  }
47
- if (process.platform !== "win32" && existsSync(this.socketPath)) {
48
- try {
49
- unlinkSync(this.socketPath);
50
- } catch {
51
- }
55
+ this.socketIno = null;
56
+ }
57
+ ownsSocketFile() {
58
+ try {
59
+ return this.socketIno !== null && statSync(this.socketPath).ino === this.socketIno;
60
+ } catch {
61
+ return false;
52
62
  }
53
63
  }
54
64
  handleClient(sock) {
@@ -77,17 +87,27 @@ class RpcServer extends EventEmitter {
77
87
  sock.write(this.errorEnvelope(null, -32700, err instanceof Error ? err.message : String(err)));
78
88
  return;
79
89
  }
80
- const handler = this.handlers[req.method];
81
- if (!handler) {
82
- sock.write(this.errorEnvelope(req.id ?? null, -32601, `Method not found: ${req.method}`));
90
+ if (Array.isArray(req)) {
91
+ sock.write(this.errorEnvelope(null, -32600, "Batch requests are not supported"));
92
+ return;
93
+ }
94
+ if (typeof req !== "object" || req === null || typeof req.method !== "string") {
95
+ sock.write(this.errorEnvelope(null, -32600, "Invalid request"));
96
+ return;
97
+ }
98
+ const isNotification = !Object.prototype.hasOwnProperty.call(req, "id");
99
+ const id = req.id ?? null;
100
+ const handler = Object.prototype.hasOwnProperty.call(this.handlers, req.method) ? this.handlers[req.method] : void 0;
101
+ if (typeof handler !== "function") {
102
+ if (!isNotification) sock.write(this.errorEnvelope(id, -32601, `Method not found: ${req.method}`));
83
103
  return;
84
104
  }
85
105
  try {
86
106
  const result = await handler(req.params);
87
- sock.write(JSON.stringify({ jsonrpc: "2.0", id: req.id ?? null, result }) + "\n");
107
+ if (!isNotification) sock.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
88
108
  } catch (err) {
89
109
  const msg = err instanceof Error ? err.message : String(err);
90
- sock.write(this.errorEnvelope(req.id ?? null, -32e3, msg));
110
+ if (!isNotification) sock.write(this.errorEnvelope(id, -32e3, msg));
91
111
  }
92
112
  }
93
113
  errorEnvelope(id, code, message) {
package/dist/index.js CHANGED
@@ -1,51 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { resolve } from "node:path";
2
+ import { resolve, basename, join } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
- import { printConfig, resolveCodeSearchConfig, parseCliFlags } from "./core/config.js";
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
- }
12
- let mode = "remote";
13
- let remoteUrl = null;
14
- let apiKey;
15
- let source = null;
16
- let projectName = null;
17
- let githubToken;
18
- let force = false;
19
- let port;
20
- let transport;
21
- let authMode;
22
- for (let i = 0; i < args.length; i++) {
23
- if (args[i] === "--remote" && args[i + 1]) {
24
- mode = "remote";
25
- remoteUrl = args[++i];
26
- } else if (args[i] === "--local") {
27
- mode = "local";
28
- if (args[i + 1] && !args[i + 1].startsWith("--")) {
29
- source = args[++i];
30
- }
31
- } else if (args[i] === "--serve") {
32
- mode = "serve";
33
- } else if (args[i] === "--api-key" && args[i + 1]) {
34
- apiKey = args[++i];
35
- } else if (args[i] === "--project" && args[i + 1]) {
36
- projectName = args[++i];
37
- } else if (args[i] === "--token" && args[i + 1]) {
38
- githubToken = args[++i];
39
- } else if (args[i] === "--force") {
40
- force = true;
41
- } else if (args[i] === "--port" && args[i + 1]) {
42
- port = args[++i];
43
- } else if (args[i] === "--transport" && args[i + 1]) {
44
- transport = args[++i];
45
- } else if (args[i] === "--auth" && args[i + 1]) {
46
- authMode = args[++i];
47
- } else if (args[i] === "--help" || args[i] === "-h") {
48
- console.error(`
4
+ import { homedir } from "node:os";
5
+ import { printConfig, resolveCodeSearchConfig, parseCliFlags, exportDataDir } from "./core/config.js";
6
+ import { loadServerConfig } from "./core/env-config.js";
7
+ import { validateProjectName } from "./core/project-name.js";
8
+ import { configureLogger, getComponentLogger } from "./observability/logger.js";
9
+ const log = getComponentLogger("code-search-mcp");
10
+ const HELP = `
49
11
  Code Search MCP \u2014 multi-repo code intelligence via MCP
50
12
 
51
13
  REMOTE MODE (default \u2014 connects to a deployed server):
@@ -70,7 +32,7 @@ REMOTE MODE (default \u2014 connects to a deployed server):
70
32
  }
71
33
  }
72
34
 
73
- LOCAL MODE (self-hosted \u2014 indexes repos locally):
35
+ LOCAL MODE (self-hosted \u2014 indexes repos locally, serves over stdio):
74
36
  code-search-mcp --local /path/to/repos Index local directory
75
37
  code-search-mcp --local github:org-name Clone from GitHub org
76
38
  code-search-mcp --local --project my-project Serve existing index
@@ -78,45 +40,111 @@ LOCAL MODE (self-hosted \u2014 indexes repos locally):
78
40
  Options:
79
41
  --project <name> Project name (default: derived from source)
80
42
  --token <token> GitHub token (or GITHUB_TOKEN env)
81
- --force Force full re-index
43
+ --force Force a full re-index at startup
82
44
 
83
- SERVER MODE (deploy as HTTP service):
84
- code-search-mcp --serve Start HTTP server
85
- code-search-mcp --serve --port 3100 --auth api-key
45
+ SERVER MODE (deploy as HTTP service \u2014 Streamable HTTP on /mcp):
46
+ code-search-mcp --serve [path] Start HTTP server (project "default")
47
+ code-search-mcp --serve /repos --port 3100 --auth api-key
48
+ (--local <path> --serve and --serve --local <path> are equivalent)
86
49
 
87
50
  Options:
88
- --port <port> HTTP port (default: 3100)
89
- --auth <mode> none | api-key | jwt
90
- --transport <mode> streamable-http | sse
51
+ --port <port> HTTP port (default: 3100) = --server.port
52
+ --host <host> Bind address = --server.host
53
+ --auth <mode> none | api-key | jwt = --auth.mode
54
+ --project <name> Project name (default: "default")
91
55
 
92
56
  Environment:
93
- CODE_SEARCH_TRANSPORT streamable-http | sse
94
57
  CODE_SEARCH_PORT HTTP port (default: 3100)
95
58
  CODE_SEARCH_AUTH_MODE none | api-key | jwt
96
59
  CODE_SEARCH_AUTH_API_KEYS Comma-separated API keys
97
60
  CODE_SEARCH_EMBEDDING_PROVIDER auto | codestral | openai | voyage | ollama | custom
98
61
  CODE_SEARCH_EMBEDDING_API_KEY Unified embedding API key
99
62
  CODE_SEARCH_DATA_DIR Data directory override
100
- `);
101
- process.exit(0);
102
- } else if (!args[i].startsWith("-")) {
103
- const arg = args[i];
104
- if (arg.startsWith("http://") || arg.startsWith("https://")) {
105
- mode = "remote";
106
- remoteUrl = arg;
107
- } else {
108
- mode = "local";
109
- source = arg;
110
- }
63
+
64
+ CONFIG (every mode):
65
+ --<dotted.path> <value> Any CodeSearchConfig leaf, e.g. --retrieval.max-chunks 12
66
+ --print-config Print the resolved config (secrets redacted) and exit
67
+ `;
68
+ function usageError(msg) {
69
+ log.error(`${msg} \u2014 see --help`);
70
+ process.exit(2);
71
+ }
72
+ const { patch, rest, unknown } = parseCliFlags(process.argv.slice(2));
73
+ if (unknown.length) usageError(`unknown flag(s): ${unknown.join(", ")}`);
74
+ let printOnly = false;
75
+ let serve = false;
76
+ let local = false;
77
+ let remoteUrl = null;
78
+ let apiKey;
79
+ let source = null;
80
+ let projectName = null;
81
+ let githubToken;
82
+ let force = false;
83
+ for (let i = 0; i < rest.length; i++) {
84
+ const arg = rest[i];
85
+ const value = () => {
86
+ const v = rest[i + 1];
87
+ if (v === void 0 || v.startsWith("--")) usageError(`${arg} needs a value`);
88
+ i++;
89
+ return v;
90
+ };
91
+ const optionalPath = () => rest[i + 1] !== void 0 && !rest[i + 1].startsWith("-") ? rest[++i] : null;
92
+ switch (arg) {
93
+ case "--print-config":
94
+ printOnly = true;
95
+ break;
96
+ case "--remote":
97
+ remoteUrl = value();
98
+ break;
99
+ case "--local":
100
+ local = true;
101
+ source = optionalPath() ?? source;
102
+ break;
103
+ case "--serve":
104
+ serve = true;
105
+ source = optionalPath() ?? source;
106
+ break;
107
+ case "--api-key":
108
+ apiKey = value();
109
+ break;
110
+ case "--project":
111
+ projectName = value();
112
+ break;
113
+ case "--token":
114
+ githubToken = value();
115
+ break;
116
+ case "--force":
117
+ force = true;
118
+ break;
119
+ case "--help":
120
+ case "-h":
121
+ process.stderr.write(HELP);
122
+ process.exit(0);
123
+ default:
124
+ if (arg.startsWith("-")) usageError(`unknown flag: ${arg}`);
125
+ if (/^https?:\/\//.test(arg)) remoteUrl = arg;
126
+ else source = arg;
111
127
  }
112
128
  }
129
+ const mode = serve ? "serve" : remoteUrl ? "remote" : local || source ? "local" : "remote";
130
+ const workspaceDir = source && !source.startsWith("github:") ? resolve(source) : process.cwd();
131
+ const preview = resolveCodeSearchConfig({ cli: patch, workspaceDir });
132
+ if (preview.server.transport === "sse") {
133
+ usageError("SSE transport is not supported \u2014 the server speaks Streamable HTTP on /mcp (drop --transport / CODE_SEARCH_TRANSPORT)");
134
+ }
135
+ if (mode === "serve") patch.server = { ...patch.server, transport: "streamable-http" };
136
+ if (printOnly) {
137
+ process.stdout.write(printConfig(resolveCodeSearchConfig({ cli: patch, workspaceDir })) + "\n");
138
+ process.exit(0);
139
+ }
140
+ configureLogger({ structured: preview.telemetry.structuredLogs });
113
141
  (async () => {
114
142
  try {
115
143
  if (mode === "remote") {
116
144
  const serverUrl = remoteUrl ?? process.env.CODE_SEARCH_SERVER ?? process.env.CODE_SEARCH_REMOTE_URL;
117
145
  const key = apiKey ?? process.env.CODE_SEARCH_API_KEY ?? process.env.CODE_SEARCH_AUTH_API_KEY;
118
146
  if (!serverUrl) {
119
- console.error(`[code-search-mcp] No remote server configured.
147
+ log.error(`No remote server configured.
120
148
 
121
149
  Set CODE_SEARCH_SERVER environment variable:
122
150
  export CODE_SEARCH_SERVER=https://your-server:3100
@@ -136,14 +164,8 @@ For help:
136
164
  await startRemoteProxy({ serverUrl, apiKey: key });
137
165
  return;
138
166
  }
139
- if (mode === "serve") {
140
- if (port) process.env.CODE_SEARCH_PORT = port;
141
- if (authMode) process.env.CODE_SEARCH_AUTH_MODE = authMode;
142
- process.env.CODE_SEARCH_TRANSPORT = transport === "sse" ? "sse" : "streamable-http";
143
- const { startServer: startServer2 } = await import("./server.js");
144
- await startServer2(projectName ?? "default", source ? resolve(source) : null);
145
- return;
146
- }
167
+ const config = loadServerConfig({ cli: patch, workspaceDir });
168
+ exportDataDir(config.__unified);
147
169
  let directoryPath = null;
148
170
  if (source?.startsWith("github:")) {
149
171
  const { cloneOrUpdateOrg } = await import("./sources/github-org.js");
@@ -151,34 +173,36 @@ For help:
151
173
  const slashIdx = spec.indexOf("/");
152
174
  const org = slashIdx > 0 ? spec.slice(0, slashIdx) : spec;
153
175
  const pattern = slashIdx > 0 ? spec.slice(slashIdx + 1) : void 0;
154
- if (!projectName) projectName = org;
155
- console.error(`[code-search-mcp] Cloning repos from github:${org}${pattern ? `/${pattern}` : ""}...`);
176
+ if (!projectName && mode === "local") projectName = org;
177
+ log.info(`Cloning repos from github:${org}${pattern ? `/${pattern}` : ""}...`);
156
178
  const repos = await cloneOrUpdateOrg(org, {
157
179
  pattern,
158
180
  token: githubToken,
159
- onProgress: (m) => console.error(m)
181
+ onProgress: (m) => log.info(m)
160
182
  });
161
- console.error(`[code-search-mcp] ${repos.length} repos ready`);
162
- const { homedir } = await import("node:os");
163
- const { join } = await import("node:path");
183
+ log.info(`${repos.length} repos ready`);
164
184
  directoryPath = join(homedir(), ".code-search", org);
165
185
  } else if (source) {
166
186
  directoryPath = resolve(source);
167
187
  if (!existsSync(directoryPath)) {
168
- console.error(`[code-search-mcp] Directory not found: ${directoryPath}`);
188
+ log.error(`Directory not found: ${directoryPath}`);
169
189
  process.exit(1);
170
190
  }
171
- if (!projectName) {
172
- projectName = directoryPath.split("/").filter(Boolean).pop() || "project";
173
- }
174
- } else if (!projectName) {
191
+ } else if (local && !projectName) {
175
192
  directoryPath = process.cwd();
176
- projectName = directoryPath.split("/").filter(Boolean).pop() || "project";
193
+ }
194
+ if (!projectName) {
195
+ projectName = mode === "serve" ? "default" : directoryPath && basename(directoryPath) || "project";
196
+ }
197
+ try {
198
+ validateProjectName(projectName);
199
+ } catch (err) {
200
+ usageError(err.message);
177
201
  }
178
202
  const { startServer } = await import("./server.js");
179
- await startServer(projectName, directoryPath);
203
+ await startServer(projectName, directoryPath, { force });
180
204
  } catch (err) {
181
- console.error(`[code-search-mcp] Fatal:`, err);
205
+ log.error(`Fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
182
206
  process.exit(1);
183
207
  }
184
208
  })();