@liendev/lien 0.30.0 → 0.33.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
@@ -8525,7 +8525,13 @@ var ListFunctionsSchema = external_exports.object({
8525
8525
  language: external_exports.string().optional().describe(
8526
8526
  "Filter by programming language.\n\nExamples: 'typescript', 'python', 'javascript', 'php'\n\nIf omitted, searches all languages."
8527
8527
  ),
8528
- symbolType: external_exports.enum(["function", "method", "class", "interface"]).optional().describe("Filter by symbol type. If omitted, returns all types.")
8528
+ symbolType: external_exports.enum(["function", "method", "class", "interface"]).optional().describe("Filter by symbol type. If omitted, returns all types."),
8529
+ limit: external_exports.number().int().min(1).max(200).default(50).describe(
8530
+ "Number of results to return.\n\nDefault: 50\nIncrease to 200 for broad exploration."
8531
+ ),
8532
+ offset: external_exports.number().int().min(0).max(1e4).default(0).describe(
8533
+ "Skip first N results before applying limit, equivalent to pagination offset.\n\nDefault: 0"
8534
+ )
8529
8535
  });
8530
8536
 
8531
8537
  // src/mcp/schemas/dependents.schema.ts
@@ -8578,7 +8584,11 @@ IMPORTANT: Phrase queries as full questions starting with "How", "Where", "What"
8578
8584
 
8579
8585
  Use natural language describing what the code DOES, not function names. For exact string matching, use grep instead.
8580
8586
 
8581
- Results include a relevance category (highly_relevant, relevant, loosely_related, not_relevant) for each match.`
8587
+ Returns:
8588
+ - results[]: { content, score, relevance, metadata: { file, startLine, endLine, language?, symbolName?, symbolType?, signature?, enclosingSymbol? } }
8589
+ - enclosingSymbol: "Class.method" for methods, "functionName" for standalone functions, absent for block chunks
8590
+ - relevance: "highly_relevant" | "relevant" | "loosely_related" (not_relevant auto-filtered)
8591
+ - groupedByRepo?: Record<repoId, results[]> (when crossRepo=true)`
8582
8592
  ),
8583
8593
  toMCPToolSchema(
8584
8594
  FindSimilarSchema,
@@ -8594,7 +8604,13 @@ Optional filters:
8594
8604
  - language: Filter by programming language (e.g., "typescript", "python")
8595
8605
  - pathHint: Filter by file path substring (e.g., "src/api", "components")
8596
8606
 
8597
- Low-relevance results (not_relevant) are automatically pruned.`
8607
+ Low-relevance results (not_relevant) are automatically pruned.
8608
+
8609
+ Returns:
8610
+ - results[]: { content, score, relevance, metadata: { file, startLine, endLine, language?, symbolName?, signature?, enclosingSymbol? } }
8611
+ - enclosingSymbol: "Class.method" for methods, "functionName" for standalone functions, absent for block chunks
8612
+ - relevance: "highly_relevant" | "relevant" | "loosely_related" (not_relevant auto-filtered)
8613
+ - filtersApplied?: { language?, pathHint?, prunedLowRelevance: number }`
8598
8614
  ),
8599
8615
  toMCPToolSchema(
8600
8616
  GetFilesContextSchema,
@@ -8653,7 +8669,16 @@ Examples:
8653
8669
 
8654
8670
  Filter by symbol type (function, method, class, interface) to narrow results.
8655
8671
 
8656
- 10x faster than semantic_search for structural/architectural queries. Use semantic_search instead when searching by what code DOES.`
8672
+ 10x faster than semantic_search for structural/architectural queries. Use semantic_search instead when searching by what code DOES.
8673
+
8674
+ Results are paginated (default: 50, max: 200). Use \`offset\` to page through large result sets.
8675
+
8676
+ Returns:
8677
+ - results[]: { content, metadata: { file, startLine, endLine, language?, symbolName?, symbolType?, signature?, enclosingSymbol? } }
8678
+ - enclosingSymbol: "Class.method" for methods, "functionName" for standalone functions, absent for block chunks
8679
+ - method: "symbols" | "content" (query method used)
8680
+ - hasMore: boolean (more results available)
8681
+ - nextOffset?: number (offset for next page, when hasMore=true)`
8657
8682
  ),
