code-graph-builder 0.22.0 → 0.23.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 +176 -131
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -51,14 +51,16 @@ const T = {
51
51
  /**
52
52
  * Interactive single-select menu.
53
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).
54
+ * Returns the index of the selected option, -1 if cancelled (Ctrl+C),
55
+ * or -2 if the user pressed ← (back to previous step).
55
56
  *
56
57
  * @param {string[]} options - Display labels for each option
57
58
  * @param {string} prefix - Tree prefix for each line (e.g. " │ ")
58
59
  * @param {number} defaultIndex - Initially highlighted index
60
+ * @param {boolean} allowBack - Whether ← arrow triggers back (-2)
59
61
  * @returns {Promise<number>}
60
62
  */
61
- function selectMenu(options, prefix = " ", defaultIndex = 0) {
63
+ function selectMenu(options, prefix = " ", defaultIndex = 0, allowBack = false) {
62
64
  return new Promise((resolve) => {
63
65
  const out = process.stderr;
64
66
  let cursor = defaultIndex;
@@ -71,10 +73,12 @@ function selectMenu(options, prefix = " ", defaultIndex = 0) {
71
73
  const CYAN = "\x1b[36m";
72
74
  const RESET = "\x1b[0m";
73
75
 
76
+ const backHint = allowBack ? `${DIM} ← back${RESET}` : "";
77
+
74
78
  function render(initial = false) {
75
79
  // Move cursor up to overwrite previous render (skip on first draw)
76
80
  if (!initial) {
77
- out.write(`\x1b[${options.length}A`);
81
+ out.write(`\x1b[${options.length + (allowBack ? 1 : 0)}A`);
78
82
  }
79
83
  for (let i = 0; i < options.length; i++) {
80
84
  const isActive = i === cursor;
@@ -88,6 +92,9 @@ function selectMenu(options, prefix = " ", defaultIndex = 0) {
88
92
  // Clear line then write
89
93
  out.write(`\x1b[2K${prefix}${radio} ${label}\n`);
90
94
  }
95
+ if (allowBack) {
96
+ out.write(`\x1b[2K${prefix}${DIM}← Back to previous step${RESET}\n`);
97
+ }
91
98
  }
92
99
 
93
100
  // Hide cursor
@@ -116,6 +123,13 @@ function selectMenu(options, prefix = " ", defaultIndex = 0) {
116
123
  return;
117
124
  }
118
125
 
126
+ // Arrow left — back to previous step
127
+ if (key === "\x1b[D" && allowBack) {
128
+ cleanup();
129
+ resolve(-2);
130
+ return;
131
+ }
132
+
119
133
  // Arrow up / k
120
134
  if (key === "\x1b[A" || key === "k") {
121
135
  cursor = (cursor - 1 + options.length) % options.length;
@@ -328,28 +342,18 @@ async function runSetup() {
328
342
  // Load existing config
329
343
  const existing = loadEnvFile();
330
344
 
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
-
336
- const workspace =
337
- (await ask(` ${T.SIDE} Path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
338
-
339
- log(` ${T.LAST} ${T.OK} ${workspace}`);
340
- log();
341
-
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}`);
348
-
349
- if (existing.LLM_API_KEY) {
350
- log(` ${T.SIDE} Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
351
- log(` ${T.SIDE}`);
352
- }
345
+ // Step results preserved across back/forward navigation
346
+ let workspace = existing.CGB_WORKSPACE || WORKSPACE_DIR;
347
+ let llmKey = existing.LLM_API_KEY || "";
348
+ let llmBaseUrl = existing.LLM_BASE_URL || "";
349
+ let llmModel = existing.LLM_MODEL || "";
350
+ let llmProviderName = "skipped";
351
+ let embedKey = "";
352
+ let embedUrl = "";
353
+ let embedModel = "";
354
+ let embedKeyEnv = "DASHSCOPE_API_KEY";
355
+ let embedUrlEnv = "DASHSCOPE_BASE_URL";
356
+ let embedProviderName = "skipped";
353
357
 
354
358
  const llmOptions = [
355
359
  "Moonshot / Kimi platform.moonshot.cn",
@@ -369,66 +373,6 @@ async function runSetup() {
369
373
  { name: "LiteLLM", url: "http://localhost:4000/v1", model: "gpt-4o" },
370
374
  ];
371
375
 
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));
377
-
378
- let llmKey = existing.LLM_API_KEY || "";
379
- let llmBaseUrl = existing.LLM_BASE_URL || "";
380
- let llmModel = existing.LLM_MODEL || "";
381
- let llmProviderName = "skipped";
382
-
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;
389
-
390
- log(` ${T.SIDE}`);
391
- llmKey = (await ask(` ${T.SIDE} API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
392
-
393
- if (llmKey) {
394
- const urlOverride = (await ask(` ${T.SIDE} Base URL [${llmBaseUrl}]: `)).trim();
395
- if (urlOverride) llmBaseUrl = urlOverride;
396
- const modelOverride = (await ask(` ${T.SIDE} Model [${llmModel}]: `)).trim();
397
- if (modelOverride) llmModel = modelOverride;
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;
409
- }
410
- // llmChoice === 6 or -1 → skip
411
-
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();
418
-
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}`);
425
-
426
- if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
427
- const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
428
- log(` ${T.SIDE} Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
429
- log(` ${T.SIDE}`);
430
- }
431
-
432
376
  const embedOptions = [
433
377
  "DashScope / Qwen dashscope.console.aliyun.com (free tier)",
434
378
  "OpenAI Embeddings platform.openai.com",
@@ -441,56 +385,157 @@ async function runSetup() {
441
385
  { name: "OpenAI", url: "https://api.openai.com/v1", model: "text-embedding-3-small", keyEnv: "OPENAI_API_KEY", urlEnv: "OPENAI_BASE_URL" },
442
386
  ];
443
387
 
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));
388
+ // --- Step-based wizard with ← back support ---
389
+ let step = 1;
448
390
 
449
- let embedKey = "";
450
- let embedUrl = "";
451
- let embedModel = "";
452
- let embedKeyEnv = "DASHSCOPE_API_KEY";
453
- let embedUrlEnv = "DASHSCOPE_BASE_URL";
454
- let embedProviderName = "skipped";
391
+ while (step >= 1 && step <= 3) {
392
+
393
+ // ─── Step 1: Workspace ───
394
+ if (step === 1) {
395
+ log(` ${T.DOT} Step 1/3 Workspace`);
396
+ log(` ${T.SIDE}`);
397
+ log(` ${T.BRANCH} Stores indexed repos, graphs, and embeddings`);
455
398
 
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() ||
467
- existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
468
-
469
- if (embedKey) {
470
- const urlOverride = (await ask(` ${T.SIDE} Base URL [${embedUrl}]: `)).trim();
471
- if (urlOverride) embedUrl = urlOverride;
472
- const modelOverride = (await ask(` ${T.SIDE} Model [${embedModel}]: `)).trim();
473
- if (modelOverride) embedModel = modelOverride;
399
+ workspace =
400
+ (await ask(` ${T.SIDE} Path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
401
+
402
+ log(` ${T.LAST} ${T.OK} ${workspace}`);
403
+ log();
404
+ step = 2;
405
+ continue;
474
406
  }
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
407
 
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})`);
408
+ // ─── Step 2: LLM Provider ───
409
+ if (step === 2) {
410
+ log(` ${T.DOT} Step 2/3 LLM Provider`);
411
+ log(` ${T.SIDE}`);
412
+ log(` ${T.BRANCH} For natural language queries & descriptions`);
413
+ log(` ${T.SIDE} Use ↑↓ navigate, Enter confirm, ← back`);
414
+ log(` ${T.SIDE}`);
415
+
416
+ if (existing.LLM_API_KEY) {
417
+ log(` ${T.SIDE} Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
418
+ log(` ${T.SIDE}`);
419
+ }
420
+
421
+ rl.close();
422
+ const llmChoice = await selectMenu(llmOptions, ` ${T.SIDE} `, 6, true);
423
+ rl = createInterface({ input: process.stdin, output: process.stderr });
424
+ ask = (q) => new Promise((resolve) => rl.question(q, resolve));
425
+
426
+ if (llmChoice === -2) { log(); step = 1; continue; }
427
+ if (llmChoice === -1) { rl.close(); return; }
428
+
429
+ llmKey = existing.LLM_API_KEY || "";
430
+ llmBaseUrl = existing.LLM_BASE_URL || "";
431
+ llmModel = existing.LLM_MODEL || "";
432
+ llmProviderName = "skipped";
433
+
434
+ if (llmChoice >= 0 && llmChoice < 5) {
435
+ const provider = llmProviders[llmChoice];
436
+ llmBaseUrl = provider.url;
437
+ llmModel = provider.model;
438
+ llmProviderName = provider.name;
439
+
440
+ log(` ${T.SIDE}`);
441
+ llmKey = (await ask(` ${T.SIDE} API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
442
+
443
+ if (llmKey) {
444
+ const urlOverride = (await ask(` ${T.SIDE} Base URL [${llmBaseUrl}]: `)).trim();
445
+ if (urlOverride) llmBaseUrl = urlOverride;
446
+ const modelOverride = (await ask(` ${T.SIDE} Model [${llmModel}]: `)).trim();
447
+ if (modelOverride) llmModel = modelOverride;
448
+ }
449
+ } else if (llmChoice === 5) {
450
+ llmProviderName = "Custom";
451
+ const defUrl = llmBaseUrl || existing.LLM_BASE_URL || "";
452
+ const defModel = llmModel || existing.LLM_MODEL || "gpt-4o";
453
+ const defKey = existing.LLM_API_KEY || "";
454
+ log(` ${T.SIDE}`);
455
+ llmBaseUrl = (await ask(` ${T.SIDE} API Base URL${defUrl ? ` [${defUrl}]` : ""}: `)).trim() || defUrl;
456
+ llmModel = (await ask(` ${T.SIDE} Model${defModel ? ` [${defModel}]` : ""}: `)).trim() || defModel;
457
+ llmKey = (await ask(` ${T.SIDE} API Key${defKey ? ` [${mask(defKey)}]` : " (sk-...)"}: `)).trim() || defKey;
458
+ }
459
+
460
+ if (llmKey) {
461
+ log(` ${T.LAST} ${T.OK} ${llmProviderName} / ${llmModel}`);
462
+ } else {
463
+ log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
464
+ }
465
+ log();
466
+ step = 3;
467
+ continue;
468
+ }
469
+
470
+ // ─── Step 3: Embedding Provider ───
471
+ if (step === 3) {
472
+ log(` ${T.DOT} Step 3/3 Embedding Provider`);
473
+ log(` ${T.SIDE}`);
474
+ log(` ${T.BRANCH} For semantic code search`);
475
+ log(` ${T.SIDE} Use ↑↓ navigate, Enter confirm, ← back`);
476
+ log(` ${T.SIDE}`);
477
+
478
+ if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
479
+ const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
480
+ log(` ${T.SIDE} Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
481
+ log(` ${T.SIDE}`);
482
+ }
483
+
484
+ rl.close();
485
+ const embedChoice = await selectMenu(embedOptions, ` ${T.SIDE} `, 3, true);
486
+ rl = createInterface({ input: process.stdin, output: process.stderr });
487
+ ask = (q) => new Promise((resolve) => rl.question(q, resolve));
488
+
489
+ if (embedChoice === -2) { log(); step = 2; continue; }
490
+ if (embedChoice === -1) { rl.close(); return; }
491
+
492
+ embedKey = "";
493
+ embedUrl = "";
494
+ embedModel = "";
495
+ embedKeyEnv = "DASHSCOPE_API_KEY";
496
+ embedUrlEnv = "DASHSCOPE_BASE_URL";
497
+ embedProviderName = "skipped";
498
+
499
+ if (embedChoice >= 0 && embedChoice < 2) {
500
+ const ep = embedProvidersList[embedChoice];
501
+ embedUrl = ep.url;
502
+ embedModel = ep.model;
503
+ embedKeyEnv = ep.keyEnv;
504
+ embedUrlEnv = ep.urlEnv;
505
+ embedProviderName = ep.name;
506
+
507
+ log(` ${T.SIDE}`);
508
+ embedKey = (await ask(` ${T.SIDE} API Key: `)).trim() ||
509
+ existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
510
+
511
+ if (embedKey) {
512
+ const urlOverride = (await ask(` ${T.SIDE} Base URL [${embedUrl}]: `)).trim();
513
+ if (urlOverride) embedUrl = urlOverride;
514
+ const modelOverride = (await ask(` ${T.SIDE} Model [${embedModel}]: `)).trim();
515
+ if (modelOverride) embedModel = modelOverride;
516
+ }
517
+ } else if (embedChoice === 2) {
518
+ embedProviderName = "Custom";
519
+ const defEmbedUrl = existing.EMBED_BASE_URL || existing.DASHSCOPE_BASE_URL || "";
520
+ const defEmbedModel = existing.EMBED_MODEL || "text-embedding-3-small";
521
+ const defEmbedKey = existing.EMBED_API_KEY || existing.DASHSCOPE_API_KEY || "";
522
+ log(` ${T.SIDE}`);
523
+ embedUrl = (await ask(` ${T.SIDE} API Base URL${defEmbedUrl ? ` [${defEmbedUrl}]` : ""}: `)).trim() || defEmbedUrl;
524
+ embedModel = (await ask(` ${T.SIDE} Model${defEmbedModel ? ` [${defEmbedModel}]` : ""}: `)).trim() || defEmbedModel;
525
+ embedKey = (await ask(` ${T.SIDE} API Key${defEmbedKey ? ` [${mask(defEmbedKey)}]` : ""}: `)).trim() || defEmbedKey;
526
+ embedKeyEnv = "EMBED_API_KEY";
527
+ embedUrlEnv = "EMBED_BASE_URL";
528
+ }
529
+
530
+ if (embedKey) {
531
+ log(` ${T.LAST} ${T.OK} ${embedProviderName} / ${embedModel}`);
532
+ } else {
533
+ log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
534
+ }
535
+
536
+ step = 4; // done
537
+ continue;
538
+ }
494
539
  }
495
540
 
496
541
  rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-builder",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Code knowledge graph builder with MCP server for AI-assisted code navigation",
5
5
  "license": "MIT",
6
6
  "bin": {