@liendev/lien 0.24.0 → 0.26.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 +4 -4
- package/dist/index.js +131 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -39,7 +39,7 @@ npm install -g @liendev/lien
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
# 3. Restart Cursor and start asking questions!
|
|
42
|
+
# 3. Restart your AI assistant (Cursor, Claude Code) and start asking questions!
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
That's it—zero configuration needed. Lien auto-detects your project and indexes on first use.
|
|
@@ -99,7 +99,7 @@ Lien tracks code complexity with intuitive outputs:
|
|
|
99
99
|
## Documentation
|
|
100
100
|
|
|
101
101
|
- **[Installation](https://lien.dev/guide/installation)** - npm, npx, or local setup
|
|
102
|
-
- **[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
|
|
103
103
|
- **[Configuration](https://lien.dev/guide/configuration)** - Customize indexing, thresholds, performance
|
|
104
104
|
- **[CLI Commands](https://lien.dev/guide/cli-commands)** - Full command reference
|
|
105
105
|
- **[MCP Tools](https://lien.dev/guide/mcp-tools)** - Complete API reference for all 6 tools
|
package/dist/index.js
CHANGED
|
@@ -8292,11 +8292,17 @@ var SemanticSearchSchema = external_exports.object({
|
|
|
8292
8292
|
|
|
8293
8293
|
// src/mcp/schemas/similarity.schema.ts
|
|
8294
8294
|
var FindSimilarSchema = external_exports.object({
|
|
8295
|
-
code: external_exports.string().min(
|
|
8295
|
+
code: external_exports.string().min(24, "Code snippet must be at least 24 characters").describe(
|
|
8296
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."
|
|
8297
8297
|
),
|
|
8298
8298
|
limit: external_exports.number().int().min(1, "Limit must be at least 1").max(20, "Limit cannot exceed 20").default(5).describe(
|
|
8299
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'"
|
|
8300
8306
|
)
|
|
8301
8307
|
});
|
|
8302
8308
|
|
|
@@ -8378,7 +8384,13 @@ Results include a relevance category (highly_relevant, relevant, loosely_related
|
|
|
8378
8384
|
- Finding duplicate implementations
|
|
8379
8385
|
- Refactoring similar patterns together
|
|
8380
8386
|
|
|
8381
|
-
Provide at least
|
|
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.`
|
|
8382
8394
|
),
|
|
8383
8395
|
toMCPToolSchema(
|
|
8384
8396
|
GetFilesContextSchema,
|
|
@@ -8553,12 +8565,14 @@ async function handleSemanticSearch(args, ctx) {
|
|
|
8553
8565
|
await checkAndReconnect();
|
|
8554
8566
|
const queryEmbedding = await embeddings.embed(query);
|
|
8555
8567
|
let results;
|
|
8568
|
+
let crossRepoFallback = false;
|
|
8556
8569
|
if (crossRepo && vectorDB instanceof QdrantDB) {
|
|
8557
8570
|
results = await vectorDB.searchCrossRepo(queryEmbedding, limit, { repoIds });
|
|
8558
8571
|
log(`Found ${results.length} results across ${Object.keys(groupResultsByRepo(results)).length} repos`);
|
|
8559
8572
|
} else {
|
|
8560
8573
|
if (crossRepo) {
|
|
8561
8574
|
log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo search.");
|
|
8575
|
+
crossRepoFallback = true;
|
|
8562
8576
|
}
|
|
8563
8577
|
results = await vectorDB.search(queryEmbedding, limit, query);
|
|
8564
8578
|
log(`Found ${results.length} results`);
|
|
@@ -8570,12 +8584,28 @@ async function handleSemanticSearch(args, ctx) {
|
|
|
8570
8584
|
if (crossRepo && vectorDB instanceof QdrantDB) {
|
|
8571
8585
|
response.groupedByRepo = groupResultsByRepo(results);
|
|
8572
8586
|
}
|
|
8587
|
+
if (crossRepoFallback) {
|
|
8588
|
+
response.note = "Cross-repo search requires Qdrant backend. Fell back to single-repo search.";
|
|
8589
|
+
}
|
|
8573
8590
|
return response;
|
|
8574
8591
|
}
|
|
8575
8592
|
)(args);
|
|
8576
8593
|
}
|
|
8577
8594
|
|
|
8578
8595
|
// src/mcp/handlers/find-similar.ts
|
|
8596
|
+
function applyLanguageFilter(results, language) {
|
|
8597
|
+
const lang = language.toLowerCase();
|
|
8598
|
+
return results.filter((r) => r.metadata.language?.toLowerCase() === lang);
|
|
8599
|
+
}
|
|
8600
|
+
function applyPathHintFilter(results, pathHint) {
|
|
8601
|
+
const hint = pathHint.toLowerCase();
|
|
8602
|
+
return results.filter((r) => (r.metadata.file?.toLowerCase() ?? "").includes(hint));
|
|
8603
|
+
}
|
|
8604
|
+
function pruneIrrelevantResults(results) {
|
|
8605
|
+
const beforePrune = results.length;
|
|
8606
|
+
const filtered = results.filter((r) => r.relevance !== "not_relevant");
|
|
8607
|
+
return { filtered, prunedCount: beforePrune - filtered.length };
|
|
8608
|
+
}
|
|
8579
8609
|
async function handleFindSimilar(args, ctx) {
|
|
8580
8610
|
const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
8581
8611
|
return await wrapToolHandler(
|
|
@@ -8584,11 +8614,27 @@ async function handleFindSimilar(args, ctx) {
|
|
|
8584
8614
|
log(`Finding similar code...`);
|
|
8585
8615
|
await checkAndReconnect();
|
|
8586
8616
|
const codeEmbedding = await embeddings.embed(validatedArgs.code);
|
|
8587
|
-
const
|
|
8588
|
-
|
|
8617
|
+
const limit = validatedArgs.limit ?? 5;
|
|
8618
|
+
const extraLimit = limit + 10;
|
|
8619
|
+
let results = await vectorDB.search(codeEmbedding, extraLimit, validatedArgs.code);
|
|
8620
|
+
const filtersApplied = { prunedLowRelevance: 0 };
|
|
8621
|
+
if (validatedArgs.language) {
|
|
8622
|
+
filtersApplied.language = validatedArgs.language;
|
|
8623
|
+
results = applyLanguageFilter(results, validatedArgs.language);
|
|
8624
|
+
}
|
|
8625
|
+
if (validatedArgs.pathHint) {
|
|
8626
|
+
filtersApplied.pathHint = validatedArgs.pathHint;
|
|
8627
|
+
results = applyPathHintFilter(results, validatedArgs.pathHint);
|
|
8628
|
+
}
|
|
8629
|
+
const { filtered, prunedCount } = pruneIrrelevantResults(results);
|
|
8630
|
+
filtersApplied.prunedLowRelevance = prunedCount;
|
|
8631
|
+
const finalResults = filtered.slice(0, limit);
|
|
8632
|
+
log(`Found ${finalResults.length} similar chunks`);
|
|
8633
|
+
const hasFilters = filtersApplied.language || filtersApplied.pathHint || filtersApplied.prunedLowRelevance > 0;
|
|
8589
8634
|
return {
|
|
8590
8635
|
indexInfo: getIndexMetadata(),
|
|
8591
|
-
results
|
|
8636
|
+
results: finalResults,
|
|
8637
|
+
...hasFilters && { filtersApplied }
|
|
8592
8638
|
};
|
|
8593
8639
|
}
|
|
8594
8640
|
)(args);
|
|
@@ -8743,6 +8789,25 @@ function buildFilesData(filepaths, fileChunksMap, relatedChunksMap, testAssociat
|
|
|
8743
8789
|
});
|
|
8744
8790
|
return filesData;
|
|
8745
8791
|
}
|
|
8792
|
+
function buildScanLimitNote(hitScanLimit) {
|
|
8793
|
+
return hitScanLimit ? "Scanned 10,000 chunks (limit reached). Test associations may be incomplete for large codebases." : void 0;
|
|
8794
|
+
}
|
|
8795
|
+
function buildSingleFileResponse(filepath, filesData, indexInfo, note) {
|
|
8796
|
+
return {
|
|
8797
|
+
indexInfo,
|
|
8798
|
+
file: filepath,
|
|
8799
|
+
chunks: filesData[filepath].chunks,
|
|
8800
|
+
testAssociations: filesData[filepath].testAssociations,
|
|
8801
|
+
...note && { note }
|
|
8802
|
+
};
|
|
8803
|
+
}
|
|
8804
|
+
function buildMultiFileResponse(filesData, indexInfo, note) {
|
|
8805
|
+
return {
|
|
8806
|
+
indexInfo,
|
|
8807
|
+
files: filesData,
|
|
8808
|
+
...note && { note }
|
|
8809
|
+
};
|
|
8810
|
+
}
|
|
8746
8811
|
async function handleGetFilesContext(args, ctx) {
|
|
8747
8812
|
const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
8748
8813
|
return await wrapToolHandler(
|
|
@@ -8769,7 +8834,8 @@ async function handleGetFilesContext(args, ctx) {
|
|
|
8769
8834
|
);
|
|
8770
8835
|
}
|
|
8771
8836
|
const allChunks = await vectorDB.scanWithFilter({ limit: SCAN_LIMIT });
|
|
8772
|
-
|
|
8837
|
+
const hitScanLimit = allChunks.length === SCAN_LIMIT;
|
|
8838
|
+
if (hitScanLimit) {
|
|
8773
8839
|
log(
|
|
8774
8840
|
`Scanned ${SCAN_LIMIT} chunks (limit reached). Test associations may be incomplete for large codebases.`,
|
|
8775
8841
|
"warning"
|
|
@@ -8792,20 +8858,9 @@ async function handleGetFilesContext(args, ctx) {
|
|
|
8792
8858
|
0
|
|
8793
8859
|
);
|
|
8794
8860
|
log(`Found ${totalChunks} total chunks`);
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
indexInfo: getIndexMetadata(),
|
|
8799
|
-
file: filepath,
|
|
8800
|
-
chunks: filesData[filepath].chunks,
|
|
8801
|
-
testAssociations: filesData[filepath].testAssociations
|
|
8802
|
-
};
|
|
8803
|
-
} else {
|
|
8804
|
-
return {
|
|
8805
|
-
indexInfo: getIndexMetadata(),
|
|
8806
|
-
files: filesData
|
|
8807
|
-
};
|
|
8808
|
-
}
|
|
8861
|
+
const note = buildScanLimitNote(hitScanLimit);
|
|
8862
|
+
const indexInfo = getIndexMetadata();
|
|
8863
|
+
return isSingleFile ? buildSingleFileResponse(filepaths[0], filesData, indexInfo, note) : buildMultiFileResponse(filesData, indexInfo, note);
|
|
8809
8864
|
}
|
|
8810
8865
|
)(args);
|
|
8811
8866
|
}
|
|
@@ -9081,6 +9136,14 @@ async function handleGetDependents(args, ctx) {
|
|
|
9081
9136
|
log(
|
|
9082
9137
|
`Found ${analysis.dependents.length} dependent files (risk: ${riskLevel}${analysis.complexityMetrics.filesWithComplexityData > 0 ? ", complexity-boosted" : ""})`
|
|
9083
9138
|
);
|
|
9139
|
+
const notes = [];
|
|
9140
|
+
const crossRepoFallback = crossRepo && !(vectorDB instanceof QdrantDB3);
|
|
9141
|
+
if (crossRepoFallback) {
|
|
9142
|
+
notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
|
|
9143
|
+
}
|
|
9144
|
+
if (analysis.hitLimit) {
|
|
9145
|
+
notes.push("Scanned 10,000 chunks (limit reached). Results may be incomplete.");
|
|
9146
|
+
}
|
|
9084
9147
|
const response = {
|
|
9085
9148
|
indexInfo: getIndexMetadata(),
|
|
9086
9149
|
filepath: validatedArgs.filepath,
|
|
@@ -9088,7 +9151,7 @@ async function handleGetDependents(args, ctx) {
|
|
|
9088
9151
|
riskLevel,
|
|
9089
9152
|
dependents: analysis.dependents,
|
|
9090
9153
|
complexityMetrics: analysis.complexityMetrics,
|
|
9091
|
-
|
|
9154
|
+
...notes.length > 0 && { note: notes.join(" ") }
|
|
9092
9155
|
};
|
|
9093
9156
|
if (crossRepo && vectorDB instanceof QdrantDB3) {
|
|
9094
9157
|
response.groupedByRepo = groupDependentsByRepo(analysis.dependents, analysis.allChunks);
|
|
@@ -9135,6 +9198,35 @@ function groupViolationsByRepo(violations, allChunks) {
|
|
|
9135
9198
|
}
|
|
9136
9199
|
return grouped;
|
|
9137
9200
|
}
|
|
9201
|
+
async function fetchCrossRepoChunks(vectorDB, crossRepo, repoIds, log) {
|
|
9202
|
+
if (!crossRepo) {
|
|
9203
|
+
return { chunks: [], fallback: false };
|
|
9204
|
+
}
|
|
9205
|
+
if (vectorDB instanceof QdrantDB4) {
|
|
9206
|
+
const chunks = await vectorDB.scanCrossRepo({ limit: 1e5, repoIds });
|
|
9207
|
+
log(`Scanned ${chunks.length} chunks across repos`);
|
|
9208
|
+
return { chunks, fallback: false };
|
|
9209
|
+
}
|
|
9210
|
+
return { chunks: [], fallback: true };
|
|
9211
|
+
}
|
|
9212
|
+
function processViolations(report, threshold, top) {
|
|
9213
|
+
const allViolations = (0, import_collect.default)(Object.entries(report.files)).flatMap(
|
|
9214
|
+
([, fileData]) => fileData.violations.map((v) => transformViolation(v, fileData))
|
|
9215
|
+
).sortByDesc("complexity").all();
|
|
9216
|
+
const violations = threshold !== void 0 ? allViolations.filter((v) => v.complexity >= threshold) : allViolations;
|
|
9217
|
+
const severityCounts = (0, import_collect.default)(violations).countBy("severity").all();
|
|
9218
|
+
return {
|
|
9219
|
+
violations,
|
|
9220
|
+
topViolations: violations.slice(0, top),
|
|
9221
|
+
bySeverity: {
|
|
9222
|
+
error: severityCounts["error"] || 0,
|
|
9223
|
+
warning: severityCounts["warning"] || 0
|
|
9224
|
+
}
|
|
9225
|
+
};
|
|
9226
|
+
}
|
|
9227
|
+
function buildCrossRepoFallbackNote(fallback) {
|
|
9228
|
+
return fallback ? "Cross-repo analysis requires Qdrant backend. Fell back to single-repo analysis." : void 0;
|
|
9229
|
+
}
|
|
9138
9230
|
async function handleGetComplexity(args, ctx) {
|
|
9139
9231
|
const { vectorDB, log, checkAndReconnect, getIndexMetadata } = ctx;
|
|
9140
9232
|
return await wrapToolHandler(
|
|
@@ -9143,23 +9235,20 @@ async function handleGetComplexity(args, ctx) {
|
|
|
9143
9235
|
const { crossRepo, repoIds, files, top, threshold } = validatedArgs;
|
|
9144
9236
|
log(`Analyzing complexity${crossRepo ? " (cross-repo)" : ""}...`);
|
|
9145
9237
|
await checkAndReconnect();
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
log(`Scanned ${allChunks.length} chunks across repos`);
|
|
9153
|
-
}
|
|
9238
|
+
const { chunks: allChunks, fallback } = await fetchCrossRepoChunks(
|
|
9239
|
+
vectorDB,
|
|
9240
|
+
crossRepo,
|
|
9241
|
+
repoIds,
|
|
9242
|
+
log
|
|
9243
|
+
);
|
|
9154
9244
|
const analyzer = new ComplexityAnalyzer(vectorDB);
|
|
9155
|
-
const report = await analyzer.analyze(files, crossRepo &&
|
|
9245
|
+
const report = await analyzer.analyze(files, crossRepo && !fallback, repoIds);
|
|
9156
9246
|
log(`Analyzed ${report.summary.filesAnalyzed} files`);
|
|
9157
|
-
const
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
const bySeverity = (0, import_collect.default)(violations).countBy("severity").all();
|
|
9247
|
+
const { violations, topViolations, bySeverity } = processViolations(
|
|
9248
|
+
report,
|
|
9249
|
+
threshold,
|
|
9250
|
+
top ?? 10
|
|
9251
|
+
);
|
|
9163
9252
|
const response = {
|
|
9164
9253
|
indexInfo: getIndexMetadata(),
|
|
9165
9254
|
summary: {
|
|
@@ -9167,17 +9256,17 @@ async function handleGetComplexity(args, ctx) {
|
|
|
9167
9256
|
avgComplexity: report.summary.avgComplexity,
|
|
9168
9257
|
maxComplexity: report.summary.maxComplexity,
|
|
9169
9258
|
violationCount: violations.length,
|
|
9170
|
-
bySeverity
|
|
9171
|
-
error: bySeverity["error"] || 0,
|
|
9172
|
-
warning: bySeverity["warning"] || 0
|
|
9173
|
-
}
|
|
9259
|
+
bySeverity
|
|
9174
9260
|
},
|
|
9175
9261
|
violations: topViolations
|
|
9176
9262
|
};
|
|
9177
|
-
if (crossRepo &&
|
|
9263
|
+
if (crossRepo && !fallback && allChunks.length > 0) {
|
|
9178
9264
|
response.groupedByRepo = groupViolationsByRepo(topViolations, allChunks);
|
|
9179
|
-
}
|
|
9265
|
+
}
|
|
9266
|
+
const note = buildCrossRepoFallbackNote(fallback);
|
|
9267
|
+
if (note) {
|
|
9180
9268
|
log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo analysis.", "warning");
|
|
9269
|
+
response.note = note;
|
|
9181
9270
|
}
|
|
9182
9271
|
return response;
|
|
9183
9272
|
}
|