8658
8683
  toMCPToolSchema(
8659
8684
  GetDependentsSchema,
@@ -8663,11 +8688,15 @@ Filter by symbol type (function, method, class, interface) to narrow results.
8663
8688
  - "Is this safe to delete?"
8664
8689
  - "What imports this module?"
8665
8690
 
8666
- Returns:
8667
- - List of files that import the target
8668
- - Risk level (low/medium/high/critical) based on dependent count and complexity
8691
+ Example: get_dependents({ filepath: "src/utils/validate.ts" })
8669
8692
 
8670
- Example: get_dependents({ filepath: "src/utils/validate.ts" })`
8693
+ Returns:
8694
+ - dependentCount / productionDependentCount / testDependentCount
8695
+ - riskLevel: "low" | "medium" | "high" | "critical"
8696
+ - dependents[]: { filepath, isTestFile, usages[]? }
8697
+ - complexityMetrics: { averageComplexity, maxComplexity, highComplexityDependents[] }
8698
+ - totalUsageCount?: number (when symbol parameter provided)
8699
+ - groupedByRepo?: Record<repoId, dependents[]> (when crossRepo=true)`
8671
8700
  ),
8672
8701
  toMCPToolSchema(
8673
8702
  GetComplexitySchema,
@@ -8690,19 +8719,108 @@ Examples:
8690
8719
  get_complexity({ files: ["src/auth.ts", "src/api/user.ts"] })
8691
8720
  get_complexity({ threshold: 15 })
8692
8721
 
8693
- Returns violations with metricType ('cyclomatic', 'cognitive', 'halstead_effort',
8694
- or 'halstead_bugs'), risk levels, and dependent counts.
8695
- Human-readable output: "23 (needs ~23 tests)", "\u{1F9E0} 45", "~2h 30m", "2.27 bugs".`
8722
+ Returns:
8723
+ - summary: { filesAnalyzed, avgComplexity, maxComplexity, violationCount, bySeverity: { error, warning } }
8724
+ - violations[]: { filepath, symbolName, symbolType, complexity, metricType, threshold, severity, riskLevel, dependentCount }
8725
+ - metricType: "cyclomatic" | "cognitive" | "halstead_effort" | "halstead_bugs"
8726
+ - severity: "error" | "warning"
8727
+ - groupedByRepo?: Record<repoId, violations[]> (when crossRepo=true)`
8696
8728
  )
8697
8729
  ];
8698
8730
 
8699
8731
  // src/mcp/utils/tool-wrapper.ts
8700
8732
  import { LienError, LienErrorCode } from "@liendev/core";
