@liendev/lien 0.38.1 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +161 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -3666,42 +3666,146 @@ import { dirname as dirname3, join as join3 } from "path";
|
|
|
3666
3666
|
// src/cli/init.ts
|
|
3667
3667
|
init_banner();
|
|
3668
3668
|
import fs from "fs/promises";
|
|
3669
|
+
import os from "os";
|
|
3669
3670
|
import path from "path";
|
|
3670
3671
|
import chalk2 from "chalk";
|
|
3671
|
-
var
|
|
3672
|
-
|
|
3673
|
-
|
|
3672
|
+
var EDITORS = {
|
|
3673
|
+
cursor: {
|
|
3674
|
+
name: "Cursor",
|
|
3675
|
+
configPath: (rootDir) => path.join(rootDir, ".cursor", "mcp.json"),
|
|
3676
|
+
configKey: "mcpServers",
|
|
3677
|
+
buildEntry: () => ({ command: "lien", args: ["serve"] }),
|
|
3678
|
+
restartMessage: "Restart Cursor to activate."
|
|
3679
|
+
},
|
|
3680
|
+
"claude-code": {
|
|
3681
|
+
name: "Claude Code",
|
|
3682
|
+
configPath: (rootDir) => path.join(rootDir, ".mcp.json"),
|
|
3683
|
+
configKey: "mcpServers",
|
|
3684
|
+
buildEntry: () => ({ command: "lien", args: ["serve"] }),
|
|
3685
|
+
restartMessage: "Restart Claude Code to activate."
|
|
3686
|
+
},
|
|
3687
|
+
windsurf: {
|
|
3688
|
+
name: "Windsurf",
|
|
3689
|
+
configPath: () => path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
3690
|
+
configKey: "mcpServers",
|
|
3691
|
+
buildEntry: (rootDir) => ({
|
|
3692
|
+
command: "lien",
|
|
3693
|
+
args: ["serve", "--root", path.resolve(rootDir)]
|
|
3694
|
+
}),
|
|
3695
|
+
restartMessage: "Restart Windsurf to activate."
|
|
3696
|
+
},
|
|
3697
|
+
opencode: {
|
|
3698
|
+
name: "OpenCode",
|
|
3699
|
+
configPath: (rootDir) => path.join(rootDir, "opencode.json"),
|
|
3700
|
+
configKey: "mcp",
|
|
3701
|
+
buildEntry: () => ({ type: "local", command: ["lien", "serve"] }),
|
|
3702
|
+
restartMessage: "Restart OpenCode to activate."
|
|
3703
|
+
},
|
|
3704
|
+
"kilo-code": {
|
|
3705
|
+
name: "Kilo Code",
|
|
3706
|
+
configPath: (rootDir) => path.join(rootDir, ".kilocode", "mcp.json"),
|
|
3707
|
+
configKey: "mcpServers",
|
|
3708
|
+
buildEntry: () => ({ command: "lien", args: ["serve"] }),
|
|
3709
|
+
restartMessage: "Restart VS Code to activate."
|
|
3710
|
+
},
|
|
3711
|
+
antigravity: {
|
|
3712
|
+
name: "Antigravity",
|
|
3713
|
+
configPath: null,
|
|
3714
|
+
configKey: "mcpServers",
|
|
3715
|
+
buildEntry: () => ({ command: "lien", args: ["serve"] }),
|
|
3716
|
+
restartMessage: "Add this to your Antigravity MCP settings."
|
|
3717
|
+
}
|
|
3674
3718
|
};
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
const
|
|
3680
|
-
|
|
3719
|
+
function isPlainObject(value) {
|
|
3720
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3721
|
+
}
|
|
3722
|
+
function displayPath(configPath, rootDir) {
|
|
3723
|
+
const rel = path.relative(rootDir, configPath);
|
|
3724
|
+
if (!rel.startsWith("..")) return rel;
|
|
3725
|
+
const home = os.homedir();
|
|
3726
|
+
if (configPath.startsWith(home)) return "~" + configPath.slice(home.length);
|
|
3727
|
+
return configPath;
|
|
3728
|
+
}
|
|
3729
|
+
async function readJsonFile(filePath) {
|
|
3681
3730
|
try {
|
|
3682
|
-
const raw = await fs.readFile(
|
|
3731
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
3683
3732
|
const parsed = JSON.parse(raw);
|
|
3684
|
-
|
|
3685
|
-
existingConfig = parsed;
|
|
3686
|
-
}
|
|
3733
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
3687
3734
|
} catch {
|
|
3735
|
+
return null;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
async function writeEditorConfig(editor, rootDir) {
|
|
3739
|
+
const configPath = editor.configPath(rootDir);
|
|
3740
|
+
const entry = editor.buildEntry(rootDir);
|
|
3741
|
+
const key = editor.configKey;
|
|
3742
|
+
const label = displayPath(configPath, rootDir);
|
|
3743
|
+
const existingConfig = await readJsonFile(configPath);
|
|
3744
|
+
const existingSection = existingConfig?.[key];
|
|
3745
|
+
if (existingSection?.lien) {
|
|
3746
|
+
console.log(chalk2.green(`
|
|
3747
|
+
\u2713 Already configured \u2014 ${label} contains lien entry`));
|
|
3748
|
+
return;
|
|
3688
3749
|
}
|
|
3689
|
-
if (existingConfig
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
await fs.writeFile(mcpConfigPath, JSON.stringify(existingConfig, null, 2) + "\n");
|
|
3697
|
-
console.log(chalk2.green("\n\u2713 Added lien to existing .cursor/mcp.json"));
|
|
3750
|
+
if (existingConfig) {
|
|
3751
|
+
const section = isPlainObject(existingSection) ? { ...existingSection } : {};
|
|
3752
|
+
section.lien = entry;
|
|
3753
|
+
existingConfig[key] = section;
|
|
3754
|
+
await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2) + "\n");
|
|
3755
|
+
console.log(chalk2.green(`
|
|
3756
|
+
\u2713 Added lien to existing ${label}`));
|
|
3698
3757
|
} else {
|
|
3699
|
-
await fs.mkdir(
|
|
3700
|
-
const config = {
|
|
3701
|
-
await fs.writeFile(
|
|
3702
|
-
console.log(chalk2.green(
|
|
3758
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
3759
|
+
const config = { [key]: { lien: entry } };
|
|
3760
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3761
|
+
console.log(chalk2.green(`
|
|
3762
|
+
\u2713 Created ${label}`));
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
async function promptForEditor() {
|
|
3766
|
+
const { default: inquirer } = await import("inquirer");
|
|
3767
|
+
const { editor } = await inquirer.prompt([
|
|
3768
|
+
{
|
|
3769
|
+
type: "list",
|
|
3770
|
+
name: "editor",
|
|
3771
|
+
message: "Which editor are you using?",
|
|
3772
|
+
choices: [
|
|
3773
|
+
{ name: "Cursor", value: "cursor" },
|
|
3774
|
+
{ name: "Claude Code", value: "claude-code" },
|
|
3775
|
+
{ name: "Windsurf", value: "windsurf" },
|
|
3776
|
+
{ name: "OpenCode", value: "opencode" },
|
|
3777
|
+
{ name: "Kilo Code", value: "kilo-code" },
|
|
3778
|
+
{ name: "Antigravity", value: "antigravity" }
|
|
3779
|
+
],
|
|
3780
|
+
default: "cursor"
|
|
3781
|
+
}
|
|
3782
|
+
]);
|
|
3783
|
+
return editor;
|
|
3784
|
+
}
|
|
3785
|
+
async function initCommand(options = {}) {
|
|
3786
|
+
showCompactBanner();
|
|
3787
|
+
const rootDir = options.path || process.cwd();
|
|
3788
|
+
let editorId;
|
|
3789
|
+
if (options.editor) {
|
|
3790
|
+
editorId = options.editor;
|
|
3791
|
+
} else if (!process.stdout.isTTY) {
|
|
3792
|
+
console.error(chalk2.red("Error: Use --editor to specify your editor in non-interactive mode."));
|
|
3793
|
+
process.exit(1);
|
|
3794
|
+
} else {
|
|
3795
|
+
editorId = await promptForEditor();
|
|
3796
|
+
}
|
|
3797
|
+
const editor = EDITORS[editorId];
|
|
3798
|
+
if (editor.configPath) {
|
|
3799
|
+
await writeEditorConfig(editor, rootDir);
|
|
3800
|
+
console.log(chalk2.dim(` ${editor.restartMessage}
|
|
3801
|
+
`));
|
|
3802
|
+
} else {
|
|
3803
|
+
const entry = editor.buildEntry(rootDir);
|
|
3804
|
+
const snippet = { [editor.configKey]: { lien: entry } };
|
|
3805
|
+
console.log(chalk2.yellow(`
|
|
3806
|
+
${editor.restartMessage}`));
|
|
3807
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
3703
3808
|
}
|
|
3704
|
-
console.log(chalk2.dim(" Restart Cursor to activate.\n"));
|
|
3705
3809
|
const legacyConfigPath = path.join(rootDir, ".lien.config.json");
|
|
3706
3810
|
try {
|
|
3707
3811
|
await fs.access(legacyConfigPath);
|
|
@@ -3716,7 +3820,7 @@ init_banner();
|
|
|
3716
3820
|
import chalk3 from "chalk";
|
|
3717
3821
|
import fs2 from "fs/promises";
|
|
3718
3822
|
import path2 from "path";
|
|
3719
|
-
import
|
|
3823
|
+
import os2 from "os";
|
|
3720
3824
|
import { createRequire as createRequire2 } from "module";
|
|
3721
3825
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3722
3826
|
import {
|
|
@@ -3724,13 +3828,11 @@ import {
|
|
|
3724
3828
|
getCurrentBranch,
|
|
3725
3829
|
getCurrentCommit,
|
|
3726
3830
|
readVersionFile,
|
|
3727
|
-
extractRepoId,
|
|
3728
3831
|
DEFAULT_CONCURRENCY,
|
|
3729
3832
|
DEFAULT_EMBEDDING_BATCH_SIZE,
|
|
3730
|
-
DEFAULT_CHUNK_SIZE,
|
|
3731
|
-
DEFAULT_CHUNK_OVERLAP,
|
|
3732
3833
|
DEFAULT_GIT_POLL_INTERVAL_MS
|
|
3733
3834
|
} from "@liendev/core";
|
|
3835
|
+
import { extractRepoId, DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_OVERLAP } from "@liendev/parser";
|
|
3734
3836
|
var VALID_FORMATS = ["text", "json"];
|
|
3735
3837
|
async function getFileStats(filePath) {
|
|
3736
3838
|
try {
|
|
@@ -3844,7 +3946,7 @@ async function statusCommand(options = {}) {
|
|
|
3844
3946
|
}
|
|
3845
3947
|
const rootDir = process.cwd();
|
|
3846
3948
|
const repoId = extractRepoId(rootDir);
|
|
3847
|
-
const indexPath = path2.join(
|
|
3949
|
+
const indexPath = path2.join(os2.homedir(), ".lien", "indices", repoId);
|
|
3848
3950
|
if (format === "json") {
|
|
3849
3951
|
await outputJson(rootDir, indexPath);
|
|
3850
3952
|
return;
|
|
@@ -4147,7 +4249,7 @@ import {
|
|
|
4147
4249
|
detectEcosystems,
|
|
4148
4250
|
getEcosystemExcludePatterns,
|
|
4149
4251
|
ALWAYS_IGNORE_PATTERNS
|
|
4150
|
-
} from "@liendev/
|
|
4252
|
+
} from "@liendev/parser";
|
|
4151
4253
|
var FileWatcher = class {
|
|
4152
4254
|
watcher = null;
|
|
4153
4255
|
rootDir;
|
|
@@ -8703,6 +8805,7 @@ var GetComplexitySchema = external_exports.object({
|
|
|
8703
8805
|
threshold: external_exports.number().int().min(1, "Threshold must be at least 1").optional().describe(
|
|
8704
8806
|
"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."
|
|
8705
8807
|
),
|
|
8808
|
+
metricType: external_exports.enum(["cyclomatic", "cognitive", "halstead_effort", "halstead_bugs"]).optional().describe("Filter violations to a specific metric type. If omitted, returns all types."),
|
|
8706
8809
|
crossRepo: external_exports.boolean().default(false).describe(
|
|
8707
8810
|
"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."
|
|
8708
8811
|
),
|
|
@@ -8858,7 +8961,7 @@ Use for tech debt analysis and refactoring prioritization:
|
|
|
8858
8961
|
|
|
8859
8962
|
Examples:
|
|
8860
8963
|
get_complexity({ top: 10 })
|
|
8861
|
-
get_complexity({ files: ["src/auth.ts", "
|
|
8964
|
+
get_complexity({ files: ["src/auth.ts"], metricType: "cognitive" })
|
|
8862
8965
|
get_complexity({ threshold: 15 })
|
|
8863
8966
|
|
|
8864
8967
|
Returns:
|
|
@@ -9287,7 +9390,7 @@ import {
|
|
|
9287
9390
|
getCanonicalPath,
|
|
9288
9391
|
isTestFile,
|
|
9289
9392
|
MAX_CHUNKS_PER_FILE
|
|
9290
|
-
} from "@liendev/
|
|
9393
|
+
} from "@liendev/parser";
|
|
9291
9394
|
var SCAN_LIMIT = 1e4;
|
|
9292
9395
|
async function searchFileChunks(filepaths, ctx) {
|
|
9293
9396
|
const { vectorDB, workspaceRoot } = ctx;
|
|
@@ -9543,7 +9646,7 @@ import {
|
|
|
9543
9646
|
matchesFile as matchesFile2,
|
|
9544
9647
|
getCanonicalPath as getCanonicalPath2,
|
|
9545
9648
|
isTestFile as isTestFile2
|
|
9546
|
-
} from "@liendev/
|
|
9649
|
+
} from "@liendev/parser";
|
|
9547
9650
|
var COMPLEXITY_THRESHOLDS = {
|
|
9548
9651
|
HIGH_COMPLEXITY_DEPENDENT: 10,
|
|
9549
9652
|
// Individual file is complex
|
|
@@ -10183,15 +10286,15 @@ async function fetchCrossRepoChunks(vectorDB, crossRepo, repoIds, log) {
|
|
|
10183
10286
|
}
|
|
10184
10287
|
return { chunks: [], fallback: true };
|
|
10185
10288
|
}
|
|
10186
|
-
function processViolations(report, threshold, top) {
|
|
10289
|
+
function processViolations(report, threshold, top, metricType) {
|
|
10187
10290
|
const allViolations = (0, import_collect.default)(Object.entries(report.files)).flatMap(
|
|
10188
10291
|
([
|
|
10189
10292
|
,
|
|
10190
10293
|
/* filepath unused */
|
|
10191
10294
|
fileData
|
|
10192
|
-
]) => fileData.violations.map((v) => transformViolation(v, fileData))
|
|
10295
|
+
]) => fileData.violations.filter((v) => !metricType || v.metricType === metricType).filter((v) => threshold === void 0 || v.complexity >= threshold).map((v) => transformViolation(v, fileData))
|
|
10193
10296
|
).sortByDesc("complexity").all();
|
|
10194
|
-
const violations =
|
|
10297
|
+
const violations = allViolations;
|
|
10195
10298
|
const severityCounts = (0, import_collect.default)(violations).countBy("severity").all();
|
|
10196
10299
|
return {
|
|
10197
10300
|
violations,
|
|
@@ -10208,7 +10311,7 @@ function buildCrossRepoFallbackNote(fallback) {
|
|
|
10208
10311
|
async function handleGetComplexity(args, ctx) {
|
|
10209
10312
|
const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
10210
10313
|
return await wrapToolHandler(GetComplexitySchema, async (validatedArgs) => {
|
|
10211
|
-
const { crossRepo, repoIds, files, top, threshold } = validatedArgs;
|
|
10314
|
+
const { crossRepo, repoIds, files, top, threshold, metricType } = validatedArgs;
|
|
10212
10315
|
log(`Analyzing complexity${crossRepo ? " (cross-repo)" : ""}...`);
|
|
10213
10316
|
await checkAndReconnect();
|
|
10214
10317
|
const { chunks: allChunks, fallback } = await fetchCrossRepoChunks(
|
|
@@ -10223,7 +10326,8 @@ async function handleGetComplexity(args, ctx) {
|
|
|
10223
10326
|
const { violations, topViolations, bySeverity } = processViolations(
|
|
10224
10327
|
report,
|
|
10225
10328
|
threshold,
|
|
10226
|
-
top ?? 10
|
|
10329
|
+
top ?? 10,
|
|
10330
|
+
metricType
|
|
10227
10331
|
);
|
|
10228
10332
|
const note = buildCrossRepoFallbackNote(fallback);
|
|
10229
10333
|
if (note) {
|
|
@@ -10460,9 +10564,9 @@ import {
|
|
|
10460
10564
|
indexMultipleFiles as indexMultipleFiles2,
|
|
10461
10565
|
isGitAvailable,
|
|
10462
10566
|
isGitRepo as isGitRepo2,
|
|
10463
|
-
DEFAULT_GIT_POLL_INTERVAL_MS as DEFAULT_GIT_POLL_INTERVAL_MS2
|
|
10464
|
-
createGitignoreFilter as createGitignoreFilter2
|
|
10567
|
+
DEFAULT_GIT_POLL_INTERVAL_MS as DEFAULT_GIT_POLL_INTERVAL_MS2
|
|
10465
10568
|
} from "@liendev/core";
|
|
10569
|
+
import { createGitignoreFilter as createGitignoreFilter2 } from "@liendev/parser";
|
|
10466
10570
|
|
|
10467
10571
|
// src/mcp/file-change-handler.ts
|
|
10468
10572
|
import fs3 from "fs/promises";
|
|
@@ -10470,10 +10574,9 @@ import {
|
|
|
10470
10574
|
indexMultipleFiles,
|
|
10471
10575
|
indexSingleFile,
|
|
10472
10576
|
ManifestManager,
|
|
10473
|
-
|
|
10474
|
-
normalizeToRelativePath as normalizeToRelativePath2,
|
|
10475
|
-
createGitignoreFilter
|
|
10577
|
+
normalizeToRelativePath as normalizeToRelativePath2
|
|
10476
10578
|
} from "@liendev/core";
|
|
10579
|
+
import { computeContentHash, createGitignoreFilter } from "@liendev/parser";
|
|
10477
10580
|
async function handleFileDeletion(filepath, vectorDB, manifest, log) {
|
|
10478
10581
|
log(`\u{1F5D1}\uFE0F File deleted: ${filepath}`);
|
|
10479
10582
|
try {
|
|
@@ -11318,9 +11421,9 @@ async function complexityCommand(options) {
|
|
|
11318
11421
|
// src/cli/config.ts
|
|
11319
11422
|
import chalk8 from "chalk";
|
|
11320
11423
|
import path9 from "path";
|
|
11321
|
-
import
|
|
11424
|
+
import os3 from "os";
|
|
11322
11425
|
import { loadGlobalConfig, mergeGlobalConfig } from "@liendev/core";
|
|
11323
|
-
var CONFIG_PATH = path9.join(
|
|
11426
|
+
var CONFIG_PATH = path9.join(os3.homedir(), ".lien", "config.json");
|
|
11324
11427
|
var ALLOWED_KEYS = {
|
|
11325
11428
|
backend: {
|
|
11326
11429
|
values: ["lancedb", "qdrant"],
|
|
@@ -11418,7 +11521,16 @@ try {
|
|
|
11418
11521
|
}
|
|
11419
11522
|
var program = new Command();
|
|
11420
11523
|
program.name("lien").description("Local semantic code search for AI assistants via MCP").version(packageJson3.version);
|
|
11421
|
-
program.command("init").description("Initialize Lien in the current directory").
|
|
11524
|
+
program.command("init").description("Initialize Lien in the current directory").addOption(
|
|
11525
|
+
new Option("-e, --editor <editor>", "Editor to configure MCP for").choices([
|
|
11526
|
+
"cursor",
|
|
11527
|
+
"claude-code",
|
|
11528
|
+
"windsurf",
|
|
11529
|
+
"opencode",
|
|
11530
|
+
"kilo-code",
|
|
11531
|
+
"antigravity"
|
|
11532
|
+
])
|
|
11533
|
+
).option("-p, --path <path>", "Path to initialize (defaults to current directory)").action(initCommand);
|
|
11422
11534
|
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);
|
|
11423
11535
|
program.command("serve").description(
|
|
11424
11536
|
"Start the MCP server (works with Cursor, Claude Code, Windsurf, and any MCP client)"
|