code-graph-builder 0.7.0 → 0.9.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 +326 -24
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -10,16 +10,17 @@
10
10
  * npx code-graph-builder --pip # force python3 direct mode
11
11
  */
12
12
 
13
- import { spawn, execFileSync } from "node:child_process";
13
+ import { spawn, execFileSync, execSync } from "node:child_process";
14
14
  import { createInterface } from "node:readline";
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
- import { homedir } from "node:os";
16
+ import { homedir, platform } from "node:os";
17
17
  import { join } from "node:path";
18
18
 
19
19
  const PYTHON_PACKAGE = "code-graph-builder";
20
20
  const MODULE_PATH = "code_graph_builder.mcp.server";
21
21
  const WORKSPACE_DIR = join(homedir(), ".code-graph-builder");
22
22
  const ENV_FILE = join(WORKSPACE_DIR, ".env");
23
+ const IS_WIN = platform() === "win32";
23
24
 
24
25
  // ---------------------------------------------------------------------------
25
26
  // Utilities
@@ -27,16 +28,40 @@ const ENV_FILE = join(WORKSPACE_DIR, ".env");
27
28
 
28
29
  function commandExists(cmd) {
29
30
  try {
30
- execFileSync("which", [cmd], { stdio: "pipe" });
31
+ // "which" on Unix/macOS, "where" on Windows
32
+ const checker = IS_WIN ? "where" : "which";
33
+ execFileSync(checker, [cmd], { stdio: "pipe" });
31
34
  return true;
32
35
  } catch {
33
36
  return false;
34
37
  }
35
38
  }
36
39
 
