code-graph-builder 0.2.0 → 0.3.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 (2) hide show
  1. package/bin/cli.mjs +317 -54
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -1,29 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * code-graph-builder MCP server launcher
5
- *
6
- * Automatically detects the best way to run the Python MCP server:
7
- * 1. uvx (fastest, auto-installs in isolated env)
8
- * 2. pipx (similar to uvx)
9
- * 3. Direct python3 (requires prior pip install)
4
+ * code-graph-builder MCP server launcher & setup wizard
10
5
  *
11
6
  * Usage:
12
- * npx code-graph-builder # auto-detect
13
- * npx code-graph-builder --pip # force pip mode
7
+ * npx code-graph-builder # interactive setup (first run)
8
+ * npx code-graph-builder --server # start MCP server (used by MCP clients)
9
+ * npx code-graph-builder --setup # re-run setup wizard
10
+ * npx code-graph-builder --pip # force python3 direct mode
14
11
  */
15
12
 
16
13
  import { spawn, execFileSync } from "node:child_process";
14
+ import { createInterface } from "node:readline";
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
17
18
 
18
19
  const PYTHON_PACKAGE = "code-graph-builder";
19
20
  const MODULE_PATH = "code_graph_builder.mcp.server";
21
+ const WORKSPACE_DIR = join(homedir(), ".code-graph-builder");
22
+ const ENV_FILE = join(WORKSPACE_DIR, ".env");
20
23
 
21
- // Pass through all env vars (CGB_WORKSPACE, API keys, etc.)
22
- const env = { ...process.env };
24
+ // ---------------------------------------------------------------------------
25
+ // Utilities
26
+ // ---------------------------------------------------------------------------
23
27
 
