Jelajahi Sumber

feat: Replace figlet with @clack/prompts for polished CLI experience

Replaces ASCII art banner and basic readline prompts with @clack/prompts for a modern interactive CLI. Adds animated shimmer progress bars with spinner glyphs during indexing operations. Improves installer UX with structured prompts, better error handling, and cleaner output formatting throughout all CLI commands.
Colby McHenry 2 bulan lalu
induk
melakukan
ed35d65f4c

+ 49 - 20
package-lock.json

@@ -10,9 +10,9 @@
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
+        "@clack/prompts": "^1.2.0",
         "@xenova/transformers": "^2.17.0",
         "commander": "^14.0.2",
-        "figlet": "^1.8.0",
         "node-sqlite3-wasm": "^0.8.30",
         "picomatch": "^4.0.3",
         "tree-sitter-wasms": "^0.1.11",
@@ -23,7 +23,6 @@
       },
       "devDependencies": {
         "@types/better-sqlite3": "^7.6.0",
-        "@types/figlet": "^1.5.8",
         "@types/node": "^20.19.30",
         "@types/picomatch": "^4.0.2",
         "typescript": "^5.0.0",
@@ -37,6 +36,28 @@
         "sqlite-vss": "^0.1.2"
       }
     },
+    "node_modules/@clack/core": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.2.0.tgz",
+      "integrity": "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-wrap-ansi": "^0.1.3",
+        "sisteransi": "^1.0.5"
+      }
+    },
+    "node_modules/@clack/prompts": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.2.0.tgz",
+      "integrity": "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==",
+      "license": "MIT",
+      "dependencies": {
+        "@clack/core": "1.2.0",
+        "fast-string-width": "^1.1.0",
+        "fast-wrap-ansi": "^0.1.3",
+        "sisteransi": "^1.0.5"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.21.5",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -875,13 +896,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/figlet": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.7.0.tgz",
-      "integrity": "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/@types/long": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -1476,19 +1490,28 @@
       "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
       "license": "MIT"
     },
-    "node_modules/figlet": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
-      "integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
+    "node_modules/fast-string-truncated-width": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz",
+      "integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==",
+      "license": "MIT"
+    },
+    "node_modules/fast-string-width": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-1.1.0.tgz",
+      "integrity": "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==",
       "license": "MIT",
       "dependencies": {
-        "commander": "^14.0.0"
-      },
-      "bin": {
-        "figlet": "bin/index.js"
-      },
-      "engines": {
-        "node": ">= 17.0.0"
+        "fast-string-truncated-width": "^1.2.0"
+      }
+    },
+    "node_modules/fast-wrap-ansi": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.1.6.tgz",
+      "integrity": "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-string-width": "^1.1.0"
       }
     },
     "node_modules/file-uri-to-path": {
@@ -2082,6 +2105,12 @@
         "is-arrayish": "^0.3.1"
       }
     },
+    "node_modules/sisteransi": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+      "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+      "license": "MIT"
+    },
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

+ 1 - 2
package.json

