@liendev/lien 0.28.1 → 0.29.1

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
@@ -8745,6 +8745,111 @@ function wrapToolHandler(schema, handler) {
8745
8745
  };
8746
8746
  }
8747
8747
 
8748
+ // src/mcp/utils/metadata-shaper.ts
8749
+ var FIELD_ALLOWLISTS = {
8750
+ semantic_search: /* @__PURE__ */ new Set([
8751
+ "language",
8752
+ "type",
8753
+ "symbolName",
8754
+ "symbolType",
8755
+ "signature",
8756
+ "parentClass",
8757
+ "parameters",
8758
+ "exports",
8759
+ "repoId"
8760
+ ]),
8761
+ find_similar: /* @__PURE__ */ new Set([
8762
+ "language",
8763
+ "type",
8764
+ "symbolName",
8765
+ "symbolType",
8766
+ "signature",
8767
+ "parentClass",
8768
+ "parameters",
8769
+ "exports"
8770
+ ]),
8771
+ get_files_context: /* @__PURE__ */ new Set([
8772
+ "language",
8773
+ "type",
8774
+ "symbolName",
8775
+ "symbolType",
8776
+ "signature",
8777
+ "parentClass",
8778
+ "parameters",
8779
+ "exports",
8780
+ "imports",
8781
+ "importedSymbols",
8782
+ "callSites",
8783
+ "symbols"
8784
+ ]),
8785
+ list_functions: /* @__PURE__ */ new Set([
8786
+ "language",
8787
+ "type",
8788
+ "symbolName",
8789
+ "symbolType",
8790
+ "signature",
8791
+ "parentClass",
8792
+ "parameters",
8793
+ "exports",
8794
+ "symbols"
8795
+ ])
8796
+ };
8797
+ function deduplicateResults(results) {
8798
+ const seen = /* @__PURE__ */ new Set();
8799
+ return results.filter((r) => {
8800
+ const key = JSON.stringify([r.metadata.repoId ?? "", r.metadata.file, r.metadata.startLine, r.metadata.endLine]);
8801
+ if (seen.has(key)) return false;
8802
+ seen.add(key);
8803
+ return true;
8804
+ });
8805
+ }
8806
+ function cleanMetadataValue(key, value) {
8807
+ if (value === void 0 || value === "") return null;
8808
+ if (Array.isArray(value)) {
8809
+ const filtered = value.filter((v) => v !== "");
8810
+ return filtered.length > 0 ? filtered : null;
8811
+ }
8812
+ if (key === "symbols" && typeof value === "object" && value !== null) {
8813
+ const symbols = value;
8814
+ const filterArr = (arr) => Array.isArray(arr) ? arr.filter((s) => s !== "") : [];
8815
+ const filtered = {
8816
+ functions: filterArr(symbols.functions),
8817
+ classes: filterArr(symbols.classes),
8818
+ interfaces: filterArr(symbols.interfaces)
8819
+ };
8820
+ const hasAny = filtered.functions.length > 0 || filtered.classes.length > 0 || filtered.interfaces.length > 0;
8821
+ return hasAny ? filtered : null;
8822
+ }
8823
+ return value;
8824
+ }
8825
+ function pickMetadata(metadata, allowlist) {
8826
+ const result = {
8827
+ file: metadata.file,
8828
+ startLine: metadata.startLine,
8829
+ endLine: metadata.endLine
8830
+ };
8831
+ const out = result;
8832
+ for (const key of allowlist) {
8833
+ if (key === "file" || key === "startLine" || key === "endLine") continue;
8834
+ const cleaned = cleanMetadataValue(key, metadata[key]);
8835
+ if (cleaned !== null) {
8836
+ out[key] = cleaned;
8837
+ }
8838
+ }
8839
+ return result;
8840
+ }
8841
+ function shapeResultMetadata(result, tool) {
8842
+ return {
8843
+ content: result.content,
8844
+ metadata: pickMetadata(result.metadata, FIELD_ALLOWLISTS[tool]),
8845
+ score: result.score,
8846
+ relevance: result.relevance
8847
+ };
8848
+ }
8849
+ function shapeResults(results, tool) {
8850
+ return results.map((r) => shapeResultMetadata(r, tool));
8851
+ }
8852
+
8748
8853
  // src/mcp/handlers/semantic-search.ts
8749
8854
  import { QdrantDB } from "@liendev/core";