40
+ /**
41
+ * Find a working Python command. On Windows the command is typically
42
+ * "python" (the py-launcher or Store stub), while on Unix it is "python3".
43
+ * Returns the command string or null if none is found.
44
+ */
45
+ function findPython() {
46
+ const candidates = IS_WIN
47
+ ? ["python", "python3", "py"]
48
+ : ["python3", "python"];
49
+ for (const cmd of candidates) {
50
+ try {
51
+ const ver = execFileSync(cmd, ["--version"], { stdio: "pipe" }).toString().trim();
52
+ // Ensure it is Python 3.x
53
+ if (ver.includes("3.")) return cmd;
54
+ } catch { /* skip */ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ const PYTHON_CMD = findPython();
60
+
37
61
  function pythonPackageInstalled() {
62
+ if (!PYTHON_CMD) return false;
38
63
  try {
39
- execFileSync("python3", ["-c", `import ${MODULE_PATH.split(".")[0]}`], {
64
+ execFileSync(PYTHON_CMD, ["-c", `import ${MODULE_PATH.split(".")[0]}`], {
40
65
  stdio: "pipe",
41
66
  });
42
67
  return true;
@@ -260,7 +285,134 @@ async function runSetup() {
260
285
  log(" Embedding: " + embedDisplay);
261
286
  log(" Workspace: " + workspace);
262
287
  log("");
263
- log("── Next steps ──────────────────────────────────────────────");
288
+
289
+ // --- Verify installation ---
290
+ log("── Verifying installation ──────────────────────────────────");
291
+ log("");
292
+
293
+ // Step 1: Python available?
294
+ if (!PYTHON_CMD) {
295
+ log(" ✗ Python 3 not found on PATH");
296
+ log(" Install Python 3.10+ and re-run: npx code-graph-builder --setup");
297
+ log("");
298
+ rl.close();
299
+ return;
300
+ }
301
+ log(` ✓ Python found: ${PYTHON_CMD}`);
302
+
303
+ // Step 2: Package installed? If not, auto-install.
304
+ if (!pythonPackageInstalled()) {
305
+ log(` … Installing ${PYTHON_PACKAGE} via pip...`);
306
+ const pip = findPip();
307
+ if (pip) {
308
+ try {
309
+ execSync(
310
+ [...pip, "install", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
311
+ { stdio: "pipe", shell: true }
312
+ );
313
+ } catch { /* handled below */ }
314
+ }
315
+ }
316
+
317
+ if (pythonPackageInstalled()) {
318
+ log(` ✓ Python package installed: ${PYTHON_PACKAGE}`);
319
+ } else {
320
+ log(` ✗ Python package not installed`);
321
+ log(` Run manually: pip install ${PYTHON_PACKAGE}`);
322
+ log("");
323
+ rl.close();
324
+ return;
325
+ }
326
+
327
+ // Step 3: MCP server smoke test — spawn server, send initialize, check tools/list
328
+ log(" … Starting MCP server smoke test...");
329
+
330
+ const verified = await new Promise((resolve) => {
331
+ const envVars = loadEnvFile();
332
+ const mergedEnv = { ...process.env, ...envVars };
333
+ if (!mergedEnv.CGB_WORKSPACE) mergedEnv.CGB_WORKSPACE = WORKSPACE_DIR;
334
+
335
+ const child = spawn(PYTHON_CMD, ["-m", MODULE_PATH], {
336
+ stdio: ["pipe", "pipe", "pipe"],
337
+ env: mergedEnv,
338
+ shell: IS_WIN,
339
+ });
340
+
341
+ let stdout = "";
342
+ let resolved = false;
343
+
344
+ const finish = (success, detail) => {
345
+ if (resolved) return;
346
+ resolved = true;
347
+ try { child.kill(); } catch {}
348
+ resolve({ success, detail });
349
+ };
350
+
351
+ // Timeout after 15s
352
+ const timer = setTimeout(() => finish(false, "Server did not respond within 15s"), 15000);
353
+
354
+ child.stdout.on("data", (chunk) => {
355
+ stdout += chunk.toString();
356
+ // Look for a valid JSON-RPC response
357
+ const lines = stdout.split("\n");
358
+ for (const line of lines) {
359
+ // MCP uses Content-Length header framing
360
+ if (line.startsWith("{")) {
361
+ try {
362
+ const msg = JSON.parse(line);
363
+ if (msg.result && msg.result.capabilities) {
364
+ // Got initialize response, now request tools/list
365
+ const toolsReq =
366
+ `Content-Length: 80\r\n\r\n` +
367
+ JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
368
+ child.stdin.write(toolsReq);
369
+ stdout = "";
370
+ return;
371
+ }
372
+ if (msg.result && msg.result.tools) {
373
+ clearTimeout(timer);
374
+ finish(true, `${msg.result.tools.length} tools available`);
375
+ return;
376
+ }
377
+ } catch { /* partial JSON, wait for more */ }
378
+ }
379
+ }
380
+ });
381
+
382
+ child.on("error", (err) => {
383
+ clearTimeout(timer);
384
+ finish(false, err.message);
385
+ });
386
+
387
+ child.on("exit", (code) => {
388
+ clearTimeout(timer);
389
+ if (!resolved) finish(false, `Server exited with code ${code}`);
390
+ });
391
+
392
+ // Send MCP initialize request
393
+ const initReq = JSON.stringify({
394
+ jsonrpc: "2.0",
395
+ id: 1,
396
+ method: "initialize",
397
+ params: {
398
+ protocolVersion: "2024-11-05",
399
+ capabilities: {},
400
+ clientInfo: { name: "setup-verify", version: "1.0.0" },
401
+ },
402
+ });
403
+ const header = `Content-Length: ${Buffer.byteLength(initReq)}\r\n\r\n`;
404
+ child.stdin.write(header + initReq);
405
+ });
406
+
407
+ if (verified.success) {
408
+ log(` ✓ MCP server started successfully (${verified.detail})`);
409
+ } else {
410
+ log(` ✗ MCP server smoke test failed: ${verified.detail}`);
411
+ log(" The server may still work — try: npx code-graph-builder --server");
412
+ }
413
+
414
+ log("");
415
+ log("── Setup complete ─────────────────────────────────────────");
264
416
  log("");
265
417
  log(" Add to your MCP client config:");
266
418
  log("");
@@ -268,7 +420,7 @@ async function runSetup() {
268
420
  log(' "mcpServers": {');
269
421
  log(' "code-graph-builder": {');
270
422
  log(' "command": "npx",');
271
- log(' "args": ["-y", "code-graph-builder", "--server"]');
423
+ log(' "args": ["-y", "code-graph-builder@latest", "--server"]');
272
424
  log(" }");
273
425
  log(" }");
274
426
  log(" }");
@@ -294,6 +446,7 @@ function runServer(cmd, args) {
294
446
  const child = spawn(cmd, args, {
295
447
  stdio: "inherit",
296
448
  env: mergedEnv,
449
+ shell: IS_WIN, // Windows needs shell for .cmd/.ps1 scripts (uvx, pipx, etc.)
297
450
  });
298
451
 
299
452
  child.on("error", (err) => {
@@ -306,25 +459,171 @@ function runServer(cmd, args) {
306
459
  });
307
460
  }
308
461
 
462
+ /**
463
+ * Find a working pip command. Returns [cmd, ...prefixArgs] or null.
464
+ * Tries: pip3, pip, python3 -m pip, python -m pip
465
+ */
466
+ function findPip() {
467
+ // Standalone pip
468
+ for (const cmd of IS_WIN ? ["pip", "pip3"] : ["pip3", "pip"]) {
469
+ if (commandExists(cmd)) return [cmd];
470
+ }
471
+ // python -m pip fallback
472
+ if (PYTHON_CMD) {
473
+ try {
474
+ execFileSync(PYTHON_CMD, ["-m", "pip", "--version"], { stdio: "pipe" });
475
+ return [PYTHON_CMD, "-m", "pip"];
476
+ } catch { /* skip */ }
477
+ }
478
+ return null;
479
+ }
480
+
481
+ /**
482
+ * Auto-install the Python package via pip, then start the server.
483
+ */
484
+ function autoInstallAndStart(extraArgs) {
485
+ const pip = findPip();
486
+ if (!pip) {
487
+ process.stderr.write(
488
+ `code-graph-builder requires Python 3.10+ with pip.\n\n` +
489
+ (PYTHON_CMD
490
+ ? `Python found (${PYTHON_CMD}) but pip is not available.\n\n`
491
+ : `Python 3 not found on PATH.\n\n`) +
492
+ `Please install Python 3.10+ first, then run:\n` +
493
+ ` npx code-graph-builder --server\n`
494
+ );
495
+ process.exit(1);
496
+ }
497
+
498
+ process.stderr.write(`Installing ${PYTHON_PACKAGE}...\n`);
499
+
500
+ try {
501
+ execSync(
502
+ [...pip, "install", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
503
+ { stdio: "inherit", shell: true }
504
+ );
505
+ } catch (err) {
506
+ process.stderr.write(
507
+ `\nFailed to install ${PYTHON_PACKAGE}.\n` +
508
+ `Try manually: ${pip.join(" ")} install ${PYTHON_PACKAGE}\n`
509
+ );
510
+ process.exit(1);
511
+ }
512
+
513
+ // Verify installation succeeded
514
+ if (!pythonPackageInstalled()) {
515
+ process.stderr.write(
516
+ `\nInstallation completed but package not importable.\n` +
517
+ `Try manually: ${pip.join(" ")} install ${PYTHON_PACKAGE}\n`
518
+ );
519
+ process.exit(1);
520
+ }
521
+
522
+ process.stderr.write(`${PYTHON_PACKAGE} installed successfully.\n`);
523
+ runServer(PYTHON_CMD, ["-m", MODULE_PATH]);
524
+ }
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // Uninstall — remove Python package, config, workspace data, Claude MCP entry
528
+ // ---------------------------------------------------------------------------
529
+
530
+ async function runUninstall() {
531
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
532
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
533
+ const log = (msg) => process.stderr.write(msg + "\n");
534
+
535
+ log("");
536
+ log("╔══════════════════════════════════════════════════════════╗");
537
+ log("║ code-graph-builder Uninstall ║");
538
+ log("╚══════════════════════════════════════════════════════════╝");
539
+ log("");
540
+
541
+ // 1. Show what will be removed
542
+ const pip = findPip();
543
+ const hasPythonPkg = pythonPackageInstalled();
544
+ const hasWorkspace = existsSync(WORKSPACE_DIR);
545
+ const hasEnv = existsSync(ENV_FILE);
546
+
547
+ // Check Claude Code MCP config
548
+ let hasClaudeConfig = false;
549
+ try {
550
+ execFileSync("claude", ["mcp", "list"], { stdio: "pipe" });
551
+ hasClaudeConfig = true;
552
+ } catch { /* claude CLI not available */ }
553
+
554
+ log(" The following will be removed:");
555
+ log("");
556
+ if (hasPythonPkg) log(" ✓ Python package: code-graph-builder");
557
+ else log(" - Python package: not installed");
558
+ if (hasWorkspace) log(` ✓ Workspace data: ${WORKSPACE_DIR}`);
559
+ else log(" - Workspace data: not found");
560
+ if (hasEnv) log(` ✓ Config file: ${ENV_FILE}`);
561
+ if (hasClaudeConfig) log(" ✓ Claude Code MCP server entry");
562
+ log("");
563
+
564
+ const answer = (await ask(" Proceed with uninstall? [y/N]: ")).trim().toLowerCase();
565
+ rl.close();
566
+
567
+ if (answer !== "y" && answer !== "yes") {
568
+ log("\n Uninstall cancelled.\n");
569
+ process.exit(0);
570
+ }
571
+
572
+ log("");
573
+
574
+ // 2. Remove Claude Code MCP entry
575
+ if (hasClaudeConfig) {
576
+ try {
577
+ execSync("claude mcp remove code-graph-builder", { stdio: "pipe", shell: true });
578
+ log(" ✓ Removed Claude Code MCP entry");
579
+ } catch {
580
+ log(" ⚠ Could not remove Claude Code MCP entry (may not exist)");
581
+ }
582
+ }
583
+
584
+ // 3. Uninstall Python package
585
+ if (hasPythonPkg && pip) {
586
+ try {
587
+ execSync(
588
+ [...pip, "uninstall", "-y", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
589
+ { stdio: "inherit", shell: true }
590
+ );
591
+ log(" ✓ Uninstalled Python package");
592
+ } catch {
593
+ log(" ⚠ Failed to uninstall Python package. Try manually: pip uninstall code-graph-builder");
594
+ }
595
+ }
596
+
597
+ // 4. Remove workspace data
598
+ if (hasWorkspace) {
599
+ const { rmSync } = await import("node:fs");
600
+ try {
601
+ rmSync(WORKSPACE_DIR, { recursive: true, force: true });
602
+ log(` ✓ Removed workspace: ${WORKSPACE_DIR}`);
603
+ } catch (err) {
604
+ log(` ⚠ Failed to remove workspace: ${err.message}`);
605
+ }
606
+ }
607
+
608
+ log("");
609
+ log(" Uninstall complete.");
610
+ log(" To also clear the npx cache: npx clear-npx-cache");
611
+ log("");
612
+ }
613
+
309
614
  function startServer(extraArgs = []) {
310
- if (commandExists("uvx")) {
615
+ // Prefer pip-installed package first (most reliable, includes all deps)
616
+ if (pythonPackageInstalled()) {
617
+ runServer(PYTHON_CMD, ["-m", MODULE_PATH]);
618
+ } else if (commandExists("uvx")) {
311
619
  runServer("uvx", [PYTHON_PACKAGE, ...extraArgs]);
312
620
  } else if (commandExists("uv")) {
313
621
  runServer("uv", ["tool", "run", PYTHON_PACKAGE, ...extraArgs]);
314
622
  } else if (commandExists("pipx")) {
315
623
  runServer("pipx", ["run", PYTHON_PACKAGE, ...extraArgs]);
316
- } else if (pythonPackageInstalled()) {
317
- runServer("python3", ["-m", MODULE_PATH]);
318
624
  } else {
319
- process.stderr.write(
320
- `code-graph-builder requires Python 3.10+.\n\n` +
321
- `Install options:\n` +
322
- ` 1. pip install ${PYTHON_PACKAGE}\n` +
323
- ` 2. curl -LsSf https://astral.sh/uv/install.sh | sh (installs uv)\n` +
324
- ` 3. pip install pipx\n\n` +
325
- `Then run: npx code-graph-builder --server\n`
326
- );
327
- process.exit(1);
625
+ // Auto-install via pip
626
+ autoInstallAndStart(extraArgs);
328
627
  }
329
628
  }
330
629
 
@@ -341,25 +640,28 @@ if (mode === "--setup") {
341
640
  } else if (mode === "--server" || mode === "--pip" || mode === "--python") {
342
641
  // Start MCP server directly
343
642
  if (mode === "--pip" || mode === "--python") {
344
- if (!pythonPackageInstalled()) {
643
+ if (!PYTHON_CMD || !pythonPackageInstalled()) {
345
644
  process.stderr.write(
346
645
  `Error: Python package '${PYTHON_PACKAGE}' is not installed.\n` +
347
646
  `Run: pip install ${PYTHON_PACKAGE}\n`
348
647
  );
349
648
  process.exit(1);
350
649
  }
351
- runServer("python3", ["-m", MODULE_PATH]);
650
+ runServer(PYTHON_CMD, ["-m", MODULE_PATH]);
352
651
  } else {
353
652
  startServer(args.slice(1));
354
653
  }
654
+ } else if (mode === "--uninstall") {
655
+ runUninstall();
355
656
  } else if (mode === "--help" || mode === "-h") {
356
657
  process.stderr.write(
357
658
  `code-graph-builder - Code knowledge graph MCP server\n\n` +
358
659
  `Usage:\n` +
359
- ` npx code-graph-builder Interactive setup wizard\n` +
360
- ` npx code-graph-builder --server Start MCP server\n` +
361
- ` npx code-graph-builder --setup Re-run setup wizard\n` +
362
- ` npx code-graph-builder --help Show this help\n\n` +
660
+ ` npx code-graph-builder Interactive setup wizard\n` +
661
+ ` npx code-graph-builder --server Start MCP server\n` +
662
+ ` npx code-graph-builder --setup Re-run setup wizard\n` +
663
+ ` npx code-graph-builder --uninstall Completely uninstall\n` +
664
+ ` npx code-graph-builder --help Show this help\n\n` +
363
665
  `Config: ${ENV_FILE}\n`
364
666
  );
365
667
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-builder",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Code knowledge graph builder with MCP server for AI-assisted code navigation",
5
5
  "license": "MIT",
6
6
  "bin": {