@liendev/lien 0.23.0 → 0.25.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Give AI deep understanding of your codebase through semantic search. 100% local, 100% private.**
6
6
 
7
- Lien connects AI coding assistants like Cursor to your codebase through the Model Context Protocol (MCP). Ask questions in natural language, get precise answers from semantic search—all running locally on your machine.
7
+ Lien connects AI coding assistants like Cursor and Claude Code to your codebase through the Model Context Protocol (MCP). Ask questions in natural language, get precise answers from semantic search—all running locally on your machine.
8
8
 
9
9
  📚 **[Full Documentation](https://lien.dev)** | 🚀 **[Getting Started](https://lien.dev/guide/getting-started)** | 🔍 **[How It Works](https://lien.dev/how-it-works)**
10
10
 
@@ -15,7 +15,7 @@ Lien connects AI coding assistants like Cursor to your codebase through the Mode
15
15
  - 🔒 **100% Local & Private** - All code analysis happens on your machine
16
16
  - 🚀 **Semantic Search** - Natural language queries: "How does authentication work?"
17
17
  - 🌐 **Cross-Repo Search** - Search across all repositories in your organization (Qdrant backend)
18
- - 🎯 **MCP Integration** - Works seamlessly with Cursor and other MCP-compatible tools
18
+ - 🎯 **MCP Integration** - Works seamlessly with Cursor, Claude Code, and other MCP-compatible tools
19
19
  - ⚡ **Fast** - Sub-500ms queries, minutes to index large codebases
20
20
  - 🆓 **Free Forever** - No API costs, no subscriptions, no usage limits
21
21
  - 📦 **Framework-Aware** - Auto-detects Node.js, Laravel, Shopify; supports 15+ languages
@@ -26,15 +26,10 @@ Lien connects AI coding assistants like Cursor to your codebase through the Mode
26
26
  ## Quick Start
27
27
 
28
28
  ```bash
29
- # Install
29
+ # 1. Install
30
30
  npm install -g @liendev/lien
31
31
 
32
- # Setup in your project
33
- cd /path/to/your/project
34
- lien init
35
- lien index
36
-
37
- # Configure Cursor - create .cursor/mcp.json
32
+ # 2. Add to your project - create .cursor/mcp.json
38
33
  {
39
34
  "mcpServers": {
40
35
  "lien": {
@@ -44,9 +39,11 @@ lien index
44
39
  }
45
40
  }
46
41
 
47
- # Restart Cursor and start asking questions!
42
+ # 3. Restart your AI assistant (Cursor, Claude Code) and start asking questions!
48
43
  ```
49
44
 
45
+ That's it—zero configuration needed. Lien auto-detects your project and indexes on first use.
46
+
50
47
  **👉 [Full installation guide](https://lien.dev/guide/installation)**
51
48
 
52
49
  ### Qdrant Backend (Cross-Repo Search)
@@ -102,7 +99,7 @@ Lien tracks code complexity with intuitive outputs:
102
99
  ## Documentation
103
100
 
104
101
  - **[Installation](https://lien.dev/guide/installation)** - npm, npx, or local setup
105
- - **[Getting Started](https://lien.dev/guide/getting-started)** - Step-by-step configuration for Cursor
102
+ - **[Getting Started](https://lien.dev/guide/getting-started)** - Step-by-step configuration for Cursor or Claude Code
106
103
  - **[Configuration](https://lien.dev/guide/configuration)** - Customize indexing, thresholds, performance
107
104
  - **[CLI Commands](https://lien.dev/guide/cli-commands)** - Full command reference
108
105
  - **[MCP Tools](https://lien.dev/guide/mcp-tools)** - Complete API reference for all 6 tools
package/dist/index.js CHANGED
@@ -3591,15 +3591,13 @@ var require_dist = __commonJS({
3591
3591
  // src/cli/index.ts
3592
3592
  import { Command } from "commander";
3593
3593
  import { createRequire as createRequire3 } from "module";
3594
- import { fileURLToPath as fileURLToPath4 } from "url";
3594
+ import { fileURLToPath as fileURLToPath3 } from "url";
3595
3595
  import { dirname as dirname3, join as join3 } from "path";
3596
3596
 
3597
3597
  // src/cli/init.ts
3598
3598
  import fs from "fs/promises";
3599
3599
  import path from "path";
3600
- import { fileURLToPath as fileURLToPath2 } from "url";
3601
3600
  import chalk2 from "chalk";
3602
- import inquirer from "inquirer";
3603
3601
 
3604
3602
  // src/utils/banner.ts
3605
3603
  import figlet from "figlet";
@@ -3661,8 +3659,6 @@ function showCompactBanner() {
3661
3659
  }
3662
3660
 
3663
3661
  // src/cli/init.ts
3664
- var __filename2 = fileURLToPath2(import.meta.url);
3665
- var __dirname2 = path.dirname(__filename2);
3666
3662
  async function initCommand(options = {}) {
3667
3663
  showCompactBanner();
3668
3664
  console.log(chalk2.bold("\nLien Initialization\n"));
@@ -3695,129 +3691,6 @@ async function initCommand(options = {}) {
3695
3691
  console.log(chalk2.dim(" You can safely delete it."));
3696
3692
  } catch {
3697
3693
  }
3698
- await promptAndInstallCursorRules(rootDir, options);
3699
- }
3700
- async function getPathType(filepath) {
3701
- try {
3702
- const stats = await fs.stat(filepath);
3703
- if (stats.isDirectory()) return "directory";
3704
- if (stats.isFile()) return "file";
3705
- return "other";
3706
- } catch {
3707
- return "none";
3708
- }
3709
- }
3710
- async function convertRulesFileToDirectory(rulesPath, templatePath) {
3711
- const existingRules = await fs.readFile(rulesPath, "utf-8");
3712
- const parentDir = path.dirname(rulesPath);
3713
- const baseName = path.basename(rulesPath);
3714
- const tempDir = await fs.mkdtemp(path.join(parentDir, baseName + "_tmp_"));
3715
- const backupPath = rulesPath + ".backup";
3716
- try {
3717
- await fs.writeFile(path.join(tempDir, "project.mdc"), existingRules);
3718
- await fs.copyFile(templatePath, path.join(tempDir, "lien.mdc"));
3719
- try {
3720
- await fs.unlink(backupPath);
3721
- } catch {
3722
- }
3723
- await fs.rename(rulesPath, backupPath);
3724
- try {
3725
- await fs.rename(tempDir, rulesPath);
3726
- try {
3727
- await fs.unlink(backupPath);
3728
- } catch {
3729
- console.log(chalk2.yellow("\u26A0\uFE0F Could not remove backup file, but conversion succeeded"));
3730
- console.log(chalk2.dim(`Backup file: ${backupPath}`));
3731
- }
3732
- } catch (renameErr) {
3733
- try {
3734
- await fs.rename(backupPath, rulesPath);
3735
- } catch (restoreErr) {
3736
- console.log(chalk2.red("\u274C Failed to restore original .cursor/rules from backup after failed conversion."));
3737
- console.log(chalk2.red(` - Original error: ${renameErr instanceof Error ? renameErr.message : renameErr}`));
3738
- console.log(chalk2.red(` - Restore error: ${restoreErr instanceof Error ? restoreErr.message : restoreErr}`));
3739
- console.log(chalk2.red(` - Backup file location: ${backupPath}`));
3740
- throw new Error("Failed to convert .cursor/rules to directory and failed to restore from backup. Manual recovery needed.");
3741
- }
3742
- throw renameErr;
3743
- }
3744
- console.log(chalk2.green("\u2713 Converted .cursor/rules to directory"));
3745
- console.log(chalk2.green(" - Your project rules: .cursor/rules/project.mdc"));
3746
- console.log(chalk2.green(" - Lien rules: .cursor/rules/lien.mdc"));
3747
- } catch (err) {
3748
- try {
3749
- await fs.rm(tempDir, { recursive: true, force: true });
3750
- } catch {
3751
- }
3752
- throw err;
3753
- }
3754
- }
3755
- async function handleExistingRulesDirectory(rulesPath, templatePath) {
3756
- const targetPath = path.join(rulesPath, "lien.mdc");
3757
- try {
3758
- await fs.access(targetPath);
3759
- console.log(chalk2.dim("lien.mdc already exists in .cursor/rules/, skipping..."));
3760
- return;
3761
- } catch {
3762
- }
3763
- await fs.copyFile(templatePath, targetPath);
3764
- console.log(chalk2.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
3765
- }
3766
- async function handleExistingRulesFile(rulesPath, templatePath, options) {
3767
- if (options.yes) {
3768
- console.log(chalk2.dim("Skipped Cursor rules installation (preserving existing .cursor/rules file)"));
3769
- return;
3770
- }
3771
- const { convertToDir } = await inquirer.prompt([{
3772
- type: "confirm",
3773
- name: "convertToDir",
3774
- message: "Existing .cursor/rules file found. Convert to directory and preserve your rules?",
3775
- default: true
3776
- }]);
3777
- if (convertToDir) {
3778
- await convertRulesFileToDirectory(rulesPath, templatePath);
3779
- } else {
3780
- console.log(chalk2.dim("Skipped Cursor rules installation (preserving existing file)"));
3781
- }
3782
- }
3783
- async function handleInvalidRulesPath() {
3784
- console.log(chalk2.yellow("\u26A0\uFE0F .cursor/rules exists but is not a regular file or directory"));
3785
- console.log(chalk2.dim("Skipped Cursor rules installation"));
3786
- }
3787
- async function handleFreshRulesInstall(rulesPath, templatePath) {
3788
- await fs.mkdir(rulesPath, { recursive: true });
3789
- await fs.copyFile(templatePath, path.join(rulesPath, "lien.mdc"));
3790
- console.log(chalk2.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
3791
- }
3792
- async function installCursorRulesFiles(rootDir, options) {
3793
- const cursorRulesDir = path.join(rootDir, ".cursor");
3794
- await fs.mkdir(cursorRulesDir, { recursive: true });
3795
- const templatePath = path.join(__dirname2, "../CURSOR_RULES_TEMPLATE.md");
3796
- const rulesPath = path.join(cursorRulesDir, "rules");
3797
- const pathType = await getPathType(rulesPath);
3798
- const handlers = {
3799
- directory: () => handleExistingRulesDirectory(rulesPath, templatePath),
3800
- file: () => handleExistingRulesFile(rulesPath, templatePath, options),
3801
- other: () => handleInvalidRulesPath(),
3802
- none: () => handleFreshRulesInstall(rulesPath, templatePath)
3803
- };
3804
- await handlers[pathType]();
3805
- }
3806
- async function promptAndInstallCursorRules(rootDir, options) {
3807
- const shouldInstall = options.yes || (await inquirer.prompt([{
3808
- type: "confirm",
3809
- name: "installCursorRules",
3810
- message: "Install recommended Cursor rules?",
3811
- default: true
3812
- }])).installCursorRules;
3813
- if (!shouldInstall) return;
3814
- try {
3815
- await installCursorRulesFiles(rootDir, options);
3816
- } catch (error) {
3817
- console.log(chalk2.yellow("\u26A0\uFE0F Could not install Cursor rules"));
3818
- console.log(chalk2.dim(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
3819
- console.log(chalk2.dim("You can manually copy CURSOR_RULES_TEMPLATE.md to .cursor/rules/lien.mdc"));
3820
- }
3821
3694
  }
3822
3695
 
3823
3696
  // src/cli/status.ts
@@ -4119,7 +3992,7 @@ import path4 from "path";
4119
3992
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4120
3993
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4121
3994
  import { createRequire as createRequire2 } from "module";
4122
- import { fileURLToPath as fileURLToPath3 } from "url";
3995
+ import { fileURLToPath as fileURLToPath2 } from "url";
4123
3996
  import { dirname as dirname2, join as join2 } from "path";
4124
3997
  import {
4125
3998
  LocalEmbeddings,
@@ -8419,11 +8292,17 @@ var SemanticSearchSchema = external_exports.object({
8419
8292
 
8420
8293
  // src/mcp/schemas/similarity.schema.ts
8421
8294
  var FindSimilarSchema = external_exports.object({
8422
- code: external_exports.string().min(10, "Code snippet must be at least 10 characters").describe(
8295
+ code: external_exports.string().min(24, "Code snippet must be at least 24 characters").describe(
8423
8296
  "Code snippet to find similar implementations for.\n\nProvide a representative code sample that demonstrates the pattern you want to find similar examples of in the codebase."
8424
8297
  ),
8425
8298
  limit: external_exports.number().int().min(1, "Limit must be at least 1").max(20, "Limit cannot exceed 20").default(5).describe(
8426
8299
  "Number of similar code blocks to return.\n\nDefault: 5"
8300
+ ),
8301
+ language: external_exports.string().min(1, "Language filter cannot be empty").optional().describe(
8302
+ "Filter by programming language.\n\nExamples: 'typescript', 'python', 'javascript', 'php'\n\nIf omitted, searches all languages."
8303
+ ),
8304
+ pathHint: external_exports.string().min(1, "Path hint cannot be empty").optional().describe(
8305
+ "Filter by file path substring.\n\nOnly returns results where the file path contains this string (case-insensitive).\n\nExamples: 'src/api', 'components', 'utils'"
8427
8306
  )
8428
8307
  });
8429
8308
 
@@ -8487,7 +8366,7 @@ var tools = [
8487
8366
  toMCPToolSchema(
8488
8367
  SemanticSearchSchema,
8489
8368
  "semantic_search",
8490
- `Search codebase by MEANING, not text. USE THIS INSTEAD OF grep/ripgrep for finding implementations, features, or understanding how code works.
8369
+ `Search codebase by MEANING, not text. Complements grep - use this for discovery and understanding, grep for exact matches.
8491
8370
 
8492
8371
  Examples:
8493
8372
  - "Where is authentication handled?" \u2192 semantic_search({ query: "handles user authentication" })
@@ -8505,7 +8384,13 @@ Results include a relevance category (highly_relevant, relevant, loosely_related
8505
8384
  - Finding duplicate implementations
8506
8385
  - Refactoring similar patterns together
8507
8386
 
8508
- Provide at least 10 characters of code to match against. Results include a relevance category for each match.`
8387
+ Provide at least 24 characters of code to match against. Results include a relevance category for each match.
8388
+
8389
+ Optional filters:
8390
+ - language: Filter by programming language (e.g., "typescript", "python")
8391
+ - pathHint: Filter by file path substring (e.g., "src/api", "components")
8392
+
8393
+ Low-relevance results (not_relevant) are automatically pruned.`
8509
8394
  ),
8510
8395
  toMCPToolSchema(
8511
8396
  GetFilesContextSchema,
@@ -8703,6 +8588,19 @@ async function handleSemanticSearch(args, ctx) {
8703
8588
  }
8704
8589
 
8705
8590
  // src/mcp/handlers/find-similar.ts
8591
+ function applyLanguageFilter(results, language) {
8592
+ const lang = language.toLowerCase();
8593
+ return results.filter((r) => r.metadata.language?.toLowerCase() === lang);
8594
+ }
8595
+ function applyPathHintFilter(results, pathHint) {
8596
+ const hint = pathHint.toLowerCase();
8597
+ return results.filter((r) => (r.metadata.file?.toLowerCase() ?? "").includes(hint));
8598
+ }
8599
+ function pruneIrrelevantResults(results) {
8600
+ const beforePrune = results.length;
8601
+ const filtered = results.filter((r) => r.relevance !== "not_relevant");
8602
+ return { filtered, prunedCount: beforePrune - filtered.length };
8603
+ }
8706
8604
  async function handleFindSimilar(args, ctx) {
8707
8605
  const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
8708
8606
  return await wrapToolHandler(
@@ -8711,11 +8609,27 @@ async function handleFindSimilar(args, ctx) {
8711
8609
  log(`Finding similar code...`);
8712
8610
  await checkAndReconnect();
8713
8611
  const codeEmbedding = await embeddings.embed(validatedArgs.code);
8714
- const results = await vectorDB.search(codeEmbedding, validatedArgs.limit, validatedArgs.code);
8715
- log(`Found ${results.length} similar chunks`);
8612
+ const limit = validatedArgs.limit ?? 5;
8613
+ const extraLimit = limit + 10;
8614
+ let results = await vectorDB.search(codeEmbedding, extraLimit, validatedArgs.code);
8615
+ const filtersApplied = { prunedLowRelevance: 0 };
8616
+ if (validatedArgs.language) {
8617
+ filtersApplied.language = validatedArgs.language;
8618
+ results = applyLanguageFilter(results, validatedArgs.language);
8619
+ }
8620
+ if (validatedArgs.pathHint) {
8621
+ filtersApplied.pathHint = validatedArgs.pathHint;
8622
+ results = applyPathHintFilter(results, validatedArgs.pathHint);
8623
+ }
8624
+ const { filtered, prunedCount } = pruneIrrelevantResults(results);
8625
+ filtersApplied.prunedLowRelevance = prunedCount;
8626
+ const finalResults = filtered.slice(0, limit);
8627
+ log(`Found ${finalResults.length} similar chunks`);
8628
+ const hasFilters = filtersApplied.language || filtersApplied.pathHint || filtersApplied.prunedLowRelevance > 0;
8716
8629
  return {
8717
8630
  indexInfo: getIndexMetadata(),
8718
- results
8631
+ results: finalResults,
8632
+ ...hasFilters && { filtersApplied }
8719
8633
  };
8720
8634
  }
8721
8635
  )(args);
@@ -8938,6 +8852,25 @@ async function handleGetFilesContext(args, ctx) {
8938
8852
  }
8939
8853
 
8940
8854
  // src/mcp/handlers/list-functions.ts
8855
+ async function performContentScan(vectorDB, args, log) {
8856
+ log("Falling back to content scan...");
8857
+ let results = await vectorDB.scanWithFilter({
8858
+ language: args.language,
8859
+ limit: 200
8860
+ // Fetch more, we'll filter by symbolName
8861
+ });
8862
+ if (args.pattern) {
8863
+ const regex = new RegExp(args.pattern, "i");
8864
+ results = results.filter((r) => {
8865
+ const symbolName = r.metadata?.symbolName;
8866
+ return symbolName && regex.test(symbolName);
8867
+ });
8868
+ }
8869
+ return {
8870
+ results: results.slice(0, 50),
8871
+ method: "content"
8872
+ };
8873
+ }
8941
8874
  async function handleListFunctions(args, ctx) {
8942
8875
  const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
8943
8876
  return await wrapToolHandler(
@@ -8945,38 +8878,29 @@ async function handleListFunctions(args, ctx) {
8945
8878
  async (validatedArgs) => {
8946
8879
  log("Listing functions with symbol metadata...");
8947
8880
  await checkAndReconnect();
8948
- let results;
8949
- let usedMethod = "symbols";
8881
+ let queryResult;
8950
8882
  try {
8951
- results = await vectorDB.querySymbols({
8883
+ const results = await vectorDB.querySymbols({
8952
8884
  language: validatedArgs.language,
8953
8885
  pattern: validatedArgs.pattern,
8954
8886
  limit: 50
8955
8887
  });
8956
8888
  if (results.length === 0 && (validatedArgs.language || validatedArgs.pattern)) {
8957
8889
  log("No symbol results, falling back to content scan...");
8958
- results = await vectorDB.scanWithFilter({
8959
- language: validatedArgs.language,
8960
- pattern: validatedArgs.pattern,
8961
- limit: 50
8962
- });
8963
- usedMethod = "content";
8890
+ queryResult = await performContentScan(vectorDB, validatedArgs, log);
8891
+ } else {
8892
+ queryResult = { results, method: "symbols" };
8964
8893
  }
8965
8894
  } catch (error) {
8966
- log(`Symbol query failed, falling back to content scan: ${error}`);
8967
- results = await vectorDB.scanWithFilter({
8968
- language: validatedArgs.language,
8969
- pattern: validatedArgs.pattern,
8970
- limit: 50
8971
- });
8972
- usedMethod = "content";
8895
+ log(`Symbol query failed: ${error}`);
8896
+ queryResult = await performContentScan(vectorDB, validatedArgs, log);
8973
8897
  }
8974
- log(`Found ${results.length} matches using ${usedMethod} method`);
8898
+ log(`Found ${queryResult.results.length} matches using ${queryResult.method} method`);
8975
8899
  return {
8976
8900
  indexInfo: getIndexMetadata(),
8977
- method: usedMethod,
8978
- results,
8979
- note: usedMethod === "content" ? 'Using content search. Run "lien reindex" to enable faster symbol-based queries.' : void 0
8901
+ method: queryResult.method,
8902
+ results: queryResult.results,
8903
+ note: queryResult.method === "content" ? 'Using content search. Run "lien reindex" to enable faster symbol-based queries.' : void 0
8980
8904
  };
8981
8905
  }
8982
8906
  )(args);
@@ -9377,14 +9301,14 @@ function registerMCPHandlers(server, toolContext, log) {
9377
9301
  }
9378
9302
 
9379
9303
  // src/mcp/server.ts
9380
- var __filename3 = fileURLToPath3(import.meta.url);
9381
- var __dirname3 = dirname2(__filename3);
9304
+ var __filename2 = fileURLToPath2(import.meta.url);
9305
+ var __dirname2 = dirname2(__filename2);
9382
9306
  var require3 = createRequire2(import.meta.url);
9383
9307
  var packageJson2;
9384
9308
  try {
9385
- packageJson2 = require3(join2(__dirname3, "../package.json"));
9309
+ packageJson2 = require3(join2(__dirname2, "../package.json"));
9386
9310
  } catch {
9387
- packageJson2 = require3(join2(__dirname3, "../../package.json"));
9311
+ packageJson2 = require3(join2(__dirname2, "../../package.json"));
9388
9312
  }
9389
9313
  async function initializeDatabase(rootDir, log) {
9390
9314
  const embeddings = new LocalEmbeddings();
@@ -9719,14 +9643,14 @@ async function complexityCommand(options) {
9719
9643
  }
9720
9644
 
9721
9645
  // src/cli/index.ts
9722
- var __filename4 = fileURLToPath4(import.meta.url);
9723
- var __dirname4 = dirname3(__filename4);
9646
+ var __filename3 = fileURLToPath3(import.meta.url);
9647
+ var __dirname3 = dirname3(__filename3);
9724
9648
  var require4 = createRequire3(import.meta.url);
9725
9649
  var packageJson3;
9726
9650
  try {
9727
- packageJson3 = require4(join3(__dirname4, "../package.json"));
9651
+ packageJson3 = require4(join3(__dirname3, "../package.json"));
9728
9652
  } catch {
9729
- packageJson3 = require4(join3(__dirname4, "../../package.json"));
9653
+ packageJson3 = require4(join3(__dirname3, "../../package.json"));
9730
9654
  }
9731
9655
  var program = new Command();
9732
9656
  program.name("lien").description("Local semantic code search for AI assistants via MCP").version(packageJson3.version);