@farming-labs/docs 0.2.74 → 0.2.77

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.
Files changed (38) hide show
  1. package/dist/{agent-D0LEY2Vy.mjs → agent-D4hNGatV.mjs} +2 -2
  2. package/dist/agent-DRKj42ht.d.mts +938 -0
  3. package/dist/{agent-BJ0KXN-A.mjs → agent-SRIJ-oF8.mjs} +28 -6
  4. package/dist/{agent-evals-LIScgQVm.mjs → agent-evals-DmFoKy_y.mjs} +1 -1
  5. package/dist/{agent-export-B92pNe6g.mjs → agent-export-CM-p9_YD.mjs} +7 -7
  6. package/dist/agent-skills-bundle.d.mts +1 -1
  7. package/dist/{agent-skills-server-noFn1l_p.mjs → agent-skills-server-B2EXn2QD.mjs} +1 -1
  8. package/dist/{agent-skills-server-BqvIGQb1.d.mts → agent-skills-server-CJ4D0G9P.d.mts} +2 -2
  9. package/dist/agent-skills-vite.d.mts +3 -3
  10. package/dist/agent-skills-vite.mjs +2 -2
  11. package/dist/{agents-C228QGbp.mjs → agents-BeLDNCnE.mjs} +7 -16
  12. package/dist/cli/index.mjs +15 -15
  13. package/dist/client/react.d.mts +1 -1
  14. package/dist/{cloud-ask-ai-6X9pVjIY.d.mts → cloud-ask-ai-BCgHPxew.d.mts} +1 -1
  15. package/dist/{dev-Cc1cFFj-.mjs → dev-kjaxOOla.mjs} +8 -2
  16. package/dist/docs-cloud-server.d.mts +2 -2
  17. package/dist/{doctor-CgrUZ3tY.mjs → doctor-DhMVKHjp.mjs} +57 -24
  18. package/dist/{golden-evaluations-DTEPZeb0.mjs → golden-evaluations-VzE6juQl.mjs} +7 -3
  19. package/dist/index.d.mts +5 -876
  20. package/dist/index.mjs +6 -6
  21. package/dist/{mcp-YD37HlPY.mjs → mcp-CZkQJXtA.mjs} +7 -6
  22. package/dist/mcp.d.mts +30 -5
  23. package/dist/mcp.mjs +594 -69
  24. package/dist/{prompt-references-BIS1eXt4.mjs → prompt-references-B6L3AD98.mjs} +3 -3
  25. package/dist/{retrieval-digest-9Kp3uSaZ.d.mts → retrieval-digest-rponR4jM.d.mts} +24 -50
  26. package/dist/{review-Dbzkdl6K.mjs → review-nxoV8jIm.mjs} +5 -5
  27. package/dist/{robots-RbgmSvPH.mjs → robots-CgH6Stqn.mjs} +2 -2
  28. package/dist/{robots-DUO0r4UI.mjs → robots-CiQT8mX2.mjs} +3 -3
  29. package/dist/{search-hcqaEZVN.mjs → search-BTUIkhnT.mjs} +6 -6
  30. package/dist/{search-DIyIl-tE.mjs → search-Da2AKaFJ.mjs} +624 -122
  31. package/dist/server.d.mts +9 -6
  32. package/dist/server.mjs +7 -7
  33. package/dist/{sitemap-jhkS_jYx.mjs → sitemap-e0_GeCWJ.mjs} +6 -6
  34. package/dist/{sitemap-server-BfyBM7_V.mjs → sitemap-server-DHaxyPAd.mjs} +12 -3
  35. package/dist/{standards-discovery-BwL5ffGx.mjs → standards-discovery-BKlEnK_H.mjs} +32 -4
  36. package/dist/{standards-discovery-CdrOE-05.d.mts → standards-discovery-BbslY4id.d.mts} +34 -7
  37. package/dist/{types-clg5J1R4.d.mts → types-B-5Kzmkn.d.mts} +73 -1
  38. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { G as isDocsAgentDiscoveryRequest, K as isDocsAgentsRequest, Q as isDocsSkillRequest, _t as resolveDocsMarkdownRequest, dt as resolveDocsAgentsFormat, jt as resolveDocsAudienceMdxContent, lt as resolveDocsAgentFeedbackConfig, pt as resolveDocsLlmsTxtRequest, ut as resolveDocsAgentFeedbackRequest, xt as resolveDocsSkillFormat } from "./agent-BJ0KXN-A.mjs";
1
+ import { G as isDocsAgentDiscoveryRequest, K as isDocsAgentsRequest, Q as isDocsSkillRequest, _t as resolveDocsMarkdownRequest, dt as resolveDocsAgentsFormat, jt as resolveDocsAudienceMdxContent, lt as resolveDocsAgentFeedbackConfig, pt as resolveDocsLlmsTxtRequest, ut as resolveDocsAgentFeedbackRequest, xt as resolveDocsSkillFormat } from "./agent-SRIJ-oF8.mjs";
2
2
  import { S as upsertPageAgentContractMarkdown, a as findDocsGeneratedAgentContractRanges, d as PAGE_AGENT_CONTRACT_END_MARKER, l as stripDocsGeneratedAgentContractMarkers, m as PAGE_AGENT_CONTRACT_START_MARKER, o as findDocsMarkdownSection, s as parseDocsMarkdownSections, y as renderPageAgentContractMarkdown } from "./markdown-sections-7OoA7ylx.mjs";
3
3
 
4
4
  //#region src/telemetry.ts
@@ -624,6 +624,103 @@ function digestDocsRetrievalContent(value) {
624
624
  return `sha256:${Array.from(state, (word) => word.toString(16).padStart(8, "0")).join("")}`;
625
625
  }
626
626
 
627
+ //#endregion
628
+ //#region src/pagination.ts
629
+ const DOCS_PAGINATION_CURSOR_VERSION = 1;
630
+ const MAX_DOCS_PAGINATION_CURSOR_CHARS = 4096;
631
+ const MAX_DOCS_PAGINATION_STATE_CHARS = 1024;
632
+ const MAX_DOCS_PAGINATION_OFFSET = 1e7;
633
+ const DOCS_PAGINATION_DIGEST_PATTERN = /^sha256:[a-f\d]{64}$/u;
634
+ var DocsPaginationCursorError = class extends Error {
635
+ code = "invalid_cursor";
636
+ constructor() {
637
+ super("Invalid or stale pagination cursor.");
638
+ this.name = "DocsPaginationCursorError";
639
+ }
640
+ };
641
+ function paginationDigest(label, value) {
642
+ return digestDocsRetrievalContent(`${label}\0${value}`);
643
+ }
644
+ function cursorChecksum(payload) {
645
+ return paginationDigest("docs-pagination-checksum.v1", JSON.stringify(payload));
646
+ }
647
+ function encodeBase64Url(value) {
648
+ const bytes = new TextEncoder().encode(value);
649
+ let binary = "";
650
+ for (const byte of bytes) binary += String.fromCharCode(byte);
651
+ return btoa(binary).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "");
652
+ }
653
+ function decodeBase64Url(value) {
654
+ const padded = `${value.replace(/-/gu, "+").replace(/_/gu, "/")}${"=".repeat((4 - value.length % 4) % 4)}`;
655
+ const binary = atob(padded);
656
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
657
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
658
+ }
659
+ function isDigest(value) {
660
+ return typeof value === "string" && DOCS_PAGINATION_DIGEST_PATTERN.test(value);
661
+ }
662
+ function assertCursorOptions(options) {
663
+ if (!/^[a-z][a-z\d._/-]{0,127}$/u.test(options.kind) || typeof options.scope !== "string" || typeof options.snapshot !== "string") throw new TypeError("Pagination cursor options must contain a valid kind, scope, and snapshot.");
664
+ }
665
+ function createDocsPaginationCursor(offset, options) {
666
+ assertCursorOptions(options);
667
+ if (!Number.isSafeInteger(offset) || offset <= 0 || offset > MAX_DOCS_PAGINATION_OFFSET) throw new TypeError("Pagination cursor offsets must be positive safe integers.");
668
+ if (options.state !== void 0 && (options.state.length === 0 || options.state.length > MAX_DOCS_PAGINATION_STATE_CHARS)) throw new TypeError("Pagination provider state must be a bounded, non-empty string.");
669
+ const unsigned = [
670
+ DOCS_PAGINATION_CURSOR_VERSION,
671
+ options.kind,
672
+ offset,
673
+ paginationDigest("docs-pagination-scope.v1", options.scope),
674
+ paginationDigest("docs-pagination-snapshot.v1", options.snapshot),
675
+ options.state ?? null
676
+ ];
677
+ return encodeBase64Url(JSON.stringify([...unsigned, cursorChecksum(unsigned)]));
678
+ }
679
+ function resolveDocsPaginationCursor(cursor, options) {
680
+ assertCursorOptions(options);
681
+ if (cursor === void 0) return { offset: 0 };
682
+ if (cursor.length === 0 || cursor.length > MAX_DOCS_PAGINATION_CURSOR_CHARS || !/^[A-Za-z\d_-]+$/u.test(cursor)) throw new DocsPaginationCursorError();
683
+ let parsed;
684
+ try {
685
+ parsed = JSON.parse(decodeBase64Url(cursor));
686
+ } catch {
687
+ throw new DocsPaginationCursorError();
688
+ }
689
+ if (!Array.isArray(parsed) || parsed.length !== 7 || encodeBase64Url(JSON.stringify(parsed)) !== cursor) throw new DocsPaginationCursorError();
690
+ const [version, kind, offset, scopeDigest, snapshotDigest, state, checksum] = parsed;
691
+ const unsigned = [
692
+ version,
693
+ kind,
694
+ offset,
695
+ scopeDigest,
696
+ snapshotDigest,
697
+ state
698
+ ];
699
+ if (version !== DOCS_PAGINATION_CURSOR_VERSION || kind !== options.kind || !Number.isSafeInteger(offset) || offset <= 0 || offset > MAX_DOCS_PAGINATION_OFFSET || !isDigest(scopeDigest) || !isDigest(snapshotDigest) || !(state === null || typeof state === "string" && state.length > 0 && state.length <= MAX_DOCS_PAGINATION_STATE_CHARS) || !isDigest(checksum) || scopeDigest !== paginationDigest("docs-pagination-scope.v1", options.scope) || snapshotDigest !== paginationDigest("docs-pagination-snapshot.v1", options.snapshot) || checksum !== cursorChecksum(unsigned)) throw new DocsPaginationCursorError();
700
+ return {
701
+ offset,
702
+ ...typeof state === "string" ? { state } : {}
703
+ };
704
+ }
705
+ function resolveDocsPaginationOffset(cursor, options) {
706
+ return resolveDocsPaginationCursor(cursor, options).offset;
707
+ }
708
+ function paginateDocsItems(items, options) {
709
+ if (!Number.isSafeInteger(options.pageSize) || options.pageSize <= 0) throw new TypeError("Pagination page sizes must be positive safe integers.");
710
+ const offset = resolveDocsPaginationOffset(options.cursor, options);
711
+ if (offset >= items.length && options.cursor !== void 0) throw new DocsPaginationCursorError();
712
+ const pageItems = items.slice(offset, offset + options.pageSize);
713
+ const nextOffset = offset + pageItems.length;
714
+ const hasMore = nextOffset < items.length;
715
+ return {
716
+ items: pageItems,
717
+ resultCount: pageItems.length,
718
+ total: items.length,
719
+ hasMore,
720
+ ...hasMore ? { nextCursor: createDocsPaginationCursor(nextOffset, options) } : {}
721
+ };
722
+ }
723
+
627
724
  //#endregion