8750
8855
  function groupResultsByRepo(results) {
@@ -8780,17 +8885,28 @@ async function handleSemanticSearch(args, ctx) {
8780
8885
  results = await vectorDB.search(queryEmbedding, limit, query);
8781
8886
  log(`Found ${results.length} results`);
8782
8887
  }
8783
- const response = {
8784
- indexInfo: getIndexMetadata(),
8785
- results
8786
- };
8787
- if (crossRepo && vectorDB instanceof QdrantDB) {
8788
- response.groupedByRepo = groupResultsByRepo(results);
8789
- }
8888
+ results = deduplicateResults(results);
8889
+ const notes = [];
8790
8890
  if (crossRepoFallback) {
8791
- response.note = "Cross-repo search requires Qdrant backend. Fell back to single-repo search.";
8891
+ notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
8792
8892
  }
8793
- return response;
8893
+ if (results.length > 0 && results.every((r) => r.relevance === "not_relevant")) {
8894
+ notes.push("No relevant matches found.");
8895
+ log("Returning 0 results (all not_relevant)");
8896
+ return {
8897
+ indexInfo: getIndexMetadata(),
8898
+ results: [],
8899
+ note: notes.join(" ")
8900
+ };
8901
+ }
8902
+ log(`Returning ${results.length} results`);
8903
+ const shaped = shapeResults(results, "semantic_search");
8904
+ return {
8905
+ indexInfo: getIndexMetadata(),
8906
+ results: shaped,
8907
+ ...crossRepo && vectorDB instanceof QdrantDB && { groupedByRepo: groupResultsByRepo(shaped) },
8908
+ ...notes.length > 0 && { note: notes.join(" ") }
8909
+ };
8794
8910
  }
8795
8911
  )(args);
8796
8912
  }
@@ -8820,6 +8936,12 @@ async function handleFindSimilar(args, ctx) {
8820
8936
  const limit = validatedArgs.limit ?? 5;
8821
8937
  const extraLimit = limit + 10;
8822
8938
  let results = await vectorDB.search(codeEmbedding, extraLimit, validatedArgs.code);
8939
+ results = deduplicateResults(results);
8940
+ const inputCode = validatedArgs.code.trim();
8941
+ results = results.filter((r) => {
8942
+ if (r.score >= 0.1) return true;
8943
+ return r.content.trim() !== inputCode;
8944
+ });
8823
8945
  const filtersApplied = { prunedLowRelevance: 0 };
8824
8946
  if (validatedArgs.language) {
8825
8947
  filtersApplied.language = validatedArgs.language;
@@ -8836,7 +8958,7 @@ async function handleFindSimilar(args, ctx) {
8836
8958
  const hasFilters = filtersApplied.language || filtersApplied.pathHint || filtersApplied.prunedLowRelevance > 0;
8837
8959
  return {
8838
8960
  indexInfo: getIndexMetadata(),
8839
- results: finalResults,
8961
+ results: shapeResults(finalResults, "find_similar"),
8840
8962
  ...hasFilters && { filtersApplied }
8841
8963
  };
8842
8964
  }
@@ -8979,7 +9101,9 @@ async function findRelatedChunks(filepaths, fileChunksMap, ctx) {
8979
9101
  const targetCanonical = getCanonicalPath(filepath, workspaceRoot);
8980
9102
  relatedChunksMap[index] = related.filter((r) => {
8981
9103
  const chunkCanonical = getCanonicalPath(r.metadata.file, workspaceRoot);
8982
- return chunkCanonical !== targetCanonical;
9104
+ if (chunkCanonical === targetCanonical) return false;
9105
+ if (r.metadata.language === "markdown") return false;
9106
+ return true;
8983
9107
  });
8984
9108
  });
8985
9109
  return relatedChunksMap;
@@ -9015,23 +9139,15 @@ function findTestAssociations(filepaths, allChunks, ctx) {
9015
9139
  return Array.from(testFiles);
9016
9140
  });
9017
9141
  }
