code-graph-builder 0.2.0 → 0.3.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 (2) hide show
  1. package/bin/cli.mjs +259 -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,199 @@ 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 API Key ---
117
+ log("── 2/3 LLM API Key (for natural language queries & descriptions) ──");
118
+ log("");
119
+ log(" Supported providers (OpenAI-compatible):");
120
+ log(" - Moonshot / Kimi https://platform.moonshot.cn");
121
+ log(" - OpenAI https://platform.openai.com");
122
+ log(" - DeepSeek https://platform.deepseek.com");
123
+ log(" - Any OpenAI-compatible endpoint");
124
+ log("");
125
+
126
+ if (existing.LLM_API_KEY) {
127
+ log(` Current key: ${mask(existing.LLM_API_KEY)}`);
128
+ }
129
+
130
+ const llmKey =
131
+ (await ask(" LLM API Key (sk-...): ")).trim() || existing.LLM_API_KEY || "";
132
+
133
+ let llmBaseUrl = existing.LLM_BASE_URL || "";
134
+ let llmModel = existing.LLM_MODEL || "";
135
+
136
+ if (llmKey) {
137
+ log("");
138
+ log(" Detecting provider from key...");
139
+
140
+ if (llmKey.startsWith("sk-") && !llmKey.startsWith("sk-ant-")) {
141
+ // Could be Moonshot, OpenAI, or other
142
+ const urlInput = (
143
+ await ask(
144
+ ` API Base URL [${llmBaseUrl || "https://api.moonshot.cn/v1"}]: `
145
+ )
146
+ ).trim();
147
+ llmBaseUrl = urlInput || llmBaseUrl || "https://api.moonshot.cn/v1";
148
+
149
+ if (llmBaseUrl.includes("moonshot")) {
150
+ llmModel = (await ask(` Model name [kimi-k2.5]: `)).trim() || "kimi-k2.5";
151
+ } else if (llmBaseUrl.includes("openai")) {
152
+ llmModel = (await ask(` Model name [gpt-4o]: `)).trim() || "gpt-4o";
153
+ } else if (llmBaseUrl.includes("deepseek")) {
154
+ llmModel = (await ask(` Model name [deepseek-chat]: `)).trim() || "deepseek-chat";
155
+ } else {
156
+ llmModel =
157
+ (await ask(` Model name [${llmModel || "gpt-4o"}]: `)).trim() ||
158
+ llmModel ||
159
+ "gpt-4o";
160
+ }
161
+ }
162
+ }
163
+ log("");
164
+
165
+ // --- Embedding API Key ---
166
+ log("── 3/3 Embedding API Key (for semantic code search) ─────");
167
+ log("");
168
+ log(" Used for vector embedding of code (Qwen3 text-embedding-v4).");
169
+ log(" Get a free key at: https://dashscope.console.aliyun.com");
170
+ log("");
171
+
172
+ if (existing.DASHSCOPE_API_KEY) {
173
+ log(` Current key: ${mask(existing.DASHSCOPE_API_KEY)}`);
174
+ }
175
+
176
+ const dashscopeKey =
177
+ (await ask(" DashScope API Key (sk-...): ")).trim() ||
178
+ existing.DASHSCOPE_API_KEY ||
179
+ "";
180
+
181
+ rl.close();
182
+
183
+ // --- Save ---
184
+ const config = {
185
+ CGB_WORKSPACE: workspace,
186
+ LLM_API_KEY: llmKey,
187
+ LLM_BASE_URL: llmBaseUrl,
188
+ LLM_MODEL: llmModel,
189
+ DASHSCOPE_API_KEY: dashscopeKey,
190
+ DASHSCOPE_BASE_URL: "https://dashscope.aliyuncs.com/api/v1",
191
+ };
192
+
193
+ saveEnvFile(config);
194
+
195
+ log("");
196
+ log("── Configuration saved ─────────────────────────────────────");
197
+ log(` File: ${ENV_FILE}`);
198
+ log("");
199
+ log(" LLM: " + (llmKey ? `${mask(llmKey)} → ${llmModel}` : "not configured (optional)"));
200
+ log(" Embedding: " + (dashscopeKey ? mask(dashscopeKey) : "not configured (optional)"));
201
+ log(" Workspace: " + workspace);
202
+ log("");
203
+ log("── Next steps ──────────────────────────────────────────────");
204
+ log("");
205
+ log(" Add to your MCP client config:");
206
+ log("");
207
+ log(' {');
208
+ log(' "mcpServers": {');
209
+ log(' "code-graph-builder": {');
210
+ log(' "command": "npx",');
211
+ log(' "args": ["-y", "code-graph-builder", "--server"]');
212
+ log(" }");
213
+ log(" }");
214
+ log(" }");
215
+ log("");
216
+ log(" Or run directly: npx code-graph-builder --server");
217
+ log("");
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Start MCP server
222
+ // ---------------------------------------------------------------------------
223
+
53
224
  function runServer(cmd, args) {
225
+ // Merge .env file into environment
226
+ const envVars = loadEnvFile();
227
+ const mergedEnv = { ...process.env, ...envVars };
228
+
229
+ // Ensure CGB_WORKSPACE is set
230
+ if (!mergedEnv.CGB_WORKSPACE) {
231
+ mergedEnv.CGB_WORKSPACE = WORKSPACE_DIR;
232
+ }
233
+
54
234
  const child = spawn(cmd, args, {
55
235
  stdio: "inherit",
56
- env,
236
+ env: mergedEnv,
57
237
  });
58
238
 
59
239
  child.on("error", (err) => {
60
- console.error(`Failed to start MCP server: ${err.message}`);
240
+ process.stderr.write(`Failed to start MCP server: ${err.message}\n`);
61
241
  process.exit(1);
62
242
  });
63
243
 
@@ -66,44 +246,69 @@ function runServer(cmd, args) {
66
246
  });