628
725
  //#region src/agent-scope.ts
629
726
  /** Normalize framework aliases used by page metadata, agent contracts, MCP, and evaluations. */
@@ -947,6 +1044,7 @@ function agentVersionConstraintsOverlap(left, right) {
947
1044
  //#endregion
948
1045
  //#region src/search.ts
949
1046
  const DEFAULT_SEARCH_LIMIT = 10;
1047
+ const MAX_STRUCTURED_SEARCH_LIMIT = 25;
950
1048
  const MAX_SEARCH_SNIPPET_CHARS = 160;
951
1049
  const DEFAULT_MCP_PROTOCOL_VERSION = "2025-11-25";
952
1050
  const MCP_SESSION_CLEANUP_TIMEOUT_MS = 1e3;
@@ -959,8 +1057,14 @@ const MAX_SEARCH_FILTER_VALUES = 16;
959
1057
  const MAX_SEARCH_FILTER_VALUE_CHARS = 128;
960
1058
  const MAX_SEARCH_FILTER_RAW_CHARS = 4096;
961
1059
  const MAX_SEARCH_FILTER_SEGMENTS = 64;
1060
+ const MAX_SEARCH_PROVIDER_CURSOR_STATE_CHARS = 1024;
962
1061
  const MAX_PROVIDER_SCOPE_FILTER_IDS = 1e3;
963
1062
  const MAX_PROVIDER_SCOPE_FILTER_CHARS = 16e3;
1063
+ const MAX_HOSTED_PROVIDER_CURSOR_RESULTS = 1e3;
1064
+ const MAX_MCP_SEARCH_CURSOR_REPLAY_PAGES = 64;
1065
+ const MAX_MCP_SEARCH_CURSOR_RESULTS = MAX_MCP_SEARCH_CURSOR_REPLAY_PAGES;
1066
+ const DOCS_UNPAGINATED_PROVIDER_QUERY = Symbol("docs-unpaginated-provider-query");
1067
+ const DOCS_PROVIDER_SCOPE_FILTER_OVERFLOW = Symbol("docs-provider-scope-filter-overflow");
964
1068
  const ALGOLIA_BATCH_OPERATIONS = 1e3;
965
1069
  const MAX_SEARCH_WARNINGS = 16;
966
1070
  const MAX_SEARCH_WARNING_VALUES = 16;
@@ -1057,11 +1161,41 @@ function resolveDocsSearchFilters(searchParams) {
1057
1161
  tags: searchParams.getAll("tags")
1058
1162
  });
1059
1163
  }
1164
+ var DocsSearchRequestError = class extends Error {
1165
+ code = "invalid_search_request";
1166
+ constructor(message) {
1167
+ super(message);
1168
+ this.name = "DocsSearchRequestError";
1169
+ }
1170
+ };
1171
+ function resolveDocsSearchError(error) {
1172
+ if (error instanceof DocsPaginationCursorError) return {
1173
+ code: error.code,
1174
+ message: error.message
1175
+ };
1176
+ if (error instanceof DocsSearchRequestError) return {
1177
+ code: error.code,
1178
+ message: error.message
1179
+ };
1180
+ }
1060
1181
  /** Resolve search filters and the backwards-compatible structured response opt-in. */
1061
1182
  function resolveDocsSearchRequest(searchParams) {
1183
+ const structured = searchParams.get("response") === "structured";
1184
+ const cursor = searchParams.get("cursor") ?? void 0;
1185
+ if (cursor !== void 0 && !structured) throw new DocsSearchRequestError("Search cursors require the structured response format (`response=structured`).");
1186
+ if (cursor !== void 0 && (cursor.length === 0 || cursor.length > 4096 || !/^[A-Za-z\d_-]+$/u.test(cursor))) throw new DocsPaginationCursorError();
1187
+ const rawLimit = searchParams.get("limit");
1188
+ let limit;
1189
+ if (rawLimit !== null && structured) {
1190
+ if (!/^\d+$/u.test(rawLimit)) throw new DocsSearchRequestError("Search limit must be an integer between 1 and 25.");
1191
+ limit = Number(rawLimit);
1192
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_STRUCTURED_SEARCH_LIMIT) throw new DocsSearchRequestError("Search limit must be an integer between 1 and 25.");
1193
+ }
1062
1194
  return {
1063
1195
  filters: resolveDocsSearchFilters(searchParams),
1064
- structured: searchParams.get("response") === "structured"
1196
+ structured,
1197
+ ...cursor !== void 0 ? { cursor } : {},
1198
+ ...limit !== void 0 ? { limit } : {}
1065
1199
  };
1066
1200
  }
