@liendev/lien 0.20.0 → 0.21.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 +30 -1
- package/dist/index.js +661 -598
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3590,7 +3590,7 @@ var require_dist = __commonJS({
|
|
|
3590
3590
|
|
|
3591
3591
|
// src/cli/index.ts
|
|
3592
3592
|
import { Command } from "commander";
|
|
3593
|
-
import { createRequire as
|
|
3593
|
+
import { createRequire as createRequire3 } from "module";
|
|
3594
3594
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3595
3595
|
import { dirname as dirname3, join as join3 } from "path";
|
|
3596
3596
|
|
|
@@ -3598,15 +3598,8 @@ import { dirname as dirname3, join as join3 } from "path";
|
|
|
3598
3598
|
import fs from "fs/promises";
|
|
3599
3599
|
import path from "path";
|
|
3600
3600
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3601
|
-
import { createRequire as createRequire2 } from "module";
|
|
3602
3601
|
import chalk2 from "chalk";
|
|
3603
3602
|
import inquirer from "inquirer";
|
|
3604
|
-
import {
|
|
3605
|
-
defaultConfig,
|
|
3606
|
-
MigrationManager,
|
|
3607
|
-
detectAllFrameworks,
|
|
3608
|
-
getFrameworkDetector
|
|
3609
|
-
} from "@liendev/core";
|
|
3610
3603
|
|
|
3611
3604
|
// src/utils/banner.ts
|
|
3612
3605
|
import figlet from "figlet";
|
|
@@ -3670,151 +3663,39 @@ function showCompactBanner() {
|
|
|
3670
3663
|
// src/cli/init.ts
|
|
3671
3664
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
3672
3665
|
var __dirname2 = path.dirname(__filename2);
|
|
3673
|
-
var require3 = createRequire2(import.meta.url);
|
|
3674
|
-
var CLI_VERSION;
|
|
3675
|
-
try {
|
|
3676
|
-
CLI_VERSION = require3("../package.json").version;
|
|
3677
|
-
} catch {
|
|
3678
|
-
CLI_VERSION = require3("../../package.json").version;
|
|
3679
|
-
}
|
|
3680
3666
|
async function initCommand(options = {}) {
|
|
3667
|
+
showCompactBanner();
|
|
3668
|
+
console.log(chalk2.bold("\nLien Initialization\n"));
|
|
3669
|
+
console.log(chalk2.green("\u2713 No per-project configuration needed!"));
|
|
3670
|
+
console.log(chalk2.dim("\nLien now uses:"));
|
|
3671
|
+
console.log(chalk2.dim(" \u2022 Auto-detected frameworks"));
|
|
3672
|
+
console.log(chalk2.dim(" \u2022 Sensible defaults for all settings"));
|
|
3673
|
+
console.log(chalk2.dim(" \u2022 Global config (optional) at ~/.lien/config.json"));
|
|
3674
|
+
console.log(chalk2.bold("\nNext steps:"));
|
|
3675
|
+
console.log(chalk2.dim(" 1. Run"), chalk2.bold("lien index"), chalk2.dim("to index your codebase"));
|
|
3676
|
+
console.log(chalk2.dim(" 2. Run"), chalk2.bold("lien serve"), chalk2.dim("to start the MCP server"));
|
|
3677
|
+
console.log(chalk2.bold("\nGlobal Configuration (optional):"));
|
|
3678
|
+
console.log(chalk2.dim(" To use Qdrant backend, create ~/.lien/config.json:"));
|
|
3679
|
+
console.log(chalk2.dim(" {"));
|
|
3680
|
+
console.log(chalk2.dim(' "backend": "qdrant",'));
|
|
3681
|
+
console.log(chalk2.dim(' "qdrant": {'));
|
|
3682
|
+
console.log(chalk2.dim(' "url": "http://localhost:6333",'));
|
|
3683
|
+
console.log(chalk2.dim(' "apiKey": "optional-api-key"'));
|
|
3684
|
+
console.log(chalk2.dim(" }"));
|
|
3685
|
+
console.log(chalk2.dim(" }"));
|
|
3686
|
+
console.log(chalk2.dim("\n Or use environment variables:"));
|
|
3687
|
+
console.log(chalk2.dim(" LIEN_BACKEND=qdrant"));
|
|
3688
|
+
console.log(chalk2.dim(" LIEN_QDRANT_URL=http://localhost:6333"));
|
|
3689
|
+
console.log(chalk2.dim(" LIEN_QDRANT_API_KEY=your-key"));
|
|
3681
3690
|
const rootDir = options.path || process.cwd();
|
|
3682
3691
|
const configPath = path.join(rootDir, ".lien.config.json");
|
|
3683
3692
|
try {
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
} catch {
|
|
3689
|
-
}
|
|
3690
|
-
if (configExists && options.upgrade) {
|
|
3691
|
-
const migrationManager = new MigrationManager(rootDir, CLI_VERSION);
|
|
3692
|
-
await migrationManager.upgradeInteractive();
|
|
3693
|
-
return;
|
|
3694
|
-
}
|
|
3695
|
-
if (configExists && !options.upgrade) {
|
|
3696
|
-
console.log(chalk2.yellow("\u26A0\uFE0F .lien.config.json already exists"));
|
|
3697
|
-
console.log(chalk2.dim("Run"), chalk2.bold("lien init --upgrade"), chalk2.dim("to merge new config options"));
|
|
3698
|
-
return;
|
|
3699
|
-
}
|
|
3700
|
-
if (!configExists) {
|
|
3701
|
-
await createNewConfig(rootDir, options);
|
|
3702
|
-
}
|
|
3703
|
-
} catch (error) {
|
|
3704
|
-
console.error(chalk2.red("Error creating config file:"), error);
|
|
3705
|
-
process.exit(1);
|
|
3706
|
-
}
|
|
3707
|
-
}
|
|
3708
|
-
function createGenericFramework() {
|
|
3709
|
-
return {
|
|
3710
|
-
name: "generic",
|
|
3711
|
-
path: ".",
|
|
3712
|
-
enabled: true,
|
|
3713
|
-
config: {
|
|
3714
|
-
include: ["**/*.{ts,tsx,js,jsx,py,php,go,rs,java,c,cpp,cs}"],
|
|
3715
|
-
exclude: [
|
|
3716
|
-
"**/node_modules/**",
|
|
3717
|
-
"**/dist/**",
|
|
3718
|
-
"**/build/**",
|
|
3719
|
-
"**/.git/**",
|
|
3720
|
-
"**/coverage/**",
|
|
3721
|
-
"**/.next/**",
|
|
3722
|
-
"**/.nuxt/**",
|
|
3723
|
-
"**/vendor/**"
|
|
3724
|
-
]
|
|
3725
|
-
}
|
|
3726
|
-
};
|
|
3727
|
-
}
|
|
3728
|
-
async function handleNoFrameworksDetected(options) {
|
|
3729
|
-
console.log(chalk2.yellow("\n\u26A0\uFE0F No frameworks detected"));
|
|
3730
|
-
if (!options.yes) {
|
|
3731
|
-
const { useGeneric } = await inquirer.prompt([{
|
|
3732
|
-
type: "confirm",
|
|
3733
|
-
name: "useGeneric",
|
|
3734
|
-
message: "Create a generic config (index all supported file types)?",
|
|
3735
|
-
default: true
|
|
3736
|
-
}]);
|
|
3737
|
-
if (!useGeneric) {
|
|
3738
|
-
console.log(chalk2.dim("Aborted."));
|
|
3739
|
-
return null;
|
|
3740
|
-
}
|
|
3741
|
-
} else {
|
|
3742
|
-
console.log(chalk2.dim("Creating generic config (no frameworks detected)..."));
|
|
3743
|
-
}
|
|
3744
|
-
return [createGenericFramework()];
|
|
3745
|
-
}
|
|
3746
|
-
function displayDetectedFrameworks(detections) {
|
|
3747
|
-
console.log(chalk2.green(`
|
|
3748
|
-
\u2713 Found ${detections.length} framework(s):
|
|
3749
|
-
`));
|
|
3750
|
-
for (const det of detections) {
|
|
3751
|
-
const pathDisplay = det.path === "." ? "root" : det.path;
|
|
3752
|
-
console.log(chalk2.bold(` ${det.name}`), chalk2.dim(`(${det.confidence} confidence)`));
|
|
3753
|
-
console.log(chalk2.dim(` Location: ${pathDisplay}`));
|
|
3754
|
-
if (det.evidence.length > 0) {
|
|
3755
|
-
det.evidence.forEach((e) => console.log(chalk2.dim(` \u2022 ${e}`)));
|
|
3756
|
-
}
|
|
3757
|
-
console.log();
|
|
3758
|
-
}
|
|
3759
|
-
}
|
|
3760
|
-
async function confirmFrameworkConfiguration(options) {
|
|
3761
|
-
if (options.yes) return true;
|
|
3762
|
-
const { confirm } = await inquirer.prompt([{
|
|
3763
|
-
type: "confirm",
|
|
3764
|
-
name: "confirm",
|
|
3765
|
-
message: "Configure these frameworks?",
|
|
3766
|
-
default: true
|
|
3767
|
-
}]);
|
|
3768
|
-
return confirm;
|
|
3769
|
-
}
|
|
3770
|
-
async function generateSingleFrameworkConfig(det, rootDir, options) {
|
|
3771
|
-
const detector = getFrameworkDetector(det.name);
|
|
3772
|
-
if (!detector) {
|
|
3773
|
-
console.warn(chalk2.yellow(`\u26A0\uFE0F No detector found for ${det.name}, skipping`));
|
|
3774
|
-
return null;
|
|
3775
|
-
}
|
|
3776
|
-
const frameworkConfig = await detector.generateConfig(rootDir, det.path);
|
|
3777
|
-
let finalConfig = frameworkConfig;
|
|
3778
|
-
const pathDisplay = det.path === "." ? "root" : det.path;
|
|
3779
|
-
if (!options.yes) {
|
|
3780
|
-
const { customize } = await inquirer.prompt([{
|
|
3781
|
-
type: "confirm",
|
|
3782
|
-
name: "customize",
|
|
3783
|
-
message: `Customize ${det.name} settings?`,
|
|
3784
|
-
default: false
|
|
3785
|
-
}]);
|
|
3786
|
-
if (customize) {
|
|
3787
|
-
const customized = await promptForCustomization(det.name, frameworkConfig);
|
|
3788
|
-
finalConfig = { ...frameworkConfig, ...customized };
|
|
3789
|
-
} else {
|
|
3790
|
-
console.log(chalk2.dim(` \u2192 Using defaults for ${det.name} at ${pathDisplay}`));
|
|
3791
|
-
}
|
|
3792
|
-
} else {
|
|
3793
|
-
console.log(chalk2.dim(` \u2192 Using defaults for ${det.name} at ${pathDisplay}`));
|
|
3794
|
-
}
|
|
3795
|
-
return {
|
|
3796
|
-
name: det.name,
|
|
3797
|
-
path: det.path,
|
|
3798
|
-
enabled: true,
|
|
3799
|
-
config: finalConfig
|
|
3800
|
-
};
|
|
3801
|
-
}
|
|
3802
|
-
async function handleFrameworksDetected(detections, rootDir, options) {
|
|
3803
|
-
displayDetectedFrameworks(detections);
|
|
3804
|
-
if (!await confirmFrameworkConfiguration(options)) {
|
|
3805
|
-
console.log(chalk2.dim("Aborted."));
|
|
3806
|
-
return null;
|
|
3807
|
-
}
|
|
3808
|
-
const frameworks = [];
|
|
3809
|
-
for (const det of detections) {
|
|
3810
|
-
const framework = await generateSingleFrameworkConfig(det, rootDir, options);
|
|
3811
|
-
if (framework) frameworks.push(framework);
|
|
3812
|
-
}
|
|
3813
|
-
if (frameworks.length === 0) {
|
|
3814
|
-
console.log(chalk2.yellow("\n\u26A0\uFE0F No framework configs could be generated"));
|
|
3815
|
-
return null;
|
|
3693
|
+
await fs.access(configPath);
|
|
3694
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F Note: .lien.config.json found but no longer used"));
|
|
3695
|
+
console.log(chalk2.dim(" You can safely delete it."));
|
|
3696
|
+
} catch {
|
|
3816
3697
|
}
|
|
3817
|
-
|
|
3698
|
+
await promptAndInstallCursorRules(rootDir, options);
|
|
3818
3699
|
}
|
|
3819
3700
|
async function getPathType(filepath) {
|
|
3820
3701
|
try {
|
|
@@ -3938,51 +3819,6 @@ async function promptAndInstallCursorRules(rootDir, options) {
|
|
|
3938
3819
|
console.log(chalk2.dim("You can manually copy CURSOR_RULES_TEMPLATE.md to .cursor/rules/lien.mdc"));
|
|
3939
3820
|
}
|
|
3940
3821
|
}
|
|
3941
|
-
async function writeConfigAndShowSuccess(rootDir, frameworks) {
|
|
3942
|
-
const config = { ...defaultConfig, version: CLI_VERSION, frameworks };
|
|
3943
|
-
const configPath = path.join(rootDir, ".lien.config.json");
|
|
3944
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3945
|
-
console.log(chalk2.green("\n\u2713 Created .lien.config.json"));
|
|
3946
|
-
console.log(chalk2.green(`\u2713 Configured ${frameworks.length} framework(s)`));
|
|
3947
|
-
console.log(chalk2.dim("\nNext steps:"));
|
|
3948
|
-
console.log(chalk2.dim(" 1. Run"), chalk2.bold("lien index"), chalk2.dim("to index your codebase"));
|
|
3949
|
-
console.log(chalk2.dim(" 2. Run"), chalk2.bold("lien serve"), chalk2.dim("to start the MCP server"));
|
|
3950
|
-
console.log(chalk2.dim(" 3. Configure Cursor to use the MCP server (see README.md)"));
|
|
3951
|
-
}
|
|
3952
|
-
async function createNewConfig(rootDir, options) {
|
|
3953
|
-
showCompactBanner();
|
|
3954
|
-
console.log(chalk2.bold("Initializing Lien...\n"));
|
|
3955
|
-
console.log(chalk2.dim("\u{1F50D} Detecting frameworks in"), chalk2.bold(rootDir));
|
|
3956
|
-
const detections = await detectAllFrameworks(rootDir);
|
|
3957
|
-
const frameworks = detections.length === 0 ? await handleNoFrameworksDetected(options) : await handleFrameworksDetected(detections, rootDir, options);
|
|
3958
|
-
if (!frameworks) return;
|
|
3959
|
-
await promptAndInstallCursorRules(rootDir, options);
|
|
3960
|
-
await writeConfigAndShowSuccess(rootDir, frameworks);
|
|
3961
|
-
}
|
|
3962
|
-
async function promptForCustomization(frameworkName, config) {
|
|
3963
|
-
console.log(chalk2.bold(`
|
|
3964
|
-
Customizing ${frameworkName} settings:`));
|
|
3965
|
-
const answers = await inquirer.prompt([
|
|
3966
|
-
{
|
|
3967
|
-
type: "input",
|
|
3968
|
-
name: "include",
|
|
3969
|
-
message: "File patterns to include (comma-separated):",
|
|
3970
|
-
default: config.include.join(", "),
|
|
3971
|
-
filter: (input) => input.split(",").map((s) => s.trim())
|
|
3972
|
-
},
|
|
3973
|
-
{
|
|
3974
|
-
type: "input",
|
|
3975
|
-
name: "exclude",
|
|
3976
|
-
message: "File patterns to exclude (comma-separated):",
|
|
3977
|
-
default: config.exclude.join(", "),
|
|
3978
|
-
filter: (input) => input.split(",").map((s) => s.trim())
|
|
3979
|
-
}
|
|
3980
|
-
]);
|
|
3981
|
-
return {
|
|
3982
|
-
include: answers.include,
|
|
3983
|
-
exclude: answers.exclude
|
|
3984
|
-
};
|
|
3985
|
-
}
|
|
3986
3822
|
|
|
3987
3823
|
// src/cli/status.ts
|
|
3988
3824
|
import chalk3 from "chalk";
|
|
@@ -3991,12 +3827,16 @@ import path2 from "path";
|
|
|
3991
3827
|
import os from "os";
|
|
3992
3828
|
import crypto from "crypto";
|
|
3993
3829
|
import {
|
|
3994
|
-
configService,
|
|
3995
3830
|
isGitRepo,
|
|
3996
3831
|
getCurrentBranch,
|
|
3997
3832
|
getCurrentCommit,
|
|
3998
3833
|
readVersionFile,
|
|
3999
|
-
|
|
3834
|
+
DEFAULT_CONCURRENCY,
|
|
3835
|
+
DEFAULT_EMBEDDING_BATCH_SIZE,
|
|
3836
|
+
DEFAULT_CHUNK_SIZE,
|
|
3837
|
+
DEFAULT_CHUNK_OVERLAP,
|
|
3838
|
+
DEFAULT_GIT_POLL_INTERVAL_MS,
|
|
3839
|
+
DEFAULT_DEBOUNCE_MS
|
|
4000
3840
|
} from "@liendev/core";
|
|
4001
3841
|
async function statusCommand() {
|
|
4002
3842
|
const rootDir = process.cwd();
|
|
@@ -4005,12 +3845,7 @@ async function statusCommand() {
|
|
|
4005
3845
|
const indexPath = path2.join(os.homedir(), ".lien", "indices", `${projectName}-${pathHash}`);
|
|
4006
3846
|
showCompactBanner();
|
|
4007
3847
|
console.log(chalk3.bold("Status\n"));
|
|
4008
|
-
|
|
4009
|
-
console.log(chalk3.dim("Configuration:"), hasConfig ? chalk3.green("\u2713 Found") : chalk3.red("\u2717 Not initialized"));
|
|
4010
|
-
if (!hasConfig) {
|
|
4011
|
-
console.log(chalk3.yellow("\nRun"), chalk3.bold("lien init"), chalk3.yellow("to initialize"));
|
|
4012
|
-
return;
|
|
4013
|
-
}
|
|
3848
|
+
console.log(chalk3.dim("Configuration:"), chalk3.green("\u2713 Using defaults (no per-project config needed)"));
|
|
4014
3849
|
try {
|
|
4015
3850
|
const stats = await fs2.stat(indexPath);
|
|
4016
3851
|
console.log(chalk3.dim("Index location:"), indexPath);
|
|
@@ -4033,51 +3868,38 @@ async function statusCommand() {
|
|
|
4033
3868
|
console.log(chalk3.dim("Index status:"), chalk3.yellow("\u2717 Not indexed"));
|
|
4034
3869
|
console.log(chalk3.yellow("\nRun"), chalk3.bold("lien index"), chalk3.yellow("to index your codebase"));
|
|
4035
3870
|
}
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
3871
|
+
console.log(chalk3.bold("\nFeatures:"));
|
|
3872
|
+
const isRepo = await isGitRepo(rootDir);
|
|
3873
|
+
if (isRepo) {
|
|
3874
|
+
console.log(chalk3.dim("Git detection:"), chalk3.green("\u2713 Enabled"));
|
|
3875
|
+
console.log(chalk3.dim(" Poll interval:"), `${DEFAULT_GIT_POLL_INTERVAL_MS / 1e3}s`);
|
|
3876
|
+
try {
|
|
3877
|
+
const branch = await getCurrentBranch(rootDir);
|
|
3878
|
+
const commit = await getCurrentCommit(rootDir);
|
|
3879
|
+
console.log(chalk3.dim(" Current branch:"), branch);
|
|
3880
|
+
console.log(chalk3.dim(" Current commit:"), commit.substring(0, 8));
|
|
3881
|
+
const gitStateFile = path2.join(indexPath, ".git-state.json");
|
|
4043
3882
|
try {
|
|
4044
|
-
const
|
|
4045
|
-
const
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
const gitStateFile = path2.join(indexPath, ".git-state.json");
|
|
4049
|
-
try {
|
|
4050
|
-
const gitStateContent = await fs2.readFile(gitStateFile, "utf-8");
|
|
4051
|
-
const gitState = JSON.parse(gitStateContent);
|
|
4052
|
-
if (gitState.branch !== branch || gitState.commit !== commit) {
|
|
4053
|
-
console.log(chalk3.yellow(" \u26A0\uFE0F Git state changed - will reindex on next serve"));
|
|
4054
|
-
}
|
|
4055
|
-
} catch {
|
|
3883
|
+
const gitStateContent = await fs2.readFile(gitStateFile, "utf-8");
|
|
3884
|
+
const gitState = JSON.parse(gitStateContent);
|
|
3885
|
+
if (gitState.branch !== branch || gitState.commit !== commit) {
|
|
3886
|
+
console.log(chalk3.yellow(" \u26A0\uFE0F Git state changed - will reindex on next serve"));
|
|
4056
3887
|
}
|
|
4057
3888
|
} catch {
|
|
4058
3889
|
}
|
|
4059
|
-
}
|
|
4060
|
-
console.log(chalk3.dim("Git detection:"), chalk3.yellow("Enabled (not a git repo)"));
|
|
4061
|
-
} else {
|
|
4062
|
-
console.log(chalk3.dim("Git detection:"), chalk3.gray("Disabled"));
|
|
4063
|
-
}
|
|
4064
|
-
if (config.fileWatching.enabled) {
|
|
4065
|
-
console.log(chalk3.dim("File watching:"), chalk3.green("\u2713 Enabled"));
|
|
4066
|
-
console.log(chalk3.dim(" Debounce:"), `${config.fileWatching.debounceMs}ms`);
|
|
4067
|
-
} else {
|
|
4068
|
-
console.log(chalk3.dim("File watching:"), chalk3.gray("Disabled"));
|
|
4069
|
-
console.log(chalk3.dim(" Enable with:"), chalk3.bold("lien serve --watch"));
|
|
4070
|
-
}
|
|
4071
|
-
console.log(chalk3.bold("\nIndexing Settings:"));
|
|
4072
|
-
if (isModernConfig(config)) {
|
|
4073
|
-
console.log(chalk3.dim("Concurrency:"), config.core.concurrency);
|
|
4074
|
-
console.log(chalk3.dim("Batch size:"), config.core.embeddingBatchSize);
|
|
4075
|
-
console.log(chalk3.dim("Chunk size:"), config.core.chunkSize);
|
|
4076
|
-
console.log(chalk3.dim("Chunk overlap:"), config.core.chunkOverlap);
|
|
3890
|
+
} catch {
|
|
4077
3891
|
}
|
|
4078
|
-
}
|
|
4079
|
-
console.log(chalk3.
|
|
4080
|
-
}
|
|
3892
|
+
} else {
|
|
3893
|
+
console.log(chalk3.dim("Git detection:"), chalk3.yellow("Not a git repo"));
|
|
3894
|
+
}
|
|
3895
|
+
console.log(chalk3.dim("File watching:"), chalk3.green("\u2713 Enabled (default)"));
|
|
3896
|
+
console.log(chalk3.dim(" Debounce:"), `${DEFAULT_DEBOUNCE_MS}ms`);
|
|
3897
|
+
console.log(chalk3.dim(" Disable with:"), chalk3.bold("lien serve --no-watch"));
|
|
3898
|
+
console.log(chalk3.bold("\nIndexing Settings (defaults):"));
|
|
3899
|
+
console.log(chalk3.dim("Concurrency:"), DEFAULT_CONCURRENCY);
|
|
3900
|
+
console.log(chalk3.dim("Batch size:"), DEFAULT_EMBEDDING_BATCH_SIZE);
|
|
3901
|
+
console.log(chalk3.dim("Chunk size:"), DEFAULT_CHUNK_SIZE);
|
|
3902
|
+
console.log(chalk3.dim("Chunk overlap:"), DEFAULT_CHUNK_OVERLAP);
|
|
4081
3903
|
}
|
|
4082
3904
|
|
|
4083
3905
|
// src/cli/index-cmd.ts
|
|
@@ -4166,10 +3988,10 @@ async function indexCommand(options) {
|
|
|
4166
3988
|
showCompactBanner();
|
|
4167
3989
|
try {
|
|
4168
3990
|
if (options.force) {
|
|
4169
|
-
const { VectorDB:
|
|
3991
|
+
const { VectorDB: VectorDB2 } = await import("@liendev/core");
|
|
4170
3992
|
const { ManifestManager: ManifestManager2 } = await import("@liendev/core");
|
|
4171
3993
|
console.log(chalk4.yellow("Clearing existing index and manifest..."));
|
|
4172
|
-
const vectorDB = new
|
|
3994
|
+
const vectorDB = new VectorDB2(process.cwd());
|
|
4173
3995
|
await vectorDB.initialize();
|
|
4174
3996
|
await vectorDB.clear();
|
|
4175
3997
|
const manifest = new ManifestManager2(vectorDB.dbPath);
|
|
@@ -4271,13 +4093,234 @@ import path4 from "path";
|
|
|
4271
4093
|
// src/mcp/server.ts
|
|
4272
4094
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4273
4095
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4096
|
+
import { createRequire as createRequire2 } from "module";
|
|
4097
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4098
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
4099
|
+
import {
|
|
4100
|
+
LocalEmbeddings,
|
|
4101
|
+
GitStateTracker,
|
|
4102
|
+
indexMultipleFiles,
|
|
4103
|
+
indexSingleFile,
|
|
4104
|
+
ManifestManager,
|
|
4105
|
+
isGitAvailable,
|
|
4106
|
+
isGitRepo as isGitRepo2,
|
|
4107
|
+
VERSION_CHECK_INTERVAL_MS,
|
|
4108
|
+
DEFAULT_GIT_POLL_INTERVAL_MS as DEFAULT_GIT_POLL_INTERVAL_MS2,
|
|
4109
|
+
createVectorDB
|
|
4110
|
+
} from "@liendev/core";
|
|
4111
|
+
|
|
4112
|
+
// src/watcher/index.ts
|
|
4113
|
+
import chokidar from "chokidar";
|
|
4114
|
+
import path3 from "path";
|
|
4115
|
+
import { detectAllFrameworks, getFrameworkDetector, DEFAULT_DEBOUNCE_MS as DEFAULT_DEBOUNCE_MS2 } from "@liendev/core";
|
|
4116
|
+
var FileWatcher = class {
|
|
4117
|
+
watcher = null;
|
|
4118
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
4119
|
+
rootDir;
|
|
4120
|
+
onChangeHandler = null;
|
|
4121
|
+
constructor(rootDir) {
|
|
4122
|
+
this.rootDir = rootDir;
|
|
4123
|
+
}
|
|
4124
|
+
/**
|
|
4125
|
+
* Detect watch patterns from frameworks or use defaults.
|
|
4126
|
+
*/
|
|
4127
|
+
async getWatchPatterns() {
|
|
4128
|
+
try {
|
|
4129
|
+
const detectedFrameworks = await detectAllFrameworks(this.rootDir);
|
|
4130
|
+
if (detectedFrameworks.length > 0) {
|
|
4131
|
+
const frameworks = await Promise.all(
|
|
4132
|
+
detectedFrameworks.map(async (detection) => {
|
|
4133
|
+
const detector = getFrameworkDetector(detection.name);
|
|
4134
|
+
if (!detector) {
|
|
4135
|
+
return null;
|
|
4136
|
+
}
|
|
4137
|
+
const config = await detector.generateConfig(this.rootDir, detection.path);
|
|
4138
|
+
return {
|
|
4139
|
+
name: detection.name,
|
|
4140
|
+
path: detection.path,
|
|
4141
|
+
enabled: true,
|
|
4142
|
+
config
|
|
4143
|
+
};
|
|
4144
|
+
})
|
|
4145
|
+
);
|
|
4146
|
+
const validFrameworks = frameworks.filter((f) => f !== null);
|
|
4147
|
+
const includePatterns = validFrameworks.flatMap((f) => f.config.include);
|
|
4148
|
+
const excludePatterns = validFrameworks.flatMap((f) => f.config.exclude);
|
|
4149
|
+
if (includePatterns.length === 0) {
|
|
4150
|
+
return this.getDefaultPatterns();
|
|
4151
|
+
}
|
|
4152
|
+
return { include: includePatterns, exclude: excludePatterns };
|
|
4153
|
+
} else {
|
|
4154
|
+
return this.getDefaultPatterns();
|
|
4155
|
+
}
|
|
4156
|
+
} catch (error) {
|
|
4157
|
+
return this.getDefaultPatterns();
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Get default watch patterns.
|
|
4162
|
+
*/
|
|
4163
|
+
getDefaultPatterns() {
|
|
4164
|
+
return {
|
|
4165
|
+
include: ["**/*"],
|
|
4166
|
+
exclude: [
|
|
4167
|
+
"**/node_modules/**",
|
|
4168
|
+
"**/vendor/**",
|
|
4169
|
+
"**/dist/**",
|
|
4170
|
+
"**/build/**",
|
|
4171
|
+
"**/.git/**"
|
|
4172
|
+
]
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* Create chokidar watcher configuration.
|
|
4177
|
+
*/
|
|
4178
|
+
createWatcherConfig(patterns) {
|
|
4179
|
+
return {
|
|
4180
|
+
cwd: this.rootDir,
|
|
4181
|
+
ignored: patterns.exclude,
|
|
4182
|
+
persistent: true,
|
|
4183
|
+
ignoreInitial: true,
|
|
4184
|
+
// Don't trigger for existing files
|
|
4185
|
+
// Handle atomic saves from modern editors (VS Code, Sublime, etc.)
|
|
4186
|
+
// Editors write to temp file then rename - without this, we get unlink+add instead of change
|
|
4187
|
+
atomic: true,
|
|
4188
|
+
awaitWriteFinish: {
|
|
4189
|
+
stabilityThreshold: 300,
|
|
4190
|
+
// Reduced from 500ms for faster detection
|
|
4191
|
+
pollInterval: 100
|
|
4192
|
+
},
|
|
4193
|
+
// Performance optimizations
|
|
4194
|
+
usePolling: false,
|
|
4195
|
+
interval: 100,
|
|
4196
|
+
binaryInterval: 300
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
/**
|
|
4200
|
+
* Register event handlers on the watcher.
|
|
4201
|
+
*/
|
|
4202
|
+
registerEventHandlers() {
|
|
4203
|
+
if (!this.watcher) {
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
this.watcher.on("add", (filepath) => this.handleChange("add", filepath)).on("change", (filepath) => this.handleChange("change", filepath)).on("unlink", (filepath) => this.handleChange("unlink", filepath)).on("error", (error) => {
|
|
4207
|
+
console.error(`[Lien] File watcher error: ${error}`);
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* Wait for watcher to be ready with timeout fallback.
|
|
4212
|
+
*/
|
|
4213
|
+
async waitForReady() {
|
|
4214
|
+
if (!this.watcher) {
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
let readyFired = false;
|
|
4218
|
+
await Promise.race([
|
|
4219
|
+
new Promise((resolve) => {
|
|
4220
|
+
const readyHandler = () => {
|
|
4221
|
+
readyFired = true;
|
|
4222
|
+
resolve();
|
|
4223
|
+
};
|
|
4224
|
+
this.watcher.once("ready", readyHandler);
|
|
4225
|
+
}),
|
|
4226
|
+
new Promise((resolve) => {
|
|
4227
|
+
setTimeout(() => {
|
|
4228
|
+
if (!readyFired) {
|
|
4229
|
+
resolve();
|
|
4230
|
+
}
|
|
4231
|
+
}, 1e3);
|
|
4232
|
+
})
|
|
4233
|
+
]);
|
|
4234
|
+
}
|
|
4235
|
+
/**
|
|
4236
|
+
* Starts watching files for changes.
|
|
4237
|
+
*
|
|
4238
|
+
* @param handler - Callback function called when files change
|
|
4239
|
+
*/
|
|
4240
|
+
async start(handler) {
|
|
4241
|
+
if (this.watcher) {
|
|
4242
|
+
throw new Error("File watcher is already running");
|
|
4243
|
+
}
|
|
4244
|
+
this.onChangeHandler = handler;
|
|
4245
|
+
const patterns = await this.getWatchPatterns();
|
|
4246
|
+
this.watcher = chokidar.watch(patterns.include, this.createWatcherConfig(patterns));
|
|
4247
|
+
this.registerEventHandlers();
|
|
4248
|
+
await this.waitForReady();
|
|
4249
|
+
}
|
|
4250
|
+
/**
|
|
4251
|
+
* Handles a file change event with debouncing.
|
|
4252
|
+
* Debouncing prevents rapid reindexing when files are saved multiple times quickly.
|
|
4253
|
+
*/
|
|
4254
|
+
handleChange(type, filepath) {
|
|
4255
|
+
const existingTimer = this.debounceTimers.get(filepath);
|
|
4256
|
+
if (existingTimer) {
|
|
4257
|
+
clearTimeout(existingTimer);
|
|
4258
|
+
}
|
|
4259
|
+
const timer = setTimeout(() => {
|
|
4260
|
+
this.debounceTimers.delete(filepath);
|
|
4261
|
+
if (this.onChangeHandler) {
|
|
4262
|
+
const absolutePath = path3.isAbsolute(filepath) ? filepath : path3.join(this.rootDir, filepath);
|
|
4263
|
+
try {
|
|
4264
|
+
const result = this.onChangeHandler({
|
|
4265
|
+
type,
|
|
4266
|
+
filepath: absolutePath
|
|
4267
|
+
});
|
|
4268
|
+
if (result instanceof Promise) {
|
|
4269
|
+
result.catch((error) => {
|
|
4270
|
+
console.error(`[Lien] Error handling file change: ${error}`);
|
|
4271
|
+
});
|
|
4272
|
+
}
|
|
4273
|
+
} catch (error) {
|
|
4274
|
+
console.error(`[Lien] Error handling file change: ${error}`);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
}, DEFAULT_DEBOUNCE_MS2);
|
|
4278
|
+
this.debounceTimers.set(filepath, timer);
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* Stops the file watcher and cleans up resources.
|
|
4282
|
+
*/
|
|
4283
|
+
async stop() {
|
|
4284
|
+
if (!this.watcher) {
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
for (const timer of this.debounceTimers.values()) {
|
|
4288
|
+
clearTimeout(timer);
|
|
4289
|
+
}
|
|
4290
|
+
this.debounceTimers.clear();
|
|
4291
|
+
await this.watcher.close();
|
|
4292
|
+
this.watcher = null;
|
|
4293
|
+
this.onChangeHandler = null;
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Gets the list of files currently being watched.
|
|
4297
|
+
*/
|
|
4298
|
+
getWatchedFiles() {
|
|
4299
|
+
if (!this.watcher) {
|
|
4300
|
+
return [];
|
|
4301
|
+
}
|
|
4302
|
+
const watched = this.watcher.getWatched();
|
|
4303
|
+
const files = [];
|
|
4304
|
+
for (const [dir, filenames] of Object.entries(watched)) {
|
|
4305
|
+
for (const filename of filenames) {
|
|
4306
|
+
files.push(`${dir}/${filename}`);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return files;
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Checks if the watcher is currently running.
|
|
4313
|
+
*/
|
|
4314
|
+
isRunning() {
|
|
4315
|
+
return this.watcher !== null;
|
|
4316
|
+
}
|
|
4317
|
+
};
|
|
4318
|
+
|
|
4319
|
+
// src/mcp/server-config.ts
|
|
4274
4320
|
import {
|
|
4275
4321
|
CallToolRequestSchema,
|
|
4276
4322
|
ListToolsRequestSchema
|
|
4277
4323
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
4278
|
-
import { createRequire as createRequire3 } from "module";
|
|
4279
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4280
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
4281
4324
|
|
|
4282
4325
|
// src/mcp/utils/zod-to-json-schema.ts
|
|
4283
4326
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
@@ -8340,7 +8383,13 @@ var SemanticSearchSchema = external_exports.object({
|
|
|
8340
8383
|
),
|
|
8341
8384
|
limit: external_exports.number().int().min(1, "Limit must be at least 1").max(50, "Limit cannot exceed 50").default(5).describe(
|
|
8342
8385
|
"Number of results to return.\n\nDefault: 5\nIncrease to 10-15 for broad exploration."
|
|
8343
|
-
)
|
|
8386
|
+
),
|
|
8387
|
+
crossRepo: external_exports.boolean().default(false).describe(
|
|
8388
|
+
"If true, search across all repos in the organization (requires Qdrant backend).\n\nDefault: false (single-repo search)\nWhen enabled, results are grouped by repository."
|
|
8389
|
+
),
|
|
8390
|
+
repoIds: external_exports.array(external_exports.string()).optional().describe(
|
|
8391
|
+
"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."
|
|
8392
|
+
)
|
|
8344
8393
|
});
|
|
8345
8394
|
|
|
8346
8395
|
// src/mcp/schemas/similarity.schema.ts
|
|
@@ -8383,6 +8432,9 @@ var GetDependentsSchema = external_exports.object({
|
|
|
8383
8432
|
),
|
|
8384
8433
|
depth: external_exports.number().int().min(1).max(1).default(1).describe(
|
|
8385
8434
|
"Depth of transitive dependencies. Only depth=1 (direct dependents) is currently supported.\n\n1 = Direct dependents only"
|
|
8435
|
+
),
|
|
8436
|
+
crossRepo: external_exports.boolean().default(false).describe(
|
|
8437
|
+
"If true, find dependents across all repos in the organization (requires Qdrant backend).\n\nDefault: false (single-repo search)\nWhen enabled, results are grouped by repository."
|
|
8386
8438
|
)
|
|
8387
8439
|
});
|
|
8388
8440
|
|
|
@@ -8396,6 +8448,12 @@ var GetComplexitySchema = external_exports.object({
|
|
|
8396
8448
|
),
|
|
8397
8449
|
threshold: external_exports.number().int().min(1, "Threshold must be at least 1").optional().describe(
|
|
8398
8450
|
"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."
|
|
8451
|
+
),
|
|
8452
|
+
crossRepo: external_exports.boolean().default(false).describe(
|
|
8453
|
+
"If true, analyze complexity across all repos in the organization (requires Qdrant backend).\n\nDefault: false (single-repo analysis)\nWhen enabled, results are aggregated by repository."
|
|
8454
|
+
),
|
|
8455
|
+
repoIds: external_exports.array(external_exports.string()).optional().describe(
|
|
8456
|
+
"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."
|
|
8399
8457
|
)
|
|
8400
8458
|
});
|
|
8401
8459
|
|
|
@@ -8575,20 +8633,46 @@ function wrapToolHandler(schema, handler) {
|
|
|
8575
8633
|
}
|
|
8576
8634
|
|
|
8577
8635
|
// src/mcp/handlers/semantic-search.ts
|
|
8636
|
+
import { QdrantDB } from "@liendev/core";
|
|
8637
|
+
function groupResultsByRepo(results) {
|
|
8638
|
+
const grouped = {};
|
|
8639
|
+
for (const result of results) {
|
|
8640
|
+
const repoId = result.metadata.repoId || "unknown";
|
|
8641
|
+
if (!grouped[repoId]) {
|
|
8642
|
+
grouped[repoId] = [];
|
|
8643
|
+
}
|
|
8644
|
+
grouped[repoId].push(result);
|
|
8645
|
+
}
|
|
8646
|
+
return grouped;
|
|
8647
|
+
}
|
|
8578
8648
|
async function handleSemanticSearch(args, ctx) {
|
|
8579
8649
|
const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
8580
8650
|
return await wrapToolHandler(
|
|
8581
8651
|
SemanticSearchSchema,
|
|
8582
8652
|
async (validatedArgs) => {
|
|
8583
|
-
|
|
8653
|
+
const { crossRepo, repoIds, query, limit } = validatedArgs;
|
|
8654
|
+
log(`Searching for: "${query}"${crossRepo ? " (cross-repo)" : ""}`);
|
|
8584
8655
|
await checkAndReconnect();
|
|
8585
|
-
const queryEmbedding = await embeddings.embed(
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8656
|
+
const queryEmbedding = await embeddings.embed(query);
|
|
8657
|
+
let results;
|
|
8658
|
+
if (crossRepo && vectorDB instanceof QdrantDB) {
|
|
8659
|
+
results = await vectorDB.searchCrossRepo(queryEmbedding, limit, repoIds);
|
|
8660
|
+
log(`Found ${results.length} results across ${Object.keys(groupResultsByRepo(results)).length} repos`);
|
|
8661
|
+
} else {
|
|
8662
|
+
if (crossRepo) {
|
|
8663
|
+
log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo search.");
|
|
8664
|
+
}
|
|
8665
|
+
results = await vectorDB.search(queryEmbedding, limit, query);
|
|
8666
|
+
log(`Found ${results.length} results`);
|
|
8667
|
+
}
|
|
8668
|
+
const response = {
|
|
8589
8669
|
indexInfo: getIndexMetadata(),
|
|
8590
8670
|
results
|
|
8591
8671
|
};
|
|
8672
|
+
if (crossRepo && vectorDB instanceof QdrantDB) {
|
|
8673
|
+
response.groupedByRepo = groupResultsByRepo(results);
|
|
8674
|
+
}
|
|
8675
|
+
return response;
|
|
8592
8676
|
}
|
|
8593
8677
|
)(args);
|
|
8594
8678
|
}
|
|
@@ -8874,14 +8958,11 @@ async function handleListFunctions(args, ctx) {
|
|
|
8874
8958
|
}
|
|
8875
8959
|
|
|
8876
8960
|
// src/mcp/handlers/get-dependents.ts
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
HIGH: 30
|
|
8883
|
-
// High impact, careful planning needed
|
|
8884
|
-
};
|
|
8961
|
+
import { QdrantDB as QdrantDB3 } from "@liendev/core";
|
|
8962
|
+
|
|
8963
|
+
// src/mcp/handlers/dependency-analyzer.ts
|
|
8964
|
+
import { QdrantDB as QdrantDB2 } from "@liendev/core";
|
|
8965
|
+
var SCAN_LIMIT2 = 1e4;
|
|
8885
8966
|
var COMPLEXITY_THRESHOLDS = {
|
|
8886
8967
|
HIGH_COMPLEXITY_DEPENDENT: 10,
|
|
8887
8968
|
// Individual file is complex
|
|
@@ -8898,8 +8979,52 @@ var COMPLEXITY_THRESHOLDS = {
|
|
|
8898
8979
|
MEDIUM_MAX: 15
|
|
8899
8980
|
// Occasional branching
|
|
8900
8981
|
};
|
|
8901
|
-
|
|
8902
|
-
|
|
8982
|
+
async function findDependents(vectorDB, filepath, crossRepo, log) {
|
|
8983
|
+
let allChunks;
|
|
8984
|
+
if (crossRepo && vectorDB instanceof QdrantDB2) {
|
|
8985
|
+
allChunks = await vectorDB.scanCrossRepo({ limit: SCAN_LIMIT2 });
|
|
8986
|
+
} else {
|
|
8987
|
+
if (crossRepo) {
|
|
8988
|
+
log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo search.", "warning");
|
|
8989
|
+
}
|
|
8990
|
+
allChunks = await vectorDB.scanWithFilter({ limit: SCAN_LIMIT2 });
|
|
8991
|
+
}
|
|
8992
|
+
const hitLimit = allChunks.length === SCAN_LIMIT2;
|
|
8993
|
+
if (hitLimit) {
|
|
8994
|
+
log(`Scanned ${SCAN_LIMIT2} chunks (limit reached). Results may be incomplete.`, "warning");
|
|
8995
|
+
}
|
|
8996
|
+
log(`Scanning ${allChunks.length} chunks for imports...`);
|
|
8997
|
+
const workspaceRoot = process.cwd().replace(/\\/g, "/");
|
|
8998
|
+
const pathCache = /* @__PURE__ */ new Map();
|
|
8999
|
+
const normalizePathCached = (path6) => {
|
|
9000
|
+
if (!pathCache.has(path6)) pathCache.set(path6, normalizePath(path6, workspaceRoot));
|
|
9001
|
+
return pathCache.get(path6);
|
|
9002
|
+
};
|
|
9003
|
+
const importIndex = buildImportIndex(allChunks, normalizePathCached);
|
|
9004
|
+
const normalizedTarget = normalizePathCached(filepath);
|
|
9005
|
+
const dependentChunks = findDependentChunks(importIndex, normalizedTarget);
|
|
9006
|
+
const chunksByFile = /* @__PURE__ */ new Map();
|
|
9007
|
+
for (const chunk of dependentChunks) {
|
|
9008
|
+
const canonical = getCanonicalPath(chunk.metadata.file, workspaceRoot);
|
|
9009
|
+
const existing = chunksByFile.get(canonical) || [];
|
|
9010
|
+
existing.push(chunk);
|
|
9011
|
+
chunksByFile.set(canonical, existing);
|
|
9012
|
+
}
|
|
9013
|
+
const fileComplexities = calculateFileComplexities(chunksByFile);
|
|
9014
|
+
const complexityMetrics = calculateOverallComplexityMetrics(fileComplexities);
|
|
9015
|
+
const uniqueFiles = Array.from(chunksByFile.keys()).map((filepath2) => ({
|
|
9016
|
+
filepath: filepath2,
|
|
9017
|
+
isTestFile: isTestFile(filepath2)
|
|
9018
|
+
}));
|
|
9019
|
+
return {
|
|
9020
|
+
dependents: uniqueFiles,
|
|
9021
|
+
chunksByFile,
|
|
9022
|
+
fileComplexities,
|
|
9023
|
+
complexityMetrics,
|
|
9024
|
+
hitLimit,
|
|
9025
|
+
allChunks
|
|
9026
|
+
};
|
|
9027
|
+
}
|
|
8903
9028
|
function buildImportIndex(allChunks, normalizePathCached) {
|
|
8904
9029
|
const importIndex = /* @__PURE__ */ new Map();
|
|
8905
9030
|
for (const chunk of allChunks) {
|
|
@@ -8992,65 +9117,82 @@ function calculateComplexityRiskBoost(avgComplexity, maxComplexity) {
|
|
|
8992
9117
|
return "low";
|
|
8993
9118
|
}
|
|
8994
9119
|
function calculateRiskLevel(dependentCount, complexityRiskBoost) {
|
|
9120
|
+
const DEPENDENT_COUNT_THRESHOLDS = {
|
|
9121
|
+
LOW: 5,
|
|
9122
|
+
MEDIUM: 15,
|
|
9123
|
+
HIGH: 30
|
|
9124
|
+
};
|
|
9125
|
+
const RISK_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
8995
9126
|
let riskLevel = dependentCount === 0 ? "low" : dependentCount <= DEPENDENT_COUNT_THRESHOLDS.LOW ? "low" : dependentCount <= DEPENDENT_COUNT_THRESHOLDS.MEDIUM ? "medium" : dependentCount <= DEPENDENT_COUNT_THRESHOLDS.HIGH ? "high" : "critical";
|
|
8996
9127
|
if (RISK_ORDER[complexityRiskBoost] > RISK_ORDER[riskLevel]) {
|
|
8997
9128
|
riskLevel = complexityRiskBoost;
|
|
8998
9129
|
}
|
|
8999
9130
|
return riskLevel;
|
|
9000
9131
|
}
|
|
9132
|
+
function groupDependentsByRepo(dependents, chunks) {
|
|
9133
|
+
const repoMap = /* @__PURE__ */ new Map();
|
|
9134
|
+
for (const chunk of chunks) {
|
|
9135
|
+
const repoId = chunk.metadata.repoId || "unknown";
|
|
9136
|
+
const filepath = chunk.metadata.file;
|
|
9137
|
+
if (!repoMap.has(repoId)) {
|
|
9138
|
+
repoMap.set(repoId, /* @__PURE__ */ new Set());
|
|
9139
|
+
}
|
|
9140
|
+
repoMap.get(repoId).add(filepath);
|
|
9141
|
+
}
|
|
9142
|
+
const grouped = {};
|
|
9143
|
+
for (const dependent of dependents) {
|
|
9144
|
+
let foundRepo = "unknown";
|
|
9145
|
+
for (const [repoId, files] of repoMap.entries()) {
|
|
9146
|
+
if (files.has(dependent.filepath)) {
|
|
9147
|
+
foundRepo = repoId;
|
|
9148
|
+
break;
|
|
9149
|
+
}
|
|
9150
|
+
}
|
|
9151
|
+
if (!grouped[foundRepo]) {
|
|
9152
|
+
grouped[foundRepo] = [];
|
|
9153
|
+
}
|
|
9154
|
+
grouped[foundRepo].push(dependent);
|
|
9155
|
+
}
|
|
9156
|
+
return grouped;
|
|
9157
|
+
}
|
|
9158
|
+
|
|
9159
|
+
// src/mcp/handlers/get-dependents.ts
|
|
9001
9160
|
async function handleGetDependents(args, ctx) {
|
|
9002
9161
|
const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
9003
9162
|
return await wrapToolHandler(
|
|
9004
9163
|
GetDependentsSchema,
|
|
9005
9164
|
async (validatedArgs) => {
|
|
9006
|
-
|
|
9165
|
+
const { crossRepo, filepath } = validatedArgs;
|
|
9166
|
+
log(`Finding dependents of: ${filepath}${crossRepo ? " (cross-repo)" : ""}`);
|
|
9007
9167
|
await checkAndReconnect();
|
|
9008
|
-
const
|
|
9009
|
-
const
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
log(
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
const
|
|
9017
|
-
if (!pathCache.has(path6)) pathCache.set(path6, normalizePath(path6, workspaceRoot));
|
|
9018
|
-
return pathCache.get(path6);
|
|
9019
|
-
};
|
|
9020
|
-
const importIndex = buildImportIndex(allChunks, normalizePathCached);
|
|
9021
|
-
const normalizedTarget = normalizePathCached(validatedArgs.filepath);
|
|
9022
|
-
const dependentChunks = findDependentChunks(importIndex, normalizedTarget);
|
|
9023
|
-
const chunksByFile = /* @__PURE__ */ new Map();
|
|
9024
|
-
for (const chunk of dependentChunks) {
|
|
9025
|
-
const canonical = getCanonicalPath(chunk.metadata.file, workspaceRoot);
|
|
9026
|
-
const existing = chunksByFile.get(canonical) || [];
|
|
9027
|
-
existing.push(chunk);
|
|
9028
|
-
chunksByFile.set(canonical, existing);
|
|
9029
|
-
}
|
|
9030
|
-
const fileComplexities = calculateFileComplexities(chunksByFile);
|
|
9031
|
-
const complexityMetrics = calculateOverallComplexityMetrics(fileComplexities);
|
|
9032
|
-
const uniqueFiles = Array.from(chunksByFile.keys()).map((filepath) => ({
|
|
9033
|
-
filepath,
|
|
9034
|
-
isTestFile: isTestFile(filepath)
|
|
9035
|
-
}));
|
|
9036
|
-
const riskLevel = calculateRiskLevel(uniqueFiles.length, complexityMetrics.complexityRiskBoost);
|
|
9037
|
-
log(`Found ${uniqueFiles.length} dependent files (risk: ${riskLevel}${complexityMetrics.filesWithComplexityData > 0 ? ", complexity-boosted" : ""})`);
|
|
9038
|
-
return {
|
|
9168
|
+
const analysis = await findDependents(vectorDB, filepath, crossRepo ?? false, log);
|
|
9169
|
+
const riskLevel = calculateRiskLevel(
|
|
9170
|
+
analysis.dependents.length,
|
|
9171
|
+
analysis.complexityMetrics.complexityRiskBoost
|
|
9172
|
+
);
|
|
9173
|
+
log(
|
|
9174
|
+
`Found ${analysis.dependents.length} dependent files (risk: ${riskLevel}${analysis.complexityMetrics.filesWithComplexityData > 0 ? ", complexity-boosted" : ""})`
|
|
9175
|
+
);
|
|
9176
|
+
const response = {
|
|
9039
9177
|
indexInfo: getIndexMetadata(),
|
|
9040
9178
|
filepath: validatedArgs.filepath,
|
|
9041
|
-
dependentCount:
|
|
9179
|
+
dependentCount: analysis.dependents.length,
|
|
9042
9180
|
riskLevel,
|
|
9043
|
-
dependents:
|
|
9044
|
-
complexityMetrics,
|
|
9045
|
-
note: hitLimit ? `Warning: Scanned
|
|
9181
|
+
dependents: analysis.dependents,
|
|
9182
|
+
complexityMetrics: analysis.complexityMetrics,
|
|
9183
|
+
note: analysis.hitLimit ? `Warning: Scanned 10000 chunks (limit reached). Results may be incomplete.` : void 0
|
|
9046
9184
|
};
|
|
9185
|
+
if (crossRepo && vectorDB instanceof QdrantDB3) {
|
|
9186
|
+
response.groupedByRepo = groupDependentsByRepo(analysis.dependents, analysis.allChunks);
|
|
9187
|
+
}
|
|
9188
|
+
return response;
|
|
9047
9189
|
}
|
|
9048
9190
|
)(args);
|
|
9049
9191
|
}
|
|
9050
9192
|
|
|
9051
9193
|
// src/mcp/handlers/get-complexity.ts
|
|
9052
9194
|
var import_collect = __toESM(require_dist(), 1);
|
|
9053
|
-
import { ComplexityAnalyzer } from "@liendev/core";
|
|
9195
|
+
import { ComplexityAnalyzer, QdrantDB as QdrantDB4 } from "@liendev/core";
|
|
9054
9196
|
function transformViolation(v, fileData) {
|
|
9055
9197
|
return {
|
|
9056
9198
|
filepath: v.filepath,
|
|
@@ -9069,23 +9211,48 @@ function transformViolation(v, fileData) {
|
|
|
9069
9211
|
...v.halsteadDetails && { halsteadDetails: v.halsteadDetails }
|
|
9070
9212
|
};
|
|
9071
9213
|
}
|
|
9214
|
+
function groupViolationsByRepo(violations, allChunks) {
|
|
9215
|
+
const fileToRepo = /* @__PURE__ */ new Map();
|
|
9216
|
+
for (const chunk of allChunks) {
|
|
9217
|
+
const repoId = chunk.metadata.repoId || "unknown";
|
|
9218
|
+
fileToRepo.set(chunk.metadata.file, repoId);
|
|
9219
|
+
}
|
|
9220
|
+
const grouped = {};
|
|
9221
|
+
for (const violation of violations) {
|
|
9222
|
+
const repoId = fileToRepo.get(violation.filepath) || "unknown";
|
|
9223
|
+
if (!grouped[repoId]) {
|
|
9224
|
+
grouped[repoId] = [];
|
|
9225
|
+
}
|
|
9226
|
+
grouped[repoId].push(violation);
|
|
9227
|
+
}
|
|
9228
|
+
return grouped;
|
|
9229
|
+
}
|
|
9072
9230
|
async function handleGetComplexity(args, ctx) {
|
|
9073
|
-
const { vectorDB,
|
|
9231
|
+
const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
9074
9232
|
return await wrapToolHandler(
|
|
9075
9233
|
GetComplexitySchema,
|
|
9076
9234
|
async (validatedArgs) => {
|
|
9077
|
-
|
|
9235
|
+
const { crossRepo, repoIds, files, top, threshold } = validatedArgs;
|
|
9236
|
+
log(`Analyzing complexity${crossRepo ? " (cross-repo)" : ""}...`);
|
|
9078
9237
|
await checkAndReconnect();
|
|
9079
|
-
|
|
9080
|
-
|
|
9238
|
+
let allChunks = [];
|
|
9239
|
+
if (crossRepo && vectorDB instanceof QdrantDB4) {
|
|
9240
|
+
allChunks = await vectorDB.scanCrossRepo({
|
|
9241
|
+
limit: 1e5,
|
|
9242
|
+
repoIds
|
|
9243
|
+
});
|
|
9244
|
+
log(`Scanned ${allChunks.length} chunks across repos`);
|
|
9245
|
+
}
|
|
9246
|
+
const analyzer = new ComplexityAnalyzer(vectorDB);
|
|
9247
|
+
const report = await analyzer.analyze(files, crossRepo && vectorDB instanceof QdrantDB4 ? crossRepo : false, repoIds);
|
|
9081
9248
|
log(`Analyzed ${report.summary.filesAnalyzed} files`);
|
|
9082
9249
|
const allViolations = (0, import_collect.default)(Object.entries(report.files)).flatMap(
|
|
9083
9250
|
([, fileData]) => fileData.violations.map((v) => transformViolation(v, fileData))
|
|
9084
9251
|
).sortByDesc("complexity").all();
|
|
9085
|
-
const violations =
|
|
9086
|
-
const topViolations = violations.slice(0,
|
|
9252
|
+
const violations = threshold !== void 0 ? allViolations.filter((v) => v.complexity >= threshold) : allViolations;
|
|
9253
|
+
const topViolations = violations.slice(0, top);
|
|
9087
9254
|
const bySeverity = (0, import_collect.default)(violations).countBy("severity").all();
|
|
9088
|
-
|
|
9255
|
+
const response = {
|
|
9089
9256
|
indexInfo: getIndexMetadata(),
|
|
9090
9257
|
summary: {
|
|
9091
9258
|
filesAnalyzed: report.summary.filesAnalyzed,
|
|
@@ -9099,6 +9266,12 @@ async function handleGetComplexity(args, ctx) {
|
|
|
9099
9266
|
},
|
|
9100
9267
|
violations: topViolations
|
|
9101
9268
|
};
|
|
9269
|
+
if (crossRepo && vectorDB instanceof QdrantDB4 && allChunks.length > 0) {
|
|
9270
|
+
response.groupedByRepo = groupViolationsByRepo(topViolations, allChunks);
|
|
9271
|
+
} else if (crossRepo) {
|
|
9272
|
+
log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo analysis.", "warning");
|
|
9273
|
+
}
|
|
9274
|
+
return response;
|
|
9102
9275
|
}
|
|
9103
9276
|
)(args);
|
|
9104
9277
|
}
|
|
@@ -9113,168 +9286,91 @@ var toolHandlers = {
|
|
|
9113
9286
|
"get_complexity": handleGetComplexity
|
|
9114
9287
|
};
|
|
9115
9288
|
|
|
9116
|
-
// src/mcp/server.ts
|
|
9117
|
-
import {
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
isGitAvailable,
|
|
9126
|
-
isGitRepo as isGitRepo2,
|
|
9127
|
-
VERSION_CHECK_INTERVAL_MS,
|
|
9128
|
-
LienError as LienError2,
|
|
9129
|
-
LienErrorCode as LienErrorCode2
|
|
9130
|
-
} from "@liendev/core";
|
|
9131
|
-
|
|
9132
|
-
// src/watcher/index.ts
|
|
9133
|
-
import chokidar from "chokidar";
|
|
9134
|
-
import path3 from "path";
|
|
9135
|
-
import { isLegacyConfig, isModernConfig as isModernConfig2 } from "@liendev/core";
|
|
9136
|
-
var FileWatcher = class {
|
|
9137
|
-
watcher = null;
|
|
9138
|
-
debounceTimers = /* @__PURE__ */ new Map();
|
|
9139
|
-
config;
|
|
9140
|
-
rootDir;
|
|
9141
|
-
onChangeHandler = null;
|
|
9142
|
-
constructor(rootDir, config) {
|
|
9143
|
-
this.rootDir = rootDir;
|
|
9144
|
-
this.config = config;
|
|
9145
|
-
}
|
|
9146
|
-
/**
|
|
9147
|
-
* Starts watching files for changes.
|
|
9148
|
-
*
|
|
9149
|
-
* @param handler - Callback function called when files change
|
|
9150
|
-
*/
|
|
9151
|
-
async start(handler) {
|
|
9152
|
-
if (this.watcher) {
|
|
9153
|
-
throw new Error("File watcher is already running");
|
|
9154
|
-
}
|
|
9155
|
-
this.onChangeHandler = handler;
|
|
9156
|
-
let includePatterns;
|
|
9157
|
-
let excludePatterns;
|
|
9158
|
-
if (isLegacyConfig(this.config)) {
|
|
9159
|
-
includePatterns = this.config.indexing.include;
|
|
9160
|
-
excludePatterns = this.config.indexing.exclude;
|
|
9161
|
-
} else if (isModernConfig2(this.config)) {
|
|
9162
|
-
includePatterns = this.config.frameworks.flatMap((f) => f.config.include);
|
|
9163
|
-
excludePatterns = this.config.frameworks.flatMap((f) => f.config.exclude);
|
|
9164
|
-
} else {
|
|
9165
|
-
includePatterns = ["**/*"];
|
|
9166
|
-
excludePatterns = [];
|
|
9167
|
-
}
|
|
9168
|
-
this.watcher = chokidar.watch(includePatterns, {
|
|
9169
|
-
cwd: this.rootDir,
|
|
9170
|
-
ignored: excludePatterns,
|
|
9171
|
-
persistent: true,
|
|
9172
|
-
ignoreInitial: true,
|
|
9173
|
-
// Don't trigger for existing files
|
|
9174
|
-
// Handle atomic saves from modern editors (VS Code, Sublime, etc.)
|
|
9175
|
-
// Editors write to temp file then rename - without this, we get unlink+add instead of change
|
|
9176
|
-
atomic: true,
|
|
9177
|
-
awaitWriteFinish: {
|
|
9178
|
-
stabilityThreshold: 300,
|
|
9179
|
-
// Reduced from 500ms for faster detection
|
|
9180
|
-
pollInterval: 100
|
|
9181
|
-
},
|
|
9182
|
-
// Performance optimizations
|
|
9183
|
-
usePolling: false,
|
|
9184
|
-
interval: 100,
|
|
9185
|
-
binaryInterval: 300
|
|
9186
|
-
});
|
|
9187
|
-
this.watcher.on("add", (filepath) => this.handleChange("add", filepath)).on("change", (filepath) => this.handleChange("change", filepath)).on("unlink", (filepath) => this.handleChange("unlink", filepath)).on("error", (error) => {
|
|
9188
|
-
console.error(`[Lien] File watcher error: ${error}`);
|
|
9189
|
-
});
|
|
9190
|
-
await new Promise((resolve) => {
|
|
9191
|
-
this.watcher.on("ready", () => {
|
|
9192
|
-
resolve();
|
|
9193
|
-
});
|
|
9194
|
-
});
|
|
9195
|
-
}
|
|
9196
|
-
/**
|
|
9197
|
-
* Handles a file change event with debouncing.
|
|
9198
|
-
* Debouncing prevents rapid reindexing when files are saved multiple times quickly.
|
|
9199
|
-
*/
|
|
9200
|
-
handleChange(type, filepath) {
|
|
9201
|
-
const existingTimer = this.debounceTimers.get(filepath);
|
|
9202
|
-
if (existingTimer) {
|
|
9203
|
-
clearTimeout(existingTimer);
|
|
9204
|
-
}
|
|
9205
|
-
const timer = setTimeout(() => {
|
|
9206
|
-
this.debounceTimers.delete(filepath);
|
|
9207
|
-
if (this.onChangeHandler) {
|
|
9208
|
-
const absolutePath = path3.isAbsolute(filepath) ? filepath : path3.join(this.rootDir, filepath);
|
|
9209
|
-
try {
|
|
9210
|
-
const result = this.onChangeHandler({
|
|
9211
|
-
type,
|
|
9212
|
-
filepath: absolutePath
|
|
9213
|
-
});
|
|
9214
|
-
if (result instanceof Promise) {
|
|
9215
|
-
result.catch((error) => {
|
|
9216
|
-
console.error(`[Lien] Error handling file change: ${error}`);
|
|
9217
|
-
});
|
|
9218
|
-
}
|
|
9219
|
-
} catch (error) {
|
|
9220
|
-
console.error(`[Lien] Error handling file change: ${error}`);
|
|
9221
|
-
}
|
|
9222
|
-
}
|
|
9223
|
-
}, this.config.fileWatching.debounceMs);
|
|
9224
|
-
this.debounceTimers.set(filepath, timer);
|
|
9225
|
-
}
|
|
9226
|
-
/**
|
|
9227
|
-
* Stops the file watcher and cleans up resources.
|
|
9228
|
-
*/
|
|
9229
|
-
async stop() {
|
|
9230
|
-
if (!this.watcher) {
|
|
9231
|
-
return;
|
|
9232
|
-
}
|
|
9233
|
-
for (const timer of this.debounceTimers.values()) {
|
|
9234
|
-
clearTimeout(timer);
|
|
9289
|
+
// src/mcp/server-config.ts
|
|
9290
|
+
import { LienError as LienError2, LienErrorCode as LienErrorCode2 } from "@liendev/core";
|
|
9291
|
+
function createMCPServerConfig(name, version) {
|
|
9292
|
+
return {
|
|
9293
|
+
name,
|
|
9294
|
+
version,
|
|
9295
|
+
capabilities: {
|
|
9296
|
+
tools: {},
|
|
9297
|
+
logging: {}
|
|
9235
9298
|
}
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9299
|
+
};
|
|
9300
|
+
}
|
|
9301
|
+
function registerMCPHandlers(server, toolContext, log) {
|
|
9302
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
9303
|
+
return { tools };
|
|
9304
|
+
});
|
|
9305
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
9306
|
+
const { name, arguments: args } = request.params;
|
|
9307
|
+
log(`Handling tool call: ${name}`);
|
|
9308
|
+
const handler = toolHandlers[name];
|
|
9309
|
+
if (!handler) {
|
|
9310
|
+
const error = new LienError2(
|
|
9311
|
+
`Unknown tool: ${name}`,
|
|
9312
|
+
LienErrorCode2.INVALID_INPUT,
|
|
9313
|
+
{ requestedTool: name, availableTools: tools.map((t) => t.name) },
|
|
9314
|
+
"medium",
|
|
9315
|
+
false,
|
|
9316
|
+
false
|
|
9317
|
+
);
|
|
9318
|
+
return {
|
|
9319
|
+
isError: true,
|
|
9320
|
+
content: [{ type: "text", text: JSON.stringify(error.toJSON(), null, 2) }]
|
|
9321
|
+
};
|
|
9247
9322
|
}
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9323
|
+
try {
|
|
9324
|
+
return await handler(args, toolContext);
|
|
9325
|
+
} catch (error) {
|
|
9326
|
+
if (error instanceof LienError2) {
|
|
9327
|
+
return {
|
|
9328
|
+
isError: true,
|
|
9329
|
+
content: [{ type: "text", text: JSON.stringify(error.toJSON(), null, 2) }]
|
|
9330
|
+
};
|
|
9253
9331
|
}
|
|
9332
|
+
console.error(`Unexpected error handling tool call ${name}:`, error);
|
|
9333
|
+
return {
|
|
9334
|
+
isError: true,
|
|
9335
|
+
content: [
|
|
9336
|
+
{
|
|
9337
|
+
type: "text",
|
|
9338
|
+
text: JSON.stringify(
|
|
9339
|
+
{
|
|
9340
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
9341
|
+
code: LienErrorCode2.INTERNAL_ERROR,
|
|
9342
|
+
tool: name
|
|
9343
|
+
},
|
|
9344
|
+
null,
|
|
9345
|
+
2
|
|
9346
|
+
)
|
|
9347
|
+
}
|
|
9348
|
+
]
|
|
9349
|
+
};
|
|
9254
9350
|
}
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
/**
|
|
9258
|
-
* Checks if the watcher is currently running.
|
|
9259
|
-
*/
|
|
9260
|
-
isRunning() {
|
|
9261
|
-
return this.watcher !== null;
|
|
9262
|
-
}
|
|
9263
|
-
};
|
|
9351
|
+
});
|
|
9352
|
+
}
|
|
9264
9353
|
|
|
9265
9354
|
// src/mcp/server.ts
|
|
9266
9355
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
9267
9356
|
var __dirname3 = dirname2(__filename3);
|
|
9268
|
-
var
|
|
9357
|
+
var require3 = createRequire2(import.meta.url);
|
|
9269
9358
|
var packageJson2;
|
|
9270
9359
|
try {
|
|
9271
|
-
packageJson2 =
|
|
9360
|
+
packageJson2 = require3(join2(__dirname3, "../package.json"));
|
|
9272
9361
|
} catch {
|
|
9273
|
-
packageJson2 =
|
|
9362
|
+
packageJson2 = require3(join2(__dirname3, "../../package.json"));
|
|
9274
9363
|
}
|
|
9275
9364
|
async function initializeDatabase(rootDir, log) {
|
|
9276
9365
|
const embeddings = new LocalEmbeddings();
|
|
9277
|
-
|
|
9366
|
+
log("Creating vector database...");
|
|
9367
|
+
const vectorDB = await createVectorDB(rootDir);
|
|
9368
|
+
if (!vectorDB) {
|
|
9369
|
+
throw new Error("createVectorDB returned undefined or null");
|
|
9370
|
+
}
|
|
9371
|
+
if (typeof vectorDB.initialize !== "function") {
|
|
9372
|
+
throw new Error(`Invalid vectorDB instance: ${vectorDB.constructor?.name || "unknown"}. Expected VectorDBInterface but got: ${JSON.stringify(Object.keys(vectorDB))}`);
|
|
9373
|
+
}
|
|
9278
9374
|
log("Loading embedding model...");
|
|
9279
9375
|
await embeddings.initialize();
|
|
9280
9376
|
log("Loading vector database...");
|
|
@@ -9282,9 +9378,9 @@ async function initializeDatabase(rootDir, log) {
|
|
|
9282
9378
|
log("Embeddings and vector DB ready");
|
|
9283
9379
|
return { embeddings, vectorDB };
|
|
9284
9380
|
}
|
|
9285
|
-
async function handleAutoIndexing(vectorDB,
|
|
9381
|
+
async function handleAutoIndexing(vectorDB, rootDir, log) {
|
|
9286
9382
|
const hasIndex = await vectorDB.hasData();
|
|
9287
|
-
if (!hasIndex
|
|
9383
|
+
if (!hasIndex) {
|
|
9288
9384
|
log("\u{1F4E6} No index found - running initial indexing...");
|
|
9289
9385
|
log("\u23F1\uFE0F This may take 5-20 minutes depending on project size");
|
|
9290
9386
|
try {
|
|
@@ -9295,16 +9391,9 @@ async function handleAutoIndexing(vectorDB, config, rootDir, log) {
|
|
|
9295
9391
|
log(`\u26A0\uFE0F Initial indexing failed: ${error}`, "warning");
|
|
9296
9392
|
log("You can manually run: lien index", "warning");
|
|
9297
9393
|
}
|
|
9298
|
-
} else if (!hasIndex) {
|
|
9299
|
-
log("\u26A0\uFE0F No index found. Auto-indexing is disabled in config.", "warning");
|
|
9300
|
-
log('Run "lien index" to index your codebase.', "warning");
|
|
9301
9394
|
}
|
|
9302
9395
|
}
|
|
9303
|
-
async function setupGitDetection(
|
|
9304
|
-
if (!config.gitDetection.enabled) {
|
|
9305
|
-
log("Git detection disabled by configuration");
|
|
9306
|
-
return { gitTracker: null, gitPollInterval: null };
|
|
9307
|
-
}
|
|
9396
|
+
async function setupGitDetection(rootDir, vectorDB, embeddings, verbose, log) {
|
|
9308
9397
|
const gitAvailable = await isGitAvailable();
|
|
9309
9398
|
const isRepo = await isGitRepo2(rootDir);
|
|
9310
9399
|
if (!gitAvailable) {
|
|
@@ -9322,7 +9411,7 @@ async function setupGitDetection(config, rootDir, vectorDB, embeddings, verbose,
|
|
|
9322
9411
|
const changedFiles = await gitTracker.initialize();
|
|
9323
9412
|
if (changedFiles && changedFiles.length > 0) {
|
|
9324
9413
|
log(`\u{1F33F} Git changes detected: ${changedFiles.length} files changed`);
|
|
9325
|
-
const count = await indexMultipleFiles(changedFiles, vectorDB, embeddings,
|
|
9414
|
+
const count = await indexMultipleFiles(changedFiles, vectorDB, embeddings, { verbose });
|
|
9326
9415
|
log(`\u2713 Reindexed ${count} files`);
|
|
9327
9416
|
} else {
|
|
9328
9417
|
log("\u2713 Index is up to date with git state");
|
|
@@ -9330,25 +9419,28 @@ async function setupGitDetection(config, rootDir, vectorDB, embeddings, verbose,
|
|
|
9330
9419
|
} catch (error) {
|
|
9331
9420
|
log(`Failed to check git state on startup: ${error}`, "warning");
|
|
9332
9421
|
}
|
|
9333
|
-
|
|
9422
|
+
const pollIntervalSeconds = DEFAULT_GIT_POLL_INTERVAL_MS2 / 1e3;
|
|
9423
|
+
log(`\u2713 Git detection enabled (checking every ${pollIntervalSeconds}s)`);
|
|
9334
9424
|
const gitPollInterval = setInterval(async () => {
|
|
9335
9425
|
try {
|
|
9336
9426
|
const changedFiles = await gitTracker.detectChanges();
|
|
9337
9427
|
if (changedFiles && changedFiles.length > 0) {
|
|
9338
9428
|
log(`\u{1F33F} Git change detected: ${changedFiles.length} files changed`);
|
|
9339
|
-
indexMultipleFiles(changedFiles, vectorDB, embeddings,
|
|
9429
|
+
indexMultipleFiles(changedFiles, vectorDB, embeddings, { verbose }).then((count) => log(`\u2713 Background reindex complete: ${count} files`)).catch((error) => log(`Background reindex failed: ${error}`, "warning"));
|
|
9340
9430
|
}
|
|
9341
9431
|
} catch (error) {
|
|
9342
9432
|
log(`Git detection check failed: ${error}`, "warning");
|
|
9343
9433
|
}
|
|
9344
|
-
},
|
|
9434
|
+
}, DEFAULT_GIT_POLL_INTERVAL_MS2);
|
|
9345
9435
|
return { gitTracker, gitPollInterval };
|
|
9346
9436
|
}
|
|
9347
|
-
async function setupFileWatching(watch,
|
|
9348
|
-
const fileWatchingEnabled = watch !== void 0 ? watch :
|
|
9349
|
-
if (!fileWatchingEnabled)
|
|
9437
|
+
async function setupFileWatching(watch, rootDir, vectorDB, embeddings, verbose, log) {
|
|
9438
|
+
const fileWatchingEnabled = watch !== void 0 ? watch : true;
|
|
9439
|
+
if (!fileWatchingEnabled) {
|
|
9440
|
+
return null;
|
|
9441
|
+
}
|
|
9350
9442
|
log("\u{1F440} Starting file watcher...");
|
|
9351
|
-
const fileWatcher = new FileWatcher(rootDir
|
|
9443
|
+
const fileWatcher = new FileWatcher(rootDir);
|
|
9352
9444
|
try {
|
|
9353
9445
|
await fileWatcher.start(async (event) => {
|
|
9354
9446
|
const { type, filepath } = event;
|
|
@@ -9365,7 +9457,7 @@ async function setupFileWatching(watch, config, rootDir, vectorDB, embeddings, v
|
|
|
9365
9457
|
} else {
|
|
9366
9458
|
const action = type === "add" ? "added" : "changed";
|
|
9367
9459
|
log(`\u{1F4DD} File ${action}: ${filepath}`);
|
|
9368
|
-
indexSingleFile(filepath, vectorDB, embeddings,
|
|
9460
|
+
indexSingleFile(filepath, vectorDB, embeddings, { verbose }).catch((error) => log(`Failed to reindex ${filepath}: ${error}`, "warning"));
|
|
9369
9461
|
}
|
|
9370
9462
|
});
|
|
9371
9463
|
log(`\u2713 File watching enabled (watching ${fileWatcher.getWatchedFiles().length} files)`);
|
|
@@ -9375,56 +9467,52 @@ async function setupFileWatching(watch, config, rootDir, vectorDB, embeddings, v
|
|
|
9375
9467
|
return null;
|
|
9376
9468
|
}
|
|
9377
9469
|
}
|
|
9378
|
-
function
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
log(
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9470
|
+
function setupTransport(log) {
|
|
9471
|
+
const transport = new StdioServerTransport();
|
|
9472
|
+
transport.onclose = () => {
|
|
9473
|
+
log("Transport closed");
|
|
9474
|
+
};
|
|
9475
|
+
transport.onerror = (error) => {
|
|
9476
|
+
log(`Transport error: ${error}`);
|
|
9477
|
+
};
|
|
9478
|
+
return transport;
|
|
9479
|
+
}
|
|
9480
|
+
function setupCleanupHandlers(versionCheckInterval, gitPollInterval, fileWatcher, log) {
|
|
9481
|
+
return async () => {
|
|
9482
|
+
log("Shutting down MCP server...");
|
|
9483
|
+
clearInterval(versionCheckInterval);
|
|
9484
|
+
if (gitPollInterval) clearInterval(gitPollInterval);
|
|
9485
|
+
if (fileWatcher) await fileWatcher.stop();
|
|
9486
|
+
process.exit(0);
|
|
9487
|
+
};
|
|
9488
|
+
}
|
|
9489
|
+
function setupVersionChecking(vectorDB, log) {
|
|
9490
|
+
const checkAndReconnect = async () => {
|
|
9394
9491
|
try {
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
return { isError: true, content: [{ type: "text", text: JSON.stringify(error.toJSON(), null, 2) }] };
|
|
9492
|
+
if (await vectorDB.checkVersion()) {
|
|
9493
|
+
log("Index version changed, reconnecting...");
|
|
9494
|
+
await vectorDB.reconnect();
|
|
9399
9495
|
}
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
isError: true,
|
|
9403
|
-
content: [{
|
|
9404
|
-
type: "text",
|
|
9405
|
-
text: JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error", code: LienErrorCode2.INTERNAL_ERROR, tool: name }, null, 2)
|
|
9406
|
-
}]
|
|
9407
|
-
};
|
|
9496
|
+
} catch (error) {
|
|
9497
|
+
log(`Version check failed: ${error}`, "warning");
|
|
9408
9498
|
}
|
|
9499
|
+
};
|
|
9500
|
+
const getIndexMetadata = () => ({
|
|
9501
|
+
indexVersion: vectorDB.getCurrentVersion(),
|
|
9502
|
+
indexDate: vectorDB.getVersionDate()
|
|
9409
9503
|
});
|
|
9504
|
+
const interval = setInterval(checkAndReconnect, VERSION_CHECK_INTERVAL_MS);
|
|
9505
|
+
return { interval, checkAndReconnect, getIndexMetadata };
|
|
9410
9506
|
}
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
const earlyLog = (message, level = "info") => {
|
|
9507
|
+
function createEarlyLog(verbose) {
|
|
9508
|
+
return (message, level = "info") => {
|
|
9414
9509
|
if (verbose || level === "warning" || level === "error") {
|
|
9415
9510
|
console.error(`[Lien MCP] [${level}] ${message}`);
|
|
9416
9511
|
}
|
|
9417
9512
|
};
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
process.exit(1);
|
|
9422
|
-
});
|
|
9423
|
-
const server = new Server(
|
|
9424
|
-
{ name: "lien", version: packageJson2.version },
|
|
9425
|
-
{ capabilities: { tools: {}, logging: {} } }
|
|
9426
|
-
);
|
|
9427
|
-
const log = (message, level = "info") => {
|
|
9513
|
+
}
|
|
9514
|
+
function createMCPLog(server, verbose) {
|
|
9515
|
+
return (message, level = "info") => {
|
|
9428
9516
|
if (verbose || level === "warning" || level === "error") {
|
|
9429
9517
|
server.sendLoggingMessage({
|
|
9430
9518
|
level,
|
|
@@ -9435,45 +9523,61 @@ async function startMCPServer(options) {
|
|
|
9435
9523
|
});
|
|
9436
9524
|
}
|
|
9437
9525
|
};
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
}
|
|
9445
|
-
} catch (error) {
|
|
9446
|
-
log(`Version check failed: ${error}`, "warning");
|
|
9526
|
+
}
|
|
9527
|
+
async function initializeComponents(rootDir, earlyLog) {
|
|
9528
|
+
try {
|
|
9529
|
+
const result = await initializeDatabase(rootDir, earlyLog);
|
|
9530
|
+
if (!result.vectorDB || typeof result.vectorDB.initialize !== "function") {
|
|
9531
|
+
throw new Error(`Invalid vectorDB instance: ${result.vectorDB?.constructor?.name || "undefined"}. Missing initialize method.`);
|
|
9447
9532
|
}
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
const
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
};
|
|
9533
|
+
return result;
|
|
9534
|
+
} catch (error) {
|
|
9535
|
+
console.error(`Failed to initialize: ${error}`);
|
|
9536
|
+
if (error instanceof Error && error.stack) {
|
|
9537
|
+
console.error(error.stack);
|
|
9538
|
+
}
|
|
9539
|
+
process.exit(1);
|
|
9540
|
+
}
|
|
9541
|
+
}
|
|
9542
|
+
function createMCPServer() {
|
|
9543
|
+
const serverConfig = createMCPServerConfig("lien", packageJson2.version);
|
|
9544
|
+
return new Server(
|
|
9545
|
+
{ name: serverConfig.name, version: serverConfig.version },
|
|
9546
|
+
{ capabilities: serverConfig.capabilities }
|
|
9547
|
+
);
|
|
9548
|
+
}
|
|
9549
|
+
async function setupAndConnectServer(server, toolContext, log, versionCheckInterval, options) {
|
|
9550
|
+
const { rootDir, verbose, watch } = options;
|
|
9551
|
+
const { vectorDB, embeddings } = toolContext;
|
|
9552
|
+
registerMCPHandlers(server, toolContext, log);
|
|
9553
|
+
await handleAutoIndexing(vectorDB, rootDir, log);
|
|
9554
|
+
const { gitPollInterval } = await setupGitDetection(rootDir, vectorDB, embeddings, verbose, log);
|
|
9555
|
+
const fileWatcher = await setupFileWatching(watch, rootDir, vectorDB, embeddings, verbose, log);
|
|
9556
|
+
const cleanup = setupCleanupHandlers(versionCheckInterval, gitPollInterval, fileWatcher, log);
|
|
9467
9557
|
process.on("SIGINT", cleanup);
|
|
9468
9558
|
process.on("SIGTERM", cleanup);
|
|
9469
|
-
const transport =
|
|
9559
|
+
const transport = setupTransport(log);
|
|
9470
9560
|
transport.onclose = () => {
|
|
9471
|
-
log("Transport closed");
|
|
9472
9561
|
cleanup().catch(() => process.exit(0));
|
|
9473
9562
|
};
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9563
|
+
try {
|
|
9564
|
+
await server.connect(transport);
|
|
9565
|
+
log("MCP server started and listening on stdio");
|
|
9566
|
+
} catch (error) {
|
|
9567
|
+
console.error(`Failed to connect MCP transport: ${error}`);
|
|
9568
|
+
process.exit(1);
|
|
9569
|
+
}
|
|
9570
|
+
}
|
|
9571
|
+
async function startMCPServer(options) {
|
|
9572
|
+
const { rootDir, verbose, watch } = options;
|
|
9573
|
+
const earlyLog = createEarlyLog(verbose);
|
|
9574
|
+
earlyLog("Initializing MCP server...");
|
|
9575
|
+
const { embeddings, vectorDB } = await initializeComponents(rootDir, earlyLog);
|
|
9576
|
+
const server = createMCPServer();
|
|
9577
|
+
const log = createMCPLog(server, verbose);
|
|
9578
|
+
const { interval: versionCheckInterval, checkAndReconnect, getIndexMetadata } = setupVersionChecking(vectorDB, log);
|
|
9579
|
+
const toolContext = { vectorDB, embeddings, rootDir, log, checkAndReconnect, getIndexMetadata };
|
|
9580
|
+
await setupAndConnectServer(server, toolContext, log, versionCheckInterval, { rootDir, verbose, watch });
|
|
9477
9581
|
}
|
|
9478
9582
|
|
|
9479
9583
|
// src/cli/serve.ts
|
|
@@ -9525,8 +9629,7 @@ async function serveCommand(options) {
|
|
|
9525
9629
|
import chalk6 from "chalk";
|
|
9526
9630
|
import fs4 from "fs";
|
|
9527
9631
|
import path5 from "path";
|
|
9528
|
-
import { VectorDB
|
|
9529
|
-
import { configService as configService3 } from "@liendev/core";
|
|
9632
|
+
import { VectorDB } from "@liendev/core";
|
|
9530
9633
|
import { ComplexityAnalyzer as ComplexityAnalyzer2 } from "@liendev/core";
|
|
9531
9634
|
import { formatReport } from "@liendev/core";
|
|
9532
9635
|
var VALID_FAIL_ON = ["error", "warning"];
|
|
@@ -9555,47 +9658,6 @@ function validateFilesExist(files, rootDir) {
|
|
|
9555
9658
|
process.exit(1);
|
|
9556
9659
|
}
|
|
9557
9660
|
}
|
|
9558
|
-
function parseThresholdValue(value, flagName) {
|
|
9559
|
-
if (!value) return null;
|
|
9560
|
-
const parsed = parseInt(value, 10);
|
|
9561
|
-
if (isNaN(parsed)) {
|
|
9562
|
-
console.error(chalk6.red(`Error: Invalid ${flagName} value "${value}". Must be a number`));
|
|
9563
|
-
process.exit(1);
|
|
9564
|
-
}
|
|
9565
|
-
if (parsed <= 0) {
|
|
9566
|
-
console.error(chalk6.red(`Error: Invalid ${flagName} value "${value}". Must be a positive number`));
|
|
9567
|
-
process.exit(1);
|
|
9568
|
-
}
|
|
9569
|
-
return parsed;
|
|
9570
|
-
}
|
|
9571
|
-
function parseThresholdOverrides(options) {
|
|
9572
|
-
const baseThreshold = parseThresholdValue(options.threshold, "--threshold");
|
|
9573
|
-
const cyclomaticOverride = parseThresholdValue(options.cyclomaticThreshold, "--cyclomatic-threshold");
|
|
9574
|
-
const cognitiveOverride = parseThresholdValue(options.cognitiveThreshold, "--cognitive-threshold");
|
|
9575
|
-
return {
|
|
9576
|
-
// Specific flags take precedence over --threshold
|
|
9577
|
-
cyclomatic: cyclomaticOverride ?? baseThreshold,
|
|
9578
|
-
cognitive: cognitiveOverride ?? baseThreshold
|
|
9579
|
-
};
|
|
9580
|
-
}
|
|
9581
|
-
function applyThresholdOverrides(config, overrides) {
|
|
9582
|
-
if (overrides.cyclomatic === null && overrides.cognitive === null) return;
|
|
9583
|
-
const cfg = config;
|
|
9584
|
-
if (!cfg.complexity) {
|
|
9585
|
-
cfg.complexity = {
|
|
9586
|
-
enabled: true,
|
|
9587
|
-
thresholds: { testPaths: 15, mentalLoad: 15 }
|
|
9588
|
-
};
|
|
9589
|
-
} else if (!cfg.complexity.thresholds) {
|
|
9590
|
-
cfg.complexity.thresholds = { testPaths: 15, mentalLoad: 15 };
|
|
9591
|
-
}
|
|
9592
|
-
if (overrides.cyclomatic !== null) {
|
|
9593
|
-
cfg.complexity.thresholds.testPaths = overrides.cyclomatic;
|
|
9594
|
-
}
|
|
9595
|
-
if (overrides.cognitive !== null) {
|
|
9596
|
-
cfg.complexity.thresholds.mentalLoad = overrides.cognitive;
|
|
9597
|
-
}
|
|
9598
|
-
}
|
|
9599
9661
|
async function ensureIndexExists(vectorDB) {
|
|
9600
9662
|
try {
|
|
9601
9663
|
await vectorDB.scanWithFilter({ limit: 1 });
|
|
@@ -9611,13 +9673,14 @@ async function complexityCommand(options) {
|
|
|
9611
9673
|
validateFailOn(options.failOn);
|
|
9612
9674
|
validateFormat(options.format);
|
|
9613
9675
|
validateFilesExist(options.files, rootDir);
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9676
|
+
if (options.threshold || options.cyclomaticThreshold || options.cognitiveThreshold) {
|
|
9677
|
+
console.warn(chalk6.yellow("Warning: Threshold overrides via CLI flags are not supported."));
|
|
9678
|
+
console.warn(chalk6.yellow("Use the MCP tool with threshold parameter for custom thresholds."));
|
|
9679
|
+
}
|
|
9680
|
+
const vectorDB = new VectorDB(rootDir);
|
|
9617
9681
|
await vectorDB.initialize();
|
|
9618
9682
|
await ensureIndexExists(vectorDB);
|
|
9619
|
-
|
|
9620
|
-
const analyzer = new ComplexityAnalyzer2(vectorDB, config);
|
|
9683
|
+
const analyzer = new ComplexityAnalyzer2(vectorDB);
|
|
9621
9684
|
const report = await analyzer.analyze(options.files);
|
|
9622
9685
|
console.log(formatReport(report, options.format));
|
|
9623
9686
|
if (options.failOn) {
|
|
@@ -9633,12 +9696,12 @@ async function complexityCommand(options) {
|
|
|
9633
9696
|
// src/cli/index.ts
|
|
9634
9697
|
var __filename4 = fileURLToPath4(import.meta.url);
|
|
9635
9698
|
var __dirname4 = dirname3(__filename4);
|
|
9636
|
-
var
|
|
9699
|
+
var require4 = createRequire3(import.meta.url);
|
|
9637
9700
|
var packageJson3;
|
|
9638
9701
|
try {
|
|
9639
|
-
packageJson3 =
|
|
9702
|
+
packageJson3 = require4(join3(__dirname4, "../package.json"));
|
|
9640
9703
|
} catch {
|
|
9641
|
-
packageJson3 =
|
|
9704
|
+
packageJson3 = require4(join3(__dirname4, "../../package.json"));
|
|
9642
9705
|
}
|
|
9643
9706
|
var program = new Command();
|
|
9644
9707
|
program.name("lien").description("Local semantic code search for AI assistants via MCP").version(packageJson3.version);
|