code-graph-builder 0.19.0 → 0.21.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 +464 -254
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { spawn, execFileSync, execSync } from "node:child_process";
14
14
  import { createInterface } from "node:readline";
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs";
16
16
  import { homedir, platform } from "node:os";
17
17
  import { join } from "node:path";
18
18
 
@@ -21,6 +21,147 @@ 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
23
  const IS_WIN = platform() === "win32";
24
+ const PYPI_INDEX = "https://pypi.org/simple";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Tree-style UI helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const T = {
31
+ // Box drawing
32
+ TOP: "╭",
33
+ BOT: "╰",
34
+ SIDE: "│",
35
+ TEE: "├",
36
+ BEND: "╰",
37
+ DASH: "─",
38
+ // Status
39
+ OK: "✓",
40
+ FAIL: "✗",
41
+ WARN: "⚠",
42
+ WORK: "…",
43
+ DOT: "●",
44
+ // Indents
45
+ PIPE: "│ ",
46
+ SPACE: " ",
47
+ BRANCH: "├─ ",
48
+ LAST: "╰─ ",
49
+ };
50
+
51
+ /**
52
+ * Interactive single-select menu.
53
+ * Arrow keys to navigate, Space to select, Enter to confirm.
54
+ * Returns the index of the selected option, or -1 if cancelled (Ctrl+C).
55
+ *
56
+ * @param {string[]} options - Display labels for each option
57
+ * @param {string} prefix - Tree prefix for each line (e.g. " │ ")
58
+ * @param {number} defaultIndex - Initially highlighted index
59
+ * @returns {Promise<number>}
60
+ */
61
+ function selectMenu(options, prefix = " ", defaultIndex = 0) {
62
+ return new Promise((resolve) => {
63
+ const out = process.stderr;
64
+ let cursor = defaultIndex;
65
+ let selected = -1;
66
+
67
+ const RADIO_ON = "◉";
68
+ const RADIO_OFF = "○";
69
+ const DIM = "\x1b[2m";
70
+ const BOLD = "\x1b[1m";
71
+ const CYAN = "\x1b[36m";
72
+ const RESET = "\x1b[0m";
73
+
74
+ function render(initial = false) {
75
+ // Move cursor up to overwrite previous render (skip on first draw)
76
+ if (!initial) {
77
+ out.write(`\x1b[${options.length}A`);
78
+ }
79
+ for (let i = 0; i < options.length; i++) {
80
+ const isActive = i === cursor;
81
+ const isSelected = i === selected;
82
+ const radio = (isSelected || (selected === -1 && isActive)) && isActive
83
+ ? `${CYAN}${RADIO_ON}${RESET}`
84
+ : `${DIM}${RADIO_OFF}${RESET}`;
85
+ const label = isActive
86
+ ? `${BOLD}${CYAN}${options[i]}${RESET}`
87
+ : `${options[i]}`;
88
+ // Clear line then write
89
+ out.write(`\x1b[2K${prefix}${radio} ${label}\n`);
90
+ }
91
+ }
92
+
93
+ // Hide cursor
94
+ out.write("\x1b[?25l");
95
+ render(true);
96
+
97
+ const stdin = process.stdin;
98
+ const wasRaw = stdin.isRaw;
99
+ stdin.setRawMode(true);
100
+ stdin.resume();
101
+
102
+ function cleanup() {
103
+ stdin.setRawMode(wasRaw || false);
104
+ stdin.removeListener("data", onKey);
105
+ // Show cursor
106
+ out.write("\x1b[?25h");
107
+ }
108
+
109
+ function onKey(buf) {
110
+ const key = buf.toString();
111
+
112
+ // Ctrl+C
113
+ if (key === "\x03") {
114
+ cleanup();
115
+ resolve(-1);
116
+ return;
117
+ }
118
+
119
+ // Arrow up / k
120
+ if (key === "\x1b[A" || key === "k") {
121
+ cursor = (cursor - 1 + options.length) % options.length;
122
+ render();
123
+ return;
124
+ }
125
+
126
+ // Arrow down / j
127
+ if (key === "\x1b[B" || key === "j") {
128
+ cursor = (cursor + 1) % options.length;
129
+ render();
130
+ return;
131
+ }
132
+
133
+ // Space — toggle selection
134
+ if (key === " ") {
135
+ selected = cursor;
136
+ render();
137
+ return;
138
+ }
139
+
140
+ // Enter — confirm
141
+ if (key === "\r" || key === "\n") {
142
+ if (selected === -1) selected = cursor;
143
+ cleanup();
144
+ resolve(selected);
145
+ return;
146
+ }
147
+ }
148
+
149
+ stdin.on("data", onKey);
150
+ });
151
+ }
152
+
153
+ function box(title) {
154
+ const pad = 54;
155
+ const inner = ` ${title} `;
156
+ const fill = pad - inner.length;
157
+ const left = Math.floor(fill / 2);
158
+ const right = fill - left;
159
+ return [
160
+ ` ${T.TOP}${"─".repeat(pad)}╮`,
161
+ ` ${T.SIDE}${" ".repeat(left)}${inner}${" ".repeat(right)}${T.SIDE}`,
162
+ ` ${T.BOT}${"─".repeat(pad)}╯`,
163
+ ].join("\n");
164
+ }
24
165
 
25
166
  // ---------------------------------------------------------------------------
26
167
  // Utilities
@@ -28,7 +169,6 @@ const IS_WIN = platform() === "win32";
28
169
 