1067
1201
  function hasDocsSearchFilters(filters) {
@@ -1071,7 +1205,7 @@ function resolveProviderScopeDocumentIds(query, context, corpusId) {
1071
1205
  if (!hasDocsSearchFilters(query.filters ?? {})) return void 0;
1072
1206
  const documents = corpusId ? buildDocsSearchDocuments(context.pages, context.chunking ?? { strategy: "section" }, "human") : context.documents;
1073
1207
  const ids = Array.from(new Set(documents.map((document) => corpusId ? makeHostedProviderDocumentId(corpusId, document.id) : document.id)));
1074
- return ids.length <= MAX_PROVIDER_SCOPE_FILTER_IDS ? ids : [];
1208
+ return ids.length <= MAX_PROVIDER_SCOPE_FILTER_IDS ? ids : DOCS_PROVIDER_SCOPE_FILTER_OVERFLOW;
1075
1209
  }
1076
1210
  function docsSearchFiltersMatch(expected, actual) {
1077
1211
  return SEARCH_FILTER_FIELDS.every((field) => {
@@ -2257,8 +2391,12 @@ function cleanSearchResultText(value) {
2257
2391
  if (!value) return void 0;
2258
2392
  return normalizeWhitespace(stripHtml(stripMarkdownText(value))) || void 0;
2259
2393
  }
2394
+ function getHostedIndexGenerationTag(indexGeneration) {
2395
+ return `docs-generation:${indexGeneration}`;
2396
+ }
2260
2397
  function buildAlgoliaRecord(document, corpusId) {
2261
2398
  const providerDocumentId = corpusId ? makeHostedProviderDocumentId(corpusId, document.id) : document.id;
2399
+ const generationTag = document.source?.indexGeneration ? getHostedIndexGenerationTag(document.source.indexGeneration) : void 0;
2262
2400
  const record = {
2263
2401
  objectID: providerDocumentId,
2264
2402
  id: providerDocumentId,
@@ -2273,7 +2411,7 @@ function buildAlgoliaRecord(document, corpusId) {
2273
2411
  version: document.version,
2274
2412
  package: document.package,
2275
2413
  tags: document.tags,
2276
- _tags: corpusId ? [corpusId] : void 0,
2414
+ _tags: corpusId || generationTag ? [corpusId, generationTag].filter((value) => Boolean(value)) : void 0,
2277
2415
  ...serializeHostedRetrievalSource(document.source, corpusId, document.id)
2278
2416
  };
2279
2417
  const encoder = new TextEncoder();
@@ -2313,39 +2451,50 @@ function buildAlgoliaRecord(document, corpusId) {
2313
2451
  if (sizeOf(record) > ALGOLIA_MAX_RECORD_BYTES) throw new Error(`Algolia record ${document.id} still exceeds the record limit after trimming.`);
2314
2452
  return record;
2315
2453
  }
2454
+ async function searchSimpleDocsPage(query, context) {
2455
+ const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
2456
+ const offset = query.offset ?? 0;
2457
+ const matches = context.documents.map((document) => ({
2458
+ document,
2459
+ score: scoreDocument(query.query, document)
2460
+ })).filter((item) => item.score > 0).sort((a, b) => {
2461
+ if (b.score !== a.score) return b.score - a.score;
2462
+ return a.document.url.localeCompare(b.document.url);
2463
+ });
2464
+ const results = matches.slice(offset, offset + limit).map(({ document, score }) => ({
2465
+ id: document.id,
2466
+ url: document.url,
2467
+ content: cleanSearchResultText(document.section ? `${document.title} — ${document.section}` : document.title) ?? (document.section ? `${document.title} — ${document.section}` : document.title),
2468
+ description: cleanSearchResultText(buildSnippet(document, query.query) ?? document.description),
2469
+ type: document.type,
2470
+ score,
2471
+ section: document.section,
2472
+ source: document.source
2473
+ }));
2474
+ return {
2475
+ results: context.deferSourceProvenance ? results : await enrichDocsSearchResultsWithSources({
2476
+ results,
2477
+ pages: context.pages,
2478
+ audience: resolveDocsSearchAudience(context.audience),
2479
+ chunking: context.chunking ?? { strategy: "section" },
2480
+ locale: query.locale ?? context.locale,
2481
+ baseUrl: context.baseUrl,
2482
+ indexGeneration: context.indexGeneration,
2483
+ filters: query.filters
2484
+ }),
2485
+ total: matches.length
2486
+ };
2487
+ }
2316
2488
  function createSimpleSearchAdapter() {
2317
2489
  return {
2318
2490
  name: "simple",
2319
2491
  async search(query, context) {
2320
- const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
2321
- const results = context.documents.map((document) => ({
2322
- document,
2323
- score: scoreDocument(query.query, document)
2324
- })).filter((item) => item.score > 0).sort((a, b) => {
2325
- if (b.score !== a.score) return b.score - a.score;
2326
- return a.document.url.localeCompare(b.document.url);
2327
- }).slice(0, limit).map(({ document, score }) => ({
2328
- id: document.id,
2329
- url: document.url,
2330
- content: cleanSearchResultText(document.section ? `${document.title} — ${document.section}` : document.title) ?? (document.section ? `${document.title} — ${document.section}` : document.title),
2331
- description: cleanSearchResultText(buildSnippet(document, query.query) ?? document.description),
2332
- type: document.type,
2333
- score,
2334
- section: document.section,
2335
- source: document.source
2336
- }));
2337
- if (context.deferSourceProvenance) return results;
2338
- return enrichDocsSearchResultsWithSources({
2339
- results,
2340
- pages: context.pages,
2341
- audience: resolveDocsSearchAudience(context.audience),
2342
- chunking: context.chunking ?? { strategy: "section" },
2343
- locale: query.locale ?? context.locale,
2344
- baseUrl: context.baseUrl,
2345
- indexGeneration: context.indexGeneration,
2346
- filters: query.filters
2347
- });
2348
- }
2492
+ return (await searchSimpleDocsPage({
2493
+ ...query,
2494
+ offset: 0
2495
+ }, context)).results;
2496
+ },
2497
+ searchPage: searchSimpleDocsPage
2349
2498
  };
2350
2499
  }
2351
2500
  function normalizeDocsSearchConfig(search) {
@@ -2373,6 +2522,46 @@ function normalizeDocsSearchConfig(search) {
2373
2522
  raw: search
2374
2523
  };
2375
2524
  }
2525
+ function getDocsSearchPaginationProviderIdentity(search) {
2526
+ const raw = search.raw;
2527
+ if (raw?.provider === "algolia") return {
2528
+ provider: "algolia",
2529
+ appId: raw.appId,
2530
+ indexName: raw.indexName,
2531
+ syncNamespace: raw.syncNamespace?.trim() || void 0
2532
+ };
2533
+ if (raw?.provider === "typesense") return {
2534
+ provider: "typesense",
2535
+ baseUrl: raw.baseUrl,
2536
+ collection: raw.collection,
2537
+ mode: raw.mode ?? "keyword",
2538
+ queryBy: raw.queryBy ?? [
2539
+ "title",
2540
+ "section",
2541
+ "content",
2542
+ "description"
2543
+ ],
2544
+ embeddings: raw.embeddings ? {
2545
+ provider: raw.embeddings.provider,
2546
+ model: raw.embeddings.model,
2547
+ baseUrl: raw.embeddings.baseUrl ?? "http://127.0.0.1:11434"
2548
+ } : void 0,
2549
+ syncNamespace: raw.syncNamespace?.trim() || void 0
2550
+ };
2551
+ if (raw?.provider === "mcp") return {
2552
+ provider: "mcp",
2553
+ endpoint: raw.endpoint,
2554
+ toolName: raw.toolName ?? "search_docs",
2555
+ protocolVersion: raw.protocolVersion ?? DEFAULT_MCP_PROTOCOL_VERSION,
2556
+ forwardAudience: raw.forwardAudience === true
2557
+ };
2558
+ if (raw?.provider === "custom") return {
2559
+ provider: "custom",
2560
+ adapterName: typeof raw.adapter === "object" && raw.adapter ? raw.adapter.name : void 0,
2561
+ paginationRevision: raw.paginationRevision?.trim() || void 0
2562
+ };
2563
+ return { provider: search.provider };
2564
+ }
2376
2565
  async function readResponseJson(response) {
2377
2566
  const text = await response.text();
2378
2567
  return text ? JSON.parse(text) : null;
@@ -2688,7 +2877,7 @@ async function ensureTypesenseImportSucceeded(response, expectedRecords) {
2688
2877
  }
2689
2878
  }
2690
2879
  function createTypesenseSearchAdapter(config) {
2691
- return {
2880
+ const adapter = {
2692
2881
  name: "typesense",
2693
2882
  async index(context) {
2694
2883
  const adminApiKey = config.adminApiKey ?? config.apiKey;
@@ -2733,9 +2922,23 @@ function createTypesenseSearchAdapter(config) {
2733
2922
  }), docsForImport.length);
2734
2923
  },
2735
2924
  async search(query, context) {
2925
+ return (await adapter.searchPage({
2926
+ ...query,
2927
+ offset: 0,
2928
+ [DOCS_UNPAGINATED_PROVIDER_QUERY]: true
2929
+ }, context)).results;
2930
+ },
2931
+ async searchPage(query, context) {
2932
+ const exactPagination = !query[DOCS_UNPAGINATED_PROVIDER_QUERY];
2736
2933
  const corpusId = resolveHostedHumanCorpusId(config.syncNamespace, context);
2737
2934
  const scopedDocumentIds = resolveProviderScopeDocumentIds(query, context, corpusId);
2738
- if (scopedDocumentIds?.length === 0) return [];
2935
+ if (scopedDocumentIds === DOCS_PROVIDER_SCOPE_FILTER_OVERFLOW) throw new Error("Typesense scope filters exceed the provider pagination limit.");
2936
+ if (scopedDocumentIds?.length === 0) return {
2937
+ results: [],
2938
+ total: 0
2939
+ };
2940
+ const pageSize = query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT;
2941
+ if ((query.offset ?? 0) % pageSize !== 0) throw new Error("Typesense search offset does not match its page boundary.");
2739
2942
  const params = new URLSearchParams({
2740
2943
  q: query.query,
2741
2944
  query_by: (config.queryBy ?? [
@@ -2744,18 +2947,23 @@ function createTypesenseSearchAdapter(config) {
2744
2947
  "content",
2745
2948
  "description"
2746
2949
  ]).join(","),
2747
- per_page: String(query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT),
2950
+ per_page: String(pageSize),
2748
2951
  prioritize_exact_match: "true",
2749
2952
  num_typos: "2",
2750
- highlight_fields: "content,title,section,description"
2953
+ highlight_fields: "content,title,section,description",
2954
+ page: String(Math.floor((query.offset ?? 0) / pageSize) + 1)
2751
2955
  });
2752
2956
  if (config.mode === "hybrid" && config.embeddings) {
2753
2957
  const vector = await createOllamaEmbedding(query.query, config.embeddings, context.signal);
2754
2958
  params.set("vector_query", `embedding:([${vector.join(",")}],k:${Math.max((query.limit ?? 10) * 4, 20)})`);
2755
2959
  }
2756
- const filterClauses = [corpusId ? `source_corpus_id:=${quoteTypesenseFilterValue(corpusId)}` : void 0, scopedDocumentIds ? `id:=[${scopedDocumentIds.map(quoteTypesenseFilterValue).join(",")}]` : void 0].filter((value) => Boolean(value));
2960
+ const filterClauses = [
2961
+ corpusId ? `source_corpus_id:=${quoteTypesenseFilterValue(corpusId)}` : void 0,
2962
+ context.indexGeneration ? `source_index_generation:=${quoteTypesenseFilterValue(context.indexGeneration)}` : void 0,
2963
+ scopedDocumentIds ? `id:=[${scopedDocumentIds.map(quoteTypesenseFilterValue).join(",")}]` : void 0
2964
+ ].filter((value) => Boolean(value));
2757
2965
  const filterBy = filterClauses.length > 0 ? filterClauses.join(" && ") : void 0;
2758
- if (filterBy && filterBy.length > MAX_PROVIDER_SCOPE_FILTER_CHARS) return [];
2966
+ if (filterBy && filterBy.length > MAX_PROVIDER_SCOPE_FILTER_CHARS) throw new Error("Typesense scope filters exceed the provider pagination limit.");
2759
2967
  const response = filterBy ? await fetch(`${getTypesenseSearchBase(config)}/multi_search`, {
2760
2968
  method: "POST",
2761
2969
  headers: {
@@ -2774,7 +2982,9 @@ function createTypesenseSearchAdapter(config) {
2774
2982
  });
2775
2983
  ensureOk(response, "Typesense search failed");
2776
2984
  const rawPayload = await readResponseJson(response);
2777
- return ((filterBy ? rawPayload.results?.[0] ?? {} : rawPayload).hits ?? []).flatMap((hit) => {
2985
+ const payload = filterBy ? rawPayload.results?.[0] ?? {} : rawPayload;
2986
+ if (exactPagination && payload.search_cutoff !== void 0 && payload.search_cutoff !== false) throw new Error("Typesense search was cut off before exact pagination completed.");
2987
+ const results = (payload.hits ?? []).flatMap((hit) => {
2778
2988
  const document = hit.document ?? {};
2779
2989
  if (!hostedRecordMatchesCorpus(document, corpusId)) return [];
2780
2990
  const section = typeof document.section === "string" ? document.section : void 0;
@@ -2797,8 +3007,18 @@ function createTypesenseSearchAdapter(config) {
2797
3007
  ...source ? { source } : {}
2798
3008
  }];
2799
3009
  });
3010
+ const exactTotal = typeof payload.found === "number" && Number.isSafeInteger(payload.found) && payload.found >= 0 ? payload.found : void 0;
3011
+ if (exactPagination && exactTotal === void 0) throw new Error("Typesense search did not return an exact result total.");
3012
+ const total = exactTotal ?? results.length;
3013
+ if (exactPagination && total > MAX_HOSTED_PROVIDER_CURSOR_RESULTS) throw new Error("Typesense search exceeds the exact cursor pagination window.");
3014
+ if (exactPagination && (query.offset ?? 0) + results.length < total && results.length !== pageSize) throw new Error("Typesense search returned an incomplete non-final page.");
3015
+ return {
3016
+ results,
3017
+ total
3018
+ };
2800
3019
  }
2801
3020
  };
3021
+ return adapter;
2802
3022
  }
2803
3023
  function resolveSearchRequestConfig(search, requestUrl) {
2804
3024
  if (!search || search === true || typeof search !== "object" || search.provider !== "mcp") return search;
@@ -2832,44 +3052,60 @@ function resolveAskAISearchRequestConfig(options) {
2832
3052
  }, options.requestUrl);
2833
3053
  }
2834
3054
  function createMcpSearchAdapter(config) {
2835
- return {
2836
- name: "mcp",
2837
- async search(query, context) {
2838
- const endpoint = resolveMcpEndpoint(config.endpoint);
2839
- const protocolVersion = config.protocolVersion ?? DEFAULT_MCP_PROTOCOL_VERSION;
2840
- const toolName = config.toolName ?? "search_docs";
2841
- const forwardAudience = config.forwardAudience === true;
2842
- const audience = resolveDocsSearchAudience(query.audience);
2843
- const baseHeaders = config.headers ?? {};
2844
- if (audience === "human" && !forwardAudience) throw new Error("MCP human-projection search requires forwardAudience: true on an audience-aware tool.");
2845
- const initializeResponse = await fetch(endpoint, {
3055
+ async function runMcpSearch(query, context, paginated) {
3056
+ const endpoint = resolveMcpEndpoint(config.endpoint);
3057
+ const protocolVersion = config.protocolVersion ?? DEFAULT_MCP_PROTOCOL_VERSION;
3058
+ const toolName = config.toolName ?? "search_docs";
3059
+ const forwardAudience = config.forwardAudience === true;
3060
+ const audience = resolveDocsSearchAudience(query.audience);
3061
+ const baseHeaders = config.headers ?? {};
3062
+ if (audience === "human" && !forwardAudience) throw new Error("MCP human-projection search requires forwardAudience: true on an audience-aware tool.");
3063
+ const initializeResponse = await fetch(endpoint, {
3064
+ method: "POST",
3065
+ headers: {
3066
+ ...baseHeaders,
3067
+ "Content-Type": "application/json",
3068
+ accept: "application/json, text/event-stream",
3069
+ "mcp-protocol-version": protocolVersion
3070
+ },
3071
+ body: JSON.stringify({
3072
+ jsonrpc: "2.0",
3073
+ id: 1,
3074
+ method: "initialize",
3075
+ params: {
3076
+ protocolVersion,
3077
+ capabilities: {},
3078
+ clientInfo: {
3079
+ name: "@farming-labs/docs-search",
3080
+ version: "0.1.2"
3081
+ }
3082
+ }
3083
+ }),
3084
+ signal: context.signal
3085
+ });
3086
+ const initializePayload = await readMcpResponsePayload(initializeResponse);
3087
+ ensureOk(initializeResponse, "MCP search initialization failed");
3088
+ ensureJsonRpcOk(initializePayload, "MCP search initialization failed");
3089
+ const sessionId = initializeResponse.headers.get("mcp-session-id") ?? void 0;
3090
+ try {
3091
+ ensureOk(await fetch(endpoint, {
2846
3092
  method: "POST",
2847
3093
  headers: {
2848
3094
  ...baseHeaders,
2849
3095
  "Content-Type": "application/json",
2850
3096
  accept: "application/json, text/event-stream",
2851
- "mcp-protocol-version": protocolVersion
3097
+ "mcp-protocol-version": protocolVersion,
3098
+ ...sessionId ? { "mcp-session-id": sessionId } : {}
2852
3099
  },
2853
3100
  body: JSON.stringify({
2854
3101
  jsonrpc: "2.0",
2855
- id: 1,
2856
- method: "initialize",
2857
- params: {
2858
- protocolVersion,
2859
- capabilities: {},
2860
- clientInfo: {
2861
- name: "@farming-labs/docs-search",
2862
- version: "0.1.2"
2863
- }
2864
- }
3102
+ method: "notifications/initialized",
3103
+ params: {}
2865
3104
  }),
2866
3105
  signal: context.signal
2867
- });
2868
- const initializePayload = await readMcpResponsePayload(initializeResponse);
2869
- ensureOk(initializeResponse, "MCP search initialization failed");
2870
- ensureJsonRpcOk(initializePayload, "MCP search initialization failed");
2871
- const sessionId = initializeResponse.headers.get("mcp-session-id") ?? void 0;
2872
- try {
3106
+ }), "MCP initialized notification failed");
3107
+ const pageSize = query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT;
3108
+ const callRemoteSearchPage = async (providerCursor, id) => {
2873
3109
  const searchResponse = await fetch(endpoint, {
2874
3110
  method: "POST",
2875
3111
  headers: {
@@ -2881,14 +3117,15 @@ function createMcpSearchAdapter(config) {
2881
3117
  },
2882
3118
  body: JSON.stringify({
2883
3119
  jsonrpc: "2.0",
2884
- id: 2,
3120
+ id,
2885
3121
  method: "tools/call",
2886
3122
  params: {
2887
3123
  name: toolName,
2888
3124
  arguments: {
2889
3125
  query: query.query,
2890
- limit: query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT,
3126
+ limit: pageSize,
2891
3127
  locale: query.locale,
3128
+ ...providerCursor !== void 0 ? { cursor: providerCursor } : {},
2892
3129
  ...forwardAudience ? { audience } : {},
2893
3130
  ...query.filters?.framework ? { framework: query.filters.framework } : {},
2894
3131
  ...query.filters?.version ? { version: query.filters.version } : {},
@@ -2903,32 +3140,107 @@ function createMcpSearchAdapter(config) {
2903
3140
  ensureOk(searchResponse, "MCP search request failed");
2904
3141
  ensureJsonRpcOk(payload, "MCP search request failed");
2905
3142
  const parsed = readMcpSearchToolPayload(payload);
2906
- if (!parsed) return [];
3143
+ if (!parsed) return {
3144
+ parsed,
3145
+ rawResults: [],
3146
+ results: []
3147
+ };
2907
3148
  if (hasDocsSearchFilters(query.filters ?? {})) {
2908
- if (Array.isArray(parsed) || parsed.format !== "docs-search.v1") return [];
3149
+ if (Array.isArray(parsed) || parsed.format !== "docs-search.v1") {
3150
+ if (paginated) throw new Error("MCP search returned unverifiable pagination data.");
3151
+ return {
3152
+ parsed,
3153
+ rawResults: [],
3154
+ results: []
3155
+ };
3156
+ }
2909
3157
  const echoedFilters = parseVerifiedMcpSearchFilters(parsed.filters);
2910
- if (!echoedFilters || !docsSearchFiltersMatch(query.filters ?? {}, echoedFilters)) return [];
2911
- }
2912
- return (Array.isArray(parsed) ? parsed : Array.isArray(parsed.results) ? parsed.results : Array.isArray(parsed.pages) ? parsed.pages : []).map((result) => mapMcpSearchResult(result, endpoint)).filter((result) => Boolean(result));
2913
- } finally {
2914
- if (sessionId) {
2915
- const cleanupController = new AbortController();
2916
- const cleanupTimeout = setTimeout(() => cleanupController.abort(), MCP_SESSION_CLEANUP_TIMEOUT_MS);
2917
- try {
2918
- await fetch(endpoint, {
2919
- method: "DELETE",
2920
- headers: {
2921
- ...baseHeaders,
2922
- "mcp-protocol-version": protocolVersion,
2923
- "mcp-session-id": sessionId
2924
- },
2925
- signal: cleanupController.signal
2926
- });
2927
- } catch {} finally {
2928
- clearTimeout(cleanupTimeout);
3158
+ if (!echoedFilters || !docsSearchFiltersMatch(query.filters ?? {}, echoedFilters)) {
3159
+ if (paginated) throw new Error("MCP search returned mismatched scope filters.");
3160
+ return {
3161
+ parsed,
3162
+ rawResults: [],
3163
+ results: []
3164
+ };
2929
3165
  }
2930
3166
  }
3167
+ const rawResults = Array.isArray(parsed) ? parsed : Array.isArray(parsed.results) ? parsed.results : Array.isArray(parsed.pages) ? parsed.pages : [];
3168
+ return {
3169
+ parsed,
3170
+ rawResults,
3171
+ results: rawResults.map((result) => mapMcpSearchResult(result, endpoint)).filter((result) => Boolean(result))
3172
+ };
3173
+ };
3174
+ if (!paginated) return (await callRemoteSearchPage(void 0, 2)).results;
3175
+ const targetOffset = query.offset ?? 0;
3176
+ if (targetOffset === 0 && query.cursor !== void 0 || targetOffset > 0 && query.cursor === void 0) throw new Error("MCP search continuation is missing or misplaced its provider cursor.");
3177
+ let consumed = 0;
3178
+ let remoteCursor;
3179
+ let expectedTotal;
3180
+ const replayResultDigests = [];
3181
+ for (let pageIndex = 0; pageIndex < MAX_MCP_SEARCH_CURSOR_REPLAY_PAGES; pageIndex += 1) {
3182
+ const { parsed, rawResults, results } = await callRemoteSearchPage(remoteCursor, pageIndex + 2);
3183
+ if (!parsed) {
3184
+ if (targetOffset === 0) return {
3185
+ results: [],
3186
+ total: 0
3187
+ };
3188
+ throw new Error("MCP search continuation ended before the requested page.");
3189
+ }
3190
+ if (Array.isArray(parsed) || parsed.format !== "docs-search.v1" || results.length !== rawResults.length || parsed.resultCount !== void 0 && parsed.resultCount !== results.length || typeof parsed.total !== "number" || !Number.isSafeInteger(parsed.total) || parsed.total < consumed + results.length || typeof parsed.hasMore !== "boolean" || parsed.nextCursor !== void 0 && (typeof parsed.nextCursor !== "string" || parsed.nextCursor.length > 768)) throw new Error("MCP search returned malformed paginated results.");
3191
+ const total = parsed.total;
3192
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : void 0;
3193
+ const hasMore = consumed + results.length < total;
3194
+ if (expectedTotal !== void 0 && total !== expectedTotal || parsed.hasMore !== hasMore || nextCursor !== void 0 !== hasMore || hasMore && results.length === 0) throw new Error("MCP search returned inconsistent pagination metadata.");
3195
+ expectedTotal ??= total;
3196
+ if (total > MAX_MCP_SEARCH_CURSOR_RESULTS) throw new Error("MCP search exceeds its bounded exact pagination window.");
3197
+ replayResultDigests.push(...results.map((result) => hashDocsRetrievalValue(JSON.stringify(result))));
3198
+ const nextOffset = consumed + results.length;
3199
+ const nextCheckpoint = hasMore ? hashDocsRetrievalValue(JSON.stringify({
3200
+ format: "docs-mcp-search-replay.v1",
3201
+ total,
3202
+ nextOffset,
3203
+ resultDigests: replayResultDigests
3204
+ })) : void 0;
3205
+ if (nextOffset === targetOffset && nextCheckpoint !== query.cursor) throw new Error("MCP search continuation cursor is stale.");
3206
+ if (consumed === targetOffset) return {
3207
+ results,
3208
+ total,
3209
+ ...nextCheckpoint !== void 0 ? { nextCursor: nextCheckpoint } : {}
3210
+ };
3211
+ if (!hasMore || nextCursor === void 0) throw new Error("MCP search continuation ended before the requested page.");
3212
+ consumed = nextOffset;
3213
+ if (consumed > targetOffset) throw new Error("MCP search continuation offset does not match a page boundary.");
3214
+ remoteCursor = nextCursor;
2931
3215
  }
3216
+ throw new Error("MCP search exceeds its bounded cursor replay limit.");
3217
+ } finally {
3218
+ if (sessionId) {
3219
+ const cleanupController = new AbortController();
3220
+ const cleanupTimeout = setTimeout(() => cleanupController.abort(), MCP_SESSION_CLEANUP_TIMEOUT_MS);
3221
+ try {
3222
+ await fetch(endpoint, {
3223
+ method: "DELETE",
3224
+ headers: {
3225
+ ...baseHeaders,
3226
+ "mcp-protocol-version": protocolVersion,
3227
+ "mcp-session-id": sessionId
3228
+ },
3229
+ signal: cleanupController.signal
3230
+ });
3231
+ } catch {} finally {
3232
+ clearTimeout(cleanupTimeout);
3233
+ }
3234
+ }
3235
+ }
3236
+ }
3237
+ return {
3238
+ name: "mcp",
3239
+ async search(query, context) {
3240
+ return await runMcpSearch(query, context, false);
3241
+ },
3242
+ async searchPage(query, context) {
3243
+ return await runMcpSearch(query, context, true);
2932
3244
  }
2933
3245
  };
2934
3246
  }
@@ -2992,7 +3304,7 @@ function quoteAlgoliaFilterValue(value) {
2992
3304
  return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
2993
3305
  }
2994
3306
  function createAlgoliaSearchAdapter(config) {
2995
- return {
3307
+ const adapter = {
2996
3308
  name: "algolia",
2997
3309
  async index(context) {
2998
3310
  if (!config.adminApiKey) return;
@@ -3011,13 +3323,31 @@ function createAlgoliaSearchAdapter(config) {
3011
3323
  })), "Failed to sync documents to Algolia", context.signal);
3012
3324
  },
3013
3325
  async search(query, context) {
3326
+ return (await adapter.searchPage({
3327
+ ...query,
3328
+ offset: 0,
3329
+ [DOCS_UNPAGINATED_PROVIDER_QUERY]: true
3330
+ }, context)).results;
3331
+ },
3332
+ async searchPage(query, context) {
3333
+ const exactPagination = !query[DOCS_UNPAGINATED_PROVIDER_QUERY];
3014
3334
  const corpusId = resolveHostedHumanCorpusId(config.syncNamespace, context);
3015
3335
  const scopedDocumentIds = resolveProviderScopeDocumentIds(query, context, corpusId);
3016
- if (scopedDocumentIds?.length === 0) return [];
3336
+ if (scopedDocumentIds === DOCS_PROVIDER_SCOPE_FILTER_OVERFLOW) throw new Error("Algolia scope filters exceed the provider pagination limit.");
3337
+ if (scopedDocumentIds?.length === 0) return {
3338
+ results: [],
3339
+ total: 0
3340
+ };
3017
3341
  const scopedIdsFilter = scopedDocumentIds?.map((id) => `objectID:${quoteAlgoliaFilterValue(id)}`).join(" OR ");
3018
- const filterClauses = [corpusId ? `_tags:${quoteAlgoliaFilterValue(corpusId)}` : void 0, scopedIdsFilter ? corpusId ? `(${scopedIdsFilter})` : scopedIdsFilter : void 0].filter((value) => Boolean(value));
3342
+ const filterClauses = [
3343
+ corpusId ? `_tags:${quoteAlgoliaFilterValue(corpusId)}` : void 0,
3344
+ context.indexGeneration ? `_tags:${quoteAlgoliaFilterValue(getHostedIndexGenerationTag(context.indexGeneration))}` : void 0,
3345
+ scopedIdsFilter ? corpusId || context.indexGeneration ? `(${scopedIdsFilter})` : scopedIdsFilter : void 0
3346
+ ].filter((value) => Boolean(value));
3019
3347
  const filters = filterClauses.length > 0 ? filterClauses.join(" AND ") : void 0;
3020
- if (filters && filters.length > MAX_PROVIDER_SCOPE_FILTER_CHARS) return [];
3348
+ if (filters && filters.length > MAX_PROVIDER_SCOPE_FILTER_CHARS) throw new Error("Algolia scope filters exceed the provider pagination limit.");
3349
+ const pageSize = query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT;
3350
+ if ((query.offset ?? 0) % pageSize !== 0) throw new Error("Algolia search offset does not match its page boundary.");
3021
3351
  const response = await fetch(`${getAlgoliaSearchBase(config)}/1/indexes/${encodeURIComponent(config.indexName)}/query`, {
3022
3352
  method: "POST",
3023
3353
  headers: {
@@ -3027,7 +3357,8 @@ function createAlgoliaSearchAdapter(config) {
3027
3357
  },
3028
3358
  body: JSON.stringify({
3029
3359
  query: query.query,
3030
- hitsPerPage: query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT,
3360
+ hitsPerPage: pageSize,
3361
+ page: Math.floor((query.offset ?? 0) / pageSize),
3031
3362
  restrictSearchableAttributes: [
3032
3363
  "title",
3033
3364
  "section",
@@ -3040,7 +3371,8 @@ function createAlgoliaSearchAdapter(config) {
3040
3371
  signal: context.signal
3041
3372
  });
3042
3373
  ensureOk(response, "Algolia search failed");
3043
- return ((await readResponseJson(response)).hits ?? []).flatMap((hit) => {
3374
+ const payload = await readResponseJson(response);
3375
+ const results = (payload.hits ?? []).flatMap((hit) => {
3044
3376
  if (!hostedRecordMatchesCorpus(hit, corpusId)) return [];
3045
3377
  const title = typeof hit.title === "string" ? hit.title : "Untitled result";
3046
3378
  const section = typeof hit.section === "string" ? hit.section : void 0;
@@ -3061,8 +3393,21 @@ function createAlgoliaSearchAdapter(config) {
3061
3393
  ...source ? { source } : {}
3062
3394
  }];
3063
3395
  });
3396
+ const exactTotal = typeof payload.nbHits === "number" && Number.isSafeInteger(payload.nbHits) && payload.nbHits >= 0 ? payload.nbHits : void 0;
3397
+ const exactPageCount = typeof payload.nbPages === "number" && Number.isSafeInteger(payload.nbPages) && payload.nbPages >= 0 ? payload.nbPages : void 0;
3398
+ if (exactPagination && (exactTotal === void 0 || exactPageCount === void 0)) throw new Error("Algolia search did not return exact reachable pagination metadata.");
3399
+ const total = exactTotal ?? results.length;
3400
+ if (exactPagination && total > MAX_HOSTED_PROVIDER_CURSOR_RESULTS) throw new Error("Algolia search exceeds the exact cursor pagination window.");
3401
+ if (exactPagination && payload.exhaustiveNbHits === false) throw new Error("Algolia search returned an approximate result total.");
3402
+ if (exactPagination && exactPageCount !== void 0 && total > exactPageCount * pageSize) throw new Error("Algolia search total exceeds its reachable pagination window.");
3403
+ if (exactPagination && (query.offset ?? 0) + results.length < total && results.length !== pageSize) throw new Error("Algolia search returned an incomplete non-final page.");
3404
+ return {
3405
+ results,
3406
+ total
3407
+ };
3064
3408
  }
3065
3409
  };
3410
+ return adapter;
3066
3411
  }
3067
3412
  async function resolveSearchAdapter(search, context) {
3068
3413
  const raw = search.raw;
@@ -3146,9 +3491,29 @@ async function maybeSyncSearchIndex(adapter, search, context) {
3146
3491
  }
3147
3492
  }
3148
3493
  }
3149
- async function performDocsSearch(options) {
3494
+ function normalizeDocsSearchAdapterPage(value) {
3495
+ if (Array.isArray(value)) return {
3496
+ results: value,
3497
+ total: value.length
3498
+ };
3499
+ if (!value || !Array.isArray(value.results) || !Number.isSafeInteger(value.total) || value.total < 0 || value.results.length > value.total || value.nextCursor !== void 0 && (typeof value.nextCursor !== "string" || value.nextCursor.length === 0 || JSON.stringify([
3500
+ 1,
3501
+ "provider",
3502
+ value.total,
3503
+ value.nextCursor
3504
+ ]).length > MAX_SEARCH_PROVIDER_CURSOR_STATE_CHARS)) throw new Error("Search adapter returned invalid pagination metadata.");
3505
+ return value;
3506
+ }
3507
+ async function performDocsSearchInternal(options) {
3150
3508
  const search = normalizeDocsSearchConfig(options.search);
3151
- if (!search.enabled) return [];
3509
+ if (!search.enabled) {
3510
+ if (options.paginationMode === "provider" || options.expectedTotal !== void 0 && options.expectedTotal !== 0) throw new DocsPaginationCursorError();
3511
+ return {
3512
+ results: [],
3513
+ total: 0,
3514
+ paginationMode: "local"
3515
+ };
3516
+ }
3152
3517
  const audience = resolveDocsSearchAudience(options.audience);
3153
3518
  const filters = normalizeDocsSearchFilters(options.filters);
3154
3519
  const hasFilters = hasDocsSearchFilters(filters);
@@ -3181,6 +3546,8 @@ async function performDocsSearch(options) {
3181
3546
  const query = {
3182
3547
  query: options.query,
3183
3548
  limit: options.limit ?? search.maxResults,
3549
+ ...options.paginated ? { offset: options.offset ?? 0 } : {},
3550
+ ...options.providerCursor !== void 0 ? { cursor: options.providerCursor } : {},
3184
3551
  locale: options.locale,
3185
3552
  pathname: options.pathname,
3186
3553
  audience,
@@ -3197,6 +3564,16 @@ async function performDocsSearch(options) {
3197
3564
  return currentIndexGeneration;
3198
3565
  };
3199
3566
  const requireCurrentIndexGeneration = search.provider === "algolia" || search.provider === "typesense";
3567
+ let hostedIndexGeneration;
3568
+ const getHostedIndexGeneration = () => {
3569
+ hostedIndexGeneration ??= audience === "human" ? getCurrentIndexGeneration() : buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
3570
+ audience: "human",
3571
+ chunking: search.chunking,
3572
+ locale: options.locale,
3573
+ baseUrl: indexBaseUrl
3574
+ });
3575
+ return hostedIndexGeneration;
3576
+ };
3200
3577
  const finalizeResults = async (results) => enrichDocsSearchResultsWithSources({
3201
3578
  results,
3202
3579
  pages: scopedPages,
@@ -3210,6 +3587,20 @@ async function performDocsSearch(options) {
3210
3587
  filters,
3211
3588
  requireCurrentIndexGeneration
3212
3589
  });
3590
+ const createLocalPage = async () => {
3591
+ const fallback = options.paginated ? await createSimpleSearchAdapter().searchPage(query, context) : normalizeDocsSearchAdapterPage(await createSimpleSearchAdapter().search(query, context));
3592
+ if (options.expectedTotal !== void 0 && fallback.total !== options.expectedTotal) throw new DocsPaginationCursorError();
3593
+ return {
3594
+ results: await finalizeResults(fallback.results),
3595
+ total: options.paginated ? fallback.total : fallback.results.length,
3596
+ paginationMode: "local"
3597
+ };
3598
+ };
3599
+ const fallBackFromProvider = () => {
3600
+ if (options.paginationMode === "provider") throw new DocsPaginationCursorError();
3601
+ return createLocalPage();
3602
+ };
3603
+ if (options.paginated && options.paginationMode === "local") return createLocalPage();
3213
3604
  try {
3214
3605
  const adapter = await resolveSearchAdapter(search, context);
3215
3606
  if (shouldSyncOnSearch(search) && typeof adapter.index === "function") {
@@ -3223,11 +3614,17 @@ async function performDocsSearch(options) {
3223
3614
  siteTitle: options.siteTitle,
3224
3615
  baseUrl: indexBaseUrl,
3225
3616
  indexBaseUrl,
3617
+ ...options.paginated && requireCurrentIndexGeneration ? { indexGeneration: await getHostedIndexGeneration() } : {},
3226
3618
  chunking: search.chunking,
3227
3619
  signal: options.signal
3228
3620
  });
3229
3621
  }
3230
- const adapterSearch = adapter.search(query, context);
3622
+ if (options.paginated && requireCurrentIndexGeneration) context.indexGeneration = await getHostedIndexGeneration();
3623
+ if (options.paginated && search.provider === "custom" && (!adapter.searchPage || search.raw?.provider !== "custom" || !search.raw.paginationRevision?.trim())) {
3624
+ if (options.paginationMode === "provider") throw new DocsPaginationCursorError();
3625
+ return createLocalPage();
3626
+ }
3627
+ const adapterSearch = options.paginated && adapter.searchPage ? adapter.searchPage(query, context) : adapter.search(query, context);
3231
3628
  let documents;
3232
3629
  try {
3233
3630
  documents = getDocuments();
@@ -3235,8 +3632,20 @@ async function performDocsSearch(options) {
3235
3632
  adapterSearch.catch(() => void 0);
3236
3633
  throw error;
3237
3634
  }
3238
- const results = await adapterSearch;
3239
- if (search.provider === "simple") return finalizeResults(results);
3635
+ const rawAdapterResult = await adapterSearch;
3636
+ const adapterPage = options.paginated && !adapter.searchPage && Array.isArray(rawAdapterResult) ? {
3637
+ results: rawAdapterResult.slice(options.offset ?? 0, (options.offset ?? 0) + (query.limit ?? search.maxResults)),
3638
+ total: rawAdapterResult.length
3639
+ } : normalizeDocsSearchAdapterPage(rawAdapterResult);
3640
+ const results = adapterPage.results;
3641
+ if (search.provider === "simple") {
3642
+ if (options.expectedTotal !== void 0 && adapterPage.total !== options.expectedTotal) throw new DocsPaginationCursorError();
3643
+ return {
3644
+ results: await finalizeResults(results),
3645
+ total: options.paginated ? adapterPage.total : results.length,
3646
+ paginationMode: "local"
3647
+ };
3648
+ }
3240
3649
  const localAudienceProjectionResults = buildAudienceProjectionSearchResults(documents, options.query);
3241
3650
  const localPagePaths = new Set(scopedPages.map((page) => normalizeUrlRouteKey(page.url)));
3242
3651
  const allLocalPagePaths = hasFilters && search.provider === "mcp" ? new Set(options.pages.map((page) => normalizeUrlRouteKey(page.url))) : localPagePaths;
@@ -3246,12 +3655,7 @@ async function performDocsSearch(options) {
3246
3655
  baseUrl: options.baseUrl
3247
3656
  }) : void 0;
3248
3657
  const isKnownLocalProviderResult = (result) => isLocalProviderResult(result, options.baseUrl) && localPagePaths.has(normalizeUrlRouteKey(result.url));
3249
- const expectedProviderGeneration = requireCurrentIndexGeneration ? audience === "human" ? await getCurrentIndexGeneration() : await buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
3250
- audience: "human",
3251
- chunking: search.chunking,
3252
- locale: options.locale,
3253
- baseUrl: indexBaseUrl
3254
- }) : void 0;
3658
+ const expectedProviderGeneration = requireCurrentIndexGeneration ? await getHostedIndexGeneration() : void 0;
3255
3659
  const providerAudience = requireCurrentIndexGeneration ? "human" : audience;
3256
3660
  const safeAdapterResults = sanitizeExternalAudienceSearchResults(results.filter((result) => {
3257
3661
  if (result.source === void 0) return !requireCurrentIndexGeneration || isKnownLocalProviderResult(result);
@@ -3265,19 +3669,45 @@ async function performDocsSearch(options) {
3265
3669
  audience,
3266
3670
  baseUrl: options.baseUrl
3267
3671
  })), localAudienceProjectionResults, options.baseUrl, preserveUnmatched, requireCurrentIndexGeneration && audience === "agent");
3268
- if (options.supplementExternalResults === false) return finalizeResults(safeAdapterResults.slice(0, query.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT));
3672
+ if (options.paginated) {
3673
+ const endOffset = (query.offset ?? 0) + safeAdapterResults.length;
3674
+ if (safeAdapterResults.length !== results.length || safeAdapterResults.length > (query.limit ?? search.maxResults) || endOffset > adapterPage.total || requireCurrentIndexGeneration && adapterPage.total === 0 && localAudienceProjectionResults.length > 0 || endOffset < adapterPage.total && safeAdapterResults.length === 0 || adapterPage.nextCursor !== void 0 && options.providerCursor !== void 0 && adapterPage.nextCursor === options.providerCursor || adapterPage.nextCursor !== void 0 && endOffset >= adapterPage.total || options.expectedTotal !== void 0 && adapterPage.total !== options.expectedTotal) return fallBackFromProvider();
3675
+ return {
3676
+ results: await finalizeResults(safeAdapterResults),
3677
+ total: adapterPage.total,
3678
+ ...adapterPage.nextCursor !== void 0 ? { nextCursor: adapterPage.nextCursor } : {},
3679
+ paginationMode: "provider"
3680
+ };
3681
+ }
3682
+ if (options.supplementExternalResults === false) {
3683
+ const bounded = safeAdapterResults.slice(0, query.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT);
3684
+ return {
3685
+ results: await finalizeResults(bounded),
3686
+ total: bounded.length,
3687
+ paginationMode: "provider"
3688
+ };
3689
+ }
3269
3690
  const simpleAudienceResults = audience === "agent" || results.length === 0 || safeAdapterResults.length < results.length ? await createSimpleSearchAdapter().search(query, context) : [];
3270
3691
  const combinedResults = mergeSearchResults([
3271
3692
  buildExactPageSearchResults(options.query, scopedPages, audience),
3272
3693
  safeAdapterResults,
3273
3694
  simpleAudienceResults
3274
3695
  ], audience === "agent" ? (result) => getAskAIResultKey(result, options.baseUrl, options.strictExternalOrigins) : void 0);
3275
- return finalizeResults(prioritizeLiteralInsideResults(options.query, combinedResults).slice(0, query.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT));
3696
+ const bounded = prioritizeLiteralInsideResults(options.query, combinedResults).slice(0, query.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT);
3697
+ return {
3698
+ results: await finalizeResults(bounded),
3699
+ total: bounded.length,
3700
+ paginationMode: "provider"
3701
+ };
3276
3702
  } catch (error) {
3703
+ if (error instanceof DocsPaginationCursorError) throw error;
3277
3704
  if (options.failureMode === "throw") throw error;
3278
- return finalizeResults(await createSimpleSearchAdapter().search(query, context));
3705
+ return fallBackFromProvider();
3279
3706
  }
3280
3707
  }
3708
+ async function performDocsSearch(options) {
3709
+ return (await performDocsSearchInternal(options)).results;
3710
+ }
3281
3711
  function compareSearchMetadataValues(left, right) {
3282
3712
  if (left === right) return 0;
3283
3713
  return left < right ? -1 : 1;
@@ -3340,7 +3770,7 @@ function buildDocsSearchWarnings(options) {
3340
3770
  count: unknown.length
3341
3771
  });
3342
3772
  }
3343
- const resultPages = findSearchResultPages(options.pages, options.results, options.baseUrl);
3773
+ const resultPages = options.completeResultSet ? relevantPageScopes.filter(({ scope }) => docsSearchPageMatchesFilters(scope, options.filters)).map(({ page }) => page).sort((left, right) => compareSearchMetadataValues(left.url, right.url)) : findSearchResultPages(options.pages, options.results, options.baseUrl);
3344
3774
  for (const field of SEARCH_AMBIGUITY_FIELDS) {
3345
3775
  if ((options.filters[field]?.length ?? 0) > 0) continue;
3346
3776
  const values = resultPages.flatMap((page) => {
@@ -3374,37 +3804,108 @@ function buildDocsSearchWarnings(options) {
3374
3804
  return SEARCH_FILTER_FIELDS.indexOf(left.field) - SEARCH_FILTER_FIELDS.indexOf(right.field);
3375
3805
  }).slice(0, MAX_SEARCH_WARNINGS);
3376
3806
  }
3807
+ function parseDocsSearchCursorState(state, offset) {
3808
+ if (offset === 0 && state === void 0) return void 0;
3809
+ if (state === void 0) throw new DocsPaginationCursorError();
3810
+ let parsed;
3811
+ try {
3812
+ parsed = JSON.parse(state);
3813
+ } catch {
3814
+ throw new DocsPaginationCursorError();
3815
+ }
3816
+ if (!Array.isArray(parsed) || parsed.length !== 4 || JSON.stringify(parsed) !== state) throw new DocsPaginationCursorError();
3817
+ const [version, mode, total, providerCursor] = parsed;
3818
+ if (version !== 1 || mode !== "local" && mode !== "provider" || !Number.isSafeInteger(total) || total < offset || !(providerCursor === null || typeof providerCursor === "string" && providerCursor.length <= 768) || mode === "local" && providerCursor !== null) throw new DocsPaginationCursorError();
3819
+ return {
3820
+ mode,
3821
+ total,
3822
+ ...typeof providerCursor === "string" ? { providerCursor } : {}
3823
+ };
3824
+ }
3825
+ function createDocsSearchCursorState(page) {
3826
+ return JSON.stringify([
3827
+ 1,
3828
+ page.paginationMode,
3829
+ page.total,
3830
+ page.paginationMode === "provider" ? page.nextCursor ?? null : null
3831
+ ]);
3832
+ }
3377
3833
  async function performDocsSearchWithMetadata(options) {
3378
3834
  const audience = resolveDocsSearchAudience(options.audience);
3379
3835
  const filters = normalizeDocsSearchFilters(options.filters);
3380
3836
  const search = normalizeDocsSearchConfig(options.search);
3837
+ const requestedLimit = options.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT;
3838
+ const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.max(1, Math.floor(requestedLimit)) : DEFAULT_SEARCH_LIMIT;
3381
3839
  const indexGeneration = options.indexGeneration ?? await buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
3382
3840
  audience,
3383
3841
  chunking: search.chunking,
3384
3842
  locale: options.locale,
3385
3843
  baseUrl: options.baseUrl
3386
3844
  });
3387
- const results = options.query.trim() ? await performDocsSearch({
3845
+ const normalizedFilters = Object.fromEntries(SEARCH_FILTER_FIELDS.flatMap((field) => {
3846
+ const values = filters[field];
3847
+ return values?.length ? [[field, [...values].sort(compareSearchMetadataValues)]] : [];
3848
+ }));
3849
+ const provider = getDocsSearchPaginationProviderIdentity(search);
3850
+ const cursorOptions = {
3851
+ kind: "docs-search",
3852
+ scope: JSON.stringify({
3853
+ format: "docs-search-pagination.v2",
3854
+ query: options.query,
3855
+ audience,
3856
+ filters: normalizedFilters,
3857
+ locale: options.locale ? normalizeAgentLocale(options.locale) : void 0,
3858
+ pathname: options.pathname?.trim() || void 0,
3859
+ provider,
3860
+ limit
3861
+ }),
3862
+ snapshot: indexGeneration
3863
+ };
3864
+ const cursorPosition = resolveDocsPaginationCursor(options.cursor, cursorOptions);
3865
+ const offset = cursorPosition.offset;
3866
+ const cursorState = parseDocsSearchCursorState(cursorPosition.state, offset);
3867
+ const page = options.query.trim() ? await performDocsSearchInternal({
3388
3868
  ...options,
3389
3869
  audience,
3390
3870
  filters,
3391
- indexGeneration
3392
- }) : [];
3871
+ indexGeneration,
3872
+ limit,
3873
+ offset,
3874
+ paginated: true,
3875
+ providerCursor: cursorState?.providerCursor,
3876
+ paginationMode: cursorState?.mode,
3877
+ expectedTotal: cursorState?.total
3878
+ }) : {
3879
+ results: [],
3880
+ total: 0,
3881
+ paginationMode: "local"
3882
+ };
3883
+ if (options.cursor !== void 0 && offset >= page.total || page.results.length > Math.min(limit, Math.max(0, page.total - offset)) || offset < page.total && page.results.length === 0) throw new DocsPaginationCursorError();
3884
+ const nextOffset = Math.min(page.total, offset + page.results.length);
3885
+ const hasMore = nextOffset < page.total;
3886
+ const nextCursor = hasMore ? createDocsPaginationCursor(nextOffset, {
3887
+ ...cursorOptions,
3888
+ state: createDocsSearchCursorState(page)
3889
+ }) : void 0;
3393
3890
  return {
3394
3891
  format: "docs-search.v1",
3395
3892
  query: options.query,
3396
3893
  audience,
3397
3894
  filters,
3398
3895
  indexGeneration,
3399
- resultCount: results.length,
3400
- results,
3896
+ resultCount: page.results.length,
3897
+ total: page.total,
3898
+ hasMore,
3899
+ ...nextCursor ? { nextCursor } : {},
3900
+ results: page.results,
3401
3901
  warnings: buildDocsSearchWarnings({
3402
3902
  pages: options.pages,
3403
- results,
3903
+ results: page.results,
3404
3904
  filters,
3405
3905
  query: options.query,
3406
3906
  audience,
3407
- baseUrl: options.baseUrl
3907
+ baseUrl: options.baseUrl,
3908
+ completeResultSet: true
3408
3909
  })
3409
3910
  };
3410
3911
  }
@@ -3479,12 +3980,13 @@ async function buildDocsAskAIContext(options) {
3479
3980
  packageHints: inferDocsAskAIPackageHints(context)
3480
3981
  };
3481
3982
  }
3482
- function createCustomSearchAdapter(adapter) {
3983
+ function createCustomSearchAdapter(adapter, options = {}) {
3483
3984
  return {
3484
3985
  provider: "custom",
3485
- adapter
3986
+ adapter,
3987
+ ...options.paginationRevision ? { paginationRevision: options.paginationRevision } : {}
3486
3988
  };
3487
3989
  }
3488
3990
 
3489
3991
  //#endregion
3490
- export { GENERATED_AGENT_PROVENANCE_MARKER as A, resolveSidebarFolderIndexBehaviorForPath as B, agentVersionConstraintsOverlap as C, normalizeAgentVersion as D, normalizeAgentScopeValues as E, serializeGeneratedAgentDocument as F, getDocsTelemetryFeatures as G, emitDocsTelemetryEvent as H, stripGeneratedAgentProvenance as I, normalizeDocsTelemetryOrigin as J, inferDocsTelemetryAgentSurface as K, applySidebarFolderIndexBehavior as L, hashGeneratedAgentContent as M, normalizeGeneratedAgentContent as N, digestDocsRetrievalContent as O, parseGeneratedAgentDocument as P, resolvePageSidebarFolderIndexBehavior as R, agentVersionConstraintMatches as S, normalizeAgentLocale as T, emitDocsTelemetryMcpToolEvent as U, emitDocsTelemetryAgentSurfaceEvent as V, emitDocsTelemetryProjectEvent as W, resolveDocsTelemetryConfig as Y, resolveDocsSearchAudience as _, createCustomSearchAdapter as a, resolveSearchRequestConfig as b, createTypesenseSearchAdapter as c, inferDocsAskAIPackageHints as d, normalizeDocsSearchFilters as f, resolveDocsRetrievalLastModified as g, resolveAskAISearchRequestConfig as h, createAlgoliaSearchAdapter as i, GENERATED_AGENT_PROVENANCE_VERSION as j, isDocsRetrievalCanonicalUrl as k, enrichDocsSearchDocumentsWithProvenance as l, performDocsSearchWithMetadata as m, buildDocsRetrievalDigestProjection as n, createMcpSearchAdapter as o, performDocsSearch as p, isLocalDocsTelemetryOrigin as q, buildDocsSearchDocuments as r, createSimpleSearchAdapter as s, buildDocsAskAIContext as t, formatDocsAskAIPackageHints as u, resolveDocsSearchFilters as v, normalizeAgentFramework as w, agentVersionConstraintGroupsOverlap as x, resolveDocsSearchRequest as y, resolveSidebarFolderIndexBehavior as z };
3992
+ export { resolveDocsTelemetryConfig as $, DocsPaginationCursorError as A, stripGeneratedAgentProvenance as B, agentVersionConstraintGroupsOverlap as C, normalizeAgentLocale as D, normalizeAgentFramework as E, GENERATED_AGENT_PROVENANCE_VERSION as F, emitDocsTelemetryAgentSurfaceEvent as G, resolvePageSidebarFolderIndexBehavior as H, hashGeneratedAgentContent as I, emitDocsTelemetryProjectEvent as J, emitDocsTelemetryEvent as K, normalizeGeneratedAgentContent as L, digestDocsRetrievalContent as M, isDocsRetrievalCanonicalUrl as N, normalizeAgentScopeValues as O, GENERATED_AGENT_PROVENANCE_MARKER as P, normalizeDocsTelemetryOrigin as Q, parseGeneratedAgentDocument as R, resolveSearchRequestConfig as S, agentVersionConstraintsOverlap as T, resolveSidebarFolderIndexBehavior as U, applySidebarFolderIndexBehavior as V, resolveSidebarFolderIndexBehaviorForPath as W, inferDocsTelemetryAgentSurface as X, getDocsTelemetryFeatures as Y, isLocalDocsTelemetryOrigin as Z, resolveDocsRetrievalLastModified as _, createAlgoliaSearchAdapter as a, resolveDocsSearchFilters as b, createSimpleSearchAdapter as c, formatDocsAskAIPackageHints as d, inferDocsAskAIPackageHints as f, resolveAskAISearchRequestConfig as g, performDocsSearchWithMetadata as h, buildDocsSearchDocuments as i, paginateDocsItems as j, normalizeAgentVersion as k, createTypesenseSearchAdapter as l, performDocsSearch as m, buildDocsAskAIContext as n, createCustomSearchAdapter as o, normalizeDocsSearchFilters as p, emitDocsTelemetryMcpToolEvent as q, buildDocsRetrievalDigestProjection as r, createMcpSearchAdapter as s, DocsSearchRequestError as t, enrichDocsSearchDocumentsWithProvenance as u, resolveDocsSearchAudience as v, agentVersionConstraintMatches as w, resolveDocsSearchRequest as x, resolveDocsSearchError as y, serializeGeneratedAgentDocument as z };