@liendev/lien 0.35.0 → 0.36.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.
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __commonJS = (cb, mod) => function __require() {
9
12
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
13
  };
@@ -29,6 +32,70 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
32
  mod
30
33
  ));
31
34
 
35
+ // src/utils/banner.ts
36
+ import figlet from "figlet";
37
+ import chalk from "chalk";
38
+ import { createRequire } from "module";
39
+ import { fileURLToPath } from "url";
40
+ import { dirname, join } from "path";
41
+ function wrapInBox(text, footer, padding = 1) {
42
+ const lines = text.split("\n").filter((line) => line.trim().length > 0);
43
+ const maxLength = Math.max(...lines.map((line) => line.length));
44
+ const horizontalBorder = "\u2500".repeat(maxLength + padding * 2);
45
+ const top = `\u250C${horizontalBorder}\u2510`;
46
+ const bottom = `\u2514${horizontalBorder}\u2518`;
47
+ const separator = `\u251C${horizontalBorder}\u2524`;
48
+ const paddedLines = lines.map((line) => {
49
+ const padRight = " ".repeat(maxLength - line.length + padding);
50
+ const padLeft = " ".repeat(padding);
51
+ return `\u2502${padLeft}${line}${padRight}\u2502`;
52
+ });
53
+ const totalPad = maxLength - footer.length;
54
+ const leftPad = Math.floor(totalPad / 2);
55
+ const rightPad = totalPad - leftPad;
56
+ const centeredFooter = " ".repeat(leftPad) + footer + " ".repeat(rightPad);
57
+ const paddedFooter = `\u2502${" ".repeat(padding)}${centeredFooter}${" ".repeat(padding)}\u2502`;
58
+ return [top, ...paddedLines, separator, paddedFooter, bottom].join("\n");
59
+ }
60
+ function showBanner() {
61
+ const banner = figlet.textSync("LIEN", {
62
+ font: "ANSI Shadow",
63
+ horizontalLayout: "fitted",
64
+ verticalLayout: "fitted"
65
+ });
66
+ const footer = `${PACKAGE_NAME} - v${VERSION}`;
67
+ const boxedBanner = wrapInBox(banner.trim(), footer);
68
+ console.error(chalk.cyan(boxedBanner));
69
+ console.error();
70
+ }
71
+ function showCompactBanner() {
72
+ const banner = figlet.textSync("LIEN", {
73
+ font: "ANSI Shadow",
74
+ horizontalLayout: "fitted",
75
+ verticalLayout: "fitted"
76
+ });
77
+ const footer = `${PACKAGE_NAME} - v${VERSION}`;
78
+ const boxedBanner = wrapInBox(banner.trim(), footer);
79
+ console.log(chalk.cyan(boxedBanner));
80
+ console.log();
81
+ }
82
+ var __filename, __dirname, require2, packageJson, PACKAGE_NAME, VERSION;
83
+ var init_banner = __esm({
84
+ "src/utils/banner.ts"() {
85
+ "use strict";
86
+ __filename = fileURLToPath(import.meta.url);
87
+ __dirname = dirname(__filename);
88
+ require2 = createRequire(import.meta.url);
89
+ try {
90
+ packageJson = require2(join(__dirname, "../package.json"));
91
+ } catch {
92
+ packageJson = require2(join(__dirname, "../../package.json"));
93
+ }
94
+ PACKAGE_NAME = packageJson.name;
95
+ VERSION = packageJson.version;
96
+ }
97
+ });
98
+
32
99
  // ../../node_modules/collect.js/dist/methods/symbol.iterator.js
