@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 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 MCP_CONFIG = {
3672
- command: "lien",
3673
- args: ["serve"]
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
- async function initCommand(options = {}) {
3676
- showCompactBanner();
3677
- const rootDir = options.path || process.cwd();
3678
- const cursorDir = path.join(rootDir, ".cursor");
3679
- const mcpConfigPath = path.join(cursorDir, "mcp.json");
3680
- let existingConfig = null;
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(mcpConfigPath, "utf-8");
3731
+ const raw = await fs.readFile(filePath, "utf-8");
3683
3732
  const parsed = JSON.parse(raw);
3684
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
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?.mcpServers?.lien) {
3690
- console.log(chalk2.green("\n\u2713 Already configured \u2014 .cursor/mcp.json contains lien entry"));
3691
- } else if (existingConfig) {
3692
- const servers = existingConfig.mcpServers;
3693
- const safeServers = servers && typeof servers === "object" && !Array.isArray(servers) ? servers : {};
3694
- safeServers.lien = MCP_CONFIG;
3695
- existingConfig.mcpServers = safeServers;
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(cursorDir, { recursive: true });
3700
- const config = { mcpServers: { lien: MCP_CONFIG } };
3701
- await fs.writeFile(mcpConfigPath, JSON.stringify(config, null, 2) + "\n");
3702
- console.log(chalk2.green("\n\u2713 Created .cursor/mcp.json"));
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 os from "os";
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(os.homedir(), ".lien", "indices", repoId);
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/core";
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", "src/api/user.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/core";
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/core";
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 = threshold !== void 0 ? allViolations.filter((v) => v.complexity >= threshold) : allViolations;
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
- computeContentHash,
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 os2 from "os";
11424
+ import os3 from "os";
11322
11425
  import { loadGlobalConfig, mergeGlobalConfig } from "@liendev/core";
11323
- var CONFIG_PATH = path9.join(os2.homedir(), ".lien", "config.json");
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").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);
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)"