@liendev/lien 0.28.0 → 0.29.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
@@ -8475,7 +8475,7 @@ var NEVER = INVALID;
8475
8475
  // src/mcp/schemas/search.schema.ts
8476
8476
  var SemanticSearchSchema = external_exports.object({
8477
8477
  query: external_exports.string().min(3, "Query must be at least 3 characters").max(500, "Query too long (max 500 characters)").describe(
8478
- "Natural language description of what you're looking for.\n\nUse full sentences describing functionality, not exact names.\n\nGood examples:\n - 'handles user authentication'\n - 'validates email format'\n - 'processes payment transactions'\n\nBad examples:\n - 'auth' (too vague)\n - 'validateEmail' (use grep for exact names)"
8478
+ "Natural language description of what you're looking for.\n\nUse full sentences describing functionality, not exact names.\n\nGood examples:\n - 'How does the code handle user authentication?'\n - 'Where are email addresses validated?'\n - 'How are payment transactions processed?'\n\nBad examples:\n - 'auth' (too vague)\n - 'validateEmail' (use grep for exact names)"
8479
8479
  ),
8480
8480
  limit: external_exports.number().int().min(1, "Limit must be at least 1").max(50, "Limit cannot exceed 50").default(5).describe(
8481
8481
  "Number of results to return.\n\nDefault: 5\nIncrease to 10-15 for broad exploration."
@@ -8570,8 +8570,10 @@ var tools = [
8570
8570
  `Search codebase by MEANING, not text. Complements grep - use this for discovery and understanding, grep for exact matches.
8571
8571
 
8572
8572
  Examples:
8573
- - "Where is authentication handled?" \u2192 semantic_search({ query: "handles user authentication" })
8574
- - "How does payment work?" \u2192 semantic_search({ query: "processes payment transactions" })
8573
+ - "Where is authentication handled?" \u2192 semantic_search({ query: "How does the code handle user authentication?" })
8574
+ - "How does payment work?" \u2192 semantic_search({ query: "How are payment transactions processed and validated?" })
8575
+
8576
+ IMPORTANT: Phrase queries as full questions starting with "How", "Where", "What", etc. Full questions produce significantly better relevance than keyword phrases.
8575
8577
 
8576
8578
  Use natural language describing what the code DOES, not function names. For exact string matching, use grep instead.
8577
8579
 
@@ -8743,6 +8745,91 @@ function wrapToolHandler(schema, handler) {
8743
8745
  };
8744
8746
  }
8745
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 pickMetadata(metadata, allowlist) {
8807
+ const result = {
8808
+ file: metadata.file,
8809
+ startLine: metadata.startLine,
8810
+ endLine: metadata.endLine
8811
+ };
8812
+ const out = result;
8813
+ for (const key of allowlist) {
8814
+ if (key === "file" || key === "startLine" || key === "endLine") continue;
8815
+ if (metadata[key] !== void 0) {
8816
+ out[key] = metadata[key];
8817
+ }
8818
+ }
8819
+ return result;
8820
+ }
8821
+ function shapeResultMetadata(result, tool) {
8822
+ return {
8823
+ content: result.content,
8824
+ metadata: pickMetadata(result.metadata, FIELD_ALLOWLISTS[tool]),
8825
+ score: result.score,
8826
+ relevance: result.relevance
8827
+ };
8828
+ }
8829
+ function shapeResults(results, tool) {
8830
+ return results.map((r) => shapeResultMetadata(r, tool));
8831
+ }
8832
+
8746
8833
  // src/mcp/handlers/semantic-search.ts
8747
8834
  import { QdrantDB } from "@liendev/core";
8748
8835
  function groupResultsByRepo(results) {
@@ -8778,15 +8865,31 @@ async function handleSemanticSearch(args, ctx) {
8778
8865
  results = await vectorDB.search(queryEmbedding, limit, query);
8779
8866
  log(`Found ${results.length} results`);
8780
8867
  }
8868
+ results = deduplicateResults(results);
8869
+ const notes = [];
8870
+ if (crossRepoFallback) {
8871
+ notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
8872
+ }
8873
+ if (results.length > 0 && results.every((r) => r.relevance === "not_relevant")) {
8874
+ notes.push("No relevant matches found.");
8875
+ log("Returning 0 results (all not_relevant)");
8876
+ return {
8877
+ indexInfo: getIndexMetadata(),
8878
+ results: [],
8879
+ note: notes.join(" ")
8880
+ };
8881
+ }
8882
+ log(`Returning ${results.length} results`);
8883
+ const shaped = shapeResults(results, "semantic_search");
8781
8884
  const response = {
8782
8885
  indexInfo: getIndexMetadata(),
8783
- results
8886
+ results: shaped
8784
8887
  };
8785
8888
  if (crossRepo && vectorDB instanceof QdrantDB) {
8786
- response.groupedByRepo = groupResultsByRepo(results);
8889
+ response.groupedByRepo = groupResultsByRepo(shaped);
8787
8890
  }
8788
- if (crossRepoFallback) {
8789
- response.note = "Cross-repo search requires Qdrant backend. Fell back to single-repo search.";
8891
+ if (notes.length > 0) {
8892
+ response.note = notes.join(" ");
8790
8893
  }
8791
8894
  return response;
8792
8895
  }
@@ -8818,6 +8921,12 @@ async function handleFindSimilar(args, ctx) {
8818
8921
  const limit = validatedArgs.limit ?? 5;
8819
8922
  const extraLimit = limit + 10;
8820
8923
  let results = await vectorDB.search(codeEmbedding, extraLimit, validatedArgs.code);
8924
+ results = deduplicateResults(results);
8925
+ const inputCode = validatedArgs.code.trim();
8926
+ results = results.filter((r) => {
8927
+ if (r.score >= 0.1) return true;
8928
+ return r.content.trim() !== inputCode;
8929
+ });
8821
8930
  const filtersApplied = { prunedLowRelevance: 0 };
8822
8931
  if (validatedArgs.language) {
8823
8932
  filtersApplied.language = validatedArgs.language;
@@ -8834,7 +8943,7 @@ async function handleFindSimilar(args, ctx) {
8834
8943
  const hasFilters = filtersApplied.language || filtersApplied.pathHint || filtersApplied.prunedLowRelevance > 0;
8835
8944
  return {
8836
8945
  indexInfo: getIndexMetadata(),
8837
- results: finalResults,
8946
+ results: shapeResults(finalResults, "find_similar"),
8838
8947
  ...hasFilters && { filtersApplied }
8839
8948
  };
8840
8949
  }
@@ -9042,18 +9151,26 @@ function buildScanLimitNote(hitScanLimit) {
9042
9151
  return hitScanLimit ? "Scanned 10,000 chunks (limit reached). Test associations may be incomplete for large codebases." : void 0;
9043
9152
  }
9044
9153
  function buildSingleFileResponse(filepath, filesData, indexInfo, note) {
9154
+ const data = filesData[filepath];
9045
9155
  return {
9046
9156
  indexInfo,
9047
9157
  file: filepath,
9048
- chunks: filesData[filepath].chunks,
9049
- testAssociations: filesData[filepath].testAssociations,
9158
+ chunks: shapeResults(data.chunks, "get_files_context"),
9159
+ testAssociations: data.testAssociations,
9050
9160
  ...note && { note }
9051
9161
  };
9052
9162
  }
9053
9163
  function buildMultiFileResponse(filesData, indexInfo, note) {
9164
+ const shaped = {};
9165
+ for (const [filepath, data] of Object.entries(filesData)) {
9166
+ shaped[filepath] = {
9167
+ chunks: shapeResults(data.chunks, "get_files_context"),
9168
+ testAssociations: data.testAssociations
9169
+ };
9170
+ }
9054
9171
  return {
9055
9172
  indexInfo,
9056
- files: filesData,
9173
+ files: shaped,
9057
9174
  ...note && { note }
9058
9175
  };
9059
9176
  }
@@ -9158,11 +9275,12 @@ async function handleListFunctions(args, ctx) {
9158
9275
  log(`Symbol query failed: ${error}`);
9159
9276
  queryResult = await performContentScan(vectorDB, validatedArgs, log);
9160
9277
  }
9161
- log(`Found ${queryResult.results.length} matches using ${queryResult.method} method`);
9278
+ const dedupedResults = deduplicateResults(queryResult.results);
9279
+ log(`Found ${dedupedResults.length} matches using ${queryResult.method} method`);
9162
9280
  return {
9163
9281
  indexInfo: getIndexMetadata(),
9164
9282
  method: queryResult.method,
9165
- results: queryResult.results,
9283
+ results: shapeResults(dedupedResults, "list_functions"),
9166
9284
  note: queryResult.method === "content" ? 'Using content search. Run "lien reindex" to enable faster symbol-based queries.' : void 0
9167
9285
  };
9168
9286
  }