8733
+
8734
+ // src/mcp/utils/response-budget.ts
8735
+ var MAX_RESPONSE_CHARS = 12e3;
8736
+ function applyResponseBudget(result, maxChars = MAX_RESPONSE_CHARS) {
8737
+ const serialized = JSON.stringify(result);
8738
+ if (serialized.length <= maxChars) {
8739
+ return { result };
8740
+ }
8741
+ const originalChars = serialized.length;
8742
+ const cloned = JSON.parse(serialized);
8743
+ const arrays = findContentArrays(cloned);
8744
+ if (arrays.length === 0) {
8745
+ return { result };
8746
+ }
8747
+ for (const arr of arrays) {
8748
+ for (const item of arr) {
8749
+ item.content = truncateContent(item.content, 10);
8750
+ }
8751
+ }
8752
+ if (measureSize(cloned) <= maxChars) {
8753
+ return buildResult(cloned, originalChars, 1);
8754
+ }
8755
+ let currentSize = measureSize(cloned);
8756
+ for (const arr of arrays) {
8757
+ while (arr.length > 1 && currentSize > maxChars) {
8758
+ arr.pop();
8759
+ currentSize = measureSize(cloned);
8760
+ }
8761
+ }
8762
+ if (currentSize <= maxChars) {
8763
+ return buildResult(cloned, originalChars, 2);
8764
+ }
8765
+ for (const arr of arrays) {
8766
+ for (const item of arr) {
8767
+ item.content = truncateContent(item.content, 3);
8768
+ }
8769
+ }
8770
+ return buildResult(cloned, originalChars, 3);
8771
+ }
8772
+ function truncateContent(content, maxLines) {
8773
+ const lines = content.split("\n");
8774
+ if (lines.length <= maxLines) return content;
8775
+ return lines.slice(0, maxLines).join("\n") + "\n... (truncated)";
8776
+ }
8777
+ function measureSize(obj) {
8778
+ return JSON.stringify(obj).length;
8779
+ }
8780
+ function findContentArrays(obj) {
8781
+ const found = [];
8782
+ walk(obj, found);
8783
+ return found;
8784
+ }
8785
+ function walk(node, found) {
8786
+ if (node === null || typeof node !== "object") return;
8787
+ if (Array.isArray(node)) {
8788
+ if (node.length > 0 && node.every(
8789
+ (elem) => typeof elem === "object" && elem !== null && typeof elem.content === "string"
8790
+ )) {
8791
+ found.push(node);
8792
+ }
8793
+ return;
8794
+ }
8795
+ for (const value of Object.values(node)) {
8796
+ walk(value, found);
8797
+ }
8798
+ }
8799
+ function buildResult(cloned, originalChars, phase) {
8800
+ const finalChars = measureSize(cloned);
8801
+ return {
8802
+ result: cloned,
8803
+ truncation: {
8804
+ originalChars,
8805
+ finalChars,
8806
+ phase,
8807
+ message: `Response truncated from ${originalChars} to ${finalChars} chars (phase ${phase}/3). Use narrower filters or smaller limit for complete results.`
8808
+ }
8809
+ };
8810
+ }
8811
+
8812
+ // src/mcp/utils/tool-wrapper.ts
8701
8813
  function wrapToolHandler(schema, handler) {
8702
8814
  return async (args) => {
8703
8815
  try {
8704
8816
  const validated = schema.parse(args);
8705
- const result = await handler(validated);
8817
+ const rawResult = await handler(validated);
8818
+ const { result, truncation } = applyResponseBudget(rawResult);
8819
+ if (truncation && typeof result === "object" && result !== null) {
8820
+ const obj = result;
8821
+ obj.note = obj.note ? `${obj.note}
8822
+ ${truncation.message}` : truncation.message;
8823
+ }
8706
8824
  return {
8707
8825
  content: [{
8708
8826
  type: "text",
@@ -8710,45 +8828,48 @@ function wrapToolHandler(schema, handler) {
8710
8828
  }]
8711
8829
  };
8712
8830
  } catch (error) {
8713
- if (error instanceof ZodError) {
8714
- return {
8715
- isError: true,
8716
- content: [{
8717
- type: "text",
8718
- text: JSON.stringify({
8719
- error: "Invalid parameters",
8720
- code: LienErrorCode.INVALID_INPUT,
8721
- details: error.errors.map((e) => ({
8722
- field: e.path.join("."),
8723
- message: e.message
8724
- }))
8725
- }, null, 2)
8726
- }]
8727
- };
8728
- }
8729
- if (error instanceof LienError) {
8730
- return {
8731
- isError: true,
8732
- content: [{
8733
- type: "text",
8734
- text: JSON.stringify(error.toJSON(), null, 2)
8735
- }]
8736
- };
8737
- }
8738
- console.error("Unexpected error in tool handler:", error);
8739
- return {
8740
- isError: true,
8741
- content: [{
8742
- type: "text",
8743
- text: JSON.stringify({
8744
- error: error instanceof Error ? error.message : "Unknown error",
8745
- code: LienErrorCode.INTERNAL_ERROR
8746
- }, null, 2)
8747
- }]
8748
- };
8831
+ return formatErrorResponse(error);
8749
8832
  }
8750
8833
  };
8751
8834
  }
8835
+ function formatErrorResponse(error) {
8836
+ if (error instanceof ZodError) {
8837
+ return {
8838
+ isError: true,
8839
+ content: [{
8840
+ type: "text",
8841
+ text: JSON.stringify({
8842
+ error: "Invalid parameters",
8843
+ code: LienErrorCode.INVALID_INPUT,
8844
+ details: error.errors.map((e) => ({
8845
+ field: e.path.join("."),
8846
+ message: e.message
8847
+ }))
8848
+ }, null, 2)
8849
+ }]
8850
+ };
8851
+ }
8852
+ if (error instanceof LienError) {
8853
+ return {
8854
+ isError: true,
8855
+ content: [{
8856
+ type: "text",
8857
+ text: JSON.stringify(error.toJSON(), null, 2)
8858
+ }]
8859
+ };
8860
+ }
8861
+ console.error("Unexpected error in tool handler:", error);
8862
+ return {
8863
+ isError: true,
8864
+ content: [{
8865
+ type: "text",
8866
+ text: JSON.stringify({
8867
+ error: error instanceof Error ? error.message : "Unknown error",
8868
+ code: LienErrorCode.INTERNAL_ERROR
8869
+ }, null, 2)
8870
+ }]
8871
+ };
8872
+ }
8752
8873
 
8753
8874
  // src/mcp/utils/metadata-shaper.ts
8754
8875
  var FIELD_ALLOWLISTS = {
@@ -8841,6 +8962,9 @@ function pickMetadata(metadata, allowlist) {
8841
8962
  out[key] = cleaned;
8842
8963
  }
8843
8964
  }
8965
+ if (metadata.symbolName) {
8966
+ out["enclosingSymbol"] = metadata.parentClass ? `${metadata.parentClass}.${metadata.symbolName}` : metadata.symbolName;
8967
+ }
8844
8968
  return result;
8845
8969
  }
