@liendev/lien 0.38.1 → 0.39.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}`));
3757
+ } else {
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);
3698
3794
  } 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"));
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 {
@@ -3844,7 +3948,7 @@ async function statusCommand(options = {}) {
3844
3948
  }
3845
3949
  const rootDir = process.cwd();
3846
3950
  const repoId = extractRepoId(rootDir);
3847
- const indexPath = path2.join(os.homedir(), ".lien", "indices", repoId);
3951
+ const indexPath = path2.join(os2.homedir(), ".lien", "indices", repoId);
3848
3952
  if (format === "json") {
3849
3953
  await outputJson(rootDir, indexPath);
3850
3954
  return;
@@ -8703,6 +8807,7 @@ var GetComplexitySchema = external_exports.object({
8703
8807
  threshold: external_exports.number().int().min(1, "Threshold must be at least 1").optional().describe(
8704
8808
  "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
8809
  ),
8810
+ 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
8811
  crossRepo: external_exports.boolean().default(false).describe(
8707
8812
  "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
8813
  ),
@@ -8858,7 +8963,7 @@ Use for tech debt analysis and refactoring prioritization:
8858
8963
 
8859
8964
  Examples:
8860
8965
  get_complexity({ top: 10 })
8861
- get_complexity({ files: ["src/auth.ts", "src/api/user.ts"] })
8966
+ get_complexity({ files: ["src/auth.ts"], metricType: "cognitive" })
8862
8967
  get_complexity({ threshold: 15 })
8863
8968
 
8864
8969
  Returns:
@@ -10183,15 +10288,15 @@ async function fetchCrossRepoChunks(vectorDB, crossRepo, repoIds, log) {
10183
10288
  }
10184
10289
  return { chunks: [], fallback: true };
10185
10290
  }
10186
- function processViolations(report, threshold, top) {
10291
+ function processViolations(report, threshold, top, metricType) {
10187
10292
  const allViolations = (0, import_collect.default)(Object.entries(report.files)).flatMap(
10188
10293
  ([
10189
10294
  ,
10190
10295
  /* filepath unused */
10191
10296
  fileData
10192
- ]) => fileData.violations.map((v) => transformViolation(v, fileData))
10297
+ ]) => fileData.violations.filter((v) => !metricType || v.metricType === metricType).filter((v) => threshold === void 0 || v.complexity >= threshold).map((v) => transformViolation(v, fileData))
10193
10298
  ).sortByDesc("complexity").all();
10194
- const violations = threshold !== void 0 ? allViolations.filter((v) => v.complexity >= threshold) : allViolations;
10299
+ const violations = allViolations;
10195
10300
  const severityCounts = (0, import_collect.default)(violations).countBy("severity").all();
10196
10301
  return {
10197
10302
  violations,
@@ -10208,7 +10313,7 @@ function buildCrossRepoFallbackNote(fallback) {
10208
10313
  async function handleGetComplexity(args, ctx) {
10209
10314
  const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
10210
10315
  return await wrapToolHandler(GetComplexitySchema, async (validatedArgs) => {
10211
- const { crossRepo, repoIds, files, top, threshold } = validatedArgs;
10316
+ const { crossRepo, repoIds, files, top, threshold, metricType } = validatedArgs;
10212
10317
  log(`Analyzing complexity${crossRepo ? " (cross-repo)" : ""}...`);
10213
10318
  await checkAndReconnect();
10214
10319
  const { chunks: allChunks, fallback } = await fetchCrossRepoChunks(
@@ -10223,7 +10328,8 @@ async function handleGetComplexity(args, ctx) {
10223
10328
  const { violations, topViolations, bySeverity } = processViolations(
10224
10329
  report,
10225
10330
  threshold,
10226
- top ?? 10
10331
+ top ?? 10,
10332
+ metricType
10227
10333
  );
10228
10334
  const note = buildCrossRepoFallbackNote(fallback);
10229
10335
  if (note) {
@@ -11318,9 +11424,9 @@ async function complexityCommand(options) {
11318
11424
  // src/cli/config.ts
11319
11425
  import chalk8 from "chalk";
11320
11426
  import path9 from "path";
11321
- import os2 from "os";
11427
+ import os3 from "os";
11322
11428
  import { loadGlobalConfig, mergeGlobalConfig } from "@liendev/core";
11323
- var CONFIG_PATH = path9.join(os2.homedir(), ".lien", "config.json");
11429
+ var CONFIG_PATH = path9.join(os3.homedir(), ".lien", "config.json");
11324
11430
  var ALLOWED_KEYS = {
11325
11431
  backend: {
11326
11432
  values: ["lancedb", "qdrant"],
@@ -11418,7 +11524,16 @@ try {
11418
11524
  }
11419
11525
  var program = new Command();
11420
11526
  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);
11527
+ program.command("init").description("Initialize Lien in the current directory").addOption(
11528
+ new Option("-e, --editor <editor>", "Editor to configure MCP for").choices([
11529
+ "cursor",
11530
+ "claude-code",
11531
+ "windsurf",
11532
+ "opencode",
11533
+ "kilo-code",
11534
+ "antigravity"
11535
+ ])
11536
+ ).option("-p, --path <path>", "Path to initialize (defaults to current directory)").action(initCommand);
11422
11537
  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
11538
  program.command("serve").description(
11424
11539
  "Start the MCP server (works with Cursor, Claude Code, Windsurf, and any MCP client)"