24
- /**
25
- * Check if a command exists on PATH.
26
- */
27
28
  function commandExists(cmd) {
28
29
  try {
29
30
  execFileSync("which", [cmd], { stdio: "pipe" });
@@ -33,9 +34,6 @@ function commandExists(cmd) {
33
34
  }
34
35
  }
35
36
 
36
- /**
37
- * Check if the Python package is importable.
38
- */
39
37
  function pythonPackageInstalled() {
40
38
  try {
41
39
  execFileSync("python3", ["-c", `import ${MODULE_PATH.split(".")[0]}`], {
@@ -47,17 +45,257 @@ function pythonPackageInstalled() {
47
45
  }
48
46
  }
49
47
 
50
- /**
51
- * Run a command as the MCP server (replaces this process's stdio).
52
- */
48
+ function loadEnvFile() {
49
+ if (!existsSync(ENV_FILE)) return {};
50
+ const vars = {};
51
+ for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) {
52
+ const trimmed = line.trim();
53
+ if (!trimmed || trimmed.startsWith("#")) continue;
54
+ const eq = trimmed.indexOf("=");
55
+ if (eq === -1) continue;
56
+ const key = trimmed.slice(0, eq).trim();
57
+ let val = trimmed.slice(eq + 1).trim();
58
+ // Strip surrounding quotes
59
+ if ((val.startsWith('"') && val.endsWith('"')) ||
60
+ (val.startsWith("'") && val.endsWith("'"))) {
61
+ val = val.slice(1, -1);
62
+ }
63
+ vars[key] = val;
64
+ }
65
+ return vars;
66
+ }
67
+
68
+ function saveEnvFile(vars) {
69
+ mkdirSync(WORKSPACE_DIR, { recursive: true });
70
+ const lines = [
71
+ "# code-graph-builder configuration",
72
+ "# Generated by setup wizard. Edit freely.",
73
+ "",
74
+ ];
75
+ for (const [key, val] of Object.entries(vars)) {
76
+ if (val) lines.push(`${key}=${val}`);
77
+ }
78
+ lines.push("");
79
+ writeFileSync(ENV_FILE, lines.join("\n"), "utf-8");
80
+ }
81
+
82
+ function mask(s) {
83
+ if (!s || s.length < 8) return s ? "****" : "(not set)";
84
+ return s.slice(0, 4) + "****" + s.slice(-4);
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Interactive setup wizard (runs on stderr so stdout stays clean)
89
+ // ---------------------------------------------------------------------------
90
+
91
+ async function runSetup() {
92
+ const rl = createInterface({
93
+ input: process.stdin,
94
+ output: process.stderr,
95
+ });
96
+
97
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
98
+ const log = (msg) => process.stderr.write(msg + "\n");
99
+
100
+ log("");
101
+ log("╔══════════════════════════════════════════════════════════╗");
102
+ log("║ code-graph-builder Setup Wizard ║");
103
+ log("╚══════════════════════════════════════════════════════════╝");
104
+ log("");
105
+
106
+ // Load existing config
107
+ const existing = loadEnvFile();
108
+
109
+ // --- Workspace ---
110
+ log("── 1/3 Workspace ──────────────────────────────────────────");
111
+ log(`Workspace stores indexed repos, graphs, and embeddings.`);
112
+ const workspace =
113
+ (await ask(` Workspace path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
114
+ log("");
115
+
116
+ // --- LLM Provider ---
117
+ log("── 2/3 LLM Provider (for natural language queries & descriptions) ──");
118
+ log("");
119
+ log(" Select your LLM provider:");
120
+ log("");
121
+ log(" 1) Moonshot / Kimi https://platform.moonshot.cn");
122
+ log(" 2) OpenAI https://platform.openai.com");
123
+ log(" 3) DeepSeek https://platform.deepseek.com");
124
+ log(" 4) OpenRouter https://openrouter.ai");
125
+ log(" 5) Custom (any OpenAI-compatible endpoint)");
126
+ log(" 6) Skip (configure later)");
127
+ log("");
128
+
129
+ const providers = {
130
+ "1": { name: "Moonshot", url: "https://api.moonshot.cn/v1", model: "kimi-k2.5" },
131
+ "2": { name: "OpenAI", url: "https://api.openai.com/v1", model: "gpt-4o" },
132
+ "3": { name: "DeepSeek", url: "https://api.deepseek.com/v1", model: "deepseek-chat" },
133
+ "4": { name: "OpenRouter", url: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4" },
134
+ };
135
+
136
+ if (existing.LLM_API_KEY) {
137
+ log(` Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
138
+ }
139
+
140
+ const choice = (await ask(" Choose provider [1-6]: ")).trim() || "6";
141
+
142
+ let llmKey = existing.LLM_API_KEY || "";
143
+ let llmBaseUrl = existing.LLM_BASE_URL || "";
144
+ let llmModel = existing.LLM_MODEL || "";
145
+
146
+ if (choice !== "6") {
147
+ const provider = providers[choice];
148
+
149
+ if (provider) {
150
+ log(`\n → ${provider.name} selected`);
151
+ llmBaseUrl = provider.url;
152
+ llmModel = provider.model;
153
+ } else {
154
+ // Choice "5" or invalid → custom
155
+ log("\n → Custom provider");
156
+ llmBaseUrl = (await ask(" API Base URL: ")).trim() || llmBaseUrl;
157
+ llmModel = (await ask(" Model name: ")).trim() || llmModel || "gpt-4o";
158
+ }
159
+
160
+ llmKey = (await ask(` API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
161
+
162
+ if (llmKey) {
163
+ // Allow overriding URL and model
164
+ const urlOverride = (await ask(` Base URL [${llmBaseUrl}]: `)).trim();
165
+ if (urlOverride) llmBaseUrl = urlOverride;
166
+ const modelOverride = (await ask(` Model [${llmModel}]: `)).trim();
167
+ if (modelOverride) llmModel = modelOverride;
168
+ }
169
+ }
170
+ log("");
171
+
172
+ // --- Embedding Provider ---
173
+ log("── 3/3 Embedding Provider (for semantic code search) ─────");
174
+ log("");
175
+ log(" Select your embedding provider:");
176
+ log("");
177
+ log(" 1) DashScope / Qwen https://dashscope.console.aliyun.com (free tier)");
178
+ log(" 2) OpenAI Embeddings https://platform.openai.com");
179
+ log(" 3) Custom (any OpenAI-compatible embedding endpoint)");
180
+ log(" 4) Skip (configure later)");
181
+ log("");
182
+
183
+ const embedProviders = {
184
+ "1": { name: "DashScope", url: "https://dashscope.aliyuncs.com/api/v1", model: "text-embedding-v4", keyEnv: "DASHSCOPE_API_KEY", urlEnv: "DASHSCOPE_BASE_URL" },
185
+ "2": { name: "OpenAI", url: "https://api.openai.com/v1", model: "text-embedding-3-small", keyEnv: "OPENAI_API_KEY", urlEnv: "OPENAI_BASE_URL" },
186
+ };
187
+
188
+ if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
189
+ const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
190
+ log(` Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
191
+ }
192
+
193
+ const embedChoice = (await ask(" Choose provider [1-4]: ")).trim() || "4";
194
+
195
+ let embedKey = "";
196
+ let embedUrl = "";
197
+ let embedModel = "";
198
+ let embedKeyEnv = "DASHSCOPE_API_KEY";
199
+ let embedUrlEnv = "DASHSCOPE_BASE_URL";
200
+
201
+ if (embedChoice !== "4") {
202
+ const ep = embedProviders[embedChoice];
203
+
204
+ if (ep) {
205
+ log(`\n → ${ep.name} selected`);
206
+ embedUrl = ep.url;
207
+ embedModel = ep.model;
208
+ embedKeyEnv = ep.keyEnv;
209
+ embedUrlEnv = ep.urlEnv;
210
+ } else {
211
+ // Choice "3" or invalid → custom
212
+ log("\n → Custom embedding provider");
213
+ embedUrl = (await ask(" Embedding API Base URL: ")).trim();
214
+ embedModel = (await ask(" Embedding model name: ")).trim() || "text-embedding-3-small";
215
+ embedKeyEnv = "EMBED_API_KEY";
216
+ embedUrlEnv = "EMBED_BASE_URL";
217
+ }
218
+
219
+ embedKey = (await ask(` API Key: `)).trim() ||
220
+ existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
221
+
222
+ if (embedKey) {
223
+ const urlOverride = (await ask(` Base URL [${embedUrl}]: `)).trim();
224
+ if (urlOverride) embedUrl = urlOverride;
225
+ const modelOverride = (await ask(` Model [${embedModel}]: `)).trim();
226
+ if (modelOverride) embedModel = modelOverride;
227
+ }
228
+ }
229
+
230
+ rl.close();
231
+
232
+ // --- Save ---
233
+ const config = {
234
+ CGB_WORKSPACE: workspace,
235
+ LLM_API_KEY: llmKey,
236
+ LLM_BASE_URL: llmBaseUrl,
237
+ LLM_MODEL: llmModel,
238
+ };
239
+
240
+ // Save embedding config with the correct env var names
241
+ if (embedKey) {
242
+ config[embedKeyEnv] = embedKey;
243
+ config[embedUrlEnv] = embedUrl;
244
+ if (embedModel) config.EMBED_MODEL = embedModel;
245
+ }
246
+
247
+ saveEnvFile(config);
248
+
249
+ const embedDisplay = embedKey
250
+ ? `${mask(embedKey)} → ${embedModel || embedUrl}`
251
+ : "not configured (optional)";
252
+
253
+ log("");
254
+ log("── Configuration saved ─────────────────────────────────────");
255
+ log(` File: ${ENV_FILE}`);
256
+ log("");
257
+ log(" LLM: " + (llmKey ? `${mask(llmKey)} → ${llmModel}` : "not configured (optional)"));
258
+ log(" Embedding: " + embedDisplay);
259
+ log(" Workspace: " + workspace);
260
+ log("");
261
+ log("── Next steps ──────────────────────────────────────────────");
262
+ log("");
263
+ log(" Add to your MCP client config:");
264
+ log("");
265
+ log(' {');
266
+ log(' "mcpServers": {');
267
+ log(' "code-graph-builder": {');
268
+ log(' "command": "npx",');
269
+ log(' "args": ["-y", "code-graph-builder", "--server"]');
270
+ log(" }");
271
+ log(" }");
272
+ log(" }");
273
+ log("");
274
+ log(" Or run directly: npx code-graph-builder --server");
275
+ log("");
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Start MCP server
280
+ // ---------------------------------------------------------------------------
281
+
53
282
  function runServer(cmd, args) {
283
+ // Merge .env file into environment
284
+ const envVars = loadEnvFile();
285
+ const mergedEnv = { ...process.env, ...envVars };
286
+
287
+ // Ensure CGB_WORKSPACE is set
288
+ if (!mergedEnv.CGB_WORKSPACE) {
289
+ mergedEnv.CGB_WORKSPACE = WORKSPACE_DIR;
290
+ }
291
+
54
292
  const child = spawn(cmd, args, {
55
293
  stdio: "inherit",
56
- env,
294
+ env: mergedEnv,
57
295
  });
58
296
 
59
297
  child.on("error", (err) => {
60
- console.error(`Failed to start MCP server: ${err.message}`);
298
+ process.stderr.write(`Failed to start MCP server: ${err.message}\n`);
61
299
  process.exit(1);
62
300
  });
63
301
 
@@ -66,44 +304,69 @@ function runServer(cmd, args) {
66
304
  });
67
305
  }
68
306
 
307
+ function startServer(extraArgs = []) {
308
+ if (commandExists("uvx")) {
309
+ runServer("uvx", [PYTHON_PACKAGE, ...extraArgs]);
310
+ } else if (commandExists("uv")) {
311
+ runServer("uv", ["tool", "run", PYTHON_PACKAGE, ...extraArgs]);
312
+ } else if (commandExists("pipx")) {
313
+ runServer("pipx", ["run", PYTHON_PACKAGE, ...extraArgs]);
314
+ } else if (pythonPackageInstalled()) {
315
+ runServer("python3", ["-m", MODULE_PATH]);
316
+ } else {
317
+ process.stderr.write(
318
+ `code-graph-builder requires Python 3.10+.\n\n` +
319
+ `Install options:\n` +
320
+ ` 1. pip install ${PYTHON_PACKAGE}\n` +
321
+ ` 2. curl -LsSf https://astral.sh/uv/install.sh | sh (installs uv)\n` +
322
+ ` 3. pip install pipx\n\n` +
323
+ `Then run: npx code-graph-builder --server\n`
324
+ );
325
+ process.exit(1);
326
+ }
327
+ }
328
+
69
329
  // ---------------------------------------------------------------------------
70
330
  // Main
71
331
  // ---------------------------------------------------------------------------
72
332
 
73
- const forceMode = process.argv[2];
333
+ const args = process.argv.slice(2);
334
+ const mode = args[0];
74
335
 
75
- if (forceMode === "--pip" || forceMode === "--python") {
76
- // Force direct python3 mode
77
- if (!pythonPackageInstalled()) {
78
- console.error(
79
- `Error: Python package '${PYTHON_PACKAGE}' is not installed.\n` +
80
- `Run: pip install ${PYTHON_PACKAGE}`
81
- );
82
- process.exit(1);
336
+ if (mode === "--setup") {
337
+ // Explicit setup request
338
+ runSetup();
339
+ } else if (mode === "--server" || mode === "--pip" || mode === "--python") {
340
+ // Start MCP server directly
341
+ if (mode === "--pip" || mode === "--python") {
342
+ if (!pythonPackageInstalled()) {
343
+ process.stderr.write(
344
+ `Error: Python package '${PYTHON_PACKAGE}' is not installed.\n` +
345
+ `Run: pip install ${PYTHON_PACKAGE}\n`
346
+ );
347
+ process.exit(1);
348
+ }
349
+ runServer("python3", ["-m", MODULE_PATH]);
350
+ } else {
351
+ startServer(args.slice(1));
83
352
  }
84
- runServer("python3", ["-m", MODULE_PATH]);
85
- } else if (commandExists("uvx")) {
86
- // Preferred: uvx auto-installs in isolated env
87
- runServer("uvx", [PYTHON_PACKAGE, ...process.argv.slice(2)]);
88
- } else if (commandExists("uv")) {
89
- // uv available but not uvx — use uv tool run
90
- runServer("uv", ["tool", "run", PYTHON_PACKAGE, ...process.argv.slice(2)]);
91
- } else if (commandExists("pipx")) {
92
- // pipx: similar to uvx
93
- runServer("pipx", ["run", PYTHON_PACKAGE, ...process.argv.slice(2)]);
94
- } else if (pythonPackageInstalled()) {
95
- // Fallback: direct python3
96
- runServer("python3", ["-m", MODULE_PATH]);
97
- } else {
98
- // Nothing works — guide the user
99
- console.error(
100
- `code-graph-builder MCP server requires Python 3.10+.\n\n` +
101
- `Install options (pick one):\n` +
102
- ` 1. pip install ${PYTHON_PACKAGE} # then: npx code-graph-builder --pip\n` +
103
- ` 2. Install uv (recommended): curl -LsSf https://astral.sh/uv/install.sh | sh\n` +
104
- ` Then: npx code-graph-builder # auto-installs via uvx\n` +
105
- ` 3. Install pipx: pip install pipx\n` +
106
- ` Then: npx code-graph-builder # auto-installs via pipx\n`
353
+ } else if (mode === "--help" || mode === "-h") {
354
+ process.stderr.write(
355
+ `code-graph-builder - Code knowledge graph MCP server\n\n` +
356
+ `Usage:\n` +
357
+ ` npx code-graph-builder Interactive setup wizard\n` +
358
+ ` npx code-graph-builder --server Start MCP server\n` +
359
+ ` npx code-graph-builder --setup Re-run setup wizard\n` +
360
+ ` npx code-graph-builder --help Show this help\n\n` +
361
+ `Config: ${ENV_FILE}\n`
107
362
  );
108
- process.exit(1);
363
+ } else {
364
+ // No args: auto-detect
365
+ if (!existsSync(ENV_FILE)) {
366
+ // First run → setup wizard
367
+ runSetup();
368
+ } else {
369
+ // Config exists → start server
370
+ startServer(args);
371
+ }
109
372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-builder",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Code knowledge graph builder with MCP server for AI-assisted code navigation",
5
5
  "license": "MIT",
6
6
  "bin": {