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