29
170
  function commandExists(cmd) {
30
171
  try {
31
- // "which" on Unix/macOS, "where" on Windows
32
172
  const checker = IS_WIN ? "where" : "which";
33
173
  execFileSync(checker, [cmd], { stdio: "pipe" });
34
174
  return true;
@@ -37,11 +177,6 @@ function commandExists(cmd) {
37
177
  }
38
178
  }
39
179
 
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
180
  function findPython() {
46
181
  const candidates = IS_WIN
47
182
  ? ["python", "python3", "py"]
@@ -49,14 +184,15 @@ function findPython() {
49
184
  for (const cmd of candidates) {
50
185
  try {
51
186
  const ver = execFileSync(cmd, ["--version"], { stdio: "pipe" }).toString().trim();
52
- // Ensure it is Python 3.x
53
- if (ver.includes("3.")) return cmd;
187
+ if (ver.includes("3.")) return { cmd, ver };
54
188
  } catch { /* skip */ }
55
189
  }
56
190
  return null;
57
191
  }
58
192
 
59
- const PYTHON_CMD = findPython();
193
+ const pythonInfo = findPython();
194
+ const PYTHON_CMD = pythonInfo?.cmd || null;
195
+ const PYTHON_VER = pythonInfo?.ver || null;
60
196
 
61
197
  function pythonPackageInstalled() {
62
198
  if (!PYTHON_CMD) return false;
@@ -70,6 +206,17 @@ function pythonPackageInstalled() {
70
206
  }
71
207
  }
72
208
 
209
+ function getPackageVersion() {
210
+ if (!PYTHON_CMD) return null;
211
+ try {
212
+ return execFileSync(PYTHON_CMD, ["-c",
213
+ `import code_graph_builder; print(getattr(code_graph_builder, '__version__', 'unknown'))`
214
+ ], { stdio: "pipe" }).toString().trim();
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
73
220
  function loadEnvFile() {
74
221
  if (!existsSync(ENV_FILE)) return {};
75
222
  const vars = {};
@@ -80,7 +227,6 @@ function loadEnvFile() {
80
227
  if (eq === -1) continue;
81
228
  const key = trimmed.slice(0, eq).trim();
82
229
  let val = trimmed.slice(eq + 1).trim();
83
- // Strip surrounding quotes
84
230
  if ((val.startsWith('"') && val.endsWith('"')) ||
85
231
  (val.startsWith("'") && val.endsWith("'"))) {
86
232
  val = val.slice(1, -1);
@@ -109,154 +255,247 @@ function mask(s) {
109
255
  return s.slice(0, 4) + "****" + s.slice(-4);
110
256
  }
111
257
 
258
+ function findPip() {
259
+ for (const cmd of IS_WIN ? ["pip", "pip3"] : ["pip3", "pip"]) {
260
+ if (commandExists(cmd)) return [cmd];
261
+ }
262
+ if (PYTHON_CMD) {
263
+ try {
264
+ execFileSync(PYTHON_CMD, ["-m", "pip", "--version"], { stdio: "pipe" });
265
+ return [PYTHON_CMD, "-m", "pip"];
266
+ } catch { /* skip */ }
267
+ }
268
+ return null;
269
+ }
270
+
271
+ /**
272
+ * Clear npx cache for code-graph-builder to ensure latest version.
273
+ */
274
+ function clearNpxCache() {
275
+ try {
276
+ const cacheDir = execSync("npm config get cache", { stdio: "pipe", shell: true })
277
+ .toString().trim();
278
+ const npxCacheDir = join(cacheDir, "_npx");
279
+
280
+ if (existsSync(npxCacheDir)) {
281
+ for (const entry of readdirSync(npxCacheDir)) {
282
+ const pkgJsonPath = join(npxCacheDir, entry, "node_modules", "code-graph-builder", "package.json");
283
+ const altPkgJson = join(npxCacheDir, entry, "package.json");
284
+ try {
285
+ let found = false;
286
+ if (existsSync(pkgJsonPath)) {
287
+ found = true;
288
+ } else if (existsSync(altPkgJson)) {
289
+ const content = readFileSync(altPkgJson, "utf-8");
290
+ if (content.includes("code-graph-builder")) found = true;
291
+ }
292
+ if (found) {
293
+ rmSync(join(npxCacheDir, entry), { recursive: true, force: true });
294
+ }
295
+ } catch { /* skip */ }
296
+ }
297
+ }
298
+ } catch { /* cache clear is best-effort */ }
299
+ }
300
+
112
301
  // ---------------------------------------------------------------------------
113
- // Interactive setup wizard (runs on stderr so stdout stays clean)
302
+ // Interactive setup wizard
114
303
  // ---------------------------------------------------------------------------
115
304
 
116
305
  async function runSetup() {
117
- const rl = createInterface({
306
+ let rl = createInterface({
118
307
  input: process.stdin,
119
308
  output: process.stderr,
120
309
  });
121
310
 
122
- const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
123
- const log = (msg) => process.stderr.write(msg + "\n");
311
+ let ask = (q) => new Promise((resolve) => rl.question(q, resolve));
312
+ const log = (msg = "") => process.stderr.write(msg + "\n");
124
313
 
125
- log("");
126
- log("╔══════════════════════════════════════════════════════════╗");
127
- log("║ code-graph-builder Setup Wizard ║");
128
- log("╚══════════════════════════════════════════════════════════╝");
129
- log("");
314
+ log();
315
+ log(box("code-graph-builder Setup Wizard"));
316
+ log();
317
+
318
+ // --- Step 0: Clear npx cache ---
319
+ log(` ${T.DOT} Preparing`);
320
+ log(` ${T.SIDE}`);
321
+ log(` ${T.BRANCH} Clearing npx cache...`);
322
+
323
+ await clearNpxCache();
324
+
325
+ log(` ${T.LAST} ${T.OK} Cache cleared`);
326
+ log();
130
327
 
131
328
  // Load existing config
132
329
  const existing = loadEnvFile();
133
330
 
134
- // --- Workspace ---
135
- log("── 1/3 Workspace ──────────────────────────────────────────");
136
- log(`Workspace stores indexed repos, graphs, and embeddings.`);
331
+ // --- Step 1: Workspace ---
332
+ log(` ${T.DOT} Step 1/3 Workspace`);
333
+ log(` ${T.SIDE}`);
334
+ log(` ${T.BRANCH} Stores indexed repos, graphs, and embeddings`);
335
+
137
336
  const workspace =
138
- (await ask(` Workspace path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
139
- log("");
337
+ (await ask(` ${T.SIDE} Path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
140
338
 
141
- // --- LLM Provider ---
142
- log("── 2/3 LLM Provider (for natural language queries & descriptions) ──");
143
- log("");
144
- log(" Select your LLM provider:");
145
- log("");
146
- log(" 1) Moonshot / Kimi https://platform.moonshot.cn");
147
- log(" 2) OpenAI https://platform.openai.com");
148
- log(" 3) DeepSeek https://platform.deepseek.com");
149
- log(" 4) OpenRouter https://openrouter.ai");
150
- log(" 5) LiteLLM Proxy (OpenAI-compatible gateway)");
151
- log(" 6) Custom (any OpenAI-compatible endpoint)");
152
- log(" 7) Skip (configure later)");
153
- log("");
339
+ log(` ${T.LAST} ${T.OK} ${workspace}`);
340
+ log();
154
341
 
155
- const providers = {
156
- "1": { name: "Moonshot", url: "https://api.moonshot.cn/v1", model: "kimi-k2.5" },
157
- "2": { name: "OpenAI", url: "https://api.openai.com/v1", model: "gpt-4o" },
158
- "3": { name: "DeepSeek", url: "https://api.deepseek.com/v1", model: "deepseek-chat" },
159
- "4": { name: "OpenRouter", url: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4" },
160
- "5": { name: "LiteLLM", url: "http://localhost:4000/v1", model: "gpt-4o" },
161
- };
342
+ // --- Step 2: LLM Provider ---
343
+ log(` ${T.DOT} Step 2/3 LLM Provider`);
344
+ log(` ${T.SIDE}`);
345
+ log(` ${T.BRANCH} For natural language queries & descriptions`);
346
+ log(` ${T.SIDE} Use ↑↓ to navigate, Space to select, Enter to confirm`);
347
+ log(` ${T.SIDE}`);
162
348
 
163
349
  if (existing.LLM_API_KEY) {
164
- log(` Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
350
+ log(` ${T.SIDE} Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
351
+ log(` ${T.SIDE}`);
165
352
  }
166
353
 
167
- const choice = (await ask(" Choose provider [1-7]: ")).trim() || "7";
354
+ const llmOptions = [
355
+ "Moonshot / Kimi platform.moonshot.cn",
356
+ "OpenAI platform.openai.com",
357
+ "DeepSeek platform.deepseek.com",
358
+ "OpenRouter openrouter.ai",
359
+ "LiteLLM Proxy localhost:4000",
360
+ "Custom endpoint",
361
+ "Skip (configure later)",
362
+ ];
363
+
364
+ const llmProviders = [
365
+ { name: "Moonshot", url: "https://api.moonshot.cn/v1", model: "kimi-k2.5" },
366
+ { name: "OpenAI", url: "https://api.openai.com/v1", model: "gpt-4o" },
367
+ { name: "DeepSeek", url: "https://api.deepseek.com/v1", model: "deepseek-chat" },
368
+ { name: "OpenRouter", url: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4" },
369
+ { name: "LiteLLM", url: "http://localhost:4000/v1", model: "gpt-4o" },
370
+ ];
371
+
372
+ // Close readline before raw mode menu, reopen after
373
+ rl.close();
374
+ const llmChoice = await selectMenu(llmOptions, ` ${T.SIDE} `, 6);
375
+ rl = createInterface({ input: process.stdin, output: process.stderr });
376
+ ask = (q) => new Promise((resolve) => rl.question(q, resolve));
168
377
 
169
378
  let llmKey = existing.LLM_API_KEY || "";
170
379
  let llmBaseUrl = existing.LLM_BASE_URL || "";
171
380
  let llmModel = existing.LLM_MODEL || "";
381
+ let llmProviderName = "skipped";
172
382
 
173
- if (choice !== "7") {
174
- const provider = providers[choice];
175
-
176
- if (provider) {
177
- log(`\n → ${provider.name} selected`);
178
- llmBaseUrl = provider.url;
179
- llmModel = provider.model;
180
- } else {
181
- // Choice "6" or invalid → custom
182
- log("\n → Custom provider");
183
- llmBaseUrl = (await ask(" API Base URL: ")).trim() || llmBaseUrl;
184
- llmModel = (await ask(" Model name: ")).trim() || llmModel || "gpt-4o";
185
- }
383
+ if (llmChoice >= 0 && llmChoice < 5) {
384
+ // Known provider
385
+ const provider = llmProviders[llmChoice];
386
+ llmBaseUrl = provider.url;
387
+ llmModel = provider.model;
388
+ llmProviderName = provider.name;
186
389
 
187
- llmKey = (await ask(` API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
390
+ log(` ${T.SIDE}`);
391
+ llmKey = (await ask(` ${T.SIDE} API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
188
392
 
189
393
  if (llmKey) {
190
- // Allow overriding URL and model
191
- const urlOverride = (await ask(` Base URL [${llmBaseUrl}]: `)).trim();
394
+ const urlOverride = (await ask(` ${T.SIDE} Base URL [${llmBaseUrl}]: `)).trim();
192
395
  if (urlOverride) llmBaseUrl = urlOverride;
193
- const modelOverride = (await ask(` Model [${llmModel}]: `)).trim();
396
+ const modelOverride = (await ask(` ${T.SIDE} Model [${llmModel}]: `)).trim();
194
397
  if (modelOverride) llmModel = modelOverride;
195
398
  }
399
+ } else if (llmChoice === 5) {
400
+ // Custom
401
+ llmProviderName = "Custom";
402
+ const defUrl = llmBaseUrl || existing.LLM_BASE_URL || "";
403
+ const defModel = llmModel || existing.LLM_MODEL || "gpt-4o";
404
+ const defKey = existing.LLM_API_KEY || "";
405
+ log(` ${T.SIDE}`);
406
+ llmBaseUrl = (await ask(` ${T.SIDE} API Base URL${defUrl ? ` [${defUrl}]` : ""}: `)).trim() || defUrl;
407
+ llmModel = (await ask(` ${T.SIDE} Model${defModel ? ` [${defModel}]` : ""}: `)).trim() || defModel;
408
+ llmKey = (await ask(` ${T.SIDE} API Key${defKey ? ` [${mask(defKey)}]` : " (sk-...)"}: `)).trim() || defKey;
196
409
  }
197
- log("");
410
+ // llmChoice === 6 or -1 → skip
198
411
 
199
- // --- Embedding Provider ---
200
- log("── 3/3 Embedding Provider (for semantic code search) ─────");
201
- log("");
202
- log(" Select your embedding provider:");
203
- log("");
204
- log(" 1) DashScope / Qwen https://dashscope.console.aliyun.com (free tier)");
205
- log(" 2) OpenAI Embeddings https://platform.openai.com");
206
- log(" 3) Custom (any OpenAI-compatible embedding endpoint)");
207
- log(" 4) Skip (configure later)");
208
- log("");
412
+ if (llmKey) {
413
+ log(` ${T.LAST} ${T.OK} ${llmProviderName} / ${llmModel}`);
414
+ } else {
415
+ log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
416
+ }
417
+ log();
209
418
 
210
- const embedProviders = {
211
- "1": { name: "DashScope", url: "https://dashscope.aliyuncs.com/api/v1", model: "text-embedding-v4", keyEnv: "DASHSCOPE_API_KEY", urlEnv: "DASHSCOPE_BASE_URL" },
212
- "2": { name: "OpenAI", url: "https://api.openai.com/v1", model: "text-embedding-3-small", keyEnv: "OPENAI_API_KEY", urlEnv: "OPENAI_BASE_URL" },
213
- };
419
+ // --- Step 3: Embedding Provider ---
420
+ log(` ${T.DOT} Step 3/3 Embedding Provider`);
421
+ log(` ${T.SIDE}`);
422
+ log(` ${T.BRANCH} For semantic code search`);
423
+ log(` ${T.SIDE} Use ↑↓ to navigate, Space to select, Enter to confirm`);
424
+ log(` ${T.SIDE}`);
214
425
 
215
426
  if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
216
427
  const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
217
- log(` Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
428
+ log(` ${T.SIDE} Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
429
+ log(` ${T.SIDE}`);
218
430
  }
219
431
 
220
- const embedChoice = (await ask(" Choose provider [1-4]: ")).trim() || "4";
432
+ const embedOptions = [
433
+ "DashScope / Qwen dashscope.console.aliyun.com (free tier)",
434
+ "OpenAI Embeddings platform.openai.com",
435
+ "Custom endpoint",
436
+ "Skip (configure later)",
437
+ ];
438
+
439
+ const embedProvidersList = [
440
+ { name: "DashScope", url: "https://dashscope.aliyuncs.com/api/v1", model: "text-embedding-v4", keyEnv: "DASHSCOPE_API_KEY", urlEnv: "DASHSCOPE_BASE_URL" },
441
+ { name: "OpenAI", url: "https://api.openai.com/v1", model: "text-embedding-3-small", keyEnv: "OPENAI_API_KEY", urlEnv: "OPENAI_BASE_URL" },
442
+ ];
443
+
444
+ rl.close();
445
+ const embedChoice = await selectMenu(embedOptions, ` ${T.SIDE} `, 3);
446
+ rl = createInterface({ input: process.stdin, output: process.stderr });
447
+ ask = (q) => new Promise((resolve) => rl.question(q, resolve));
221
448
 
222
449
  let embedKey = "";
223
450
  let embedUrl = "";
224
451
  let embedModel = "";
225
452
  let embedKeyEnv = "DASHSCOPE_API_KEY";
226
453
  let embedUrlEnv = "DASHSCOPE_BASE_URL";
227
-
228
- if (embedChoice !== "4") {
229
- const ep = embedProviders[embedChoice];
230
-
231
- if (ep) {
232
- log(`\n → ${ep.name} selected`);
233
- embedUrl = ep.url;
234
- embedModel = ep.model;
235
- embedKeyEnv = ep.keyEnv;
236
- embedUrlEnv = ep.urlEnv;
237
- } else {
238
- // Choice "3" or invalid → custom
239
- log("\n Custom embedding provider");
240
- embedUrl = (await ask(" Embedding API Base URL: ")).trim();
241
- embedModel = (await ask(" Embedding model name: ")).trim() || "text-embedding-3-small";
242
- embedKeyEnv = "EMBED_API_KEY";
243
- embedUrlEnv = "EMBED_BASE_URL";
244
- }
245
-
246
- embedKey = (await ask(` API Key: `)).trim() ||
454
+ let embedProviderName = "skipped";
455
+
456
+ if (embedChoice >= 0 && embedChoice < 2) {
457
+ // Known provider
458
+ const ep = embedProvidersList[embedChoice];
459
+ embedUrl = ep.url;
460
+ embedModel = ep.model;
461
+ embedKeyEnv = ep.keyEnv;
462
+ embedUrlEnv = ep.urlEnv;
463
+ embedProviderName = ep.name;
464
+
465
+ log(` ${T.SIDE}`);
466
+ embedKey = (await ask(` ${T.SIDE} API Key: `)).trim() ||
247
467
  existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
248
468
 
249
469
  if (embedKey) {
250
- const urlOverride = (await ask(` Base URL [${embedUrl}]: `)).trim();
470
+ const urlOverride = (await ask(` ${T.SIDE} Base URL [${embedUrl}]: `)).trim();
251
471
  if (urlOverride) embedUrl = urlOverride;
252
- const modelOverride = (await ask(` Model [${embedModel}]: `)).trim();
472
+ const modelOverride = (await ask(` ${T.SIDE} Model [${embedModel}]: `)).trim();
253
473
  if (modelOverride) embedModel = modelOverride;
254
474
  }
475
+ } else if (embedChoice === 2) {
476
+ // Custom
477
+ embedProviderName = "Custom";
478
+ const defEmbedUrl = existing.EMBED_BASE_URL || existing.DASHSCOPE_BASE_URL || "";
479
+ const defEmbedModel = existing.EMBED_MODEL || "text-embedding-3-small";
480
+ const defEmbedKey = existing.EMBED_API_KEY || existing.DASHSCOPE_API_KEY || "";
481
+ log(` ${T.SIDE}`);
482
+ embedUrl = (await ask(` ${T.SIDE} API Base URL${defEmbedUrl ? ` [${defEmbedUrl}]` : ""}: `)).trim() || defEmbedUrl;
483
+ embedModel = (await ask(` ${T.SIDE} Model${defEmbedModel ? ` [${defEmbedModel}]` : ""}: `)).trim() || defEmbedModel;
484
+ embedKey = (await ask(` ${T.SIDE} API Key${defEmbedKey ? ` [${mask(defEmbedKey)}]` : ""}: `)).trim() || defEmbedKey;
485
+ embedKeyEnv = "EMBED_API_KEY";
486
+ embedUrlEnv = "EMBED_BASE_URL";
487
+ }
488
+ // embedChoice === 3 or -1 → skip
489
+
490
+ if (embedKey) {
491
+ log(` ${T.LAST} ${T.OK} ${embedProviderName} / ${embedModel}`);
492
+ } else {
493
+ log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
255
494
  }
256
495
 
257
496
  rl.close();
258
497
 
259
- // --- Save ---
498
+ // --- Save config ---
260
499
  const config = {
261
500
  CGB_WORKSPACE: workspace,
262
501
  LLM_API_KEY: llmKey,
@@ -264,7 +503,6 @@ async function runSetup() {
264
503
  LLM_MODEL: llmModel,
265
504
  };
266
505
 
267
- // Save embedding config with the correct env var names
268
506
  if (embedKey) {
269
507
  config[embedKeyEnv] = embedKey;
270
508
  config[embedUrlEnv] = embedUrl;
@@ -273,59 +511,65 @@ async function runSetup() {
273
511
 
274
512
  saveEnvFile(config);
275
513
 
276
- const embedDisplay = embedKey
277
- ? `${mask(embedKey)} → ${embedModel || embedUrl}`
278
- : "not configured (optional)";
514
+ log();
515
+ log(` ${T.DOT} Configuration saved`);
516
+ log(` ${T.SIDE}`);
517
+ log(` ${T.BRANCH} File: ${ENV_FILE}`);
518
+ log(` ${T.BRANCH} LLM: ${llmKey ? `${llmProviderName} / ${llmModel}` : "not configured"}`);
519
+ log(` ${T.BRANCH} Embedding: ${embedKey ? `${embedProviderName} / ${embedModel}` : "not configured"}`);
520
+ log(` ${T.LAST} Workspace: ${workspace}`);
521
+ log();
279
522
 
280
- log("");
281
- log("── Configuration saved ─────────────────────────────────────");
282
- log(` File: ${ENV_FILE}`);
283
- log("");
284
- log(" LLM: " + (llmKey ? `${mask(llmKey)} → ${llmModel}` : "not configured (optional)"));
285
- log(" Embedding: " + embedDisplay);
286
- log(" Workspace: " + workspace);
287
- log("");
288
-
289
- // --- Verify installation ---
290
- log("── Verifying installation ──────────────────────────────────");
291
- log("");
523
+ // --- Verification ---
524
+ log(` ${T.DOT} Verification`);
525
+ log(` ${T.SIDE}`);
292
526
 
293
- // Step 1: Python available?
527
+ // 1. Python
294
528
  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();
529
+ log(` ${T.BRANCH} ${T.FAIL} Python 3 not found`);
530
+ log(` ${T.LAST} Install Python 3.10+ and re-run: npx code-graph-builder@latest --setup`);
531
+ log();
299
532
  return;
300
533
  }
301
- log(` Python found: ${PYTHON_CMD}`);
534
+ log(` ${T.BRANCH} ${T.OK} ${PYTHON_VER}`);
302
535
 
303
- // Step 2: Package installed? If not, auto-install.
536
+ // 2. Package auto-install or upgrade
537
+ const pip = findPip();
304
538
  if (!pythonPackageInstalled()) {
305
- log(` Installing ${PYTHON_PACKAGE} via pip...`);
306
- const pip = findPip();
539
+ log(` ${T.SIDE} ${T.WORK} Installing ${PYTHON_PACKAGE}...`);
307
540
  if (pip) {
308
541
  try {
309
542
  execSync(
310
- [...pip, "install", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
543
+ [...pip, "install", "--upgrade", "-i", PYPI_INDEX, PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
311
544
  { stdio: "pipe", shell: true }
312
545
  );
313
546
  } catch { /* handled below */ }
314
547
  }
548
+ } else {
549
+ // Already installed — upgrade to latest
550
+ log(` ${T.SIDE} ${T.WORK} Upgrading ${PYTHON_PACKAGE} to latest...`);
551
+ if (pip) {
552
+ try {
553
+ execSync(
554
+ [...pip, "install", "--upgrade", "-i", PYPI_INDEX, PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
555
+ { stdio: "pipe", shell: true }
556
+ );
557
+ } catch { /* upgrade is best-effort */ }
558
+ }
315
559
  }
316
560
 
317
561
  if (pythonPackageInstalled()) {
318
- log(` ✓ Python package installed: ${PYTHON_PACKAGE}`);
562
+ const ver = getPackageVersion();
563
+ log(` ${T.BRANCH} ${T.OK} ${PYTHON_PACKAGE} ${ver || ""}`);
319
564
  } else {
320
- log(` Python package not installed`);
321
- log(` Run manually: pip install ${PYTHON_PACKAGE}`);
322
- log("");
323
- rl.close();
565
+ log(` ${T.BRANCH} ${T.FAIL} Package not installed`);
566
+ log(` ${T.LAST} Run manually: pip install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}`);
567
+ log();
324
568
  return;
325
569
  }
326
570
 
327
- // Step 3: MCP server smoke test — spawn server, send initialize, check tools/list
328
- log(" Starting MCP server smoke test...");
571
+ // 3. MCP server smoke test
572
+ log(` ${T.SIDE} ${T.WORK} MCP server smoke test...`);
329
573
 
330
574
  const verified = await new Promise((resolve) => {
331
575
  const envVars = loadEnvFile();
@@ -348,14 +592,12 @@ async function runSetup() {
348
592
  resolve({ success, detail });
349
593
  };
350
594
 
351
- // Timeout after 15s
352
595
  const timer = setTimeout(() => finish(false, "Server did not respond within 15s"), 15000);
353
596
 
354
- child.stderr.on("data", () => {}); // Suppress server logs
597
+ child.stderr.on("data", () => {});
355
598
 
356
599
  child.stdout.on("data", (chunk) => {
357
600
  stdout += chunk.toString();
358
- // MCP stdio uses JSON lines (one JSON-RPC message per line)
359
601
  const lines = stdout.split("\n");
360
602
  for (const line of lines) {
361
603
  const trimmed = line.trim();
@@ -363,7 +605,6 @@ async function runSetup() {
363
605
  try {
364
606
  const msg = JSON.parse(trimmed);
365
607
  if (msg.result && msg.result.capabilities) {
366
- // Got initialize response, now request tools/list
367
608
  const toolsReq = JSON.stringify({
368
609
  jsonrpc: "2.0", id: 2, method: "tools/list", params: {},
369
610
  });
@@ -373,10 +614,10 @@ async function runSetup() {
373
614
  }
374
615
  if (msg.result && msg.result.tools) {
375
616
  clearTimeout(timer);
376
- finish(true, `${msg.result.tools.length} tools available`);
617
+ finish(true, `${msg.result.tools.length} tools`);
377
618
  return;
378
619
  }
379
- } catch { /* partial JSON, wait for more */ }
620
+ } catch { /* partial JSON */ }
380
621
  }
381
622
  });
382
623
 
@@ -390,7 +631,6 @@ async function runSetup() {
390
631
  if (!resolved) finish(false, `Server exited with code ${code}`);
391
632
  });
392
633
 
393
- // Send MCP initialize request as JSON line
394
634
  const initReq = JSON.stringify({
395
635
  jsonrpc: "2.0",
396
636
  id: 1,
@@ -405,55 +645,51 @@ async function runSetup() {
405
645
  });
406
646
 
407
647
  if (verified.success) {
408
- log(` MCP server started successfully (${verified.detail})`);
648
+ log(` ${T.BRANCH} ${T.OK} MCP server (${verified.detail})`);
409
649
  } else {
410
- log(` MCP server smoke test failed: ${verified.detail}`);
411
- log(" The server may still work — try: npx code-graph-builder --server");
650
+ log(` ${T.BRANCH} ${T.FAIL} MCP smoke test: ${verified.detail}`);
412
651
  }
413
652
 
414
- // Step 4: Auto-register in Claude Code if available
415
- log("");
416
- log("── Registering MCP server ─────────────────────────────────");
417
- log("");
418
-
653
+ // 4. Claude Code registration
419
654
  if (commandExists("claude")) {
420
655
  try {
421
- // Remove existing entry first (ignore errors if not found)
422
656
  try {
423
657
  execSync("claude mcp remove code-graph-builder", { stdio: "pipe", shell: true });
424
- } catch { /* not found, fine */ }
658
+ } catch { /* not found */ }
425
659
 
426
660
  const addCmd = IS_WIN
427
661
  ? 'claude mcp add --scope user --transport stdio code-graph-builder -- cmd /c npx -y code-graph-builder@latest --server'
428
662
  : 'claude mcp add --scope user --transport stdio code-graph-builder -- npx -y code-graph-builder@latest --server';
429
663
 
430
664
  execSync(addCmd, { stdio: "pipe", shell: true });
431
- log(" Registered in Claude Code (global): code-graph-builder");
432
- } catch (err) {
433
- log(" Failed to register in Claude Code automatically");
434
- log(" Run manually:");
665
+ log(` ${T.LAST} ${T.OK} Claude Code MCP registered (global)`);
666
+ } catch {
667
+ log(` ${T.LAST} ${T.WARN} Claude Code auto-register failed`);
668
+ log(` Run manually:`);
435
669
  if (IS_WIN) {
436
- log(' claude mcp add --scope user --transport stdio code-graph-builder -- cmd /c npx -y code-graph-builder@latest --server');
670
+ log(` claude mcp add --scope user --transport stdio code-graph-builder -- cmd /c npx -y code-graph-builder@latest --server`);
437
671
  } else {
438
- log(' claude mcp add --scope user --transport stdio code-graph-builder -- npx -y code-graph-builder@latest --server');
672
+ log(` claude mcp add --scope user --transport stdio code-graph-builder -- npx -y code-graph-builder@latest --server`);
439
673
  }
440
674
  }
441
675
  } else {
442
- log(" Claude Code CLI not found. Add manually to your MCP client config:");
443
- log("");
444
- log(' {');
445
- log(' "mcpServers": {');
446
- log(' "code-graph-builder": {');
447
- log(' "command": "npx",');
448
- log(' "args": ["-y", "code-graph-builder@latest", "--server"]');
449
- log(" }");
450
- log(" }");
451
- log(" }");
676
+ log(` ${T.LAST} ${T.WARN} Claude Code CLI not found`);
677
+ log();
678
+ log(` Add to your MCP client config manually:`);
679
+ log();
680
+ log(` {`);
681
+ log(` "mcpServers": {`);
682
+ log(` "code-graph-builder": {`);
683
+ log(` "command": "npx",`);
684
+ log(` "args": ["-y", "code-graph-builder@latest", "--server"]`);
685
+ log(` }`);
686
+ log(` }`);
687
+ log(` }`);
452
688
  }
453
689
 
454
- log("");
455
- log("── Setup complete ─────────────────────────────────────────");
456
- log("");
690
+ log();
691
+ log(` ${T.DOT} Setup complete`);
692
+ log();
457
693
  }
458
694
 
459
695
  // ---------------------------------------------------------------------------
@@ -461,11 +697,9 @@ async function runSetup() {
461
697
  // ---------------------------------------------------------------------------
462
698
 
463
699
  function runServer(cmd, args) {
464
- // Merge .env file into environment
465
700
  const envVars = loadEnvFile();
466
701
  const mergedEnv = { ...process.env, ...envVars };
467
702
 
468
- // Ensure CGB_WORKSPACE is set
469
703
  if (!mergedEnv.CGB_WORKSPACE) {
470
704
  mergedEnv.CGB_WORKSPACE = WORKSPACE_DIR;
471
705
  }
@@ -473,7 +707,7 @@ function runServer(cmd, args) {
473
707
  const child = spawn(cmd, args, {
474
708
  stdio: "inherit",
475
709
  env: mergedEnv,
476
- shell: IS_WIN, // Windows needs shell for .cmd/.ps1 scripts (uvx, pipx, etc.)
710
+ shell: IS_WIN,
477
711
  });
478
712
 
479
713
  child.on("error", (err) => {
@@ -486,28 +720,6 @@ function runServer(cmd, args) {
486
720
  });
487
721
  }
488
722
 
489
- /**
490
- * Find a working pip command. Returns [cmd, ...prefixArgs] or null.
491
- * Tries: pip3, pip, python3 -m pip, python -m pip
492
- */
493
- function findPip() {
494
- // Standalone pip
495
- for (const cmd of IS_WIN ? ["pip", "pip3"] : ["pip3", "pip"]) {
496
- if (commandExists(cmd)) return [cmd];
497
- }
498
- // python -m pip fallback
499
- if (PYTHON_CMD) {
500
- try {
501
- execFileSync(PYTHON_CMD, ["-m", "pip", "--version"], { stdio: "pipe" });
502
- return [PYTHON_CMD, "-m", "pip"];
503
- } catch { /* skip */ }
504
- }
505
- return null;
506
- }
507
-
508
- /**
509
- * Auto-install the Python package via pip, then start the server.
510
- */
511
723
  function autoInstallAndStart(extraArgs) {
512
724
  const pip = findPip();
513
725
  if (!pip) {
@@ -526,22 +738,21 @@ function autoInstallAndStart(extraArgs) {
526
738
 
527
739
  try {
528
740
  execSync(
529
- [...pip, "install", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
741
+ [...pip, "install", "-i", PYPI_INDEX, PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
530
742
  { stdio: "inherit", shell: true }
531
743
  );
532
744
  } catch (err) {
533
745
  process.stderr.write(
534
746
  `\nFailed to install ${PYTHON_PACKAGE}.\n` +
535
- `Try manually: ${pip.join(" ")} install ${PYTHON_PACKAGE}\n`
747
+ `Try manually: ${pip.join(" ")} install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}\n`
536
748
  );
537
749
  process.exit(1);
538
750
  }
539
751
 
540
- // Verify installation succeeded
541
752
  if (!pythonPackageInstalled()) {
542
753
  process.stderr.write(
543
754
  `\nInstallation completed but package not importable.\n` +
544
- `Try manually: ${pip.join(" ")} install ${PYTHON_PACKAGE}\n`
755
+ `Try manually: ${pip.join(" ")} install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}\n`
545
756
  );
546
757
  process.exit(1);
547
758
  }
@@ -551,42 +762,39 @@ function autoInstallAndStart(extraArgs) {
551
762
  }
552
763
 
553
764
  // ---------------------------------------------------------------------------
554
- // Uninstall — remove Python package, config, workspace data, Claude MCP entry
765
+ // Uninstall
555
766
  // ---------------------------------------------------------------------------
556
767
 
557
768
  async function runUninstall() {
558
769
  const rl = createInterface({ input: process.stdin, output: process.stderr });
559
770
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
560
- const log = (msg) => process.stderr.write(msg + "\n");
771
+ const log = (msg = "") => process.stderr.write(msg + "\n");
561
772
 
562
- log("");
563
- log("╔══════════════════════════════════════════════════════════╗");
564
- log("║ code-graph-builder Uninstall ║");
565
- log("╚══════════════════════════════════════════════════════════╝");
566
- log("");
773
+ log();
774
+ log(box("code-graph-builder Uninstall"));
775
+ log();
567
776
 
568
- // 1. Show what will be removed
569
777
  const pip = findPip();
570
778
  const hasPythonPkg = pythonPackageInstalled();
571
779
  const hasWorkspace = existsSync(WORKSPACE_DIR);
572
780
  const hasEnv = existsSync(ENV_FILE);
573
781
 
574
- // Check if code-graph-builder is registered in Claude Code
575
782
  let hasClaudeConfig = false;
576
783
  try {
577
784
  const mcpList = execFileSync("claude", ["mcp", "list"], { stdio: "pipe" }).toString();
578
785
  hasClaudeConfig = mcpList.includes("code-graph-builder");
579
786
  } catch { /* claude CLI not available */ }
580
787
 
581
- log(" The following will be removed:");
582
- log("");
583
- if (hasPythonPkg) log(" ✓ Python package: code-graph-builder");
584
- else log(" - Python package: not installed");
585
- if (hasWorkspace) log(` Workspace data: ${WORKSPACE_DIR}`);
586
- else log(" - Workspace data: not found");
587
- if (hasEnv) log(` Config file: ${ENV_FILE}`);
588
- if (hasClaudeConfig) log(" ✓ Claude Code MCP server entry");
589
- log("");
788
+ log(` ${T.DOT} Components detected`);
789
+ log(` ${T.SIDE}`);
790
+ if (hasPythonPkg) log(` ${T.BRANCH} Python package: code-graph-builder`);
791
+ else log(` ${T.BRANCH} Python package: (not installed)`);
792
+ if (hasWorkspace) log(` ${T.BRANCH} Workspace data: ${WORKSPACE_DIR}`);
793
+ else log(` ${T.BRANCH} Workspace data: (not found)`);
794
+ if (hasEnv) log(` ${T.BRANCH} Config file: ${ENV_FILE}`);
795
+ if (hasClaudeConfig) log(` ${T.BRANCH} Claude Code MCP: registered`);
796
+ log(` ${T.LAST}`);
797
+ log();
590
798
 
591
799
  const answer = (await ask(" Proceed with uninstall? [y/N]: ")).trim().toLowerCase();
592
800
  rl.close();
@@ -596,50 +804,54 @@ async function runUninstall() {
596
804
  process.exit(0);
597
805
  }
598
806
 
599
- log("");
807
+ log();
808
+ log(` ${T.DOT} Removing`);
809
+ log(` ${T.SIDE}`);
600
810
 
601
- // 2. Remove Claude Code MCP entry
811
+ // Claude Code MCP entry
602
812
  if (hasClaudeConfig) {
603
813
  try {
604
814
  execSync("claude mcp remove code-graph-builder", { stdio: "pipe", shell: true });
605
- log(" Removed Claude Code MCP entry");
815
+ log(` ${T.BRANCH} ${T.OK} Claude Code MCP entry`);
606
816
  } catch {
607
- log(" Could not remove Claude Code MCP entry (may not exist)");
817
+ log(` ${T.BRANCH} ${T.WARN} Claude Code MCP entry (manual removal needed)`);
608
818
  }
609
819
  }
610
820
 
611
- // 3. Uninstall Python package
821
+ // Python package
612
822
  if (hasPythonPkg && pip) {
613
823
  try {
614
824
  execSync(
615
825
  [...pip, "uninstall", "-y", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
616
- { stdio: "inherit", shell: true }
826
+ { stdio: "pipe", shell: true }
617
827
  );
618
- log(" Uninstalled Python package");
828
+ log(` ${T.BRANCH} ${T.OK} Python package`);
619
829
  } catch {
620
- log(" Failed to uninstall Python package. Try manually: pip uninstall code-graph-builder");
830
+ log(` ${T.BRANCH} ${T.WARN} Python package (try: pip uninstall code-graph-builder)`);
621
831
  }
622
832
  }
623
833
 
624
- // 4. Remove workspace data
834
+ // Workspace data
625
835
  if (hasWorkspace) {
626
- const { rmSync } = await import("node:fs");
627
836
  try {
628
837
  rmSync(WORKSPACE_DIR, { recursive: true, force: true });
629
- log(` Removed workspace: ${WORKSPACE_DIR}`);
838
+ log(` ${T.BRANCH} ${T.OK} Workspace data`);
630
839
  } catch (err) {
631
- log(` Failed to remove workspace: ${err.message}`);
840
+ log(` ${T.BRANCH} ${T.WARN} Workspace: ${err.message}`);
632
841
  }
633
842
  }
634
843
 
635
- log("");
636
- log(" Uninstall complete.");
637
- log(" To also clear the npx cache: npx clear-npx-cache");
638
- log("");
844
+ // npx cache
845
+ log(` ${T.SIDE} ${T.WORK} Clearing npx cache...`);
846
+ await clearNpxCache();
847
+ log(` ${T.LAST} ${T.OK} npx cache`);
848
+
849
+ log();
850
+ log(` ${T.DOT} Uninstall complete`);
851
+ log();
639
852
  }
640
853
 
641
854
  function startServer(extraArgs = []) {
642
- // Prefer pip-installed package first (most reliable, includes all deps)
643
855
  if (pythonPackageInstalled()) {
644
856
  runServer(PYTHON_CMD, ["-m", MODULE_PATH]);
645
857
  } else if (commandExists("uvx")) {
@@ -649,7 +861,6 @@ function startServer(extraArgs = []) {
649
861
  } else if (commandExists("pipx")) {
650
862
  runServer("pipx", ["run", PYTHON_PACKAGE, ...extraArgs]);
651
863
  } else {
652
- // Auto-install via pip
653
864
  autoInstallAndStart(extraArgs);
654
865
  }
655
866
  }
@@ -662,15 +873,13 @@ const args = process.argv.slice(2);
662
873
  const mode = args[0];
663
874
 
664
875
  if (mode === "--setup") {
665
- // Explicit setup request
666
876
  runSetup();
667
877
  } else if (mode === "--server" || mode === "--pip" || mode === "--python") {
668
- // Start MCP server directly
669
878
  if (mode === "--pip" || mode === "--python") {
670
879
  if (!PYTHON_CMD || !pythonPackageInstalled()) {
671
880
  process.stderr.write(
672
881
  `Error: Python package '${PYTHON_PACKAGE}' is not installed.\n` +
673
- `Run: pip install ${PYTHON_PACKAGE}\n`
882
+ `Run: pip install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}\n`
674
883
  );
675
884
  process.exit(1);
676
885
  }
@@ -681,23 +890,24 @@ if (mode === "--setup") {
681
890
  } else if (mode === "--uninstall") {
682
891
  runUninstall();
683
892
  } else if (mode === "--help" || mode === "-h") {
684
- process.stderr.write(
685
- `code-graph-builder - Code knowledge graph MCP server\n\n` +
686
- `Usage:\n` +
687
- ` npx code-graph-builder Interactive setup wizard\n` +
688
- ` npx code-graph-builder --server Start MCP server\n` +
689
- ` npx code-graph-builder --setup Re-run setup wizard\n` +
690
- ` npx code-graph-builder --uninstall Completely uninstall\n` +
691
- ` npx code-graph-builder --help Show this help\n\n` +
692
- `Config: ${ENV_FILE}\n`
693
- );
893
+ const log = (msg) => process.stderr.write(msg + "\n");
894
+ log("");
895
+ log(box("code-graph-builder"));
896
+ log("");
897
+ log(" Usage:");
898
+ log("");
899
+ log(" npx code-graph-builder Interactive setup wizard");
900
+ log(" npx code-graph-builder --server Start MCP server");
901
+ log(" npx code-graph-builder --setup Re-run setup wizard");
902
+ log(" npx code-graph-builder --uninstall Completely uninstall");
903
+ log(" npx code-graph-builder --help Show this help");
904
+ log("");
905
+ log(` Config: ${ENV_FILE}`);
906
+ log("");
694
907
  } else {
695
- // No args: auto-detect
696
908
  if (!existsSync(ENV_FILE)) {
697
- // First run → setup wizard
698
909
  runSetup();
699
910
  } else {
700
- // Config exists → start server
701
911
  startServer(args);
702
912
  }
703
913
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-builder",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Code knowledge graph builder with MCP server for AI-assisted code navigation",
5
5
  "license": "MIT",
6
6
  "bin": {