9018
- function deduplicateChunks(fileChunks, relatedChunks, workspaceRoot) {
9019
- const seenChunks = /* @__PURE__ */ new Set();
9020
- return [...fileChunks, ...relatedChunks].filter((chunk) => {
9021
- const canonicalFile = getCanonicalPath(chunk.metadata.file, workspaceRoot);
9022
- const chunkId = `${canonicalFile}:${chunk.metadata.startLine}-${chunk.metadata.endLine}`;
9023
- if (seenChunks.has(chunkId)) return false;
9024
- seenChunks.add(chunkId);
9025
- return true;
9026
- });
9142
+ function deduplicateChunks(fileChunks, relatedChunks) {
9143
+ return deduplicateResults([...fileChunks, ...relatedChunks]);
9027
9144
  }
9028
- function buildFilesData(filepaths, fileChunksMap, relatedChunksMap, testAssociationsMap, workspaceRoot) {
9145
+ function buildFilesData(filepaths, fileChunksMap, relatedChunksMap, testAssociationsMap) {
9029
9146
  const filesData = {};
9030
9147
  filepaths.forEach((filepath, i) => {
9031
9148
  const dedupedChunks = deduplicateChunks(
9032
9149
  fileChunksMap[i],
9033
- relatedChunksMap[i] || [],
9034
- workspaceRoot
9150
+ relatedChunksMap[i] || []
9035
9151
  );
9036
9152
  filesData[filepath] = {
9037
9153
  chunks: dedupedChunks,
@@ -9044,18 +9160,26 @@ function buildScanLimitNote(hitScanLimit) {
9044
9160
  return hitScanLimit ? "Scanned 10,000 chunks (limit reached). Test associations may be incomplete for large codebases." : void 0;
9045
9161
  }
9046
9162
  function buildSingleFileResponse(filepath, filesData, indexInfo, note) {
9163
+ const data = filesData[filepath];
9047
9164
  return {
9048
9165
  indexInfo,
9049
9166
  file: filepath,
9050
- chunks: filesData[filepath].chunks,
9051
- testAssociations: filesData[filepath].testAssociations,
9167
+ chunks: shapeResults(data.chunks, "get_files_context"),
9168
+ testAssociations: data.testAssociations,
9052
9169
  ...note && { note }
9053
9170
  };
9054
9171
  }
9055
9172
  function buildMultiFileResponse(filesData, indexInfo, note) {
9173
+ const shaped = {};
9174
+ for (const [filepath, data] of Object.entries(filesData)) {
9175
+ shaped[filepath] = {
9176
+ chunks: shapeResults(data.chunks, "get_files_context"),
9177
+ testAssociations: data.testAssociations
9178
+ };
9179
+ }
9056
9180
  return {
9057
9181
  indexInfo,
9058
- files: filesData,
9182
+ files: shaped,
9059
9183
  ...note && { note }
9060
9184
  };
9061
9185
  }
@@ -9101,8 +9225,7 @@ async function handleGetFilesContext(args, ctx) {
9101
9225
  filepaths,
9102
9226
  fileChunksMap,
9103
9227
  relatedChunksMap,
9104
- testAssociationsMap,
9105
- workspaceRoot
9228
+ testAssociationsMap
9106
9229
  );
9107
9230
  const totalChunks = Object.values(filesData).reduce(
9108
9231
  (sum, f) => sum + f.chunks.length,
@@ -9160,11 +9283,12 @@ async function handleListFunctions(args, ctx) {
9160
9283
  log(`Symbol query failed: ${error}`);
9161
9284
  queryResult = await performContentScan(vectorDB, validatedArgs, log);
9162
9285
  }
9163
- log(`Found ${queryResult.results.length} matches using ${queryResult.method} method`);
9286
+ const dedupedResults = deduplicateResults(queryResult.results);
9287
+ log(`Found ${dedupedResults.length} matches using ${queryResult.method} method`);
9164
9288
  return {
9165
9289
  indexInfo: getIndexMetadata(),
9166
9290
  method: queryResult.method,
9167
- results: queryResult.results,
9291
+ results: shapeResults(dedupedResults, "list_functions"),
9168
9292
  note: queryResult.method === "content" ? 'Using content search. Run "lien reindex" to enable faster symbol-based queries.' : void 0
9169
9293
  };
9170
9294
  }
@@ -9680,7 +9804,11 @@ async function handleGetComplexity(args, ctx) {
9680
9804
  threshold,
9681
9805
  top ?? 10
9682
9806
  );
9683
- const response = {
9807
+ const note = buildCrossRepoFallbackNote(fallback);
9808
+ if (note) {
9809
+ log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo analysis.", "warning");
9810
+ }
9811
+ return {
9684
9812
  indexInfo: getIndexMetadata(),
9685
9813
  summary: {
9686
9814
  filesAnalyzed: report.summary.filesAnalyzed,
@@ -9689,17 +9817,12 @@ async function handleGetComplexity(args, ctx) {
9689
9817
  violationCount: violations.length,
9690
9818
  bySeverity
9691
9819
  },
9692
- violations: topViolations
9820
+ violations: topViolations,
9821
+ ...crossRepo && !fallback && allChunks.length > 0 && {
9822
+ groupedByRepo: groupViolationsByRepo(topViolations, allChunks)
9823
+ },
9824
+ ...note && { note }
9693
9825
  };
9694
- if (crossRepo && !fallback && allChunks.length > 0) {
9695
- response.groupedByRepo = groupViolationsByRepo(topViolations, allChunks);
9696
- }
9697
- const note = buildCrossRepoFallbackNote(fallback);
9698
- if (note) {
9699
- log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo analysis.", "warning");
9700
- response.note = note;
9701
- }
9702
- return response;
9703
9826
  }
9704
9827
  )(args);
9705
9828
  }