@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/README.md +2 -2
- package/dist/index.js +750 -633
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
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
|
|
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(
|
|
3690
|
-
console.log(chalk2.yellow("\
|
|
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
|
|
3716
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
3758
|
+
} catch {
|
|
3740
3759
|
console.log(chalk3.dim("Index status:"), chalk3.yellow("\u2717 Not indexed"));
|
|
3741
|
-
console.log(
|
|
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
|
-
|
|
3779
|
-
import
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
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 =
|
|
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(
|
|
3972
|
-
console.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(
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
8821
|
-
|
|
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
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
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
|
-
|
|
8851
|
-
|
|
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
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
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([
|
|
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
|
|
9044
|
+
if (crossRepo && vectorDB.supportsCrossRepo) {
|
|
8992
9045
|
const results2 = await vectorDB.searchCrossRepo(queryEmbedding, limit, { repoIds });
|
|
8993
|
-
log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
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 {
|
|
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
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
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
|
-
|
|
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 =
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
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
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
9538
|
-
const importsFromTarget = collectImportedSymbolsFromTarget(
|
|
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) =>
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
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,
|
|
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 =
|
|
9699
|
-
|
|
9700
|
-
|
|
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 &&
|
|
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) => ({
|
|
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:
|
|
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 && !
|
|
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(
|
|
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
|
|
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
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
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
|
|
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
|
|
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
|
-
([
|
|
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
|
|
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
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, {
|
|
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, {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
10919
|
-
|
|
10920
|
-
|
|
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 {
|
|
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, {
|
|
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(
|
|
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(
|
|
11081
|
+
console.error(chalk6.red(`Error: --root directory does not exist: ${rootDir}`));
|
|
10970
11082
|
} else if (error.code === "EACCES") {
|
|
10971
|
-
console.error(
|
|
11083
|
+
console.error(chalk6.red(`Error: --root directory is not accessible: ${rootDir}`));
|
|
10972
11084
|
} else {
|
|
10973
|
-
console.error(
|
|
10974
|
-
console.error(
|
|
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(
|
|
11092
|
+
console.error(chalk6.bold("Starting MCP server...\n"));
|
|
10981
11093
|
if (options.root) {
|
|
10982
|
-
console.error(
|
|
11094
|
+
console.error(chalk6.dim(`Serving from: ${rootDir}
|
|
10983
11095
|
`));
|
|
10984
11096
|
}
|
|
10985
11097
|
if (options.watch) {
|
|
10986
|
-
console.error(
|
|
10987
|
-
console.error(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
11030
|
-
missingFiles.forEach((file) => console.error(
|
|
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(
|
|
11039
|
-
console.log(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
11117
|
-
console.log(
|
|
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(
|
|
11122
|
-
console.log(
|
|
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(
|
|
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(
|
|
11135
|
-
console.log(
|
|
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(
|
|
11140
|
-
console.log(
|
|
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(
|
|
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(
|
|
11154
|
-
console.log(
|
|
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 ??
|
|
11159
|
-
console.log(` ${
|
|
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("-
|
|
11177
|
-
program.command("serve").description(
|
|
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("--
|
|
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();
|