33
100
  var require_symbol_iterator = __commonJS({
34
101
  "../../node_modules/collect.js/dist/methods/symbol.iterator.js"(exports, module) {
@@ -3595,115 +3662,65 @@ import { fileURLToPath as fileURLToPath3 } from "url";
3595
3662
  import { dirname as dirname3, join as join3 } from "path";
3596
3663
 
3597
3664
  // src/cli/init.ts
3665
+ init_banner();
3598
3666
  import fs from "fs/promises";
3599
3667
  import path from "path";
3600
3668
  import chalk2 from "chalk";
3601
-
3602
- // src/utils/banner.ts
3603
- import figlet from "figlet";
3604
- import chalk from "chalk";
3605
- import { createRequire } from "module";
3606
- import { fileURLToPath } from "url";
3607
- import { dirname, join } from "path";
3608
- var __filename = fileURLToPath(import.meta.url);
3609
- var __dirname = dirname(__filename);
3610
- var require2 = createRequire(import.meta.url);
3611
- var packageJson;
3612
- try {
3613
- packageJson = require2(join(__dirname, "../package.json"));
3614
- } catch {
3615
- packageJson = require2(join(__dirname, "../../package.json"));
3616
- }
3617
- var PACKAGE_NAME = packageJson.name;
3618
- var VERSION = packageJson.version;
3619
- function wrapInBox(text, footer, padding = 1) {
3620
- const lines = text.split("\n").filter((line) => line.trim().length > 0);
3621
- const maxLength = Math.max(...lines.map((line) => line.length));
3622
- const horizontalBorder = "\u2500".repeat(maxLength + padding * 2);
3623
- const top = `\u250C${horizontalBorder}\u2510`;
3624
- const bottom = `\u2514${horizontalBorder}\u2518`;
3625
- const separator = `\u251C${horizontalBorder}\u2524`;
3626
- const paddedLines = lines.map((line) => {
3627
- const padRight = " ".repeat(maxLength - line.length + padding);
3628
- const padLeft = " ".repeat(padding);
3629
- return `\u2502${padLeft}${line}${padRight}\u2502`;
3630
- });
3631
- const totalPad = maxLength - footer.length;
3632
- const leftPad = Math.floor(totalPad / 2);
3633
- const rightPad = totalPad - leftPad;
3634
- const centeredFooter = " ".repeat(leftPad) + footer + " ".repeat(rightPad);
3635
- const paddedFooter = `\u2502${" ".repeat(padding)}${centeredFooter}${" ".repeat(padding)}\u2502`;
3636
- return [top, ...paddedLines, separator, paddedFooter, bottom].join("\n");
3637
- }
3638
- function showBanner() {
3639
- const banner = figlet.textSync("LIEN", {
3640
- font: "ANSI Shadow",
3641
- horizontalLayout: "fitted",
3642
- verticalLayout: "fitted"
3643
- });
3644
- const footer = `${PACKAGE_NAME} - v${VERSION}`;
3645
- const boxedBanner = wrapInBox(banner.trim(), footer);
3646
- console.error(chalk.cyan(boxedBanner));
3647
- console.error();
3648
- }
3649
- function showCompactBanner() {
3650
- const banner = figlet.textSync("LIEN", {
3651
- font: "ANSI Shadow",
3652
- horizontalLayout: "fitted",
3653
- verticalLayout: "fitted"
3654
- });
3655
- const footer = `${PACKAGE_NAME} - v${VERSION}`;
3656
- const boxedBanner = wrapInBox(banner.trim(), footer);
3657
- console.log(chalk.cyan(boxedBanner));
3658
- console.log();
3659
- }
3660
-
3661
- // src/cli/init.ts
3669
+ var MCP_CONFIG = {
3670
+ command: "lien",
3671
+ args: ["serve"]
3672
+ };
3662
3673
  async function initCommand(options = {}) {
3663
3674
  showCompactBanner();
3664
- console.log(chalk2.bold("\nLien Initialization\n"));
3665
- console.log(chalk2.green("\u2713 No per-project configuration needed!"));
3666
- console.log(chalk2.dim("\nLien now uses:"));
3667
- console.log(chalk2.dim(" \u2022 Auto-detected frameworks"));
3668
- console.log(chalk2.dim(" \u2022 Sensible defaults for all settings"));
3669
- console.log(chalk2.dim(" \u2022 Global config (optional) at ~/.lien/config.json"));
3670
- console.log(chalk2.bold("\nNext steps:"));
3671
- console.log(chalk2.dim(" 1. Run"), chalk2.bold("lien index"), chalk2.dim("to index your codebase"));
3672
- console.log(chalk2.dim(" 2. Run"), chalk2.bold("lien serve"), chalk2.dim("to start the MCP server"));
3673
- console.log(chalk2.bold("\nGlobal Configuration (optional):"));
3674
- console.log(chalk2.dim(" To use Qdrant backend, create ~/.lien/config.json:"));
3675
- console.log(chalk2.dim(" {"));
3676
- console.log(chalk2.dim(' "backend": "qdrant",'));
3677
- console.log(chalk2.dim(' "qdrant": {'));
3678
- console.log(chalk2.dim(' "url": "http://localhost:6333",'));
3679
- console.log(chalk2.dim(' "apiKey": "optional-api-key"'));
3680
- console.log(chalk2.dim(" }"));
3681
- console.log(chalk2.dim(" }"));
3682
- console.log(chalk2.dim("\n Or use environment variables:"));
3683
- console.log(chalk2.dim(" LIEN_BACKEND=qdrant"));
3684
- console.log(chalk2.dim(" LIEN_QDRANT_URL=http://localhost:6333"));
3685
- console.log(chalk2.dim(" LIEN_QDRANT_API_KEY=your-key"));
3686
3675
  const rootDir = options.path || process.cwd();
3687
- const configPath = path.join(rootDir, ".lien.config.json");
3676
+ const cursorDir = path.join(rootDir, ".cursor");
3677
+ const mcpConfigPath = path.join(cursorDir, "mcp.json");
3678
+ let existingConfig = null;
3679
+ try {
3680
+ const raw = await fs.readFile(mcpConfigPath, "utf-8");
3681
+ const parsed = JSON.parse(raw);
3682
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3683
+ existingConfig = parsed;
3684
+ }
3685
+ } catch {
3686
+ }
3687
+ if (existingConfig?.mcpServers?.lien) {
3688
+ console.log(chalk2.green("\n\u2713 Already configured \u2014 .cursor/mcp.json contains lien entry"));
3689
+ } else if (existingConfig) {
3690
+ const servers = existingConfig.mcpServers;
3691
+ const safeServers = servers && typeof servers === "object" && !Array.isArray(servers) ? servers : {};
3692
+ safeServers.lien = MCP_CONFIG;
3693
+ existingConfig.mcpServers = safeServers;
3694
+ await fs.writeFile(mcpConfigPath, JSON.stringify(existingConfig, null, 2) + "\n");
3695
+ console.log(chalk2.green("\n\u2713 Added lien to existing .cursor/mcp.json"));
3696
+ } else {
3697
+ await fs.mkdir(cursorDir, { recursive: true });
3698
+ const config = { mcpServers: { lien: MCP_CONFIG } };
3699
+ await fs.writeFile(mcpConfigPath, JSON.stringify(config, null, 2) + "\n");
3700
+ console.log(chalk2.green("\n\u2713 Created .cursor/mcp.json"));
3701
+ }
3702
+ console.log(chalk2.dim(" Restart Cursor to activate.\n"));
3703
+ const legacyConfigPath = path.join(rootDir, ".lien.config.json");
3688
3704
  try {
3689
- await fs.access(configPath);
3690
- console.log(chalk2.yellow("\n\u26A0\uFE0F Note: .lien.config.json found but no longer used"));
3705
+ await fs.access(legacyConfigPath);
3706
+ console.log(chalk2.yellow("\u26A0\uFE0F Note: .lien.config.json found but no longer used"));
3691
3707
  console.log(chalk2.dim(" You can safely delete it."));
3692
3708
  } catch {
3693
3709
  }
3694
3710
  }
3695
3711
 
3696
3712
  // src/cli/status.ts
3713
+ init_banner();
3697
3714
  import chalk3 from "chalk";
3698
3715
  import fs2 from "fs/promises";
3699
3716
  import path2 from "path";
3700
3717
  import os from "os";
3701
- import crypto from "crypto";
3702
3718
  import {
3703
3719
  isGitRepo,
3704
3720
  getCurrentBranch,
3705
3721
  getCurrentCommit,
3706
3722
  readVersionFile,
3723
+ extractRepoId,
3707
3724
  DEFAULT_CONCURRENCY,
3708
3725
  DEFAULT_EMBEDDING_BATCH_SIZE,
3709
3726
  DEFAULT_CHUNK_SIZE,
@@ -3712,12 +3729,14 @@ import {
3712
3729
  } from "@liendev/core";
3713
3730
  async function statusCommand() {
3714
3731
  const rootDir = process.cwd();
3715
- const projectName = path2.basename(rootDir);
3716
- const pathHash = crypto.createHash("md5").update(rootDir).digest("hex").substring(0, 8);
3717
- const indexPath = path2.join(os.homedir(), ".lien", "indices", `${projectName}-${pathHash}`);
3732
+ const repoId = extractRepoId(rootDir);
3733
+ const indexPath = path2.join(os.homedir(), ".lien", "indices", repoId);
3718
3734
  showCompactBanner();
3719
3735
  console.log(chalk3.bold("Status\n"));
3720
- console.log(chalk3.dim("Configuration:"), chalk3.green("\u2713 Using defaults (no per-project config needed)"));
3736
+ console.log(
3737
+ chalk3.dim("Configuration:"),
3738
+ chalk3.green("\u2713 Using defaults (no per-project config needed)")
3739
+ );
3721
3740
  try {
3722
3741
  const stats = await fs2.stat(indexPath);
3723
3742
  console.log(chalk3.dim("Index location:"), indexPath);
@@ -3725,7 +3744,7 @@ async function statusCommand() {
3725
3744
  try {
3726
3745
  const files = await fs2.readdir(indexPath, { recursive: true });
3727
3746
  console.log(chalk3.dim("Index files:"), files.length);
3728
- } catch (e) {
3747
+ } catch {
3729
3748
  }
3730
3749
  console.log(chalk3.dim("Last modified:"), stats.mtime.toLocaleString());
3731
3750
  try {
@@ -3736,9 +3755,13 @@ async function statusCommand() {
3736
3755
  }
3737
3756
  } catch {
3738
3757
  }
3739
- } catch (error) {
3758
+ } catch {
3740
3759
  console.log(chalk3.dim("Index status:"), chalk3.yellow("\u2717 Not indexed"));
3741
- console.log(chalk3.yellow("\nRun"), chalk3.bold("lien index"), chalk3.yellow("to index your codebase"));
3760
+ console.log(
3761
+ chalk3.yellow("\nRun"),
3762
+ chalk3.bold("lien index"),
3763
+ chalk3.yellow("to index your codebase")
3764
+ );
3742
3765
  }
3743
3766
  console.log(chalk3.bold("\nFeatures:"));
3744
3767
  const isRepo = await isGitRepo(rootDir);
@@ -3775,8 +3798,9 @@ async function statusCommand() {
3775
3798
  }
3776
3799
 
3777
3800
  // src/cli/index-cmd.ts
3778
- import chalk4 from "chalk";
3779
- import ora from "ora";
3801
+ init_banner();
3802
+ import chalk5 from "chalk";
3803
+ import ora2 from "ora";
3780
3804
  import { indexCodebase } from "@liendev/core";
3781
3805
 
3782
3806
  // src/utils/loading-messages.ts
@@ -3855,17 +3879,28 @@ function getModelLoadingMessage() {
3855
3879
  return message;
3856
3880
  }
3857
3881
 
3882
+ // src/cli/utils.ts
3883
+ import ora from "ora";
3884
+ import chalk4 from "chalk";
3885
+ import { isLienError, getErrorMessage, getErrorStack } from "@liendev/core";
3886
+ function formatDuration(ms) {
3887
+ if (ms < 1e3) {
3888
+ return `${Math.round(ms)}ms`;
3889
+ }
3890
+ return `${(ms / 1e3).toFixed(1)}s`;
3891
+ }
3892
+
3858
3893
  // src/cli/index-cmd.ts
3859
3894
  async function clearExistingIndex() {
3860
3895
  const { VectorDB: VectorDB2 } = await import("@liendev/core");
3861
3896
  const { ManifestManager: ManifestManager2 } = await import("@liendev/core");
3862
- console.log(chalk4.yellow("Clearing existing index and manifest..."));
3897
+ console.log(chalk5.yellow("Clearing existing index and manifest..."));
3863
3898
  const vectorDB = new VectorDB2(process.cwd());
3864
3899
  await vectorDB.initialize();
3865
3900
  await vectorDB.clear();
3866
3901
  const manifest = new ManifestManager2(vectorDB.dbPath);
3867
3902
  await manifest.clear();
3868
- console.log(chalk4.green("\u2713 Index and manifest cleared\n"));
3903
+ console.log(chalk5.green("\u2713 Index and manifest cleared\n"));
3869
3904
  }
3870
3905
  function createProgressTracker() {
3871
3906
  return {
@@ -3932,19 +3967,24 @@ function createProgressCallback(spinner, tracker) {
3932
3967
  if (progress.filesTotal && progress.filesProcessed !== void 0) {
3933
3968
  message = `${message} (${progress.filesProcessed}/${progress.filesTotal})`;
3934
3969
  }
3935
- spinner.succeed(chalk4.green(message));
3970
+ spinner.succeed(chalk5.green(message));
3936
3971
  } else {
3937
3972
  updateSpinner(spinner, tracker);
3938
3973
  }
3939
3974
  };
3940
3975
  }
3941
- function displayFinalResult(spinner, tracker, result) {
3942
- if (!tracker.completedViaProgress) {
3943
- if (result.filesIndexed === 0) {
3944
- spinner.succeed(chalk4.green("Index is up to date - no changes detected"));
3945
- } else {
3946
- spinner.succeed(chalk4.green(`Indexed ${result.filesIndexed} files, ${result.chunksCreated} chunks`));
3947
- }
3976
+ function displayFinalResult(spinner, tracker, result, durationMs) {
3977
+ const timing = formatDuration(durationMs);
3978
+ if (tracker.completedViaProgress) {
3979
+ console.log(chalk5.dim(` Completed in ${timing}`));
3980
+ } else if (result.filesIndexed === 0) {
3981
+ spinner.succeed(chalk5.green(`Index is up to date - no changes detected in ${timing}`));
3982
+ } else {
3983
+ spinner.succeed(
3984
+ chalk5.green(
3985
+ `Indexed ${result.filesIndexed} files, ${result.chunksCreated} chunks in ${timing}`
3986
+ )
3987
+ );
3948
3988
  }
3949
3989
  }
3950
3990
  async function indexCommand(options) {
@@ -3953,7 +3993,7 @@ async function indexCommand(options) {
3953
3993
  if (options.force) {
3954
3994
  await clearExistingIndex();
3955
3995
  }
3956
- const spinner = ora({
3996
+ const spinner = ora2({
3957
3997
  text: "Starting indexing...",
3958
3998
  interval: 30
3959
3999
  // Faster refresh rate for smoother progress
@@ -3968,22 +4008,19 @@ async function indexCommand(options) {
3968
4008
  });
3969
4009
  stopMessageRotation(tracker);
3970
4010
  if (!result.success && result.error) {
3971
- spinner.fail(chalk4.red("Indexing failed"));
3972
- console.error(chalk4.red("\n" + result.error));
4011
+ spinner.fail(chalk5.red("Indexing failed"));
4012
+ console.error(chalk5.red("\n" + result.error));
3973
4013
  process.exit(1);
3974
4014
  }
3975
- displayFinalResult(spinner, tracker, result);
3976
- if (options.watch) {
3977
- console.log(chalk4.yellow("\n\u26A0\uFE0F Watch mode not yet implemented"));
3978
- }
4015
+ displayFinalResult(spinner, tracker, result, result.durationMs);
3979
4016
  } catch (error) {
3980
- console.error(chalk4.red("Error during indexing:"), error);
4017
+ console.error(chalk5.red("Error during indexing:"), error);
3981
4018
  process.exit(1);
3982
4019
  }
3983
4020
  }
3984
4021
 
3985
4022
  // src/cli/serve.ts
3986
- import chalk5 from "chalk";
4023
+ import chalk6 from "chalk";
3987
4024
  import fs5 from "fs/promises";
3988
4025
  import path4 from "path";
3989
4026
 
@@ -3993,16 +4030,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3993
4030
  import { createRequire as createRequire2 } from "module";
3994
4031
  import { fileURLToPath as fileURLToPath2 } from "url";
3995
4032
  import { dirname as dirname2, join as join2 } from "path";
3996
- import {
3997
- WorkerEmbeddings,
3998
- VERSION_CHECK_INTERVAL_MS,
3999
- createVectorDB
4000
- } from "@liendev/core";
4033
+ import { WorkerEmbeddings, VERSION_CHECK_INTERVAL_MS, createVectorDB } from "@liendev/core";
4001
4034
 
4002
4035
  // src/watcher/index.ts
4003
4036
  import chokidar from "chokidar";
4004
4037
  import path3 from "path";
4005
- import { detectEcosystems, getEcosystemExcludePatterns, ALWAYS_IGNORE_PATTERNS } from "@liendev/core";
4038
+ import {
4039
+ detectEcosystems,
4040
+ getEcosystemExcludePatterns,
4041
+ ALWAYS_IGNORE_PATTERNS
4042
+ } from "@liendev/core";
4006
4043
  var FileWatcher = class {
4007
4044
  watcher = null;
4008
4045
  rootDir;
@@ -4115,7 +4152,7 @@ var FileWatcher = class {
4115
4152
  }
4116
4153
  /**
4117
4154
  * Starts watching files for changes.
4118
- *
4155
+ *
4119
4156
  * @param handler - Callback function called when files change
4120
4157
  */
4121
4158
  async start(handler) {
@@ -4131,7 +4168,7 @@ var FileWatcher = class {
4131
4168
  /**
4132
4169
  * Enable watching .git directory for git operations.
4133
4170
  * Call this after start() to enable event-driven git detection.
4134
- *
4171
+ *
4135
4172
  * @param onGitChange - Callback invoked when git operations detected
4136
4173
  */
4137
4174
  watchGit(onGitChange) {
@@ -4185,7 +4222,7 @@ var FileWatcher = class {
4185
4222
  this.gitChangeTimer = setTimeout(async () => {
4186
4223
  try {
4187
4224
  await this.gitChangeHandler?.();
4188
- } catch (error) {
4225
+ } catch {
4189
4226
  }
4190
4227
  this.gitChangeTimer = null;
4191
4228
  }, this.GIT_DEBOUNCE_MS);
@@ -4194,7 +4231,7 @@ var FileWatcher = class {
4194
4231
  * Handles a file change event with smart batching.
4195
4232
  * Collects rapid changes across multiple files and processes them together.
4196
4233
  * Forces flush after MAX_BATCH_WAIT_MS even if changes keep arriving.
4197
- *
4234
+ *
4198
4235
  * If a batch is currently being processed by an async handler, waits for completion
4199
4236
  * before starting a new batch to prevent race conditions.
4200
4237
  */
@@ -4290,7 +4327,7 @@ var FileWatcher = class {
4290
4327
  } else {
4291
4328
  this.handleBatchComplete();
4292
4329
  }
4293
- } catch (error) {
4330
+ } catch {
4294
4331
  this.handleBatchComplete();
4295
4332
  }
4296
4333
  }
@@ -4338,7 +4375,7 @@ var FileWatcher = class {
4338
4375
  modified,
4339
4376
  deleted
4340
4377
  });
4341
- } catch (error) {
4378
+ } catch {
4342
4379
  }
4343
4380
  }
4344
4381
  /**
@@ -4400,10 +4437,7 @@ var FileWatcher = class {
4400
4437
  };
4401
4438
 
4402
4439
  // src/mcp/server-config.ts
4403
- import {
4404
- CallToolRequestSchema,
4405
- ListToolsRequestSchema
4406
- } from "@modelcontextprotocol/sdk/types.js";
4440
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4407
4441
 
4408
4442
  // src/mcp/utils/zod-to-json-schema.ts
4409
4443
  import { zodToJsonSchema } from "zod-to-json-schema";
@@ -8468,25 +8502,23 @@ var SemanticSearchSchema = external_exports.object({
8468
8502
  "Number of results to return.\n\nDefault: 5\nIncrease to 10-15 for broad exploration."
8469
8503
  ),
8470
8504
  crossRepo: external_exports.boolean().default(false).describe(
8471
- "If true, search across all repos in the organization (requires Qdrant backend).\n\nDefault: false (single-repo search)\nWhen enabled, results are grouped by repository."
8505
+ "If true, search across all repos in the organization (requires a cross-repo-capable backend, currently Qdrant).\n\nDefault: false (single-repo search)\nWhen enabled, results are grouped by repository."
8472
8506
  ),
8473
- repoIds: external_exports.array(external_exports.string()).optional().describe(
8507
+ repoIds: external_exports.array(external_exports.string().max(255)).optional().describe(
8474
8508
  "Optional: Filter to specific repos when crossRepo=true.\n\nIf provided, only searches within the specified repositories.\nIf omitted and crossRepo=true, searches all repos in the organization."
8475
8509
  )
8476
8510
  });
8477
8511
 
8478
8512
  // src/mcp/schemas/similarity.schema.ts
8479
8513
  var FindSimilarSchema = external_exports.object({
8480
- code: external_exports.string().min(24, "Code snippet must be at least 24 characters").describe(
8514
+ code: external_exports.string().min(24, "Code snippet must be at least 24 characters").max(5e4, "Code snippet too long (max 50000 characters)").describe(
8481
8515
  "Code snippet to find similar implementations for.\n\nProvide a representative code sample that demonstrates the pattern you want to find similar examples of in the codebase."
8482
8516
  ),
8483
- limit: external_exports.number().int().min(1, "Limit must be at least 1").max(20, "Limit cannot exceed 20").default(5).describe(
8484
- "Number of similar code blocks to return.\n\nDefault: 5"
8485
- ),
8486
- language: external_exports.string().min(1, "Language filter cannot be empty").optional().describe(
8517
+ limit: external_exports.number().int().min(1, "Limit must be at least 1").max(20, "Limit cannot exceed 20").default(5).describe("Number of similar code blocks to return.\n\nDefault: 5"),
8518
+ language: external_exports.string().min(1, "Language filter cannot be empty").max(50).optional().describe(
8487
8519
  "Filter by programming language.\n\nExamples: 'typescript', 'python', 'javascript', 'php'\n\nIf omitted, searches all languages."
8488
8520
  ),
8489
- pathHint: external_exports.string().min(1, "Path hint cannot be empty").optional().describe(
8521
+ pathHint: external_exports.string().min(1, "Path hint cannot be empty").max(500).optional().describe(
8490
8522
  "Filter by file path substring.\n\nOnly returns results where the file path contains this string (case-insensitive).\n\nExamples: 'src/api', 'components', 'utils'"
8491
8523
  )
8492
8524
  });
@@ -8494,8 +8526,8 @@ var FindSimilarSchema = external_exports.object({
8494
8526
  // src/mcp/schemas/file.schema.ts
8495
8527
  var GetFilesContextSchema = external_exports.object({
8496
8528
  filepaths: external_exports.union([
8497
- external_exports.string().min(1, "Filepath cannot be empty"),
8498
- external_exports.array(external_exports.string().min(1, "Filepath cannot be empty")).min(1, "Array must contain at least one filepath").max(50, "Maximum 50 files per request")
8529
+ external_exports.string().min(1, "Filepath cannot be empty").max(1e3),
8530
+ external_exports.array(external_exports.string().min(1, "Filepath cannot be empty").max(1e3)).min(1, "Array must contain at least one filepath").max(50, "Maximum 50 files per request")
8499
8531
  ]).describe(
8500
8532
  "Single filepath or array of filepaths (relative to workspace root).\n\nSingle file: 'src/components/Button.tsx'\nMultiple files: ['src/auth.ts', 'src/user.ts']\n\nMaximum 50 files per request for batch operations."
8501
8533
  ),
@@ -8506,10 +8538,10 @@ var GetFilesContextSchema = external_exports.object({
8506
8538
 
8507
8539
  // src/mcp/schemas/symbols.schema.ts
8508
8540
  var ListFunctionsSchema = external_exports.object({
8509
- pattern: external_exports.string().optional().describe(
8541
+ pattern: external_exports.string().max(200).optional().describe(
8510
8542
  "Regex pattern to match symbol names.\n\nExamples:\n - '.*Controller.*' to find all Controllers\n - 'handle.*' to find handlers\n - '.*Service$' to find Services\n\nIf omitted, returns all symbols."
8511
8543
  ),
8512
- language: external_exports.string().optional().describe(
8544
+ language: external_exports.string().max(50).optional().describe(
8513
8545
  "Filter by programming language.\n\nExamples: 'typescript', 'python', 'javascript', 'php'\n\nIf omitted, searches all languages."
8514
8546
  ),
8515
8547
  symbolType: external_exports.enum(["function", "method", "class", "interface"]).optional().describe("Filter by symbol type. If omitted, returns all types."),
@@ -8523,23 +8555,23 @@ var ListFunctionsSchema = external_exports.object({
8523
8555
 
8524
8556
  // src/mcp/schemas/dependents.schema.ts
8525
8557
  var GetDependentsSchema = external_exports.object({
8526
- filepath: external_exports.string().min(1, "Filepath cannot be empty").describe(
8558
+ filepath: external_exports.string().min(1, "Filepath cannot be empty").max(1e3).describe(
8527
8559
  "Path to file to find dependents for (relative to workspace root).\n\nExample: 'src/utils/validate.ts'\n\nReturns all files that import or depend on this file.\n\nNote: Scans up to 10,000 code chunks. For very large codebases,\nresults may be incomplete (a warning will be included if truncated)."
8528
8560
  ),
8529
- symbol: external_exports.string().min(1, "Symbol cannot be an empty string").optional().describe(
8561
+ symbol: external_exports.string().min(1, "Symbol cannot be an empty string").max(500).optional().describe(
8530
8562
  "Optional: specific exported symbol to find usages of.\n\nWhen provided, returns call sites instead of just importing files.\n\nExample: 'validateEmail' to find where validateEmail() is called.\n\nResponse includes 'usages' array showing which functions call this symbol."
8531
8563
  ),
8532
8564
  depth: external_exports.number().int().min(1).max(1).default(1).describe(
8533
8565
  "Depth of transitive dependencies. Only depth=1 (direct dependents) is currently supported.\n\n1 = Direct dependents only"
8534
8566
  ),
8535
8567
  crossRepo: external_exports.boolean().default(false).describe(
8536
- "If true, find dependents across all repos in the organization (requires Qdrant backend).\n\nDefault: false (single-repo search)\nWhen enabled, results are grouped by repository."
8568
+ "If true, find dependents across all repos in the organization (requires a cross-repo-capable backend, currently Qdrant).\n\nDefault: false (single-repo search)\nWhen enabled, results are grouped by repository."
8537
8569
  )
8538
8570
  });
8539
8571
 
8540
8572
  // src/mcp/schemas/complexity.schema.ts
8541
8573
  var GetComplexitySchema = external_exports.object({
8542
- files: external_exports.array(external_exports.string().min(1, "Filepath cannot be empty")).optional().describe(
8574
+ files: external_exports.array(external_exports.string().min(1, "Filepath cannot be empty").max(1e3)).optional().describe(
8543
8575
  "Specific files to analyze. If omitted, analyzes entire codebase.\n\nExample: ['src/auth.ts', 'src/api/user.ts']"
8544
8576
  ),
8545
8577
  top: external_exports.number().int().min(1, "Top must be at least 1").max(50, "Top cannot exceed 50").default(10).describe(
@@ -8549,9 +8581,9 @@ var GetComplexitySchema = external_exports.object({
8549
8581
  "Only return functions above this complexity threshold.\n\nNote: Violations are first identified using the threshold from lien.config.json (default: 15). This parameter filters those violations to show only items above the specified value. Setting threshold below the config threshold will not show additional functions."
8550
8582
  ),
8551
8583
  crossRepo: external_exports.boolean().default(false).describe(
8552
- "If true, analyze complexity across all repos in the organization (requires Qdrant backend).\n\nDefault: false (single-repo analysis)\nWhen enabled, results are aggregated by repository."
8584
+ "If true, analyze complexity across all repos in the organization (requires a cross-repo-capable backend, currently Qdrant).\n\nDefault: false (single-repo analysis)\nWhen enabled, results are aggregated by repository."
8553
8585
  ),
8554
- repoIds: external_exports.array(external_exports.string()).optional().describe(
8586
+ repoIds: external_exports.array(external_exports.string().max(255)).optional().describe(
8555
8587
  "Optional: Filter to specific repos when crossRepo=true.\n\nIf provided, only analyzes the specified repositories.\nIf omitted and crossRepo=true, analyzes all repos in the organization."
8556
8588
  )
8557
8589
  });
@@ -8816,10 +8848,12 @@ function wrapToolHandler(schema, handler) {
8816
8848
  ${truncation.message}` : truncation.message;
8817
8849
  }
8818
8850
  return {
8819
- content: [{
8820
- type: "text",
8821
- text: JSON.stringify(result, null, 2)
8822
- }]
8851
+ content: [
8852
+ {
8853
+ type: "text",
8854
+ text: JSON.stringify(result, null, 2)
8855
+ }
8856
+ ]
8823
8857
  };
8824
8858
  } catch (error) {
8825
8859
  return formatErrorResponse(error);
@@ -8830,42 +8864,57 @@ function formatErrorResponse(error) {
8830
8864
  if (error instanceof ZodError) {
8831
8865
  return {
8832
8866
  isError: true,
8833
- content: [{
8834
- type: "text",
8835
- text: JSON.stringify({
8836
- error: "Invalid parameters",
8837
- code: LienErrorCode.INVALID_INPUT,
8838
- details: error.errors.map((e) => ({
8839
- field: e.path.join("."),
8840
- message: e.message
8841
- }))
8842
- }, null, 2)
8843
- }]
8867
+ content: [
8868
+ {
8869
+ type: "text",
8870
+ text: JSON.stringify(
8871
+ {
8872
+ error: "Invalid parameters",
8873
+ code: LienErrorCode.INVALID_INPUT,
8874
+ details: error.errors.map((e) => ({
8875
+ field: e.path.join("."),
8876
+ message: e.message
8877
+ }))
8878
+ },
8879
+ null,
8880
+ 2
8881
+ )
8882
+ }
8883
+ ]
8844
8884
  };
8845
8885
  }
8846
8886
  if (error instanceof LienError) {
8847
8887
  return {
8848
8888
  isError: true,
8849
- content: [{
8850
- type: "text",
8851
- text: JSON.stringify(error.toJSON(), null, 2)
8852
- }]
8889
+ content: [
8890
+ {
8891
+ type: "text",
8892
+ text: JSON.stringify(error.toJSON(), null, 2)
8893
+ }
8894
+ ]
8853
8895
  };
8854
8896
  }
8855
8897
  console.error("Unexpected error in tool handler:", error);
8856
8898
  return {
8857
8899
  isError: true,
8858
- content: [{
8859
- type: "text",
8860
- text: JSON.stringify({
8861
- error: error instanceof Error ? error.message : "Unknown error",
8862
- code: LienErrorCode.INTERNAL_ERROR
8863
- }, null, 2)
8864
- }]
8900
+ content: [
8901
+ {
8902
+ type: "text",
8903
+ text: JSON.stringify(
8904
+ {
8905
+ error: error instanceof Error ? error.message : "Unknown error",
8906
+ code: LienErrorCode.INTERNAL_ERROR
8907
+ },
8908
+ null,
8909
+ 2
8910
+ )
8911
+ }
8912
+ ]
8865
8913
  };
8866
8914
  }
8867
8915
 
8868
8916
  // src/mcp/utils/metadata-shaper.ts
8917
+ import { normalizeToRelativePath } from "@liendev/core";
8869
8918
  var FIELD_ALLOWLISTS = {
8870
8919
  semantic_search: /* @__PURE__ */ new Set([
8871
8920
  "language",
@@ -8917,7 +8966,12 @@ var FIELD_ALLOWLISTS = {
8917
8966
  function deduplicateResults(results) {
8918
8967
  const seen = /* @__PURE__ */ new Set();
8919
8968
  return results.filter((r) => {
8920
- const key = JSON.stringify([r.metadata.repoId ?? "", r.metadata.file, r.metadata.startLine, r.metadata.endLine]);
8969
+ const key = JSON.stringify([
8970
+ r.metadata.repoId ?? "",
8971
+ r.metadata.file ? normalizeToRelativePath(r.metadata.file) : "",
8972
+ r.metadata.startLine,
8973
+ r.metadata.endLine
8974
+ ]);
8921
8975
  if (seen.has(key)) return false;
8922
8976
  seen.add(key);
8923
8977
  return true;
@@ -8974,7 +9028,6 @@ function shapeResults(results, tool) {
8974
9028
  }
8975
9029
 
8976
9030
  // src/mcp/handlers/semantic-search.ts
8977
- import { QdrantDB } from "@liendev/core";
8978
9031
  function groupResultsByRepo(results) {
8979
9032
  const grouped = {};
8980
9033
  for (const result of results) {
@@ -8988,13 +9041,18 @@ function groupResultsByRepo(results) {
8988
9041
  }
8989
9042
  async function executeSearch(vectorDB, queryEmbedding, params, log) {
8990
9043
  const { query, limit, crossRepo, repoIds } = params;
8991
- if (crossRepo && vectorDB instanceof QdrantDB) {
9044
+ if (crossRepo && vectorDB.supportsCrossRepo) {
8992
9045
  const results2 = await vectorDB.searchCrossRepo(queryEmbedding, limit, { repoIds });
8993
- log(`Found ${results2.length} results across ${Object.keys(groupResultsByRepo(results2)).length} repos`);
9046
+ log(
9047
+ `Found ${results2.length} results across ${Object.keys(groupResultsByRepo(results2)).length} repos`
9048
+ );
8994
9049
  return { results: results2, crossRepoFallback: false };
8995
9050
  }
8996
9051
  if (crossRepo) {
8997
- log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo search.", "warning");
9052
+ log(
9053
+ "Warning: crossRepo=true requires a cross-repo-capable backend. Falling back to single-repo search.",
9054
+ "warning"
9055
+ );
8998
9056
  }
8999
9057
  const results = await vectorDB.search(queryEmbedding, limit, query);
9000
9058
  log(`Found ${results.length} results`);
@@ -9003,7 +9061,9 @@ async function executeSearch(vectorDB, queryEmbedding, params, log) {
9003
9061
  function processResults(rawResults, crossRepoFallback, log) {
9004
9062
  const notes = [];
9005
9063
  if (crossRepoFallback) {
9006
- notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
9064
+ notes.push(
9065
+ "Cross-repo search requires a cross-repo-capable backend. Fell back to single-repo search."
9066
+ );
9007
9067
  }
9008
9068
  const results = deduplicateResults(rawResults);
9009
9069
  if (results.length > 0 && results.every((r) => r.relevance === "not_relevant")) {
@@ -9015,33 +9075,32 @@ function processResults(rawResults, crossRepoFallback, log) {
9015
9075
  }
9016
9076
  async function handleSemanticSearch(args, ctx) {
9017
9077
  const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
9018
- return await wrapToolHandler(
9019
- SemanticSearchSchema,
9020
- async (validatedArgs) => {
9021
- const { crossRepo, repoIds, query, limit } = validatedArgs;
9022
- log(`Searching for: "${query}"${crossRepo ? " (cross-repo)" : ""}`);
9023
- await checkAndReconnect();
9024
- const queryEmbedding = await embeddings.embed(query);
9025
- const { results: rawResults, crossRepoFallback } = await executeSearch(
9026
- vectorDB,
9027
- queryEmbedding,
9028
- { query, limit: limit ?? 5, crossRepo, repoIds },
9029
- log
9078
+ return await wrapToolHandler(SemanticSearchSchema, async (validatedArgs) => {
9079
+ const { crossRepo, repoIds, query, limit } = validatedArgs;
9080
+ log(`Searching for: "${query}"${crossRepo ? " (cross-repo)" : ""}`);
9081
+ await checkAndReconnect();
9082
+ const queryEmbedding = await embeddings.embed(query);
9083
+ const { results: rawResults, crossRepoFallback } = await executeSearch(
9084
+ vectorDB,
9085
+ queryEmbedding,
9086
+ { query, limit: limit ?? 5, crossRepo, repoIds },
9087
+ log
9088
+ );
9089
+ const { results, notes } = processResults(rawResults, crossRepoFallback, log);
9090
+ log(`Returning ${results.length} results`);
9091
+ const shaped = shapeResults(results, "semantic_search");
9092
+ if (shaped.length === 0) {
9093
+ notes.push(
9094
+ '0 results. Try rephrasing as a full question (e.g. "How does X work?"), or use grep for exact string matches. If the codebase was recently updated, run "lien reindex".'
9030
9095
  );
9031
- const { results, notes } = processResults(rawResults, crossRepoFallback, log);
9032
- log(`Returning ${results.length} results`);
9033
- const shaped = shapeResults(results, "semantic_search");
9034
- if (shaped.length === 0) {
9035
- notes.push('0 results. Try rephrasing as a full question (e.g. "How does X work?"), or use grep for exact string matches. If the codebase was recently updated, run "lien reindex".');
9036
- }
9037
- return {
9038
- indexInfo: getIndexMetadata(),
9039
- results: shaped,
9040
- ...crossRepo && vectorDB instanceof QdrantDB && { groupedByRepo: groupResultsByRepo(shaped) },
9041
- ...notes.length > 0 && { note: notes.join(" ") }
9042
- };
9043
9096
  }
9044
- )(args);
9097
+ return {
9098
+ indexInfo: getIndexMetadata(),
9099
+ results: shaped,
9100
+ ...crossRepo && vectorDB.supportsCrossRepo && { groupedByRepo: groupResultsByRepo(shaped) },
9101
+ ...notes.length > 0 && { note: notes.join(" ") }
9102
+ };
9103
+ })(args);
9045
9104
  }
9046
9105
 
9047
9106
  // src/mcp/handlers/find-similar.ts
@@ -9060,151 +9119,52 @@ function pruneIrrelevantResults(results) {
9060
9119
  }
9061
9120
  async function handleFindSimilar(args, ctx) {
9062
9121
  const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
9063
- return await wrapToolHandler(
9064
- FindSimilarSchema,
9065
- async (validatedArgs) => {
9066
- log(`Finding similar code...`);
9067
- await checkAndReconnect();
9068
- const codeEmbedding = await embeddings.embed(validatedArgs.code);
9069
- const limit = validatedArgs.limit ?? 5;
9070
- const extraLimit = limit + 10;
9071
- let results = await vectorDB.search(codeEmbedding, extraLimit, validatedArgs.code);
9072
- results = deduplicateResults(results);
9073
- const inputCode = validatedArgs.code.trim();
9074
- results = results.filter((r) => {
9075
- if (r.score >= 0.1) return true;
9076
- return r.content.trim() !== inputCode;
9077
- });
9078
- const filtersApplied = { prunedLowRelevance: 0 };
9079
- if (validatedArgs.language) {
9080
- filtersApplied.language = validatedArgs.language;
9081
- results = applyLanguageFilter(results, validatedArgs.language);
9082
- }
9083
- if (validatedArgs.pathHint) {
9084
- filtersApplied.pathHint = validatedArgs.pathHint;
9085
- results = applyPathHintFilter(results, validatedArgs.pathHint);
9086
- }
9087
- const { filtered, prunedCount } = pruneIrrelevantResults(results);
9088
- filtersApplied.prunedLowRelevance = prunedCount;
9089
- const finalResults = filtered.slice(0, limit);
9090
- log(`Found ${finalResults.length} similar chunks`);
9091
- const hasFilters = filtersApplied.language || filtersApplied.pathHint || filtersApplied.prunedLowRelevance > 0;
9092
- return {
9093
- indexInfo: getIndexMetadata(),
9094
- results: shapeResults(finalResults, "find_similar"),
9095
- ...hasFilters && { filtersApplied },
9096
- ...finalResults.length === 0 && { note: "0 results. Ensure the code snippet is at least 24 characters and representative of the pattern. Try grep for exact string matches." }
9097
- };
9098
- }
9099
- )(args);
9100
- }
9101
-
9102
- // src/mcp/utils/path-matching.ts
9103
- import { getSupportedExtensions } from "@liendev/core";
9104
- function escapeRegex(str) {
9105
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9106
- }
9107
- var extensionRegex = null;
9108
- function getExtensionRegex() {
9109
- if (!extensionRegex) {
9110
- const extPattern = getSupportedExtensions().map(escapeRegex).join("|");
9111
- extensionRegex = new RegExp(`\\.(${extPattern})$`);
9112
- }
9113
- return extensionRegex;
9114
- }
9115
- function normalizePath(path7, workspaceRoot) {
9116
- let normalized = path7.replace(/['"]/g, "").trim().replace(/\\/g, "/");
9117
- normalized = normalized.replace(getExtensionRegex(), "");
9118
- if (normalized.startsWith(workspaceRoot + "/")) {
9119
- normalized = normalized.substring(workspaceRoot.length + 1);
9120
- }
9121
- return normalized;
9122
- }
9123
- function matchesAtBoundary(str, pattern) {
9124
- const index = str.indexOf(pattern);
9125
- if (index === -1) return false;
9126
- const charBefore = index > 0 ? str[index - 1] : "/";
9127
- if (charBefore !== "/" && index !== 0) return false;
9128
- const endIndex = index + pattern.length;
9129
- if (endIndex === str.length) return true;
9130
- const charAfter = str[endIndex];
9131
- return charAfter === "/";
9132
- }
9133
- function matchesFile(normalizedImport, normalizedTarget) {
9134
- if (normalizedImport === normalizedTarget) return true;
9135
- if (matchesAtBoundary(normalizedImport, normalizedTarget)) {
9136
- return true;
9137
- }
9138
- if (matchesAtBoundary(normalizedTarget, normalizedImport)) {
9139
- return true;
9140
- }
9141
- const cleanedImport = normalizedImport.replace(/^(\.\.?\/)+/, "");
9142
- if (matchesAtBoundary(cleanedImport, normalizedTarget) || matchesAtBoundary(normalizedTarget, cleanedImport)) {
9143
- return true;
9144
- }
9145
- if (matchesPHPNamespace(normalizedImport, normalizedTarget)) {
9146
- return true;
9147
- }
9148
- if (matchesPythonModule(normalizedImport, normalizedTarget)) {
9149
- return true;
9150
- }
9151
- return false;
9152
- }
9153
- function matchesDirectPythonModule(moduleAsPath, targetWithoutPy) {
9154
- return targetWithoutPy === moduleAsPath || targetWithoutPy === moduleAsPath + "/__init__" || targetWithoutPy.replace(/\/__init__$/, "") === moduleAsPath;
9155
- }
9156
- function matchesParentPythonPackage(moduleAsPath, targetWithoutPy) {
9157
- return targetWithoutPy.startsWith(moduleAsPath + "/");
9158
- }
9159
- function matchesSuffixPythonModule(moduleAsPath, targetWithoutPy) {
9160
- return targetWithoutPy.endsWith("/" + moduleAsPath) || targetWithoutPy.endsWith("/" + moduleAsPath + "/__init__");
9161
- }
9162
- function matchesWithSourcePrefix(moduleAsPath, targetWithoutPy) {
9163
- const moduleIndex = targetWithoutPy.indexOf(moduleAsPath);
9164
- if (moduleIndex < 0) return false;
9165
- const prefix = targetWithoutPy.substring(0, moduleIndex);
9166
- const prefixSlashes = (prefix.match(/\//g) || []).length;
9167
- return prefixSlashes <= 1 && (prefix === "" || prefix.endsWith("/"));
9168
- }
9169
- function matchesPythonModule(importPath, targetPath) {
9170
- if (!importPath.includes(".")) {
9171
- return false;
9172
- }
9173
- const moduleAsPath = importPath.replace(/\./g, "/");
9174
- const targetWithoutPy = targetPath.replace(/\.py$/, "");
9175
- return matchesDirectPythonModule(moduleAsPath, targetWithoutPy) || matchesParentPythonPackage(moduleAsPath, targetWithoutPy) || matchesSuffixPythonModule(moduleAsPath, targetWithoutPy) || matchesWithSourcePrefix(moduleAsPath, targetWithoutPy);
9176
- }
9177
- function matchesPHPNamespace(importPath, targetPath) {
9178
- const importComponents = importPath.split("/").filter(Boolean);
9179
- const targetComponents = targetPath.split("/").filter(Boolean);
9180
- if (importComponents.length === 0 || targetComponents.length === 0) {
9181
- return false;
9182
- }
9183
- let matched = 0;
9184
- for (let i = 1; i <= importComponents.length && i <= targetComponents.length; i++) {
9185
- const impComp = importComponents[importComponents.length - i].toLowerCase();
9186
- const targetComp = targetComponents[targetComponents.length - i].toLowerCase();
9187
- if (impComp === targetComp) {
9188
- matched++;
9189
- } else {
9190
- break;
9191
- }
9192
- }
9193
- return matched === importComponents.length;
9194
- }
9195
- function getCanonicalPath(filepath, workspaceRoot) {
9196
- let canonical = filepath.replace(/\\/g, "/");
9197
- if (canonical.startsWith(workspaceRoot + "/")) {
9198
- canonical = canonical.substring(workspaceRoot.length + 1);
9199
- }
9200
- return canonical;
9201
- }
9202
- function isTestFile(filepath) {
9203
- return /\.(test|spec)\.[^/]+$/.test(filepath) || /(^|[/\\])(test|tests|__tests__)[/\\]/.test(filepath);
9122
+ return await wrapToolHandler(FindSimilarSchema, async (validatedArgs) => {
9123
+ log(`Finding similar code...`);
9124
+ await checkAndReconnect();
9125
+ const codeEmbedding = await embeddings.embed(validatedArgs.code);
9126
+ const limit = validatedArgs.limit ?? 5;
9127
+ const extraLimit = limit + 10;
9128
+ let results = await vectorDB.search(codeEmbedding, extraLimit, validatedArgs.code);
9129
+ results = deduplicateResults(results);
9130
+ const inputCode = validatedArgs.code.trim();
9131
+ results = results.filter((r) => {
9132
+ if (r.score >= 0.1) return true;
9133
+ return r.content.trim() !== inputCode;
9134
+ });
9135
+ const filtersApplied = { prunedLowRelevance: 0 };
9136
+ if (validatedArgs.language) {
9137
+ filtersApplied.language = validatedArgs.language;
9138
+ results = applyLanguageFilter(results, validatedArgs.language);
9139
+ }
9140
+ if (validatedArgs.pathHint) {
9141
+ filtersApplied.pathHint = validatedArgs.pathHint;
9142
+ results = applyPathHintFilter(results, validatedArgs.pathHint);
9143
+ }
9144
+ const { filtered, prunedCount } = pruneIrrelevantResults(results);
9145
+ filtersApplied.prunedLowRelevance = prunedCount;
9146
+ const finalResults = filtered.slice(0, limit);
9147
+ log(`Found ${finalResults.length} similar chunks`);
9148
+ const hasFilters = filtersApplied.language || filtersApplied.pathHint || filtersApplied.prunedLowRelevance > 0;
9149
+ return {
9150
+ indexInfo: getIndexMetadata(),
9151
+ results: shapeResults(finalResults, "find_similar"),
9152
+ ...hasFilters && { filtersApplied },
9153
+ ...finalResults.length === 0 && {
9154
+ note: "0 results. Ensure the code snippet is at least 24 characters and representative of the pattern. Try grep for exact string matches."
9155
+ }
9156
+ };
9157
+ })(args);
9204
9158
  }
9205
9159
 
9206
9160
  // src/mcp/handlers/get-files-context.ts
9207
- import { MAX_CHUNKS_PER_FILE } from "@liendev/core";
9161
+ import {
9162
+ normalizePath,
9163
+ matchesFile,
9164
+ getCanonicalPath,
9165
+ isTestFile,
9166
+ MAX_CHUNKS_PER_FILE
9167
+ } from "@liendev/core";
9208
9168
  var SCAN_LIMIT = 1e4;
9209
9169
  async function searchFileChunks(filepaths, ctx) {
9210
9170
  const { vectorDB, workspaceRoot } = ctx;
@@ -9234,10 +9194,7 @@ async function findRelatedChunks(filepaths, fileChunksMap, ctx) {
9234
9194
  (embedding, i) => vectorDB.search(embedding, 5, filesWithChunks[i].chunks[0].content)
9235
9195
  )
9236
9196
  );
9237
- const relatedChunksMap = Array.from(
9238
- { length: filepaths.length },
9239
- () => []
9240
- );
9197
+ const relatedChunksMap = Array.from({ length: filepaths.length }, () => []);
9241
9198
  filesWithChunks.forEach(({ filepath, index }, i) => {
9242
9199
  const related = relatedSearches[i];
9243
9200
  const targetCanonical = getCanonicalPath(filepath, workspaceRoot);
@@ -9287,10 +9244,7 @@ function deduplicateChunks(fileChunks, relatedChunks) {
9287
9244
  function buildFilesData(filepaths, fileChunksMap, relatedChunksMap, testAssociationsMap) {
9288
9245
  const filesData = {};
9289
9246
  filepaths.forEach((filepath, i) => {
9290
- const dedupedChunks = deduplicateChunks(
9291
- fileChunksMap[i],
9292
- relatedChunksMap[i] || []
9293
- );
9247
+ const dedupedChunks = deduplicateChunks(fileChunksMap[i], relatedChunksMap[i] || []);
9294
9248
  filesData[filepath] = {
9295
9249
  chunks: dedupedChunks,
9296
9250
  testAssociations: testAssociationsMap[i]
@@ -9327,61 +9281,48 @@ function buildMultiFileResponse(filesData, indexInfo, note) {
9327
9281
  }
9328
9282
  async function handleGetFilesContext(args, ctx) {
9329
9283
  const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
9330
- return await wrapToolHandler(
9331
- GetFilesContextSchema,
9332
- async (validatedArgs) => {
9333
- const filepaths = Array.isArray(validatedArgs.filepaths) ? validatedArgs.filepaths : [validatedArgs.filepaths];
9334
- const isSingleFile = !Array.isArray(validatedArgs.filepaths);
9335
- log(`Getting context for: ${filepaths.join(", ")}`);
9336
- await checkAndReconnect();
9337
- const workspaceRoot = process.cwd().replace(/\\/g, "/");
9338
- const handlerCtx = {
9339
- vectorDB,
9340
- embeddings,
9341
- log,
9342
- workspaceRoot
9343
- };
9344
- const fileChunksMap = await searchFileChunks(filepaths, handlerCtx);
9345
- let relatedChunksMap = [];
9346
- if (validatedArgs.includeRelated !== false) {
9347
- relatedChunksMap = await findRelatedChunks(
9348
- filepaths,
9349
- fileChunksMap,
9350
- handlerCtx
9351
- );
9352
- }
9353
- const allChunks = await vectorDB.scanWithFilter({ limit: SCAN_LIMIT });
9354
- const hitScanLimit = allChunks.length === SCAN_LIMIT;
9355
- if (hitScanLimit) {
9356
- log(
9357
- `Scanned ${SCAN_LIMIT} chunks (limit reached). Test associations may be incomplete for large codebases.`,
9358
- "warning"
9359
- );
9360
- }
9361
- const testAssociationsMap = findTestAssociations(
9362
- filepaths,
9363
- allChunks,
9364
- handlerCtx
9365
- );
9366
- const filesData = buildFilesData(
9367
- filepaths,
9368
- fileChunksMap,
9369
- relatedChunksMap,
9370
- testAssociationsMap
9371
- );
9372
- const totalChunks = Object.values(filesData).reduce(
9373
- (sum, f) => sum + f.chunks.length,
9374
- 0
9284
+ return await wrapToolHandler(GetFilesContextSchema, async (validatedArgs) => {
9285
+ const filepaths = Array.isArray(validatedArgs.filepaths) ? validatedArgs.filepaths : [validatedArgs.filepaths];
9286
+ const isSingleFile = !Array.isArray(validatedArgs.filepaths);
9287
+ log(`Getting context for: ${filepaths.join(", ")}`);
9288
+ await checkAndReconnect();
9289
+ const workspaceRoot = process.cwd().replace(/\\/g, "/");
9290
+ const handlerCtx = {
9291
+ vectorDB,
9292
+ embeddings,
9293
+ log,
9294
+ workspaceRoot
9295
+ };
9296
+ const fileChunksMap = await searchFileChunks(filepaths, handlerCtx);
9297
+ let relatedChunksMap = [];
9298
+ if (validatedArgs.includeRelated !== false) {
9299
+ relatedChunksMap = await findRelatedChunks(filepaths, fileChunksMap, handlerCtx);
9300
+ }
9301
+ const allChunks = await vectorDB.scanWithFilter({ limit: SCAN_LIMIT });
9302
+ const hitScanLimit = allChunks.length === SCAN_LIMIT;
9303
+ if (hitScanLimit) {
9304
+ log(
9305
+ `Scanned ${SCAN_LIMIT} chunks (limit reached). Test associations may be incomplete for large codebases.`,
9306
+ "warning"
9375
9307
  );
9376
- log(`Found ${totalChunks} total chunks`);
9377
- const note = buildScanLimitNote(hitScanLimit);
9378
- const indexInfo = getIndexMetadata();
9379
- return isSingleFile ? buildSingleFileResponse(filepaths[0], filesData, indexInfo, note) : buildMultiFileResponse(filesData, indexInfo, note);
9380
9308
  }
9381
- )(args);
9309
+ const testAssociationsMap = findTestAssociations(filepaths, allChunks, handlerCtx);
9310
+ const filesData = buildFilesData(
9311
+ filepaths,
9312
+ fileChunksMap,
9313
+ relatedChunksMap,
9314
+ testAssociationsMap
9315
+ );
9316
+ const totalChunks = Object.values(filesData).reduce((sum, f) => sum + f.chunks.length, 0);
9317
+ log(`Found ${totalChunks} total chunks`);
9318
+ const note = buildScanLimitNote(hitScanLimit);
9319
+ const indexInfo = getIndexMetadata();
9320
+ return isSingleFile ? buildSingleFileResponse(filepaths[0], filesData, indexInfo, note) : buildMultiFileResponse(filesData, indexInfo, note);
9321
+ })(args);
9382
9322
  }
9383
9323
 
9384
9324
  // src/mcp/handlers/list-functions.ts
9325
+ import { safeRegex } from "@liendev/core";
9385
9326
  async function performContentScan(vectorDB, args, fetchLimit, log) {
9386
9327
  log("Falling back to content scan...");
9387
9328
  let results = await vectorDB.scanWithFilter({
@@ -9390,11 +9331,15 @@ async function performContentScan(vectorDB, args, fetchLimit, log) {
9390
9331
  limit: fetchLimit
9391
9332
  });
9392
9333
  if (args.pattern) {
9393
- const regex = new RegExp(args.pattern, "i");
9394
- results = results.filter((r) => {
9395
- const symbolName = r.metadata?.symbolName;
9396
- return symbolName && regex.test(symbolName);
9397
- });
9334
+ const regex = safeRegex(args.pattern);
9335
+ if (regex) {
9336
+ results = results.filter((r) => {
9337
+ const symbolName = r.metadata?.symbolName;
9338
+ return symbolName && regex.test(symbolName);
9339
+ });
9340
+ } else {
9341
+ results = results.filter((r) => !!r.metadata?.symbolName);
9342
+ }
9398
9343
  }
9399
9344
  return {
9400
9345
  results,
@@ -9431,45 +9376,50 @@ function paginateResults(results, offset, limit) {
9431
9376
  }
9432
9377
  async function handleListFunctions(args, ctx) {
9433
9378
  const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
9434
- return await wrapToolHandler(
9435
- ListFunctionsSchema,
9436
- async (validatedArgs) => {
9437
- log("Listing functions with symbol metadata...");
9438
- await checkAndReconnect();
9439
- const limit = validatedArgs.limit ?? 50;
9440
- const offset = validatedArgs.offset ?? 0;
9441
- const fetchLimit = limit + offset + 1;
9442
- const queryResult = await queryWithFallback(vectorDB, validatedArgs, fetchLimit, log);
9443
- const { paginatedResults, hasMore, nextOffset } = paginateResults(queryResult.results, offset, limit);
9444
- log(`Found ${paginatedResults.length} matches using ${queryResult.method} method`);
9445
- const notes = [];
9446
- if (queryResult.results.length === 0) {
9447
- notes.push('0 results. Try a broader regex pattern (e.g. ".*") or omit the symbolType filter. Use semantic_search for behavior-based queries.');
9448
- } else if (paginatedResults.length === 0 && offset > 0) {
9449
- notes.push("No results for this page. The offset is beyond the available results; try reducing or resetting the offset to 0.");
9450
- }
9451
- if (queryResult.method === "content") {
9452
- notes.push('Using content search. Run "lien reindex" to enable faster symbol-based queries.');
9453
- }
9454
- return {
9455
- indexInfo: getIndexMetadata(),
9456
- method: queryResult.method,
9457
- hasMore,
9458
- ...nextOffset !== void 0 ? { nextOffset } : {},
9459
- results: shapeResults(paginatedResults, "list_functions"),
9460
- ...notes.length > 0 && { note: notes.join(" ") }
9461
- };
9379
+ return await wrapToolHandler(ListFunctionsSchema, async (validatedArgs) => {
9380
+ log("Listing functions with symbol metadata...");
9381
+ await checkAndReconnect();
9382
+ const limit = validatedArgs.limit ?? 50;
9383
+ const offset = validatedArgs.offset ?? 0;
9384
+ const fetchLimit = limit + offset + 1;
9385
+ const queryResult = await queryWithFallback(vectorDB, validatedArgs, fetchLimit, log);
9386
+ const { paginatedResults, hasMore, nextOffset } = paginateResults(
9387
+ queryResult.results,
9388
+ offset,
9389
+ limit
9390
+ );
9391
+ log(`Found ${paginatedResults.length} matches using ${queryResult.method} method`);
9392
+ const notes = [];
9393
+ if (queryResult.results.length === 0) {
9394
+ notes.push(
9395
+ '0 results. Try a broader regex pattern (e.g. ".*") or omit the symbolType filter. Use semantic_search for behavior-based queries.'
9396
+ );
9397
+ } else if (paginatedResults.length === 0 && offset > 0) {
9398
+ notes.push(
9399
+ "No results for this page. The offset is beyond the available results; try reducing or resetting the offset to 0."
9400
+ );
9401
+ }
9402
+ if (queryResult.method === "content") {
9403
+ notes.push('Using content search. Run "lien reindex" to enable faster symbol-based queries.');
9462
9404
  }
9463
- )(args);
9405
+ return {
9406
+ indexInfo: getIndexMetadata(),
9407
+ method: queryResult.method,
9408
+ hasMore,
9409
+ ...nextOffset !== void 0 ? { nextOffset } : {},
9410
+ results: shapeResults(paginatedResults, "list_functions"),
9411
+ ...notes.length > 0 && { note: notes.join(" ") }
9412
+ };
9413
+ })(args);
9464
9414
  }
9465
9415
 
9466
- // src/mcp/handlers/get-dependents.ts
9467
- import { QdrantDB as QdrantDB3 } from "@liendev/core";
9468
-
9469
9416
  // src/mcp/handlers/dependency-analyzer.ts
9470
- import { QdrantDB as QdrantDB2 } from "@liendev/core";
9471
9417
  import {
9472
- findTransitiveDependents
9418
+ findTransitiveDependents,
9419
+ normalizePath as normalizePath2,
9420
+ matchesFile as matchesFile2,
9421
+ getCanonicalPath as getCanonicalPath2,
9422
+ isTestFile as isTestFile2
9473
9423
  } from "@liendev/core";
9474
9424
  var COMPLEXITY_THRESHOLDS = {
9475
9425
  HIGH_COMPLEXITY_DEPENDENT: 10,
@@ -9487,11 +9437,12 @@ var COMPLEXITY_THRESHOLDS = {
9487
9437
  MEDIUM_MAX: 15
9488
9438
  // Occasional branching
9489
9439
  };
9440
+ var scanCache = null;
9490
9441
  function collectNamedSymbolsFromChunk(chunk, normalizedTarget, normalizePathCached, symbols) {
9491
9442
  const importedSymbols = chunk.metadata.importedSymbols;
9492
9443
  if (!importedSymbols || typeof importedSymbols !== "object") return;
9493
9444
  for (const [importPath, syms] of Object.entries(importedSymbols)) {
9494
- if (matchesFile(normalizePathCached(importPath), normalizedTarget)) {
9445
+ if (matchesFile2(normalizePathCached(importPath), normalizedTarget)) {
9495
9446
  for (const sym of syms) symbols.add(sym);
9496
9447
  }
9497
9448
  }
@@ -9499,7 +9450,7 @@ function collectNamedSymbolsFromChunk(chunk, normalizedTarget, normalizePathCach
9499
9450
  function collectRawImportSentinel(chunk, normalizedTarget, normalizePathCached, symbols) {
9500
9451
  const imports = chunk.metadata.imports || [];
9501
9452
  for (const imp of imports) {
9502
- if (matchesFile(normalizePathCached(imp), normalizedTarget)) symbols.add("*");
9453
+ if (matchesFile2(normalizePathCached(imp), normalizedTarget)) symbols.add("*");
9503
9454
  }
9504
9455
  }
9505
9456
  function collectSymbolsFromChunk(chunk, normalizedTarget, normalizePathCached, symbols) {
@@ -9534,8 +9485,12 @@ function findReExportedSymbols(importsFromTarget, allExports) {
9534
9485
  function buildReExportGraph(allChunksByFile, normalizedTarget, normalizePathCached) {
9535
9486
  const reExporters = [];
9536
9487
  for (const [filepath, chunks] of allChunksByFile.entries()) {
9537
- if (matchesFile(filepath, normalizedTarget)) continue;
9538
- const importsFromTarget = collectImportedSymbolsFromTarget(chunks, normalizedTarget, normalizePathCached);
9488
+ if (matchesFile2(filepath, normalizedTarget)) continue;
9489
+ const importsFromTarget = collectImportedSymbolsFromTarget(
9490
+ chunks,
9491
+ normalizedTarget,
9492
+ normalizePathCached
9493
+ );
9539
9494
  const allExports = collectExportsFromChunks(chunks);
9540
9495
  if (importsFromTarget.size === 0 || allExports.size === 0) continue;
9541
9496
  const reExportedSymbols = findReExportedSymbols(importsFromTarget, allExports);
@@ -9551,7 +9506,7 @@ function fileImportsSymbolFromAny(chunks, targetSymbol, targetPaths, normalizePa
9551
9506
  if (!importedSymbols) return false;
9552
9507
  for (const [importPath, symbols] of Object.entries(importedSymbols)) {
9553
9508
  const normalizedImport = normalizePathCached(importPath);
9554
- const matchesAny = targetPaths.some((tp) => matchesFile(normalizedImport, tp));
9509
+ const matchesAny = targetPaths.some((tp) => matchesFile2(normalizedImport, tp));
9555
9510
  if (matchesAny) {
9556
9511
  if (symbols.includes(targetSymbol)) return true;
9557
9512
  if (symbols.some((s) => s.startsWith("* as "))) return true;
@@ -9580,39 +9535,51 @@ function addChunkToImportIndex(chunk, normalizePathCached, importIndex) {
9580
9535
  }
9581
9536
  }
9582
9537
  }
9583
- function addChunkToFileMap(chunk, normalizePathCached, fileMap) {
9538
+ function addChunkToFileMap(chunk, normalizePathCached, fileMap, seenRanges) {
9584
9539
  const canonical = normalizePathCached(chunk.metadata.file);
9585
9540
  if (!fileMap.has(canonical)) {
9586
9541
  fileMap.set(canonical, []);
9542
+ seenRanges.set(canonical, /* @__PURE__ */ new Set());
9587
9543
  }
9544
+ const rangeKey = `${chunk.metadata.startLine}-${chunk.metadata.endLine}`;
9545
+ const seen = seenRanges.get(canonical);
9546
+ if (seen.has(rangeKey)) return;
9547
+ seen.add(rangeKey);
9588
9548
  fileMap.get(canonical).push(chunk);
9589
9549
  }
9590
9550
  async function scanChunksPaginated(vectorDB, crossRepo, log, normalizePathCached) {
9591
9551
  const importIndex = /* @__PURE__ */ new Map();
9592
9552
  const allChunksByFile = /* @__PURE__ */ new Map();
9553
+ const seenRanges = /* @__PURE__ */ new Map();
9593
9554
  let totalChunks = 0;
9594
- if (crossRepo && vectorDB instanceof QdrantDB2) {
9555
+ if (crossRepo && vectorDB.supportsCrossRepo) {
9595
9556
  const CROSS_REPO_LIMIT = 1e5;
9596
9557
  const allChunks = await vectorDB.scanCrossRepo({ limit: CROSS_REPO_LIMIT });
9597
9558
  totalChunks = allChunks.length;
9598
9559
  const hitLimit = totalChunks >= CROSS_REPO_LIMIT;
9599
9560
  if (hitLimit) {
9600
- log(`Warning: cross-repo scan hit ${CROSS_REPO_LIMIT} chunk limit. Results may be incomplete.`, "warning");
9561
+ log(
9562
+ `Warning: cross-repo scan hit ${CROSS_REPO_LIMIT} chunk limit. Results may be incomplete.`,
9563
+ "warning"
9564
+ );
9601
9565
  }
9602
9566
  for (const chunk of allChunks) {
9603
9567
  addChunkToImportIndex(chunk, normalizePathCached, importIndex);
9604
- addChunkToFileMap(chunk, normalizePathCached, allChunksByFile);
9568
+ addChunkToFileMap(chunk, normalizePathCached, allChunksByFile, seenRanges);
9605
9569
  }
9606
9570
  return { importIndex, allChunksByFile, totalChunks, hitLimit };
9607
9571
  }
9608
9572
  if (crossRepo) {
9609
- log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo paginated scan.", "warning");
9573
+ log(
9574
+ "Warning: crossRepo=true requires a cross-repo-capable backend. Falling back to single-repo paginated scan.",
9575
+ "warning"
9576
+ );
9610
9577
  }
9611
9578
  for await (const page of vectorDB.scanPaginated({ pageSize: 1e3 })) {
9612
9579
  totalChunks += page.length;
9613
9580
  for (const chunk of page) {
9614
9581
  addChunkToImportIndex(chunk, normalizePathCached, importIndex);
9615
- addChunkToFileMap(chunk, normalizePathCached, allChunksByFile);
9582
+ addChunkToFileMap(chunk, normalizePathCached, allChunksByFile, seenRanges);
9616
9583
  }
9617
9584
  }
9618
9585
  return { importIndex, allChunksByFile, totalChunks, hitLimit: false };
@@ -9622,7 +9589,7 @@ function createPathNormalizer() {
9622
9589
  const cache = /* @__PURE__ */ new Map();
9623
9590
  return (path7) => {
9624
9591
  if (!cache.has(path7)) {
9625
- cache.set(path7, normalizePath(path7, workspaceRoot));
9592
+ cache.set(path7, normalizePath2(path7, workspaceRoot));
9626
9593
  }
9627
9594
  return cache.get(path7);
9628
9595
  };
@@ -9631,7 +9598,7 @@ function groupChunksByFile(chunks) {
9631
9598
  const workspaceRoot = process.cwd().replace(/\\/g, "/");
9632
9599
  const chunksByFile = /* @__PURE__ */ new Map();
9633
9600
  for (const chunk of chunks) {
9634
- const canonical = getCanonicalPath(chunk.metadata.file, workspaceRoot);
9601
+ const canonical = getCanonicalPath2(chunk.metadata.file, workspaceRoot);
9635
9602
  const existing = chunksByFile.get(canonical) || [];
9636
9603
  existing.push(chunk);
9637
9604
  chunksByFile.set(canonical, existing);
@@ -9641,18 +9608,22 @@ function groupChunksByFile(chunks) {
9641
9608
  function buildDependentsList(chunksByFile, symbol, normalizedTarget, normalizePathCached, targetFileChunks, filepath, log, reExporterPaths = []) {
9642
9609
  if (symbol) {
9643
9610
  validateSymbolExport(targetFileChunks, symbol, filepath, log);
9644
- return findSymbolUsages(chunksByFile, symbol, normalizedTarget, normalizePathCached, reExporterPaths);
9611
+ return findSymbolUsages(
9612
+ chunksByFile,
9613
+ symbol,
9614
+ normalizedTarget,
9615
+ normalizePathCached,
9616
+ reExporterPaths
9617
+ );
9645
9618
  }
9646
9619
  const dependents = Array.from(chunksByFile.keys()).map((fp) => ({
9647
9620
  filepath: fp,
9648
- isTestFile: isTestFile(fp)
9621
+ isTestFile: isTestFile2(fp)
9649
9622
  }));
9650
9623
  return { dependents, totalUsageCount: void 0 };
9651
9624
  }
9652
9625
  function validateSymbolExport(targetFileChunks, symbol, filepath, log) {
9653
- const exportsSymbol = targetFileChunks.some(
9654
- (chunk) => chunk.metadata.exports?.includes(symbol)
9655
- );
9626
+ const exportsSymbol = targetFileChunks.some((chunk) => chunk.metadata.exports?.includes(symbol));
9656
9627
  if (!exportsSymbol) {
9657
9628
  log(`Warning: Symbol "${symbol}" not found in exports of ${filepath}`, "warning");
9658
9629
  }
@@ -9683,22 +9654,53 @@ function mergeTransitiveDependents(reExporters, importIndex, normalizedTarget, n
9683
9654
  log(`Found ${transitiveByFile.size} additional dependents via re-export chains`);
9684
9655
  }
9685
9656
  }
9686
- async function findDependents(vectorDB, filepath, crossRepo, log, symbol) {
9657
+ async function getOrScanChunks(vectorDB, crossRepo, log, normalizePathCached, indexVersion) {
9658
+ if (indexVersion !== void 0 && scanCache !== null && scanCache.indexVersion === indexVersion && scanCache.crossRepo === crossRepo) {
9659
+ log(`Using cached import index (${scanCache.totalChunks} chunks, version ${indexVersion})`);
9660
+ return scanCache;
9661
+ }
9662
+ const scanResult = await scanChunksPaginated(vectorDB, crossRepo, log, normalizePathCached);
9663
+ if (indexVersion !== void 0) {
9664
+ scanCache = { indexVersion, crossRepo, ...scanResult };
9665
+ }
9666
+ log(`Scanned ${scanResult.totalChunks} chunks for imports...`);
9667
+ return scanResult;
9668
+ }
9669
+ function resolveTransitiveDependents(allChunksByFile, normalizedTarget, normalizePathCached, importIndex, chunksByFile, log) {
9670
+ const reExporters = buildReExportGraph(allChunksByFile, normalizedTarget, normalizePathCached);
9671
+ if (reExporters.length > 0) {
9672
+ mergeTransitiveDependents(
9673
+ reExporters,
9674
+ importIndex,
9675
+ normalizedTarget,
9676
+ normalizePathCached,
9677
+ allChunksByFile,
9678
+ chunksByFile,
9679
+ log
9680
+ );
9681
+ }
9682
+ return reExporters;
9683
+ }
9684
+ async function findDependents(vectorDB, filepath, crossRepo, log, symbol, indexVersion) {
9687
9685
  const normalizePathCached = createPathNormalizer();
9688
9686
  const normalizedTarget = normalizePathCached(filepath);
9689
- const { importIndex, allChunksByFile, totalChunks, hitLimit } = await scanChunksPaginated(
9687
+ const { importIndex, allChunksByFile, hitLimit } = await getOrScanChunks(
9690
9688
  vectorDB,
9691
9689
  crossRepo,
9692
9690
  log,
9693
- normalizePathCached
9691
+ normalizePathCached,
9692
+ indexVersion
9694
9693
  );
9695
- log(`Scanned ${totalChunks} chunks for imports...`);
9696
9694
  const dependentChunks = findDependentChunks(importIndex, normalizedTarget);
9697
9695
  const chunksByFile = groupChunksByFile(dependentChunks);
9698
- const reExporters = buildReExportGraph(allChunksByFile, normalizedTarget, normalizePathCached);
9699
- if (reExporters.length > 0) {
9700
- mergeTransitiveDependents(reExporters, importIndex, normalizedTarget, normalizePathCached, allChunksByFile, chunksByFile, log);
9701
- }
9696
+ const reExporters = resolveTransitiveDependents(
9697
+ allChunksByFile,
9698
+ normalizedTarget,
9699
+ normalizePathCached,
9700
+ importIndex,
9701
+ chunksByFile,
9702
+ log
9703
+ );
9702
9704
  const fileComplexities = calculateFileComplexities(chunksByFile);
9703
9705
  const complexityMetrics = calculateOverallComplexityMetrics(fileComplexities);
9704
9706
  const targetFileChunks = symbol ? allChunksByFile.get(normalizedTarget) ?? [] : [];
@@ -9748,7 +9750,7 @@ function findDependentChunks(importIndex, normalizedTarget) {
9748
9750
  }
9749
9751
  }
9750
9752
  for (const [normalizedImport, chunks] of importIndex.entries()) {
9751
- if (normalizedImport !== normalizedTarget && matchesFile(normalizedImport, normalizedTarget)) {
9753
+ if (normalizedImport !== normalizedTarget && matchesFile2(normalizedImport, normalizedTarget)) {
9752
9754
  for (const chunk of chunks) {
9753
9755
  addChunk(chunk);
9754
9756
  }
@@ -9787,7 +9789,11 @@ function calculateOverallComplexityMetrics(fileComplexities) {
9787
9789
  const allMaxes = fileComplexities.map((f) => f.maxComplexity);
9788
9790
  const totalAvg = allAvgs.reduce((a, b) => a + b, 0) / allAvgs.length;
9789
9791
  const globalMax = Math.max(...allMaxes);
9790
- const highComplexityDependents = fileComplexities.filter((f) => f.maxComplexity > COMPLEXITY_THRESHOLDS.HIGH_COMPLEXITY_DEPENDENT).sort((a, b) => b.maxComplexity - a.maxComplexity).slice(0, 5).map((f) => ({ filepath: f.filepath, maxComplexity: f.maxComplexity, avgComplexity: f.avgComplexity }));
9792
+ const highComplexityDependents = fileComplexities.filter((f) => f.maxComplexity > COMPLEXITY_THRESHOLDS.HIGH_COMPLEXITY_DEPENDENT).sort((a, b) => b.maxComplexity - a.maxComplexity).slice(0, 5).map((f) => ({
9793
+ filepath: f.filepath,
9794
+ maxComplexity: f.maxComplexity,
9795
+ avgComplexity: f.avgComplexity
9796
+ }));
9791
9797
  const complexityRiskBoost = calculateComplexityRiskBoost(totalAvg, globalMax);
9792
9798
  return {
9793
9799
  averageComplexity: Math.round(totalAvg * 10) / 10,
@@ -9860,7 +9866,7 @@ function findSymbolUsages(chunksByFile, targetSymbol, normalizedTarget, normaliz
9860
9866
  const usages = extractSymbolUsagesFromChunks(chunks, targetSymbol);
9861
9867
  dependents.push({
9862
9868
  filepath,
9863
- isTestFile: isTestFile(filepath),
9869
+ isTestFile: isTestFile2(filepath),
9864
9870
  usages: usages.length > 0 ? usages : void 0
9865
9871
  });
9866
9872
  totalUsageCount += usages.length;
@@ -9913,12 +9919,14 @@ function extractSnippet(lines, callLine, startLine, symbolName) {
9913
9919
 
9914
9920
  // src/mcp/handlers/get-dependents.ts
9915
9921
  function checkCrossRepoFallback(crossRepo, vectorDB) {
9916
- return Boolean(crossRepo && !(vectorDB instanceof QdrantDB3));
9922
+ return Boolean(crossRepo && !vectorDB.supportsCrossRepo);
9917
9923
  }
9918
9924
  function buildNotes(crossRepoFallback, hitLimit) {
9919
9925
  const notes = [];
9920
9926
  if (crossRepoFallback) {
9921
- notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
9927
+ notes.push(
9928
+ "Cross-repo search requires a cross-repo-capable backend. Fell back to single-repo search."
9929
+ );
9922
9930
  }
9923
9931
  if (hitLimit) {
9924
9932
  notes.push("Scanned 10,000 chunks (limit reached). Results may be incomplete.");
@@ -9938,9 +9946,7 @@ function logRiskAssessment(analysis, riskLevel, symbol, log) {
9938
9946
  );
9939
9947
  }
9940
9948
  } else {
9941
- log(
9942
- `Found ${analysis.dependents.length} dependents ${prodTest} - risk: ${riskLevel}`
9943
- );
9949
+ log(`Found ${analysis.dependents.length} dependents ${prodTest} - risk: ${riskLevel}`);
9944
9950
  }
9945
9951
  }
9946
9952
  function buildDependentsResponse(analysis, args, riskLevel, indexInfo, notes, crossRepo, vectorDB) {
@@ -9964,46 +9970,51 @@ function buildDependentsResponse(analysis, args, riskLevel, indexInfo, notes, cr
9964
9970
  if (notes.length > 0) {
9965
9971
  response.note = notes.join(" ");
9966
9972
  }
9967
- if (crossRepo && vectorDB instanceof QdrantDB3) {
9973
+ if (crossRepo && vectorDB.supportsCrossRepo) {
9968
9974
  response.groupedByRepo = groupDependentsByRepo(analysis.dependents, analysis.allChunks);
9969
9975
  }
9970
9976
  return response;
9971
9977
  }
9972
9978
  async function handleGetDependents(args, ctx) {
9973
9979
  const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
9974
- return await wrapToolHandler(
9975
- GetDependentsSchema,
9976
- async (validatedArgs) => {
9977
- const { crossRepo, filepath, symbol } = validatedArgs;
9978
- const symbolSuffix = symbol ? ` (symbol: ${symbol})` : "";
9979
- const crossRepoSuffix = crossRepo ? " (cross-repo)" : "";
9980
- log(`Finding dependents of: ${filepath}${symbolSuffix}${crossRepoSuffix}`);
9981
- await checkAndReconnect();
9982
- const analysis = await findDependents(vectorDB, filepath, crossRepo ?? false, log, symbol);
9983
- const riskLevel = calculateRiskLevel(
9984
- analysis.dependents.length,
9985
- analysis.complexityMetrics.complexityRiskBoost,
9986
- analysis.productionDependentCount
9987
- );
9988
- logRiskAssessment(analysis, riskLevel, symbol, log);
9989
- const crossRepoFallback = checkCrossRepoFallback(crossRepo, vectorDB);
9990
- const notes = buildNotes(crossRepoFallback, analysis.hitLimit);
9991
- return buildDependentsResponse(
9992
- analysis,
9993
- validatedArgs,
9994
- riskLevel,
9995
- getIndexMetadata(),
9996
- notes,
9997
- crossRepo,
9998
- vectorDB
9999
- );
10000
- }
10001
- )(args);
9980
+ return await wrapToolHandler(GetDependentsSchema, async (validatedArgs) => {
9981
+ const { crossRepo, filepath, symbol } = validatedArgs;
9982
+ const symbolSuffix = symbol ? ` (symbol: ${symbol})` : "";
9983
+ const crossRepoSuffix = crossRepo ? " (cross-repo)" : "";
9984
+ log(`Finding dependents of: ${filepath}${symbolSuffix}${crossRepoSuffix}`);
9985
+ await checkAndReconnect();
9986
+ const indexInfo = getIndexMetadata();
9987
+ const analysis = await findDependents(
9988
+ vectorDB,
9989
+ filepath,
9990
+ crossRepo ?? false,
9991
+ log,
9992
+ symbol,
9993
+ indexInfo.indexVersion
9994
+ );
9995
+ const riskLevel = calculateRiskLevel(
9996
+ analysis.dependents.length,
9997
+ analysis.complexityMetrics.complexityRiskBoost,
9998
+ analysis.productionDependentCount
9999
+ );
10000
+ logRiskAssessment(analysis, riskLevel, symbol, log);
10001
+ const crossRepoFallback = checkCrossRepoFallback(crossRepo, vectorDB);
10002
+ const notes = buildNotes(crossRepoFallback, analysis.hitLimit);
10003
+ return buildDependentsResponse(
10004
+ analysis,
10005
+ validatedArgs,
10006
+ riskLevel,
10007
+ indexInfo,
10008
+ notes,
10009
+ crossRepo,
10010
+ vectorDB
10011
+ );
10012
+ })(args);
10002
10013
  }
10003
10014
 
10004
10015
  // src/mcp/handlers/get-complexity.ts
10005
10016
  var import_collect = __toESM(require_dist(), 1);
10006
- import { ComplexityAnalyzer, QdrantDB as QdrantDB4 } from "@liendev/core";
10017
+ import { ComplexityAnalyzer } from "@liendev/core";
10007
10018
  function transformViolation(v, fileData) {
10008
10019
  return {
10009
10020
  filepath: v.filepath,
@@ -10042,7 +10053,7 @@ async function fetchCrossRepoChunks(vectorDB, crossRepo, repoIds, log) {
10042
10053
  if (!crossRepo) {
10043
10054
  return { chunks: [], fallback: false };
10044
10055
  }
10045
- if (vectorDB instanceof QdrantDB4) {
10056
+ if (vectorDB.supportsCrossRepo) {
10046
10057
  const chunks = await vectorDB.scanCrossRepo({ limit: 1e5, repoIds });
10047
10058
  log(`Scanned ${chunks.length} chunks across repos`);
10048
10059
  return { chunks, fallback: false };
@@ -10051,7 +10062,11 @@ async function fetchCrossRepoChunks(vectorDB, crossRepo, repoIds, log) {
10051
10062
  }
10052
10063
  function processViolations(report, threshold, top) {
10053
10064
  const allViolations = (0, import_collect.default)(Object.entries(report.files)).flatMap(
10054
- ([, fileData]) => fileData.violations.map((v) => transformViolation(v, fileData))
10065
+ ([
10066
+ ,
10067
+ /* filepath unused */
10068
+ fileData
10069
+ ]) => fileData.violations.map((v) => transformViolation(v, fileData))
10055
10070
  ).sortByDesc("complexity").all();
10056
10071
  const violations = threshold !== void 0 ? allViolations.filter((v) => v.complexity >= threshold) : allViolations;
10057
10072
  const severityCounts = (0, import_collect.default)(violations).countBy("severity").all();
@@ -10065,61 +10080,61 @@ function processViolations(report, threshold, top) {
10065
10080
  };
10066
10081
  }
10067
10082
  function buildCrossRepoFallbackNote(fallback) {
10068
- return fallback ? "Cross-repo analysis requires Qdrant backend. Fell back to single-repo analysis." : void 0;
10083
+ return fallback ? "Cross-repo analysis requires a cross-repo-capable backend. Fell back to single-repo analysis." : void 0;
10069
10084
  }
10070
10085
  async function handleGetComplexity(args, ctx) {
10071
10086
  const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
10072
- return await wrapToolHandler(
10073
- GetComplexitySchema,
10074
- async (validatedArgs) => {
10075
- const { crossRepo, repoIds, files, top, threshold } = validatedArgs;
10076
- log(`Analyzing complexity${crossRepo ? " (cross-repo)" : ""}...`);
10077
- await checkAndReconnect();
10078
- const { chunks: allChunks, fallback } = await fetchCrossRepoChunks(
10079
- vectorDB,
10080
- crossRepo,
10081
- repoIds,
10082
- log
10083
- );
10084
- const analyzer = new ComplexityAnalyzer(vectorDB);
10085
- const report = await analyzer.analyze(files, crossRepo && !fallback, repoIds);
10086
- log(`Analyzed ${report.summary.filesAnalyzed} files`);
10087
- const { violations, topViolations, bySeverity } = processViolations(
10088
- report,
10089
- threshold,
10090
- top ?? 10
10087
+ return await wrapToolHandler(GetComplexitySchema, async (validatedArgs) => {
10088
+ const { crossRepo, repoIds, files, top, threshold } = validatedArgs;
10089
+ log(`Analyzing complexity${crossRepo ? " (cross-repo)" : ""}...`);
10090
+ await checkAndReconnect();
10091
+ const { chunks: allChunks, fallback } = await fetchCrossRepoChunks(
10092
+ vectorDB,
10093
+ crossRepo,
10094
+ repoIds,
10095
+ log
10096
+ );
10097
+ const analyzer = new ComplexityAnalyzer(vectorDB);
10098
+ const report = await analyzer.analyze(files, crossRepo && !fallback, repoIds);
10099
+ log(`Analyzed ${report.summary.filesAnalyzed} files`);
10100
+ const { violations, topViolations, bySeverity } = processViolations(
10101
+ report,
10102
+ threshold,
10103
+ top ?? 10
10104
+ );
10105
+ const note = buildCrossRepoFallbackNote(fallback);
10106
+ if (note) {
10107
+ log(
10108
+ "Warning: crossRepo=true requires a cross-repo-capable backend. Falling back to single-repo analysis.",
10109
+ "warning"
10091
10110
  );
10092
- const note = buildCrossRepoFallbackNote(fallback);
10093
- if (note) {
10094
- log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo analysis.", "warning");
10095
- }
10096
- return {
10097
- indexInfo: getIndexMetadata(),
10098
- summary: {
10099
- filesAnalyzed: report.summary.filesAnalyzed,
10100
- avgComplexity: report.summary.avgComplexity,
10101
- maxComplexity: report.summary.maxComplexity,
10102
- violationCount: violations.length,
10103
- bySeverity
10104
- },
10105
- violations: topViolations,
10106
- ...crossRepo && !fallback && allChunks.length > 0 && {
10107
- groupedByRepo: groupViolationsByRepo(topViolations, allChunks)
10108
- },
10109
- ...note && { note }
10110
- };
10111
10111
  }
10112
- )(args);
10112
+ return {
10113
+ indexInfo: getIndexMetadata(),
10114
+ summary: {
10115
+ filesAnalyzed: report.summary.filesAnalyzed,
10116
+ avgComplexity: report.summary.avgComplexity,
10117
+ maxComplexity: report.summary.maxComplexity,
10118
+ violationCount: violations.length,
10119
+ bySeverity
10120
+ },
10121
+ violations: topViolations,
10122
+ ...crossRepo && !fallback && allChunks.length > 0 && {
10123
+ groupedByRepo: groupViolationsByRepo(topViolations, allChunks)
10124
+ },
10125
+ ...note && { note }
10126
+ };
10127
+ })(args);
10113
10128
  }
10114
10129
 
10115
10130
  // src/mcp/handlers/index.ts
10116
10131
  var toolHandlers = {
10117
- "semantic_search": handleSemanticSearch,
10118
- "find_similar": handleFindSimilar,
10119
- "get_files_context": handleGetFilesContext,
10120
- "list_functions": handleListFunctions,
10121
- "get_dependents": handleGetDependents,
10122
- "get_complexity": handleGetComplexity
10132
+ semantic_search: handleSemanticSearch,
10133
+ find_similar: handleFindSimilar,
10134
+ get_files_context: handleGetFilesContext,
10135
+ list_functions: handleListFunctions,
10136
+ get_dependents: handleGetDependents,
10137
+ get_complexity: handleGetComplexity
10123
10138
  };
10124
10139
 
10125
10140
  // src/mcp/server-config.ts
@@ -10207,7 +10222,7 @@ function mergePendingFiles(pendingFiles, newFiles) {
10207
10222
  }
10208
10223
  }
10209
10224
  function createReindexStateManager() {
10210
- let state = {
10225
+ const state = {
10211
10226
  inProgress: false,
10212
10227
  pendingFiles: [],
10213
10228
  lastReindexTimestamp: null,
@@ -10231,12 +10246,12 @@ function createReindexStateManager() {
10231
10246
  },
10232
10247
  /**
10233
10248
  * Start a new reindex operation.
10234
- *
10249
+ *
10235
10250
  * **Important**: Silently ignores empty or null file arrays without incrementing
10236
10251
  * activeOperations. This is intentional - if there's no work to do, no operation
10237
10252
  * is started. Callers should check for empty arrays before calling if they need
10238
10253
  * to track "attempted" operations.
10239
- *
10254
+ *
10240
10255
  * @param files - Array of file paths to reindex. Empty/null arrays are ignored.
10241
10256
  */
10242
10257
  startReindex: (files) => {
@@ -10250,10 +10265,10 @@ function createReindexStateManager() {
10250
10265
  },
10251
10266
  /**
10252
10267
  * Mark a reindex operation as complete.
10253
- *
10268
+ *
10254
10269
  * Logs a warning if called without a matching startReindex.
10255
10270
  * Only clears state when all concurrent operations finish.
10256
- *
10271
+ *
10257
10272
  * @param durationMs - Duration of the reindex operation in milliseconds
10258
10273
  */
10259
10274
  completeReindex: (durationMs) => {
@@ -10272,7 +10287,7 @@ function createReindexStateManager() {
10272
10287
  },
10273
10288
  /**
10274
10289
  * Mark a reindex operation as failed.
10275
- *
10290
+ *
10276
10291
  * Logs a warning if called without a matching startReindex.
10277
10292
  * Only clears state when all concurrent operations finish/fail.
10278
10293
  */
@@ -10290,13 +10305,13 @@ function createReindexStateManager() {
10290
10305
  },
10291
10306
  /**
10292
10307
  * Manually reset state if it's stuck.
10293
- *
10308
+ *
10294
10309
  * **WARNING**: Only use this if you're certain operations have crashed without cleanup.
10295
10310
  * This will forcibly clear the inProgress flag and reset activeOperations counter.
10296
- *
10311
+ *
10297
10312
  * Use this when getState() health check detects stuck state and you've verified
10298
10313
  * no legitimate operations are running.
10299
- *
10314
+ *
10300
10315
  * @returns true if state was reset, false if state was already clean
10301
10316
  */
10302
10317
  resetIfStuck: () => {
@@ -10333,7 +10348,7 @@ import {
10333
10348
  indexSingleFile,
10334
10349
  ManifestManager,
10335
10350
  computeContentHash,
10336
- normalizeToRelativePath,
10351
+ normalizeToRelativePath as normalizeToRelativePath2,
10337
10352
  createGitignoreFilter
10338
10353
  } from "@liendev/core";
10339
10354
  async function handleFileDeletion(filepath, vectorDB, log) {
@@ -10368,7 +10383,7 @@ async function handleBatchDeletions(deletedFiles, vectorDB, log) {
10368
10383
  }
10369
10384
  async function canSkipReindex(filepath, rootDir, vectorDB, log) {
10370
10385
  const manifest = new ManifestManager(vectorDB.dbPath);
10371
- const normalizedPath = normalizeToRelativePath(filepath, rootDir);
10386
+ const normalizedPath = normalizeToRelativePath2(filepath, rootDir);
10372
10387
  const manifestData = await manifest.load();
10373
10388
  const existingEntry = manifestData?.files[normalizedPath];
10374
10389
  const { shouldReindex, newMtime } = await shouldReindexFile(filepath, existingEntry, log);
@@ -10425,7 +10440,7 @@ async function shouldReindexFile(filepath, existingEntry, log) {
10425
10440
  async function checkFilesAgainstManifest(files, rootDir, manifestFiles, log) {
10426
10441
  const results = [];
10427
10442
  for (const filepath of files) {
10428
- const normalizedPath = normalizeToRelativePath(filepath, rootDir);
10443
+ const normalizedPath = normalizeToRelativePath2(filepath, rootDir);
10429
10444
  const existingEntry = manifestFiles[normalizedPath];
10430
10445
  const { shouldReindex, newMtime } = await shouldReindexFile(filepath, existingEntry, log);
10431
10446
  results.push({ filepath, normalizedPath, shouldReindex, newMtime });
@@ -10452,7 +10467,12 @@ async function filterModifiedFilesByHash(modifiedFiles, rootDir, vectorDB, log)
10452
10467
  const manifest = new ManifestManager(vectorDB.dbPath);
10453
10468
  const manifestData = await manifest.load();
10454
10469
  if (!manifestData) return modifiedFiles;
10455
- const checkResults = await checkFilesAgainstManifest(modifiedFiles, rootDir, manifestData.files, log);
10470
+ const checkResults = await checkFilesAgainstManifest(
10471
+ modifiedFiles,
10472
+ rootDir,
10473
+ manifestData.files,
10474
+ log
10475
+ );
10456
10476
  await updateUnchangedMtimes(manifest, checkResults);
10457
10477
  return checkResults.filter((r) => r.shouldReindex).map((r) => r.filepath);
10458
10478
  }
@@ -10474,7 +10494,9 @@ async function executeReindexOperations(filesToIndex, deletedFiles, rootDir, vec
10474
10494
  const operations = [];
10475
10495
  if (filesToIndex.length > 0) {
10476
10496
  log(`\u{1F4C1} ${filesToIndex.length} file(s) changed, reindexing...`);
10477
- operations.push(indexMultipleFiles(filesToIndex, vectorDB, embeddings, { verbose: false, rootDir }));
10497
+ operations.push(
10498
+ indexMultipleFiles(filesToIndex, vectorDB, embeddings, { verbose: false, rootDir })
10499
+ );
10478
10500
  }
10479
10501
  if (deletedFiles.length > 0) {
10480
10502
  operations.push(handleBatchDeletions(deletedFiles, vectorDB, log));
@@ -10482,7 +10504,12 @@ async function executeReindexOperations(filesToIndex, deletedFiles, rootDir, vec
10482
10504
  await Promise.all(operations);
10483
10505
  }
10484
10506
  async function handleBatchEvent(event, rootDir, vectorDB, embeddings, log, reindexStateManager) {
10485
- const { filesToIndex, deletedFiles } = await prepareFilesForReindexing(event, rootDir, vectorDB, log);
10507
+ const { filesToIndex, deletedFiles } = await prepareFilesForReindexing(
10508
+ event,
10509
+ rootDir,
10510
+ vectorDB,
10511
+ log
10512
+ );
10486
10513
  const allFiles = [...filesToIndex, ...deletedFiles];
10487
10514
  if (allFiles.length === 0) {
10488
10515
  return;
@@ -10493,7 +10520,9 @@ async function handleBatchEvent(event, rootDir, vectorDB, embeddings, log, reind
10493
10520
  await executeReindexOperations(filesToIndex, deletedFiles, rootDir, vectorDB, embeddings, log);
10494
10521
  const duration = Date.now() - startTime;
10495
10522
  reindexStateManager.completeReindex(duration);
10496
- log(`\u2713 Processed ${filesToIndex.length} file(s) + ${deletedFiles.length} deletion(s) in ${duration}ms`);
10523
+ log(
10524
+ `\u2713 Processed ${filesToIndex.length} file(s) + ${deletedFiles.length} deletion(s) in ${duration}ms`
10525
+ );
10497
10526
  } catch (error) {
10498
10527
  reindexStateManager.failReindex();
10499
10528
  log(`Batch reindex failed: ${error}`, "warning");
@@ -10512,7 +10541,7 @@ async function handleUnlinkEvent(filepath, vectorDB, log, reindexStateManager) {
10512
10541
  }
10513
10542
  }
10514
10543
  function isFileIgnored(filepath, rootDir, isIgnored) {
10515
- return isIgnored(normalizeToRelativePath(filepath, rootDir));
10544
+ return isIgnored(normalizeToRelativePath2(filepath, rootDir));
10516
10545
  }
10517
10546
  function filterFileChangeEvent(event, ignoreFilter, rootDir) {
10518
10547
  return {
@@ -10555,7 +10584,15 @@ function createFileChangeHandler(rootDir, vectorDB, embeddings, log, reindexStat
10555
10584
  } else {
10556
10585
  if (isFileIgnored(event.filepath, rootDir, ignoreFilter)) return;
10557
10586
  await checkAndReconnect();
10558
- await handleSingleFileChange(event.filepath, type, rootDir, vectorDB, embeddings, log, reindexStateManager);
10587
+ await handleSingleFileChange(
10588
+ event.filepath,
10589
+ type,
10590
+ rootDir,
10591
+ vectorDB,
10592
+ embeddings,
10593
+ log,
10594
+ reindexStateManager
10595
+ );
10559
10596
  }
10560
10597
  };
10561
10598
  }
@@ -10576,7 +10613,9 @@ async function handleGitStartup(rootDir, gitTracker, vectorDB, embeddings, log,
10576
10613
  log(`\u{1F33F} Git changes detected: ${filteredFiles.length} files changed`);
10577
10614
  try {
10578
10615
  await checkAndReconnect();
10579
- const count = await indexMultipleFiles2(filteredFiles, vectorDB, embeddings, { verbose: false });
10616
+ const count = await indexMultipleFiles2(filteredFiles, vectorDB, embeddings, {
10617
+ verbose: false
10618
+ });
10580
10619
  const duration = Date.now() - startTime;
10581
10620
  reindexStateManager.completeReindex(duration);
10582
10621
  log(`\u2713 Reindexed ${count} files in ${duration}ms`);
@@ -10618,7 +10657,9 @@ function createGitPollInterval(rootDir, gitTracker, vectorDB, embeddings, log, r
10618
10657
  log(`\u{1F33F} Git change detected: ${filteredFiles.length} files changed`);
10619
10658
  try {
10620
10659
  await checkAndReconnect();
10621
- const count = await indexMultipleFiles2(filteredFiles, vectorDB, embeddings, { verbose: false });
10660
+ const count = await indexMultipleFiles2(filteredFiles, vectorDB, embeddings, {
10661
+ verbose: false
10662
+ });
10622
10663
  const duration = Date.now() - startTime;
10623
10664
  reindexStateManager.completeReindex(duration);
10624
10665
  log(`\u2713 Background reindex complete: ${count} files in ${duration}ms`);
@@ -10684,7 +10725,13 @@ function createGitChangeHandler(rootDir, gitTracker, vectorDB, embeddings, log,
10684
10725
  let lastGitReindexTime = 0;
10685
10726
  const GIT_REINDEX_COOLDOWN_MS = 5e3;
10686
10727
  return async () => {
10687
- if (shouldSkipGitReindex(gitReindexInProgress, lastGitReindexTime, GIT_REINDEX_COOLDOWN_MS, reindexStateManager, log)) {
10728
+ if (shouldSkipGitReindex(
10729
+ gitReindexInProgress,
10730
+ lastGitReindexTime,
10731
+ GIT_REINDEX_COOLDOWN_MS,
10732
+ reindexStateManager,
10733
+ log
10734
+ )) {
10688
10735
  return;
10689
10736
  }
10690
10737
  gitReindexInProgress = true;
@@ -10699,7 +10746,14 @@ function createGitChangeHandler(rootDir, gitTracker, vectorDB, embeddings, log,
10699
10746
  log
10700
10747
  );
10701
10748
  if (!filteredFiles) return;
10702
- await executeGitReindex(filteredFiles, vectorDB, embeddings, reindexStateManager, checkAndReconnect, log);
10749
+ await executeGitReindex(
10750
+ filteredFiles,
10751
+ vectorDB,
10752
+ embeddings,
10753
+ reindexStateManager,
10754
+ checkAndReconnect,
10755
+ log
10756
+ );
10703
10757
  lastGitReindexTime = Date.now();
10704
10758
  } catch (error) {
10705
10759
  log(`Git change handler failed: ${error}`, "warning");
@@ -10722,7 +10776,15 @@ async function setupGitDetection(rootDir, vectorDB, embeddings, log, reindexStat
10722
10776
  log("\u2713 Detected git repository");
10723
10777
  const gitTracker = new GitStateTracker(rootDir, vectorDB.dbPath);
10724
10778
  try {
10725
- await handleGitStartup(rootDir, gitTracker, vectorDB, embeddings, log, reindexStateManager, checkAndReconnect);
10779
+ await handleGitStartup(
10780
+ rootDir,
10781
+ gitTracker,
10782
+ vectorDB,
10783
+ embeddings,
10784
+ log,
10785
+ reindexStateManager,
10786
+ checkAndReconnect
10787
+ );
10726
10788
  } catch (error) {
10727
10789
  log(`Failed to check git state on startup: ${error}`, "warning");
10728
10790
  }
@@ -10742,7 +10804,15 @@ async function setupGitDetection(rootDir, vectorDB, embeddings, log, reindexStat
10742
10804
  }
10743
10805
  const pollIntervalSeconds = DEFAULT_GIT_POLL_INTERVAL_MS2 / 1e3;
10744
10806
  log(`\u2713 Git detection enabled (polling fallback every ${pollIntervalSeconds}s)`);
10745
- const gitPollInterval = createGitPollInterval(rootDir, gitTracker, vectorDB, embeddings, log, reindexStateManager, checkAndReconnect);
10807
+ const gitPollInterval = createGitPollInterval(
10808
+ rootDir,
10809
+ gitTracker,
10810
+ vectorDB,
10811
+ embeddings,
10812
+ log,
10813
+ reindexStateManager,
10814
+ checkAndReconnect
10815
+ );
10746
10816
  return { gitTracker, gitPollInterval };
10747
10817
  }
10748
10818
  async function filterGitChangedFiles(changedFiles, rootDir, ignoreFilter) {
@@ -10765,7 +10835,10 @@ async function filterGitChangedFiles(changedFiles, rootDir, ignoreFilter) {
10765
10835
 
10766
10836
  // src/mcp/cleanup.ts
10767
10837
  function setupCleanupHandlers(server, versionCheckInterval, gitPollInterval, fileWatcher, log) {
10838
+ let cleaningUp = false;
10768
10839
  return async () => {
10840
+ if (cleaningUp) return;
10841
+ cleaningUp = true;
10769
10842
  try {
10770
10843
  log("Shutting down MCP server...");
10771
10844
  await server.close();
@@ -10796,7 +10869,9 @@ async function initializeDatabase(rootDir, log) {
10796
10869
  throw new Error("createVectorDB returned undefined or null");
10797
10870
  }
10798
10871
  if (typeof vectorDB.initialize !== "function") {
10799
- throw new Error(`Invalid vectorDB instance: ${vectorDB.constructor?.name || "unknown"}. Expected VectorDBInterface but got: ${JSON.stringify(Object.keys(vectorDB))}`);
10872
+ throw new Error(
10873
+ `Invalid vectorDB instance: ${vectorDB.constructor?.name || "unknown"}. Expected VectorDBInterface but got: ${JSON.stringify(Object.keys(vectorDB))}`
10874
+ );
10800
10875
  }
10801
10876
  log("Loading embedding model...");
10802
10877
  await embeddings.initialize();
@@ -10828,7 +10903,14 @@ async function setupFileWatching(watch, rootDir, vectorDB, embeddings, log, rein
10828
10903
  log("\u{1F440} Starting file watcher...");
10829
10904
  const fileWatcher = new FileWatcher(rootDir);
10830
10905
  try {
10831
- const handler = createFileChangeHandler(rootDir, vectorDB, embeddings, log, reindexStateManager, checkAndReconnect);
10906
+ const handler = createFileChangeHandler(
10907
+ rootDir,
10908
+ vectorDB,
10909
+ embeddings,
10910
+ log,
10911
+ reindexStateManager,
10912
+ checkAndReconnect
10913
+ );
10832
10914
  await fileWatcher.start(handler);
10833
10915
  log(`\u2713 File watching enabled (watching ${fileWatcher.getWatchedFiles().length} files)`);
10834
10916
  return fileWatcher;
@@ -10915,9 +10997,31 @@ async function setupAndConnectServer(server, toolContext, log, versionCheckInter
10915
10997
  const { vectorDB, embeddings } = toolContext;
10916
10998
  registerMCPHandlers(server, toolContext, log);
10917
10999
  await handleAutoIndexing(vectorDB, rootDir, log);
10918
- const fileWatcher = await setupFileWatching(watch, rootDir, vectorDB, embeddings, log, reindexStateManager, toolContext.checkAndReconnect);
10919
- const { gitPollInterval } = await setupGitDetection(rootDir, vectorDB, embeddings, log, reindexStateManager, fileWatcher, toolContext.checkAndReconnect);
10920
- const cleanup = setupCleanupHandlers(server, versionCheckInterval, gitPollInterval, fileWatcher, log);
11000
+ const fileWatcher = await setupFileWatching(
11001
+ watch,
11002
+ rootDir,
11003
+ vectorDB,
11004
+ embeddings,
11005
+ log,
11006
+ reindexStateManager,
11007
+ toolContext.checkAndReconnect
11008
+ );
11009
+ const { gitPollInterval } = await setupGitDetection(
11010
+ rootDir,
11011
+ vectorDB,
11012
+ embeddings,
11013
+ log,
11014
+ reindexStateManager,
11015
+ fileWatcher,
11016
+ toolContext.checkAndReconnect
11017
+ );
11018
+ const cleanup = setupCleanupHandlers(
11019
+ server,
11020
+ versionCheckInterval,
11021
+ gitPollInterval,
11022
+ fileWatcher,
11023
+ log
11024
+ );
10921
11025
  process.on("SIGINT", cleanup);
10922
11026
  process.on("SIGTERM", cleanup);
10923
11027
  const transport = setupTransport(log);
@@ -10940,7 +11044,11 @@ async function startMCPServer(options) {
10940
11044
  const server = createMCPServer();
10941
11045
  const log = createMCPLog(server, verbose);
10942
11046
  const reindexStateManager = createReindexStateManager();
10943
- const { interval: versionCheckInterval, checkAndReconnect, getIndexMetadata } = setupVersionChecking(vectorDB, log, reindexStateManager);
11047
+ const {
11048
+ interval: versionCheckInterval,
11049
+ checkAndReconnect,
11050
+ getIndexMetadata
11051
+ } = setupVersionChecking(vectorDB, log, reindexStateManager);
10944
11052
  const toolContext = {
10945
11053
  vectorDB,
10946
11054
  embeddings,
@@ -10950,10 +11058,14 @@ async function startMCPServer(options) {
10950
11058
  getIndexMetadata,
10951
11059
  getReindexState: () => reindexStateManager.getState()
10952
11060
  };
10953
- await setupAndConnectServer(server, toolContext, log, versionCheckInterval, reindexStateManager, { rootDir, watch });
11061
+ await setupAndConnectServer(server, toolContext, log, versionCheckInterval, reindexStateManager, {
11062
+ rootDir,
11063
+ watch
11064
+ });
10954
11065
  }
10955
11066
 
10956
11067
  // src/cli/serve.ts
11068
+ init_banner();
10957
11069
  async function serveCommand(options) {
10958
11070
  const rootDir = options.root ? path4.resolve(options.root) : process.cwd();
10959
11071
  try {
@@ -10961,30 +11073,30 @@ async function serveCommand(options) {
10961
11073
  try {
10962
11074
  const stats = await fs5.stat(rootDir);
10963
11075
  if (!stats.isDirectory()) {
10964
- console.error(chalk5.red(`Error: --root path is not a directory: ${rootDir}`));
11076
+ console.error(chalk6.red(`Error: --root path is not a directory: ${rootDir}`));
10965
11077
  process.exit(1);
10966
11078
  }
10967
11079
  } catch (error) {
10968
11080
  if (error.code === "ENOENT") {
10969
- console.error(chalk5.red(`Error: --root directory does not exist: ${rootDir}`));
11081
+ console.error(chalk6.red(`Error: --root directory does not exist: ${rootDir}`));
10970
11082
  } else if (error.code === "EACCES") {
10971
- console.error(chalk5.red(`Error: --root directory is not accessible: ${rootDir}`));
11083
+ console.error(chalk6.red(`Error: --root directory is not accessible: ${rootDir}`));
10972
11084
  } else {
10973
- console.error(chalk5.red(`Error: Failed to access --root directory: ${rootDir}`));
10974
- console.error(chalk5.dim(error.message));
11085
+ console.error(chalk6.red(`Error: Failed to access --root directory: ${rootDir}`));
11086
+ console.error(chalk6.dim(error.message));
10975
11087
  }
10976
11088
  process.exit(1);
10977
11089
  }
10978
11090
  }
10979
11091
  showBanner();
10980
- console.error(chalk5.bold("Starting MCP server...\n"));
11092
+ console.error(chalk6.bold("Starting MCP server...\n"));
10981
11093
  if (options.root) {
10982
- console.error(chalk5.dim(`Serving from: ${rootDir}
11094
+ console.error(chalk6.dim(`Serving from: ${rootDir}
10983
11095
  `));
10984
11096
  }
10985
11097
  if (options.watch) {
10986
- console.error(chalk5.yellow("\u26A0\uFE0F --watch flag is deprecated (file watching is now default)"));
10987
- console.error(chalk5.dim(" Use --no-watch to disable file watching\n"));
11098
+ console.error(chalk6.yellow("\u26A0\uFE0F --watch flag is deprecated (file watching is now default)"));
11099
+ console.error(chalk6.dim(" Use --no-watch to disable file watching\n"));
10988
11100
  }
10989
11101
  const watch = options.noWatch ? false : options.watch ? true : void 0;
10990
11102
  await startMCPServer({
@@ -10993,13 +11105,13 @@ async function serveCommand(options) {
10993
11105
  watch
10994
11106
  });
10995
11107
  } catch (error) {
10996
- console.error(chalk5.red("Failed to start MCP server:"), error);
11108
+ console.error(chalk6.red("Failed to start MCP server:"), error);
10997
11109
  process.exit(1);
10998
11110
  }
10999
11111
  }
11000
11112
 
11001
11113
  // src/cli/complexity.ts
11002
- import chalk6 from "chalk";
11114
+ import chalk7 from "chalk";
11003
11115
  import fs6 from "fs";
11004
11116
  import path5 from "path";
11005
11117
  import { VectorDB } from "@liendev/core";
@@ -11009,13 +11121,17 @@ var VALID_FAIL_ON = ["error", "warning"];
11009
11121
  var VALID_FORMATS = ["text", "json", "sarif"];
11010
11122
  function validateFailOn(failOn) {
11011
11123
  if (failOn && !VALID_FAIL_ON.includes(failOn)) {
11012
- console.error(chalk6.red(`Error: Invalid --fail-on value "${failOn}". Must be either 'error' or 'warning'`));
11124
+ console.error(
11125
+ chalk7.red(`Error: Invalid --fail-on value "${failOn}". Must be either 'error' or 'warning'`)
11126
+ );
11013
11127
  process.exit(1);
11014
11128
  }
11015
11129
  }
11016
11130
  function validateFormat(format) {
11017
11131
  if (!VALID_FORMATS.includes(format)) {
11018
- console.error(chalk6.red(`Error: Invalid --format value "${format}". Must be one of: text, json, sarif`));
11132
+ console.error(
11133
+ chalk7.red(`Error: Invalid --format value "${format}". Must be one of: text, json, sarif`)
11134
+ );
11019
11135
  process.exit(1);
11020
11136
  }
11021
11137
  }
@@ -11026,8 +11142,8 @@ function validateFilesExist(files, rootDir) {
11026
11142
  return !fs6.existsSync(fullPath);
11027
11143
  });
11028
11144
  if (missingFiles.length > 0) {
11029
- console.error(chalk6.red(`Error: File${missingFiles.length > 1 ? "s" : ""} not found:`));
11030
- missingFiles.forEach((file) => console.error(chalk6.red(` - ${file}`)));
11145
+ console.error(chalk7.red(`Error: File${missingFiles.length > 1 ? "s" : ""} not found:`));
11146
+ missingFiles.forEach((file) => console.error(chalk7.red(` - ${file}`)));
11031
11147
  process.exit(1);
11032
11148
  }
11033
11149
  }
@@ -11035,8 +11151,12 @@ async function ensureIndexExists(vectorDB) {
11035
11151
  try {
11036
11152
  await vectorDB.scanWithFilter({ limit: 1 });
11037
11153
  } catch {
11038
- console.error(chalk6.red("Error: Index not found"));
11039
- console.log(chalk6.yellow("\nRun"), chalk6.bold("lien index"), chalk6.yellow("to index your codebase first"));
11154
+ console.error(chalk7.red("Error: Index not found"));
11155
+ console.log(
11156
+ chalk7.yellow("\nRun"),
11157
+ chalk7.bold("lien index"),
11158
+ chalk7.yellow("to index your codebase first")
11159
+ );
11040
11160
  process.exit(1);
11041
11161
  }
11042
11162
  }
@@ -11046,10 +11166,6 @@ async function complexityCommand(options) {
11046
11166
  validateFailOn(options.failOn);
11047
11167
  validateFormat(options.format);
11048
11168
  validateFilesExist(options.files, rootDir);
11049
- if (options.threshold || options.cyclomaticThreshold || options.cognitiveThreshold) {
11050
- console.warn(chalk6.yellow("Warning: Threshold overrides via CLI flags are not supported."));
11051
- console.warn(chalk6.yellow("Use the MCP tool with threshold parameter for custom thresholds."));
11052
- }
11053
11169
  const vectorDB = new VectorDB(rootDir);
11054
11170
  await vectorDB.initialize();
11055
11171
  await ensureIndexExists(vectorDB);
@@ -11061,22 +11177,19 @@ async function complexityCommand(options) {
11061
11177
  if (hasViolations) process.exit(1);
11062
11178
  }
11063
11179
  } catch (error) {
11064
- console.error(chalk6.red("Error analyzing complexity:"), error);
11180
+ console.error(chalk7.red("Error analyzing complexity:"), error);
11065
11181
  process.exit(1);
11066
11182
  }
11067
11183
  }
11068
11184
 
11069
11185
  // src/cli/config.ts
11070
- import chalk7 from "chalk";
11186
+ import chalk8 from "chalk";
11071
11187
  import path6 from "path";
11072
11188
  import os2 from "os";
11073
- import {
11074
- loadGlobalConfig,
11075
- mergeGlobalConfig
11076
- } from "@liendev/core";
11189
+ import { loadGlobalConfig, mergeGlobalConfig } from "@liendev/core";
11077
11190
  var CONFIG_PATH = path6.join(os2.homedir(), ".lien", "config.json");
11078
11191
  var ALLOWED_KEYS = {
11079
- "backend": {
11192
+ backend: {
11080
11193
  values: ["lancedb", "qdrant"],
11081
11194
  description: "Vector database backend"
11082
11195
  },
@@ -11113,50 +11226,50 @@ function buildPartialConfig(key, value) {
11113
11226
  async function configSetCommand(key, value) {
11114
11227
  const allowed = ALLOWED_KEYS[key];
11115
11228
  if (!allowed) {
11116
- console.error(chalk7.red(`Unknown config key: "${key}"`));
11117
- console.log(chalk7.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11229
+ console.error(chalk8.red(`Unknown config key: "${key}"`));
11230
+ console.log(chalk8.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11118
11231
  process.exit(1);
11119
11232
  }
11120
11233
  if (allowed.values.length > 0 && !allowed.values.includes(value)) {
11121
- console.error(chalk7.red(`Invalid value "${value}" for ${key}`));
11122
- console.log(chalk7.dim("Valid values:"), allowed.values.join(", "));
11234
+ console.error(chalk8.red(`Invalid value "${value}" for ${key}`));
11235
+ console.log(chalk8.dim("Valid values:"), allowed.values.join(", "));
11123
11236
  process.exit(1);
11124
11237
  }
11125
11238
  if (key === "qdrant.apiKey") {
11126
11239
  const existing = await loadGlobalConfig();
11127
11240
  if (!existing.qdrant?.url) {
11128
- console.error(chalk7.red("Set qdrant.url first before setting qdrant.apiKey"));
11241
+ console.error(chalk8.red("Set qdrant.url first before setting qdrant.apiKey"));
11129
11242
  process.exit(1);
11130
11243
  }
11131
11244
  }
11132
11245
  const partial = buildPartialConfig(key, value);
11133
11246
  await mergeGlobalConfig(partial);
11134
- console.log(chalk7.green(`Set ${key} = ${value}`));
11135
- console.log(chalk7.dim(`Config: ${CONFIG_PATH}`));
11247
+ console.log(chalk8.green(`Set ${key} = ${value}`));
11248
+ console.log(chalk8.dim(`Config: ${CONFIG_PATH}`));
11136
11249
  }
11137
11250
  async function configGetCommand(key) {
11138
11251
  if (!ALLOWED_KEYS[key]) {
11139
- console.error(chalk7.red(`Unknown config key: "${key}"`));
11140
- console.log(chalk7.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11252
+ console.error(chalk8.red(`Unknown config key: "${key}"`));
11253
+ console.log(chalk8.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11141
11254
  process.exit(1);
11142
11255
  }
11143
11256
  const config = await loadGlobalConfig();
11144
11257
  const value = getConfigValue(config, key);
11145
11258
  if (value === void 0) {
11146
- console.log(chalk7.dim(`${key}: (not set)`));
11259
+ console.log(chalk8.dim(`${key}: (not set)`));
11147
11260
  } else {
11148
11261
  console.log(`${key}: ${value}`);
11149
11262
  }
11150
11263
  }
11151
11264
  async function configListCommand() {
11152
11265
  const config = await loadGlobalConfig();
11153
- console.log(chalk7.bold("Global Configuration"));
11154
- console.log(chalk7.dim(`File: ${CONFIG_PATH}
11266
+ console.log(chalk8.bold("Global Configuration"));
11267
+ console.log(chalk8.dim(`File: ${CONFIG_PATH}
11155
11268
  `));
11156
11269
  for (const [key, meta] of Object.entries(ALLOWED_KEYS)) {
11157
11270
  const value = getConfigValue(config, key);
11158
- const display = value ?? chalk7.dim("(not set)");
11159
- console.log(` ${chalk7.cyan(key)}: ${display} ${chalk7.dim(`\u2014 ${meta.description}`)}`);
11271
+ const display = value ?? chalk8.dim("(not set)");
11272
+ console.log(` ${chalk8.cyan(key)}: ${display} ${chalk8.dim(`\u2014 ${meta.description}`)}`);
11160
11273
  }
11161
11274
  }
11162
11275
 
@@ -11173,14 +11286,18 @@ try {
11173
11286
  var program = new Command();
11174
11287
  program.name("lien").description("Local semantic code search for AI assistants via MCP").version(packageJson3.version);
11175
11288
  program.command("init").description("Initialize Lien in the current directory").option("-u, --upgrade", "Upgrade existing config with new options").option("-y, --yes", "Skip interactive prompts and use defaults").option("-p, --path <path>", "Path to initialize (defaults to current directory)").action(initCommand);
11176
- program.command("index").description("Index the codebase for semantic search").option("-f, --force", "Force full reindex (skip incremental)").option("-w, --watch", "Watch for changes and re-index automatically").option("-v, --verbose", "Show detailed logging during indexing").action(indexCommand);
11177
- program.command("serve").description("Start the MCP server for Cursor integration").option("-p, --port <port>", "Port number (for future use)", "7133").option("--no-watch", "Disable file watching for this session").option("-w, --watch", "[DEPRECATED] File watching is now enabled by default").option("-r, --root <path>", "Root directory to serve (defaults to current directory)").action(serveCommand);
11289
+ program.command("index").description("Index the codebase for semantic search").option("-f, --force", "Force full reindex (skip incremental)").option("-v, --verbose", "Show detailed logging during indexing").action(indexCommand);
11290
+ program.command("serve").description(
11291
+ "Start the MCP server (works with Cursor, Claude Code, Windsurf, and any MCP client)"
11292
+ ).option("-p, --port <port>", "Port number (for future use)", "7133").option("--no-watch", "Disable file watching for this session").option("-w, --watch", "[DEPRECATED] File watching is now enabled by default").option("-r, --root <path>", "Root directory to serve (defaults to current directory)").action(serveCommand);
11178
11293
  program.command("status").description("Show indexing status and statistics").action(statusCommand);
11179
- program.command("complexity").description("Analyze code complexity").option("--files <paths...>", "Specific files to analyze").option("--format <type>", "Output format: text, json, sarif", "text").option("--threshold <n>", "Override both complexity thresholds (cyclomatic & cognitive)").option("--cyclomatic-threshold <n>", "Override cyclomatic complexity threshold only").option("--cognitive-threshold <n>", "Override cognitive complexity threshold only").option("--fail-on <severity>", "Exit 1 if violations: error, warning").action(complexityCommand);
11294
+ program.command("complexity").description("Analyze code complexity").option("--files <paths...>", "Specific files to analyze").option("--format <type>", "Output format: text, json, sarif", "text").option("--fail-on <severity>", "Exit 1 if violations: error, warning").action(complexityCommand);
11180
11295
  var configCmd = program.command("config").description("Manage global configuration (~/.lien/config.json)");
11181
11296
  configCmd.command("set <key> <value>").description("Set a global config value").action(configSetCommand);
11182
11297
  configCmd.command("get <key>").description("Get a config value").action(configGetCommand);
11183
11298
  configCmd.command("list").description("Show all current config").action(configListCommand);
11299
+ program.addHelpText("beforeAll", `Quick start: run 'lien serve' in your project directory
11300
+ `);
11184
11301
 
11185
11302
  // src/index.ts
11186
11303
  program.parse();