8846
8970
  function shapeResultMetadata(result, tool) {
@@ -8868,6 +8992,33 @@ function groupResultsByRepo(results) {
8868
8992
  }
8869
8993
  return grouped;
8870
8994
  }
8995
+ async function executeSearch(vectorDB, queryEmbedding, params, log) {
8996
+ const { query, limit, crossRepo, repoIds } = params;
8997
+ if (crossRepo && vectorDB instanceof QdrantDB) {
8998
+ const results2 = await vectorDB.searchCrossRepo(queryEmbedding, limit, { repoIds });
8999
+ log(`Found ${results2.length} results across ${Object.keys(groupResultsByRepo(results2)).length} repos`);
9000
+ return { results: results2, crossRepoFallback: false };
9001
+ }
9002
+ if (crossRepo) {
9003
+ log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo search.", "warning");
9004
+ }
9005
+ const results = await vectorDB.search(queryEmbedding, limit, query);
9006
+ log(`Found ${results.length} results`);
9007
+ return { results, crossRepoFallback: !!crossRepo };
9008
+ }
9009
+ function processResults(rawResults, crossRepoFallback, log) {
9010
+ const notes = [];
9011
+ if (crossRepoFallback) {
9012
+ notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
9013
+ }
9014
+ const results = deduplicateResults(rawResults);
9015
+ if (results.length > 0 && results.every((r) => r.relevance === "not_relevant")) {
9016
+ notes.push("No relevant matches found.");
9017
+ log("Returning 0 results (all not_relevant)");
9018
+ return { results: [], notes };
9019
+ }
9020
+ return { results, notes };
9021
+ }
8871
9022
  async function handleSemanticSearch(args, ctx) {
8872
9023
  const { vectorDB, embeddings, log, checkAndReconnect, getIndexMetadata } = ctx;
8873
9024
  return await wrapToolHandler(
@@ -8877,35 +9028,18 @@ async function handleSemanticSearch(args, ctx) {
8877
9028
  log(`Searching for: "${query}"${crossRepo ? " (cross-repo)" : ""}`);
8878
9029
  await checkAndReconnect();
8879
9030
  const queryEmbedding = await embeddings.embed(query);
8880
- let results;
8881
- let crossRepoFallback = false;
8882
- if (crossRepo && vectorDB instanceof QdrantDB) {
8883
- results = await vectorDB.searchCrossRepo(queryEmbedding, limit, { repoIds });
8884
- log(`Found ${results.length} results across ${Object.keys(groupResultsByRepo(results)).length} repos`);
8885
- } else {
8886
- if (crossRepo) {
8887
- log("Warning: crossRepo=true requires Qdrant backend. Falling back to single-repo search.");
8888
- crossRepoFallback = true;
8889
- }
8890
- results = await vectorDB.search(queryEmbedding, limit, query);
8891
- log(`Found ${results.length} results`);
8892
- }
8893
- results = deduplicateResults(results);
8894
- const notes = [];
8895
- if (crossRepoFallback) {
8896
- notes.push("Cross-repo search requires Qdrant backend. Fell back to single-repo search.");
8897
- }
8898
- if (results.length > 0 && results.every((r) => r.relevance === "not_relevant")) {
8899
- notes.push("No relevant matches found.");
8900
- log("Returning 0 results (all not_relevant)");
8901
- return {
8902
- indexInfo: getIndexMetadata(),
8903
- results: [],
8904
- note: notes.join(" ")
8905
- };
8906
- }
9031
+ const { results: rawResults, crossRepoFallback } = await executeSearch(
9032
+ vectorDB,
9033
+ queryEmbedding,
9034
+ { query, limit: limit ?? 5, crossRepo, repoIds },
9035
+ log
9036
+ );
9037
+ const { results, notes } = processResults(rawResults, crossRepoFallback, log);
8907
9038
  log(`Returning ${results.length} results`);
8908
9039
  const shaped = shapeResults(results, "semantic_search");
9040
+ if (shaped.length === 0) {
9041
+ notes.push('0 results. Try rephrasing as a full question (e.g. "How does X work?"), or use grep for exact string matches. If the codebase was recently updated, run "lien reindex".');
9042
+ }
8909
9043
  return {
8910
9044
  indexInfo: getIndexMetadata(),
8911
9045
  results: shaped,
@@ -8964,7 +9098,8 @@ async function handleFindSimilar(args, ctx) {
8964
9098
  return {
8965
9099
  indexInfo: getIndexMetadata(),
8966
9100
  results: shapeResults(finalResults, "find_similar"),
8967
- ...hasFilters && { filtersApplied }
9101
+ ...hasFilters && { filtersApplied },
9102
+ ...finalResults.length === 0 && { note: "0 results. Ensure the code snippet is at least 24 characters and representative of the pattern. Try grep for exact string matches." }
8968
9103
  };
8969
9104
  }
8970
9105
  )(args);
@@ -9063,19 +9198,15 @@ function isTestFile(filepath) {
9063
9198
  }
9064
9199
 
9065
9200
  // src/mcp/handlers/get-files-context.ts
9201
+ import { MAX_CHUNKS_PER_FILE } from "@liendev/core";
9066
9202
  var SCAN_LIMIT = 1e4;
9067
9203
  async function searchFileChunks(filepaths, ctx) {
9068
- const { vectorDB, embeddings, workspaceRoot } = ctx;
9069
- const fileEmbeddings = await Promise.all(
9070
- filepaths.map((fp) => embeddings.embed(fp))
9071
- );
9072
- const allFileSearches = await Promise.all(
9073
- fileEmbeddings.map(
9074
- (embedding, i) => vectorDB.search(embedding, 50, filepaths[i])
9075
- )
9076
- );
9077
- return filepaths.map((filepath, i) => {
9078
- const allResults = allFileSearches[i];
9204
+ const { vectorDB, workspaceRoot } = ctx;
9205
+ const allResults = await vectorDB.scanWithFilter({
9206
+ file: filepaths,
9207
+ limit: filepaths.length * MAX_CHUNKS_PER_FILE
9208
+ });
9209
+ return filepaths.map((filepath) => {
9079
9210
  const targetCanonical = getCanonicalPath(filepath, workspaceRoot);
9080
9211
  return allResults.filter((r) => {
9081
9212
  const chunkCanonical = getCanonicalPath(r.metadata.file, workspaceRoot);
@@ -9245,13 +9376,12 @@ async function handleGetFilesContext(args, ctx) {
9245
9376
  }
9246
9377
 
9247
9378
  // src/mcp/handlers/list-functions.ts
9248
- import { SYMBOL_TYPE_MATCHES } from "@liendev/core";
9249
- async function performContentScan(vectorDB, args, log) {
9379
+ async function performContentScan(vectorDB, args, fetchLimit, log) {
9250
9380
  log("Falling back to content scan...");
9251
9381
  let results = await vectorDB.scanWithFilter({
9252
9382
  language: args.language,
9253
- limit: 200
9254
- // Fetch more, we'll filter by symbolName
9383
+ symbolType: args.symbolType,
9384
+ limit: fetchLimit
9255
9385
  });
9256
9386
  if (args.pattern) {
9257
9387
  const regex = new RegExp(args.pattern, "i");
@@ -9260,16 +9390,37 @@ async function performContentScan(vectorDB, args, log) {
9260
9390
  return symbolName && regex.test(symbolName);
9261
9391
  });
9262
9392
  }
9263
- if (args.symbolType) {
9264
- const allowedTypes = SYMBOL_TYPE_MATCHES[args.symbolType];
9265
- results = results.filter((r) => {
9266
- const recordType2 = r.metadata?.symbolType;
9267
- return recordType2 && allowedTypes?.has(recordType2);
9393
+ return {
9394
+ results,
9395
+ method: "content"
9396
+ };
9397
+ }
9398
+ async function queryWithFallback(vectorDB, args, fetchLimit, log) {
9399
+ try {
9400
+ const results = await vectorDB.querySymbols({
9401
+ language: args.language,
9402
+ pattern: args.pattern,
9403
+ symbolType: args.symbolType,
9404
+ limit: fetchLimit
9268
9405
  });
9406
+ if (results.length === 0 && (args.language || args.pattern || args.symbolType)) {
9407
+ log("No symbol results, falling back to content scan...");
9408
+ return await performContentScan(vectorDB, args, fetchLimit, log);
9409
+ }
9410
+ return { results, method: "symbols" };
9411
+ } catch (error) {
9412
+ log(`Symbol query failed: ${error}`);
9413
+ return await performContentScan(vectorDB, args, fetchLimit, log);
9269
9414
  }
9415
+ }
9416
+ function paginateResults(results, offset, limit) {
9417
+ const dedupedResults = deduplicateResults(results);
9418
+ const hasMore = dedupedResults.length > offset + limit;
9419
+ const paginatedResults = dedupedResults.slice(offset, offset + limit);
9270
9420
  return {
9271
- results: results.slice(0, 50),
9272
- method: "content"
9421
+ paginatedResults,
9422
+ hasMore,
9423
+ ...hasMore ? { nextOffset: offset + limit } : {}
9273
9424
  };
9274
9425
  }
9275
9426
  async function handleListFunctions(args, ctx) {
@@ -9279,31 +9430,28 @@ async function handleListFunctions(args, ctx) {
9279
9430
  async (validatedArgs) => {
9280
9431
  log("Listing functions with symbol metadata...");
9281
9432
  await checkAndReconnect();
9282
- let queryResult;
9283
- try {
9284
- const results = await vectorDB.querySymbols({
9285
- language: validatedArgs.language,
9286
- pattern: validatedArgs.pattern,
9287
- symbolType: validatedArgs.symbolType,
9288
- limit: 50
9289
- });
9290
- if (results.length === 0 && (validatedArgs.language || validatedArgs.pattern || validatedArgs.symbolType)) {
9291
- log("No symbol results, falling back to content scan...");
9292
- queryResult = await performContentScan(vectorDB, validatedArgs, log);
9293
- } else {
9294
- queryResult = { results, method: "symbols" };
9295
- }
9296
- } catch (error) {
9297
- log(`Symbol query failed: ${error}`);
9298
- queryResult = await performContentScan(vectorDB, validatedArgs, log);
9433
+ const limit = validatedArgs.limit ?? 50;
9434
+ const offset = validatedArgs.offset ?? 0;
9435
+ const fetchLimit = limit + offset + 1;
9436
+ const queryResult = await queryWithFallback(vectorDB, validatedArgs, fetchLimit, log);
9437
+ const { paginatedResults, hasMore, nextOffset } = paginateResults(queryResult.results, offset, limit);
9438
+ log(`Found ${paginatedResults.length} matches using ${queryResult.method} method`);
9439
+ const notes = [];
9440
+ if (queryResult.results.length === 0) {
9441
+ notes.push('0 results. Try a broader regex pattern (e.g. ".*") or omit the symbolType filter. Use semantic_search for behavior-based queries.');
9442
+ } else if (paginatedResults.length === 0 && offset > 0) {
9443
+ notes.push("No results for this page. The offset is beyond the available results; try reducing or resetting the offset to 0.");
9444
+ }
9445
+ if (queryResult.method === "content") {
9446
+ notes.push('Using content search. Run "lien reindex" to enable faster symbol-based queries.');
9299
9447
  }
9300
- const dedupedResults = deduplicateResults(queryResult.results);
9301
- log(`Found ${dedupedResults.length} matches using ${queryResult.method} method`);
9302
9448
  return {
9303
9449
  indexInfo: getIndexMetadata(),
9304
9450
  method: queryResult.method,
9305
- results: shapeResults(dedupedResults, "list_functions"),
9306
- note: queryResult.method === "content" ? 'Using content search. Run "lien reindex" to enable faster symbol-based queries.' : void 0
9451
+ hasMore,
9452
+ ...nextOffset !== void 0 ? { nextOffset } : {},
9453
+ results: shapeResults(paginatedResults, "list_functions"),
9454
+ ...notes.length > 0 && { note: notes.join(" ") }
9307
9455
  };
9308
9456
  }
9309
9457
  )(args);