@@ -34,9 +34,9 @@
   "author": "",
   "license": "MIT",
   "dependencies": {
+    "@clack/prompts": "^1.2.0",
     "@xenova/transformers": "^2.17.0",
     "commander": "^14.0.2",
-    "figlet": "^1.8.0",
     "node-sqlite3-wasm": "^0.8.30",
     "picomatch": "^4.0.3",
     "tree-sitter-wasms": "^0.1.11",
@@ -44,7 +44,6 @@
   },
   "devDependencies": {
     "@types/better-sqlite3": "^7.6.0",
-    "@types/figlet": "^1.5.8",
     "@types/node": "^20.19.30",
     "@types/picomatch": "^4.0.2",
     "typescript": "^5.0.0",

+ 215 - 128
src/bin/codegraph.ts

@@ -45,6 +45,12 @@ async function loadCodeGraph(): Promise<typeof import('../index')> {
 
 type IndexProgress = import('../index').IndexProgress;
 
+// Dynamic import helper — tsc compiles import() to require() in CJS mode,
+// which fails for ESM-only packages. This bypasses the transformation.
+// eslint-disable-next-line @typescript-eslint/no-implied-eval
+const importESM = new Function('specifier', 'return import(specifier)') as
+  (specifier: string) => Promise<typeof import('@clack/prompts')>;
+
 // Check if running with no arguments - run installer
 if (process.argv.length === 2) {
   import('../installer').then(({ runInstaller }) =>
@@ -168,59 +174,140 @@ function formatDuration(ms: number): string {
   return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
 }
 
-/**
- * Create a progress bar string
- */
-function progressBar(current: number, total: number, width: number = 30): string {
-  const percent = total > 0 ? current / total : 0;
-  const filled = Math.round(width * percent);
-  const empty = width - filled;
-  const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
-  const percentStr = `${Math.round(percent * 100)}%`.padStart(4);
-  return `${bar} ${percentStr}`;
+// =============================================================================
+// Shimmer Progress Renderer (devpit-style animation)
+// =============================================================================
+
+const SPINNER_GLYPHS = ['·', '✢', '✳', '✶', '✻', '✽'];
+const ANIM_INTERVAL = 150;
+const FRAMES_PER_GLYPH = 3;
+
+function lerp(a: number, b: number, t: number): number {
+  return Math.round(a + (b - a) * t);
 }
 
-/**
- * Truncate a string to fit a given visible width, adding ellipsis if needed
- */
-function truncate(str: string, maxWidth: number): string {
-  if (maxWidth <= 0) return '';
-  if (str.length <= maxWidth) return str;
-  if (maxWidth <= 1) return str.charAt(0);
-  return '\u2026' + str.slice(-(maxWidth - 1));
+function shimmerColor(frame: number): string {
+  const t = (Math.sin(frame * 2 * Math.PI / 13) + 1) / 2;
+  const r = lerp(160, 251, t);
+  const g = lerp(100, 191, t);
+  const b = lerp(9, 36, t);
+  return `\x1b[38;2;${r};${g};${b}m\x1b[1m`;
 }
 
-/**
- * Print a progress update (overwrites current line)
- */
-function printProgress(progress: IndexProgress): void {
-  const phaseNames: Record<string, string> = {
-    scanning: 'Scanning files',
-    parsing: 'Parsing code',
-    storing: 'Storing data',
-    resolving: 'Resolving refs',
-  };
+const PHASE_NAMES: Record<string, string> = {
+  scanning: 'Scanning files',
+  parsing: 'Parsing code',
+  storing: 'Storing data',
+  resolving: 'Resolving refs',
+};
 
-  const phaseName = phaseNames[progress.phase] || progress.phase;
-  const cols = process.stdout.columns || 80;
-
-  if (progress.total > 0) {
-    const bar = progressBar(progress.current, progress.total);
-    // "Phase: [bar] XX% filename" — calculate space left for filename
-    // phaseName + ": " = phaseName.length + 2, bar visible = 30 + 1 + 4 = 35, space before file = 1
-    const prefixWidth = phaseName.length + 2 + 35;
-    const fileMaxWidth = cols - prefixWidth - 1;
-    const file = progress.currentFile ? chalk.dim(` ${truncate(progress.currentFile, fileMaxWidth)}`) : '';
-    process.stdout.write(`\r${chalk.cyan(phaseName)}: ${bar}${file}\x1b[K`);
-  } else {
-    // No known total (e.g. scanning) — show a running count
-    const countStr = progress.current > 0 ? ` ${formatNumber(progress.current)} found` : '';
-    const prefixWidth = phaseName.length + 1 + countStr.length;
-    const fileMaxWidth = cols - prefixWidth - 1;
-    const file = progress.currentFile ? chalk.dim(` ${truncate(progress.currentFile, fileMaxWidth)}`) : '';
-    const count = progress.current > 0 ? ` ${chalk.green(formatNumber(progress.current))} found` : '';
-    process.stdout.write(`\r${chalk.cyan(phaseName)}:${count}${file}\x1b[K`);
+interface ShimmerProgress {
+  onProgress: (progress: IndexProgress) => void;
+  stop: () => void;
+}
+
+function createShimmerProgress(): ShimmerProgress {
+  const startTime = Date.now();
+  let interval: ReturnType<typeof setInterval> | null = null;
+  let currentMessage = '';
+  let currentPercent = -1;
+  let currentCount = 0;
+  let lastPhase = '';
+
+  function animFrame(): number {
+    return Math.floor((Date.now() - startTime) / ANIM_INTERVAL);
+  }
+
+  function spinnerGlyph(): string {
+    const idx = Math.floor(animFrame() / FRAMES_PER_GLYPH) % SPINNER_GLYPHS.length;
+    return SPINNER_GLYPHS[idx] ?? '·';
+  }
+
+  function renderBar(filled: number, empty: number): string {
+    if (filled === 0) return `${colors.dim}${'░'.repeat(empty)}${colors.reset}`;
+    // Shimmer sweeps left-to-right across the filled portion
+    const cycleFrames = 24;
+    const shimmerPos = ((animFrame() % cycleFrames) / cycleFrames) * (filled + 6) - 3;
+    const shimmerWidth = 3;
+    let bar = '';
+    for (let i = 0; i < filled; i++) {
+      const dist = Math.abs(i - shimmerPos);
+      const t = Math.max(0, 1 - dist / shimmerWidth);
+      const r = lerp(160, 251, t);
+      const g = lerp(100, 191, t);
+      const b = lerp(9, 36, t);
+      bar += `\x1b[38;2;${r};${g};${b}m\x1b[1m█`;
+    }
+    bar += `${colors.reset}${colors.dim}${'░'.repeat(empty)}${colors.reset}`;
+    return bar;
+  }
+
+  function render() {
+    const glyph = spinnerGlyph();
+    const frame = animFrame();
+    const color = shimmerColor(frame);
+    const rst = colors.reset;
+    const dm = colors.dim;
+
+    let line: string;
+    if (currentPercent >= 0) {
+      const barWidth = 25;
+      const filled = Math.round(barWidth * currentPercent / 100);
+      const empty = barWidth - filled;
+      line = `${dm}│${rst}  ${color}${glyph}${rst} ${currentMessage}  ${renderBar(filled, empty)}  ${currentPercent}%`;
+    } else if (currentCount > 0) {
+      line = `${dm}│${rst}  ${color}${glyph}${rst} ${currentMessage}... ${formatNumber(currentCount)} found`;
+    } else {
+      line = `${dm}│${rst}  ${color}${glyph}${rst} ${currentMessage}...`;
+    }
+
+    process.stdout.write(`\r\x1b[K${line}`);
   }
+
+  function finishPhase() {
+    if (!currentMessage) return;
+    process.stdout.write(`\r\x1b[K`);
+    let detail = '';
+    if (currentPercent >= 0) detail = ' — done';
+    else if (currentCount > 0) detail = ` — ${formatNumber(currentCount)} found`;
+    process.stdout.write(`${colors.dim}│${colors.reset}  ${colors.green}◆${colors.reset} ${currentMessage}${detail}\n`);
+  }
+
+  // Start animation loop
+  interval = setInterval(render, ANIM_INTERVAL);
+
+  return {
+    onProgress(progress: IndexProgress) {
+      const phaseName = PHASE_NAMES[progress.phase] || progress.phase;
+
+      if (progress.phase !== lastPhase && lastPhase) {
+        finishPhase();
+      }
+      lastPhase = progress.phase;
+      currentMessage = phaseName;
+
+      if (progress.total > 0) {
+        currentPercent = Math.round((progress.current / progress.total) * 100);
+        currentCount = 0;
+      } else if (progress.current > 0) {
+        currentPercent = -1;
+        currentCount = progress.current;
+      } else {
+        currentPercent = -1;
+        currentCount = 0;
+      }
+
+      // Render immediately — scanning is synchronous so setInterval can't fire
+      render();
+    },
+    stop() {
+      if (interval) {
+        clearInterval(interval);
+        interval = null;
+      }
+      finishPhase();
+    },
+  };
 }
 
 /**
@@ -251,30 +338,37 @@ function warn(message: string): void {
   console.log(chalk.yellow('⚠') + ' ' + message);
 }
 
+type IndexResult = {
+  success: boolean;
+  filesIndexed: number;
+  filesSkipped: number;
+  filesErrored: number;
+  nodesCreated: number;
+  edgesCreated: number;
+  errors: Array<{ message: string; filePath?: string; severity: string; code?: string }>;
+  durationMs: number;
+};
+
 /**
- * Print a summary of indexing results with clear error breakdown
+ * Print indexing results using clack log methods
  */
-function printIndexResult(result: { success: boolean; filesIndexed: number; filesSkipped: number; filesErrored: number; nodesCreated: number; edgesCreated: number; errors: Array<{ message: string; filePath?: string; severity: string; code?: string }>; durationMs: number }, projectPath?: string): void {
+function printIndexResult(clack: typeof import('@clack/prompts'), result: IndexResult, projectPath?: string): void {
   const hasErrors = result.filesErrored > 0;
 
-  // Always show what was indexed
   if (result.filesIndexed > 0) {
     if (hasErrors) {
-      success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} could not be parsed)`);
+      clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} could not be parsed)`);
     } else {
-      success(`Indexed ${formatNumber(result.filesIndexed)} files`);
+      clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files`);
     }
-    info(`Created ${formatNumber(result.nodesCreated)} nodes and ${formatNumber(result.edgesCreated)} edges`);
-    info(`Completed in ${formatDuration(result.durationMs)}`);
+    clack.log.info(`${formatNumber(result.nodesCreated)} nodes, ${formatNumber(result.edgesCreated)} edges in ${formatDuration(result.durationMs)}`);
   } else if (hasErrors) {
-    error(`Indexing failed — all ${formatNumber(result.filesErrored)} files had errors`);
+    clack.log.error(`Indexing failed — all ${formatNumber(result.filesErrored)} files had errors`);
   } else {
-    warn('No files found to index');
+    clack.log.warn('No files found to index');
   }
 
-  // Show error breakdown if there were errors
   if (hasErrors) {
-    // Group errors by code for a concise summary
     const errorsByCode = new Map<string, number>();
     for (const err of result.errors) {
       if (err.severity === 'error') {
@@ -292,26 +386,20 @@ function printIndexResult(result: { success: boolean; filesIndexed: number; file
       parser_error: 'parser initialization failures',
     };
 
-    console.log('');
-    console.log(chalk.dim('  Error breakdown:'));
-    for (const [code, count] of errorsByCode) {
-      const label = codeLabels[code] || code;
-      console.log(chalk.dim(`    ${formatNumber(count)} ${label}`));
-    }
+    const breakdown = Array.from(errorsByCode)
+      .map(([code, count]) => `${formatNumber(count)} ${codeLabels[code] || code}`)
+      .join('\n');
+    clack.note(breakdown, 'Error breakdown');
 
-    // Write detailed error log to .codegraph/errors.log
     if (projectPath) {
       writeErrorLog(projectPath, result.errors);
+      clack.log.info('See .codegraph/errors.log for details');
     }
 
-    // Reassure the user the index is usable
     if (result.filesIndexed > 0) {
-      console.log('');
-      info('The index is fully usable — only the failed files are missing from the graph.');
-      info('This is common in large repos with test fixtures or generated files that use non-standard syntax.');
+      clack.log.info('The index is fully usable — only the failed files are missing.');
     }
   } else if (projectPath) {
-    // No errors — clean up any stale error log
     const logPath = path.join(projectPath, '.codegraph', 'errors.log');
     if (fs.existsSync(logPath)) {
       fs.unlinkSync(logPath);
@@ -363,7 +451,6 @@ function writeErrorLog(projectPath: string, errors: Array<{ message: string; fil
   }
 
   fs.writeFileSync(logPath, lines.join('\n') + '\n');
-  info(`See .codegraph/errors.log for the full list of failed files`);
 }
 
 // =============================================================================
@@ -378,47 +465,41 @@ program
   .description('Initialize CodeGraph in a project directory')
   .option('-i, --index', 'Run initial indexing after initialization')
   .action(async (pathArg: string | undefined, options: { index?: boolean }) => {
-    // init should always target the exact path given (or cwd), never walk up parents
     const projectPath = path.resolve(pathArg || process.cwd());
+    const clack = await importESM('@clack/prompts');
 
-    console.log(chalk.bold('\nInitializing CodeGraph...\n'));
+    clack.intro('Initializing CodeGraph');
 
     try {
-      // Check if already initialized
       if (isInitialized(projectPath)) {
-        warn(`CodeGraph already initialized in ${projectPath}`);
-        info('Use "codegraph index" to re-index or "codegraph sync" to update');
+        clack.log.warn(`Already initialized in ${projectPath}`);
+        clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
+        clack.outro('');
         return;
       }
 
-      // Initialize
       const { default: CodeGraph } = await loadCodeGraph();
-      const cg = await CodeGraph.init(projectPath, {
-        index: false, // We'll handle indexing ourselves for progress
-      });
-
-      success(`Initialized CodeGraph in ${projectPath}`);
-      info(`Created .codegraph/ directory`);
+      const cg = await CodeGraph.init(projectPath, { index: false });
+      clack.log.success(`Initialized in ${projectPath}`);
 
-      // Run initial index if requested
       if (options.index) {
-        console.log('\nIndexing project...\n');
+        process.stdout.write(`${colors.dim}│${colors.reset}\n`);
+        const progress = createShimmerProgress();
 
         const result = await cg.indexAll({
-          onProgress: printProgress,
+          onProgress: progress.onProgress,
         });
 
-        // Clear progress line
-        process.stdout.write('\r\x1b[K');
-
-        printIndexResult(result, projectPath);
+        progress.stop();
+        printIndexResult(clack, result, projectPath);
       } else {
-        info('Run "codegraph index" to index the project');
+        clack.log.info('Run "codegraph index" to index the project');
       }
 
+      clack.outro('Done');
       cg.destroy();
     } catch (err) {
-      error(`Failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
+      clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
   });
@@ -489,35 +570,38 @@ program
       const { default: CodeGraph } = await loadCodeGraph();
       const cg = await CodeGraph.open(projectPath);
 
-      if (!options.quiet) {
-        console.log(chalk.bold('\nIndexing project...\n'));
+      if (options.quiet) {
+        // Quiet mode: no UI, just run
+        if (options.force) cg.clear();
+        const result = await cg.indexAll();
+        if (!result.success) process.exit(1);
+        cg.destroy();
+        return;
       }
 
-      // Clear existing data if force
+      const clack = await importESM('@clack/prompts');
+      clack.intro('Indexing project');
+
       if (options.force) {
         cg.clear();
-        if (!options.quiet) {
-          info('Cleared existing index');
-        }
+        clack.log.info('Cleared existing index');
       }
 
+      process.stdout.write(`${colors.dim}│${colors.reset}\n`);
+      const progress = createShimmerProgress();
+
       const result = await cg.indexAll({
-        onProgress: options.quiet ? undefined : printProgress,
+        onProgress: progress.onProgress,
       });
 
-      // Clear progress line
-      if (!options.quiet) {
-        process.stdout.write('\r\x1b[K');
-      }
-
-      if (!options.quiet) {
-        printIndexResult(result, projectPath);
-      }
+      progress.stop();
+      printIndexResult(clack, result, projectPath);
 
       if (!result.success) {
         process.exit(1);
       }
 
+      clack.outro('Done');
       cg.destroy();
     } catch (err) {
       error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
@@ -546,35 +630,38 @@ program
       const { default: CodeGraph } = await loadCodeGraph();
       const cg = await CodeGraph.open(projectPath);
 
+      if (options.quiet) {
+        await cg.sync();
+        cg.destroy();
+        return;
+      }
+
+      const clack = await importESM('@clack/prompts');
+      clack.intro('Syncing CodeGraph');
+
+      process.stdout.write(`${colors.dim}│${colors.reset}\n`);
+      const progress = createShimmerProgress();
+
       const result = await cg.sync({
-        onProgress: options.quiet ? undefined : printProgress,
+        onProgress: progress.onProgress,
       });
 
-      // Clear progress line
-      if (!options.quiet) {
-        process.stdout.write('\r\x1b[K');
-      }
+      progress.stop();
 
       const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved;
 
-      if (!options.quiet) {
-        if (totalChanges === 0) {
-          success('Already up to date');
-        } else {
-          success(`Synced ${formatNumber(totalChanges)} changed files`);
-          if (result.filesAdded > 0) {
-            info(`  Added: ${result.filesAdded}`);
-          }
-          if (result.filesModified > 0) {
-            info(`  Modified: ${result.filesModified}`);
-          }
-          if (result.filesRemoved > 0) {
-            info(`  Removed: ${result.filesRemoved}`);
-          }
-          info(`Updated ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
-        }
+      if (totalChanges === 0) {
+        clack.log.info('Already up to date');
+      } else {
+        clack.log.success(`Synced ${formatNumber(totalChanges)} changed files`);
+        const details: string[] = [];
+        if (result.filesAdded > 0) details.push(`Added: ${result.filesAdded}`);
+        if (result.filesModified > 0) details.push(`Modified: ${result.filesModified}`);
+        if (result.filesRemoved > 0) details.push(`Removed: ${result.filesRemoved}`);
+        clack.log.info(`${details.join(', ')} — ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
       }
 
+      clack.outro('Done');
       cg.destroy();
     } catch (err) {
       if (!options.quiet) {

+ 0 - 132
src/installer/banner.ts

@@ -1,132 +0,0 @@
-/**
- * Banner and branding for the CodeGraph installer
- */
-
-import * as figlet from 'figlet';
-import * as path from 'path';
-import * as fs from 'fs';
-
-// =============================================================================
-// ANSI Color Helpers (same pattern as CLI to avoid chalk ESM issues)
-// =============================================================================
-
-const colors = {
-  reset: '\x1b[0m',
-  bold: '\x1b[1m',
-  dim: '\x1b[2m',
-  red: '\x1b[31m',
-  green: '\x1b[32m',
-  yellow: '\x1b[33m',
-  blue: '\x1b[34m',
-  magenta: '\x1b[35m',
-  cyan: '\x1b[36m',
-  white: '\x1b[37m',
-  gray: '\x1b[90m',
-};
-
-export const chalk = {
-  bold: (s: string) => `${colors.bold}${s}${colors.reset}`,
-  dim: (s: string) => `${colors.dim}${s}${colors.reset}`,
-  red: (s: string) => `${colors.red}${s}${colors.reset}`,
-  green: (s: string) => `${colors.green}${s}${colors.reset}`,
-  yellow: (s: string) => `${colors.yellow}${s}${colors.reset}`,
-  blue: (s: string) => `${colors.blue}${s}${colors.reset}`,
-  magenta: (s: string) => `${colors.magenta}${s}${colors.reset}`,
-  cyan: (s: string) => `${colors.cyan}${s}${colors.reset}`,
-  white: (s: string) => `${colors.white}${s}${colors.reset}`,
-  gray: (s: string) => `${colors.gray}${s}${colors.reset}`,
-};
-
-/**
- * Get the package version
- */
-function getVersion(): string {
-  try {
-    const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
-    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
-    return packageJson.version;
-  } catch {
-    return '0.0.0';
-  }
-}
-
-/**
- * Display the CodeGraph banner
- */
-export function showBanner(): void {
-  // Generate ASCII art using figlet
-  let banner: string;
-  try {
-    banner = figlet.textSync('CODEGRAPH', {
-      font: 'ANSI Shadow',
-      horizontalLayout: 'default',
-    });
-  } catch {
-    // Fallback if figlet fails
-    banner = `
-   ██████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██████╗  █████╗ ██████╗ ██╗  ██╗
-  ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║  ██║
-  ██║     ██║   ██║██║  ██║█████╗  ██║  ███╗██████╔╝███████║██████╔╝███████║
-  ██║     ██║   ██║██║  ██║██╔══╝  ██║   ██║██╔══██╗██╔══██║██╔═══╝ ██╔══██║
-  ╚██████╗╚██████╔╝██████╔╝███████╗╚██████╔╝██║  ██║██║  ██║██║     ██║  ██║
-   ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝
-`;
-  }
-
-  console.log();
-  console.log(chalk.cyan(banner));
-  console.log();
-  console.log(`  ${chalk.bold('CodeGraph')} v${getVersion()}`);
-  console.log('  Semantic code intelligence for Claude Code');
-  console.log(chalk.dim('  Created by: Colby McHenry'));
-  console.log();
-}
-
-/**
- * Show success checkmark
- */
-export function success(message: string): void {
-  console.log(chalk.green('  ✓') + ' ' + message);
-}
-
-/**
- * Show error message
- */
-export function error(message: string): void {
-  console.log(chalk.red('  ✗') + ' ' + message);
-}
-
-/**
- * Show info message
- */
-export function info(message: string): void {
-  console.log(chalk.blue('  ℹ') + ' ' + message);
-}
-
-/**
- * Show warning message
- */
-export function warn(message: string): void {
-  console.log(chalk.yellow('  ⚠') + ' ' + message);
-}
-
-/**
- * Show the "next steps" section after installation
- */
-export function showNextSteps(location: 'global' | 'local'): void {
-  console.log();
-  console.log(chalk.bold('  Done!') + ' Restart Claude Code to use CodeGraph.');
-  console.log();
-
-  if (location === 'global') {
-    console.log(chalk.dim('  Quick start:'));
-    console.log(chalk.dim('    cd your-project'));
-    console.log(chalk.cyan('    codegraph init -i'));
-    console.log();
-    console.log(chalk.dim('  To uninstall:'));
-    console.log(chalk.dim('    npm uninstall -g @colbymchenry/codegraph'));
-  } else {
-    console.log(chalk.dim('  CodeGraph is ready to use in this project!'));
-  }
-  console.log();
-}

+ 43 - 0
src/installer/clack.d.ts

@@ -0,0 +1,43 @@
+/**
+ * Type declarations for @clack/prompts
+ *
+ * The package ships ESM-only (.d.mts) which TypeScript can't resolve
+ * with moduleResolution "node". We declare the subset we use here.
+ */
+
+declare module '@clack/prompts' {
+  export function intro(title?: string): void;
+  export function outro(message?: string): void;
+  export function cancel(message?: string): void;
+  export function isCancel(value: unknown): value is symbol;
+
+  export function confirm(opts: {
+    message: string;
+    active?: string;
+    inactive?: string;
+    initialValue?: boolean;
+  }): Promise<boolean | symbol>;
+
+  export function select<Value>(opts: {
+    message: string;
+    options: { value: Value; label: string; hint?: string }[];
+    initialValue?: Value;
+  }): Promise<Value | symbol>;
+
+  export function spinner(): {
+    start(message?: string): void;
+    stop(message?: string): void;
+    message(message?: string): void;
+  };
+
+  export function note(message: string, title?: string): void;
+
+  export const log: {
+    message(message: string): void;
+    info(message: string): void;
+    success(message: string): void;
+    step(message: string): void;
+    warn(message: string): void;
+    error(message: string): void;
+  };
+}

+ 1 - 1
src/installer/config-writer.ts

@@ -6,7 +6,7 @@
 import * as fs from 'fs';
 import * as path from 'path';
 import * as os from 'os';
-import { InstallLocation } from './prompts';
+export type InstallLocation = 'global' | 'local';
 import {
   CLAUDE_MD_TEMPLATE,
   CODEGRAPH_SECTION_START,

+ 214 - 110
src/installer/index.ts

@@ -1,14 +1,24 @@
 /**
  * CodeGraph Interactive Installer
  *
- * Provides a beautiful interactive CLI experience for setting up CodeGraph
- * with Claude Code.
+ * Uses @clack/prompts for a polished interactive CLI experience.
  */
 
 import { execSync } from 'child_process';
-import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
-import { promptInstallLocation, promptAutoAllow, promptConfirm, InstallLocation } from './prompts';
-import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
+import * as path from 'path';
+import * as fs from 'fs';
+import {
+  writeMcpConfig, writePermissions, writeClaudeMd, writeHooks,
+  hasMcpConfig, hasPermissions, hasHooks,
+} from './config-writer';
+
+import type { InstallLocation } from './config-writer';
+
+// Dynamic import helper — tsc compiles import() to require() in CJS mode,
+// which fails for ESM-only packages. This bypasses the transformation.
+// eslint-disable-next-line @typescript-eslint/no-implied-eval
+const importESM = new Function('specifier', 'return import(specifier)') as
+  (specifier: string) => Promise<typeof import('@clack/prompts')>;
 
 /**
  * Format a number with commas
@@ -17,110 +27,140 @@ function formatNumber(n: number): string {
   return n.toLocaleString();
 }
 
+/**
+ * Get the package version
+ */
+function getVersion(): string {
+  try {
+    const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
+    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+    return packageJson.version;
+  } catch {
+    return '0.0.0';
+  }
+}
+
 /**
  * Run the interactive installer
  */
 export async function runInstaller(): Promise<void> {
-  // Show the banner
-  showBanner();
+  const clack = await importESM('@clack/prompts');
 
-  try {
-    // Step 1: Install codegraph globally (with user consent).
-    // The global install is needed because Claude Code hooks and the MCP server
-    // invoke `codegraph` by name — the temporary npx binary vanishes when npx exits.
-    console.log(chalk.bold('  Install codegraph globally?') + chalk.dim(' (Required for hooks & MCP server)'));
-    console.log();
-    const shouldInstallGlobally = await promptConfirm('Install globally via npm', true);
-
-    if (shouldInstallGlobally) {
-      console.log(chalk.dim('  Installing codegraph globally...'));
-      try {
-        execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
-        success('Installed codegraph command globally');
-      } catch {
-        info('Could not install globally (permission denied)');
-        info('Try: sudo npm install -g @colbymchenry/codegraph');
-      }
-    } else {
-      info('Skipped global install — hooks and MCP server may not work without it');
-      info('You can install later: npm install -g @colbymchenry/codegraph');
+  clack.intro(`CodeGraph v${getVersion()}`);
+
+  // Step 1: Install globally
+  const shouldInstallGlobally = await clack.confirm({
+    message: 'Install codegraph globally? (Required for hooks & MCP server)',
+    initialValue: true,
+  });
+
+  if (clack.isCancel(shouldInstallGlobally)) {
+    clack.cancel('Installation cancelled.');
+    process.exit(0);
+  }
+
+  if (shouldInstallGlobally) {
+    const s = clack.spinner();
+    s.start('Installing codegraph globally...');
+    try {
+      execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
+      s.stop('Installed codegraph globally');
+    } catch {
+      s.stop('Could not install globally (permission denied)');
+      clack.log.warn('Try: sudo npm install -g @colbymchenry/codegraph');
     }
-    console.log();
+  } else {
+    clack.log.info('Skipped global install — hooks and MCP server may not work without it');
+  }
 
-    // Step 2: Ask for installation location
-    const location = await promptInstallLocation();
-    console.log();
+  // Step 2: Installation location
+  const location = await clack.select({
+    message: 'Where would you like to install?',
+    options: [
+      { value: 'global' as const, label: 'Global', hint: '~/.claude — available in all projects' },
+      { value: 'local' as const, label: 'Local', hint: './.claude — this project only' },
+    ],
+    initialValue: 'global' as const,
+  });
 
-    // Step 3: Ask about auto-allow permissions
-    const autoAllow = await promptAutoAllow();
-    console.log();
+  if (clack.isCancel(location)) {
+    clack.cancel('Installation cancelled.');
+    process.exit(0);
+  }
 
-    // Step 4: Write MCP configuration
-    const alreadyHasMcp = hasMcpConfig(location);
-    writeMcpConfig(location);
+  // Step 3: Auto-allow permissions
+  const autoAllow = await clack.confirm({
+    message: 'Auto-allow CodeGraph commands? (Skips permission prompts)',
+    initialValue: true,
+  });
 
-    if (alreadyHasMcp) {
-      success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
-    } else {
-      success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
-    }
+  if (clack.isCancel(autoAllow)) {
+    clack.cancel('Installation cancelled.');
+    process.exit(0);
+  }
 
-    if (autoAllow) {
-      const alreadyHasPerms = hasPermissions(location);
-      writePermissions(location);
+  // Step 4: Write configuration files
+  writeConfigs(clack, location, autoAllow);
 
-      if (alreadyHasPerms) {
-        success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
-      } else {
-        success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
-      }
-    }
+  // Step 5: For local install, initialize the project
+  if (location === 'local') {
+    await initializeLocalProject(clack);
+  }
 
-    // Step 6: Write auto-sync hooks
-    const alreadyHasHooks = hasHooks(location);
-    writeHooks(location);
+  // Done
+  if (location === 'global') {
+    clack.note(
+      'cd your-project\ncodegraph init -i',
+      'Quick start',
+    );
+  }
 
-    if (alreadyHasHooks) {
-      success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
-    } else {
-      success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
-    }
+  clack.outro('Done! Restart Claude Code to use CodeGraph.');
+}
 
-    // Step 7: Write CLAUDE.md instructions
-    const claudeMdResult = writeClaudeMd(location);
-    const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
+/**
+ * Write all configuration files and log results
+ */
+function writeConfigs(
+  clack: typeof import('@clack/prompts'),
+  location: InstallLocation,
+  autoAllow: boolean,
+): void {
+  const locationLabel = location === 'global' ? '~/.claude' : './.claude';
 
-    if (claudeMdResult.created) {
-      success(`Created ${claudeMdPath} with CodeGraph instructions`);
-    } else if (claudeMdResult.updated) {
-      success(`Updated CodeGraph section in ${claudeMdPath}`);
-    } else {
-      success(`Added CodeGraph instructions to ${claudeMdPath}`);
-    }
+  // MCP config
+  const mcpAction = hasMcpConfig(location) ? 'Updated' : 'Added';
+  writeMcpConfig(location);
+  clack.log.success(`${mcpAction} MCP server in ${locationLabel}.json`);
 
-    // Step 7: For local install, initialize the project
-    if (location === 'local') {
-      await initializeLocalProject();
-    }
+  // Permissions
+  if (autoAllow) {
+    const permAction = hasPermissions(location) ? 'Updated' : 'Added';
+    writePermissions(location);
+    clack.log.success(`${permAction} permissions in ${locationLabel}/settings.json`);
+  }
 
-    // Show next steps
-    showNextSteps(location);
-  } catch (err) {
-    console.log();
-    if (err instanceof Error && err.message.includes('readline was closed')) {
-      // User cancelled with Ctrl+C
-      console.log(chalk.dim('  Installation cancelled.'));
-    } else {
-      error(`Installation failed: ${err instanceof Error ? err.message : String(err)}`);
-    }
-    process.exit(1);
+  // Hooks
+  const hookAction = hasHooks(location) ? 'Updated' : 'Added';
+  writeHooks(location);
+  clack.log.success(`${hookAction} auto-sync hooks in ${locationLabel}/settings.json`);
+
+  // CLAUDE.md
+  const claudeMdResult = writeClaudeMd(location);
+  const claudeMdPath = `${locationLabel}/CLAUDE.md`;
+  if (claudeMdResult.created) {
+    clack.log.success(`Created ${claudeMdPath}`);
+  } else if (claudeMdResult.updated) {
+    clack.log.success(`Updated ${claudeMdPath}`);
+  } else {
+    clack.log.success(`Added CodeGraph instructions to ${claudeMdPath}`);
   }
 }
 
 /**
  * Initialize CodeGraph in the current project (for local installs)
  */
-async function initializeLocalProject(): Promise<void> {
+async function initializeLocalProject(clack: typeof import('@clack/prompts')): Promise<void> {
   const projectPath = process.cwd();
 
   // Lazy-load CodeGraph (requires native modules)
@@ -129,52 +169,116 @@ async function initializeLocalProject(): Promise<void> {
     CodeGraph = (await import('../index')).default;
   } catch (err) {
     const msg = err instanceof Error ? err.message : String(err);
-    error(`Could not load native modules: ${msg}`);
-    info('Skipping project initialization. You can run "codegraph init -i" later.');
-    info('If this persists, try a Node.js LTS version (20 or 22).');
+    clack.log.error(`Could not load native modules: ${msg}`);
+    clack.log.info('Skipping project initialization. Run "codegraph init -i" later.');
     return;
   }
 
   // Check if already initialized
   if (CodeGraph.isInitialized(projectPath)) {
-    info('CodeGraph already initialized in this project');
+    clack.log.info('CodeGraph already initialized in this project');
     return;
   }
 
-  console.log();
-  console.log(chalk.dim('  Initializing CodeGraph in current project...'));
-
-  // Initialize CodeGraph
+  // Initialize
   const cg = await CodeGraph.init(projectPath);
-  success('Created .codegraph/ directory');
+  clack.log.success('Created .codegraph/ directory');
+
+  // Index the project with shimmer progress
+  const SPINNER_GLYPHS = ['·', '✢', '✳', '✶', '✻', '✽'];
+  const ANIM_INTERVAL = 150;
+  const FRAMES_PER_GLYPH = 3;
+  const _lerp = (a: number, b: number, t: number) => Math.round(a + (b - a) * t);
+  const _shimmerColor = (frame: number) => {
+    const t = (Math.sin(frame * 2 * Math.PI / 13) + 1) / 2;
+    return `\x1b[38;2;${_lerp(160, 251, t)};${_lerp(100, 191, t)};${_lerp(9, 36, t)}m\x1b[1m`;
+  };
+  const rst = '\x1b[0m';
+  const dm = '\x1b[2m';
+  const grn = '\x1b[32m';
+  const phaseNames: Record<string, string> = {
+    scanning: 'Scanning files',
+    parsing: 'Parsing code',
+    storing: 'Storing data',
+    resolving: 'Resolving refs',
+  };
+
+  const _startTime = Date.now();
+  const _animFrame = () => Math.floor((Date.now() - _startTime) / ANIM_INTERVAL);
+  let curMsg = '';
+  let curPercent = -1;
+  let curCount = 0;
+  let lastPhase = '';
+
+  const renderBar = (filled: number, empty: number): string => {
+    if (filled === 0) return `${dm}${'░'.repeat(empty)}${rst}`;
+    const cycleFrames = 24;
+    const shimmerPos = ((_animFrame() % cycleFrames) / cycleFrames) * (filled + 6) - 3;
+    const shimmerWidth = 3;
+    let bar = '';
+    for (let i = 0; i < filled; i++) {
+      const dist = Math.abs(i - shimmerPos);
+      const t = Math.max(0, 1 - dist / shimmerWidth);
+      const r = _lerp(160, 251, t);
+      const g = _lerp(100, 191, t);
+      const b = _lerp(9, 36, t);
+      bar += `\x1b[38;2;${r};${g};${b}m\x1b[1m█`;
+    }
+    return bar + `${rst}${dm}${'░'.repeat(empty)}${rst}`;
+  };
+
+  const renderTick = () => {
+    const frame = _animFrame();
+    const glyph = SPINNER_GLYPHS[Math.floor(frame / FRAMES_PER_GLYPH) % SPINNER_GLYPHS.length];
+    const color = _shimmerColor(frame);
+    let line: string;
+    if (curPercent >= 0) {
+      const barW = 25, filled = Math.round(barW * curPercent / 100), empty = barW - filled;
+      line = `${dm}│${rst}  ${color}${glyph}${rst} ${curMsg}  ${renderBar(filled, empty)}  ${curPercent}%`;
+    } else if (curCount > 0) {
+      line = `${dm}│${rst}  ${color}${glyph}${rst} ${curMsg}... ${formatNumber(curCount)} found`;
+    } else {
+      line = `${dm}│${rst}  ${color}${glyph}${rst} ${curMsg}...`;
+    }
+    process.stdout.write(`\r\x1b[K${line}`);
+  };
+
+  const finishPhase = () => {
+    if (!curMsg) return;
+    process.stdout.write(`\r\x1b[K`);
+    let detail = '';
+    if (curPercent >= 0) detail = ' — done';
+    else if (curCount > 0) detail = ` — ${formatNumber(curCount)} found`;
+    process.stdout.write(`${dm}│${rst}  ${grn}◆${rst} ${curMsg}${detail}\n`);
+  };
+
+  process.stdout.write(`${dm}│${rst}\n`);
+  const ticker = setInterval(renderTick, ANIM_INTERVAL);
 
-  // Index the project
   const result = await cg.indexAll({
     onProgress: (progress) => {
-      // Simple progress indicator
-      const phaseNames: Record<string, string> = {
-        scanning: 'Scanning files',
-        parsing: 'Parsing code',
-        storing: 'Storing data',
-        resolving: 'Resolving refs',
-      };
       const phaseName = phaseNames[progress.phase] || progress.phase;
-      const percent = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
-      process.stdout.write(`\r  ${chalk.dim(phaseName)}... ${percent}%\x1b[K`);
+      if (progress.phase !== lastPhase && lastPhase) finishPhase();
+      lastPhase = progress.phase;
+      curMsg = phaseName;
+      if (progress.total > 0) { curPercent = Math.round((progress.current / progress.total) * 100); curCount = 0; }
+      else if (progress.current > 0) { curPercent = -1; curCount = progress.current; }
+      else { curPercent = -1; curCount = 0; }
+      renderTick();
     },
   });
 
-  // Clear progress line
-  process.stdout.write('\r\x1b[K');
+  clearInterval(ticker);
+  finishPhase();
 
   if (result.filesErrored > 0) {
-    success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} files failed, ${formatNumber(result.nodesCreated)} symbols)`);
+    clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} failed, ${formatNumber(result.nodesCreated)} symbols)`);
   } else {
-    success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.nodesCreated)} symbols)`);
+    clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.nodesCreated)} symbols)`);
   }
 
   cg.close();
 }
 
-// Export for use in CLI
-export { InstallLocation };
+// Re-export for CLI
+export type { InstallLocation };

+ 0 - 93
src/installer/prompts.ts

@@ -1,93 +0,0 @@
-/**
- * User prompts for the CodeGraph installer
- * Uses built-in readline to avoid ESM issues with inquirer
- */
-
-import * as readline from 'readline';
-import { chalk } from './banner';
-
-export type InstallLocation = 'global' | 'local';
-
-/**
- * Create a readline interface for prompts
- */
-function createInterface(): readline.Interface {
-  return readline.createInterface({
-    input: process.stdin,
-    output: process.stdout,
-  });
-}
-
-/**
- * Prompt the user with a question and return their answer
- */
-function prompt(rl: readline.Interface, question: string): Promise<string> {
-  return new Promise((resolve) => {
-    rl.question(question, (answer) => {
-      resolve(answer.trim());
-    });
-  });
-}
-
-/**
- * Prompt for installation location (global or local)
- */
-export async function promptInstallLocation(): Promise<InstallLocation> {
-  const rl = createInterface();
-
-  console.log(chalk.bold('  Where would you like to install?'));
-  console.log();
-  console.log('  1) Global (~/.claude) - available in all projects');
-  console.log('  2) Local (./.claude) - this project only');
-  console.log();
-
-  const answer = await prompt(rl, '  Choice [1]: ');
-  rl.close();
-
-  // Default to '1' if empty, parse the answer
-  const choice = answer === '' ? '1' : answer;
-
-  if (choice === '2') {
-    return 'local';
-  }
-  return 'global';
-}
-
-/**
- * Prompt for auto-allow permissions
- */
-export async function promptAutoAllow(): Promise<boolean> {
-  const rl = createInterface();
-
-  console.log();
-  console.log(chalk.bold('  Auto-allow CodeGraph commands?') + chalk.dim(' (Skips permission prompts)'));
-  console.log();
-  console.log('  1) Yes - auto-approve all codegraph_* tools');
-  console.log('  2) No - ask for permission each time');
-  console.log();
-
-  const answer = await prompt(rl, '  Choice [1]: ');
-  rl.close();
-
-  // Default to '1' if empty
-  const choice = answer === '' ? '1' : answer;
-
-  return choice !== '2';
-}
-
-/**
- * Prompt for confirmation (yes/no)
- */
-export async function promptConfirm(message: string, defaultYes: boolean = true): Promise<boolean> {
-  const rl = createInterface();
-
-  const defaultStr = defaultYes ? 'Y/n' : 'y/N';
-  const answer = await prompt(rl, `  ${message} [${defaultStr}]: `);
-  rl.close();
-
-  if (answer === '') {
-    return defaultYes;
-  }
-
-  return answer.toLowerCase().startsWith('y');
-}