67
247
  }
68
248
 
249
+ function startServer(extraArgs = []) {
250
+ if (commandExists("uvx")) {
251
+ runServer("uvx", [PYTHON_PACKAGE, ...extraArgs]);
252
+ } else if (commandExists("uv")) {
253
+ runServer("uv", ["tool", "run", PYTHON_PACKAGE, ...extraArgs]);
254
+ } else if (commandExists("pipx")) {
255
+ runServer("pipx", ["run", PYTHON_PACKAGE, ...extraArgs]);
256
+ } else if (pythonPackageInstalled()) {
257
+ runServer("python3", ["-m", MODULE_PATH]);
258
+ } else {
259
+ process.stderr.write(
260
+ `code-graph-builder requires Python 3.10+.\n\n` +
261
+ `Install options:\n` +
262
+ ` 1. pip install ${PYTHON_PACKAGE}\n` +
263
+ ` 2. curl -LsSf https://astral.sh/uv/install.sh | sh (installs uv)\n` +
264
+ ` 3. pip install pipx\n\n` +
265
+ `Then run: npx code-graph-builder --server\n`
266
+ );
267
+ process.exit(1);
268
+ }
269
+ }
270
+
69
271
  // ---------------------------------------------------------------------------
70
272
  // Main
71
273
  // ---------------------------------------------------------------------------
72
274
 
73
- const forceMode = process.argv[2];
275
+ const args = process.argv.slice(2);
276
+ const mode = args[0];
74
277
 
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);
278
+ if (mode === "--setup") {
279
+ // Explicit setup request
280
+ runSetup();
281
+ } else if (mode === "--server" || mode === "--pip" || mode === "--python") {
282
+ // Start MCP server directly
283
+ if (mode === "--pip" || mode === "--python") {
284
+ if (!pythonPackageInstalled()) {
285
+ process.stderr.write(
286
+ `Error: Python package '${PYTHON_PACKAGE}' is not installed.\n` +
287
+ `Run: pip install ${PYTHON_PACKAGE}\n`
288
+ );
289
+ process.exit(1);
290
+ }
291
+ runServer("python3", ["-m", MODULE_PATH]);
292
+ } else {
293
+ startServer(args.slice(1));
83
294
  }
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`
295
+ } else if (mode === "--help" || mode === "-h") {
296
+ process.stderr.write(
297
+ `code-graph-builder - Code knowledge graph MCP server\n\n` +
298
+ `Usage:\n` +
299
+ ` npx code-graph-builder Interactive setup wizard\n` +
300
+ ` npx code-graph-builder --server Start MCP server\n` +
301
+ ` npx code-graph-builder --setup Re-run setup wizard\n` +
302
+ ` npx code-graph-builder --help Show this help\n\n` +
303
+ `Config: ${ENV_FILE}\n`
107
304
  );
108
- process.exit(1);
305
+ } else {
306
+ // No args: auto-detect
307
+ if (!existsSync(ENV_FILE)) {
308
+ // First run → setup wizard
309
+ runSetup();
310
+ } else {
311
+ // Config exists → start server
312
+ startServer(args);
313
+ }
109
314
  }
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.0",
4
4
  "description": "Code knowledge graph builder with MCP server for AI-assisted code navigation",
5
5
  "license": "MIT",
6
6
  "bin": {