@bpinternal/site-scout 0.1.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.cjs ADDED
@@ -0,0 +1,1797 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ cachedDeps: () => cachedDeps,
35
+ discover: () => discover,
36
+ fetchJson: () => fetchJson,
37
+ fetchText: () => fetchText,
38
+ findKnowledgeUrls: () => findKnowledgeUrls,
39
+ hostOf: () => hostOf,
40
+ select: () => select,
41
+ siteOrigin: () => siteOrigin
42
+ });
43
+ module.exports = __toCommonJS(src_exports);
44
+
45
+ // src/web-fetch.ts
46
+ var import_promises = require("dns/promises");
47
+ var FETCH_TIMEOUT_MS = 6e3;
48
+ var DNS_TIMEOUT_MS = 2e3;
49
+ var MAX_REDIRECTS = 3;
50
+ var MAX_RESPONSE_BYTES = 2e6;
51
+ var SITE_FETCH_USER_AGENT = "BotpressVibe/1.0 (+https://botpress.com) site-analyzer";
52
+ async function readBodyCapped(res) {
53
+ const reader = res.body?.getReader?.();
54
+ if (!reader) {
55
+ const text2 = await res.text();
56
+ return text2.length > MAX_RESPONSE_BYTES ? null : text2;
57
+ }
58
+ const decoder = new TextDecoder();
59
+ let out = "";
60
+ let bytes = 0;
61
+ try {
62
+ for (; ; ) {
63
+ const { done, value } = await reader.read();
64
+ if (done) break;
65
+ bytes += value?.byteLength ?? 0;
66
+ if (bytes > MAX_RESPONSE_BYTES) {
67
+ try {
68
+ await reader.cancel();
69
+ } catch {
70
+ }
71
+ return null;
72
+ }
73
+ out += decoder.decode(value, { stream: true });
74
+ }
75
+ out += decoder.decode();
76
+ return out;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ __name(readBodyCapped, "readBodyCapped");
82
+ async function fetchText(url, baseHost, timeoutMs = FETCH_TIMEOUT_MS) {
83
+ const res = await safeFetch(url, baseHost, timeoutMs);
84
+ if (!res) return null;
85
+ return readBodyCapped(res);
86
+ }
87
+ __name(fetchText, "fetchText");
88
+ async function fetchJson(url, baseHost, timeoutMs = FETCH_TIMEOUT_MS) {
89
+ const res = await safeFetch(url, baseHost, timeoutMs, "application/json");
90
+ if (!res) return null;
91
+ const text2 = await readBodyCapped(res);
92
+ if (text2 == null) return null;
93
+ try {
94
+ return JSON.parse(text2);
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ __name(fetchJson, "fetchJson");
100
+ async function safeFetch(url, baseHost, timeoutMs, accept) {
101
+ if (typeof fetch !== "function") return null;
102
+ let target = url;
103
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
104
+ if (!isFetchableSameSite(target, baseHost)) return null;
105
+ const host = hostOf(target);
106
+ if (!host || !await hostResolvesToAllowed(host)) return null;
107
+ const ctl = new AbortController();
108
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
109
+ let res;
110
+ try {
111
+ res = await fetch(target, {
112
+ signal: ctl.signal,
113
+ headers: { "user-agent": SITE_FETCH_USER_AGENT, ...accept ? { accept } : {} },
114
+ redirect: "manual"
115
+ });
116
+ } catch {
117
+ clearTimeout(timer);
118
+ return null;
119
+ }
120
+ clearTimeout(timer);
121
+ const status = typeof res.status === "number" ? res.status : 200;
122
+ if (status >= 300 && status < 400) {
123
+ const loc = res.headers?.get?.("location");
124
+ if (!loc) return null;
125
+ try {
126
+ target = new URL(loc, target).toString();
127
+ } catch {
128
+ return null;
129
+ }
130
+ continue;
131
+ }
132
+ if (!res.ok) return null;
133
+ const len = Number(res.headers?.get?.("content-length") ?? "");
134
+ if (Number.isFinite(len) && len > MAX_RESPONSE_BYTES) return null;
135
+ return res;
136
+ }
137
+ return null;
138
+ }
139
+ __name(safeFetch, "safeFetch");
140
+ function siteOrigin(baseUrl) {
141
+ try {
142
+ return new URL(baseUrl).origin;
143
+ } catch {
144
+ try {
145
+ return new URL(`https://${baseUrl}`).origin;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ }
151
+ __name(siteOrigin, "siteOrigin");
152
+ function hostOf(url) {
153
+ try {
154
+ return new URL(url).hostname.toLowerCase() || null;
155
+ } catch {
156
+ try {
157
+ return new URL(`https://${url}`).hostname.toLowerCase() || null;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ }
163
+ __name(hostOf, "hostOf");
164
+ function registrableDomain(host) {
165
+ return host.split(".").slice(-2).join(".");
166
+ }
167
+ __name(registrableDomain, "registrableDomain");
168
+ function isIpLiteral(host) {
169
+ if (host.includes(":")) return true;
170
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
171
+ }
172
+ __name(isIpLiteral, "isIpLiteral");
173
+ function isFetchableSameSite(url, baseHost) {
174
+ let u;
175
+ try {
176
+ u = new URL(url);
177
+ } catch {
178
+ return false;
179
+ }
180
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
181
+ const host = u.hostname.toLowerCase();
182
+ if (!host || host === "localhost" || !host.includes(".") || isIpLiteral(host)) return false;
183
+ return registrableDomain(host) === registrableDomain(baseHost);
184
+ }
185
+ __name(isFetchableSameSite, "isFetchableSameSite");
186
+ function isDisallowedIpv4(ip) {
187
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
188
+ if (!m) return false;
189
+ const a = Number(m[1]);
190
+ const b = Number(m[2]);
191
+ const c = Number(m[3]);
192
+ if (a === 0 || a === 10 || a === 127) return true;
193
+ if (a === 169 && b === 254) return true;
194
+ if (a === 172 && b >= 16 && b <= 31) return true;
195
+ if (a === 192 && b === 168) return true;
196
+ if (a === 100 && b >= 64 && b <= 127) return true;
197
+ if (a === 192 && b === 0 && c === 0) return true;
198
+ if (a === 198 && (b === 18 || b === 19)) return true;
199
+ if (a >= 224) return true;
200
+ return false;
201
+ }
202
+ __name(isDisallowedIpv4, "isDisallowedIpv4");
203
+ function isDisallowedIp(ip) {
204
+ const addr = ip.toLowerCase().replace(/^\[|\]$/g, "").replace(/%.*$/, "");
205
+ const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(addr);
206
+ if (mapped) return isDisallowedIpv4(mapped[1]);
207
+ if (addr.includes(":")) {
208
+ if (addr === "::1" || addr === "::") return true;
209
+ if (addr.startsWith("fc") || addr.startsWith("fd")) return true;
210
+ if (/^fe[89ab]/.test(addr)) return true;
211
+ if (/^fec/.test(addr)) return true;
212
+ if (addr.startsWith("ff")) return true;
213
+ return false;
214
+ }
215
+ return isDisallowedIpv4(addr);
216
+ }
217
+ __name(isDisallowedIp, "isDisallowedIp");
218
+ async function hostResolvesToAllowed(host) {
219
+ if (isDisallowedIp(host)) return false;
220
+ try {
221
+ const records = await Promise.race([
222
+ (0, import_promises.lookup)(host, { all: true }),
223
+ new Promise((_, reject) => setTimeout(() => reject(new Error("dns timeout")), DNS_TIMEOUT_MS))
224
+ ]);
225
+ if (!records.length) return true;
226
+ return records.every((r) => !isDisallowedIp(r.address));
227
+ } catch {
228
+ return true;
229
+ }
230
+ }
231
+ __name(hostResolvesToAllowed, "hostResolvesToAllowed");
232
+
233
+ // src/guard.ts
234
+ function registrableDomain2(host) {
235
+ return host.split(".").slice(-2).join(".");
236
+ }
237
+ __name(registrableDomain2, "registrableDomain");
238
+ var DENY_DOMAINS = /* @__PURE__ */ new Set([
239
+ // search engines
240
+ "google.com",
241
+ "google.ca",
242
+ "bing.com",
243
+ "duckduckgo.com",
244
+ "baidu.com",
245
+ "yandex.com",
246
+ // social networks
247
+ "facebook.com",
248
+ "instagram.com",
249
+ "x.com",
250
+ "twitter.com",
251
+ "reddit.com",
252
+ "linkedin.com",
253
+ "tiktok.com",
254
+ "pinterest.com",
255
+ "snapchat.com",
256
+ "threads.net",
257
+ "youtube.com",
258
+ "whatsapp.com",
259
+ "telegram.org",
260
+ "discord.com",
261
+ // knowledge / UGC platforms where a "site" isn't a company KB
262
+ "wikipedia.org",
263
+ "wikimedia.org",
264
+ "quora.com",
265
+ "tumblr.com"
266
+ ]);
267
+ var DENY_HOSTS = /* @__PURE__ */ new Set([
268
+ "drive.google.com",
269
+ "docs.google.com",
270
+ "sites.google.com",
271
+ "maps.google.com",
272
+ "play.google.com",
273
+ "mail.google.com"
274
+ ]);
275
+ function isUnsupportedSite(website) {
276
+ const host = hostOf(website);
277
+ if (!host) return false;
278
+ if (DENY_HOSTS.has(host)) return true;
279
+ return DENY_DOMAINS.has(registrableDomain2(host));
280
+ }
281
+ __name(isUnsupportedSite, "isUnsupportedSite");
282
+
283
+ // src/discovery/parse-robots.ts
284
+ var import_robots_parser = __toESM(require("robots-parser"), 1);
285
+ function parseRobots(text2, robotsUrl = "https://example.com/robots.txt") {
286
+ const parser2 = (0, import_robots_parser.default)(robotsUrl, text2);
287
+ const sitemaps = dedupe(parser2.getSitemaps());
288
+ const disallow = [];
289
+ const seen = /* @__PURE__ */ new Set();
290
+ for (const rawLine of text2.split(/\r?\n/)) {
291
+ const line = rawLine.replace(/^/, "").trim();
292
+ if (!line || line.startsWith("#")) continue;
293
+ const colon = line.indexOf(":");
294
+ if (colon === -1) continue;
295
+ if (line.slice(0, colon).trim().toLowerCase() !== "disallow") continue;
296
+ let value = line.slice(colon + 1).trim();
297
+ const hash = value.indexOf("#");
298
+ if (hash !== -1) value = value.slice(0, hash).trim();
299
+ if (value && !seen.has(value)) {
300
+ seen.add(value);
301
+ disallow.push(value);
302
+ }
303
+ }
304
+ return { sitemaps, disallow };
305
+ }
306
+ __name(parseRobots, "parseRobots");
307
+ function dedupe(arr) {
308
+ const seen = /* @__PURE__ */ new Set();
309
+ const out = [];
310
+ for (const v of arr) {
311
+ if (!seen.has(v)) {
312
+ seen.add(v);
313
+ out.push(v);
314
+ }
315
+ }
316
+ return out;
317
+ }
318
+ __name(dedupe, "dedupe");
319
+
320
+ // src/discovery/parse-sitemap.ts
321
+ var import_fast_xml_parser = require("fast-xml-parser");
322
+ var parser = new import_fast_xml_parser.XMLParser({
323
+ ignoreAttributes: true,
324
+ trimValues: true,
325
+ // <loc> values are returned as strings; numbers stay parseable by us.
326
+ parseTagValue: false
327
+ });
328
+ function asArray(v) {
329
+ if (v == null) return [];
330
+ return Array.isArray(v) ? v : [v];
331
+ }
332
+ __name(asArray, "asArray");
333
+ function text(v) {
334
+ if (v == null) return void 0;
335
+ const s = String(v).trim();
336
+ return s || void 0;
337
+ }
338
+ __name(text, "text");
339
+ function parseSitemap(xml) {
340
+ let doc;
341
+ try {
342
+ doc = parser.parse(xml);
343
+ } catch {
344
+ return { kind: "unknown", sitemaps: [], urls: [] };
345
+ }
346
+ if (doc?.sitemapindex) {
347
+ const seen = /* @__PURE__ */ new Set();
348
+ const sitemaps = [];
349
+ for (const sm of asArray(doc.sitemapindex.sitemap)) {
350
+ const loc = text(sm?.loc);
351
+ if (loc && !seen.has(loc)) {
352
+ seen.add(loc);
353
+ sitemaps.push(loc);
354
+ }
355
+ }
356
+ return { kind: "index", sitemaps, urls: [] };
357
+ }
358
+ if (doc?.urlset) {
359
+ const seen = /* @__PURE__ */ new Set();
360
+ const urls = [];
361
+ for (const u of asArray(doc.urlset.url)) {
362
+ const loc = text(u?.loc);
363
+ if (!loc || seen.has(loc)) continue;
364
+ seen.add(loc);
365
+ const lastmod = text(u?.lastmod);
366
+ const changefreq = text(u?.changefreq)?.toLowerCase();
367
+ const priorityRaw = text(u?.priority);
368
+ const priority = priorityRaw != null ? Number(priorityRaw) : void 0;
369
+ urls.push({
370
+ loc,
371
+ ...lastmod ? { lastmod } : {},
372
+ ...priority != null && Number.isFinite(priority) ? { priority } : {},
373
+ ...changefreq ? { changefreq } : {}
374
+ });
375
+ }
376
+ return { kind: "urlset", sitemaps: [], urls };
377
+ }
378
+ return { kind: "unknown", sitemaps: [], urls: [] };
379
+ }
380
+ __name(parseSitemap, "parseSitemap");
381
+
382
+ // src/discovery/parse-llms.ts
383
+ var MD_LINK = /\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g;
384
+ function parseLlms(text2) {
385
+ const out = [];
386
+ const seen = /* @__PURE__ */ new Set();
387
+ for (const m of text2.matchAll(MD_LINK)) {
388
+ const title = m[1]?.trim() || void 0;
389
+ const url = m[2].trim();
390
+ if (seen.has(url)) continue;
391
+ seen.add(url);
392
+ out.push({ url, ...title ? { title } : {} });
393
+ }
394
+ return out;
395
+ }
396
+ __name(parseLlms, "parseLlms");
397
+
398
+ // src/discovery/normalize.ts
399
+ var KEEP_EXT = /* @__PURE__ */ new Set(["html", "htm", "md", "markdown", "mdx", "pdf"]);
400
+ var DROP_EXT = /* @__PURE__ */ new Set([
401
+ // machine / feeds
402
+ "xml",
403
+ "rss",
404
+ "atom",
405
+ "json",
406
+ "csv",
407
+ "txt",
408
+ "yaml",
409
+ "yml",
410
+ // images
411
+ "jpg",
412
+ "jpeg",
413
+ "png",
414
+ "gif",
415
+ "webp",
416
+ "svg",
417
+ "ico",
418
+ "bmp",
419
+ "tiff",
420
+ "avif",
421
+ // video / audio
422
+ "mp4",
423
+ "webm",
424
+ "mov",
425
+ "avi",
426
+ "mkv",
427
+ "mp3",
428
+ "wav",
429
+ "ogg",
430
+ "m4a",
431
+ // archives / binaries
432
+ "zip",
433
+ "gz",
434
+ "tar",
435
+ "rar",
436
+ "7z",
437
+ "dmg",
438
+ "exe",
439
+ "apk",
440
+ "pkg",
441
+ // fonts / code / data
442
+ "woff",
443
+ "woff2",
444
+ "ttf",
445
+ "otf",
446
+ "eot",
447
+ "js",
448
+ "mjs",
449
+ "css",
450
+ "map",
451
+ "wasm",
452
+ // office (non-pdf) — usually not worth scraping for a support KB
453
+ "doc",
454
+ "docx",
455
+ "xls",
456
+ "xlsx",
457
+ "ppt",
458
+ "pptx"
459
+ ]);
460
+ var LOCALE_SEG = /^(?:[a-z]{2})(?:[-_][a-z]{2})?$/i;
461
+ var KNOWN_LANGS = /* @__PURE__ */ new Set([
462
+ "en",
463
+ "fr",
464
+ "es",
465
+ "de",
466
+ "it",
467
+ "pt",
468
+ "nl",
469
+ "pl",
470
+ "ru",
471
+ "ja",
472
+ "zh",
473
+ "ko",
474
+ "ar",
475
+ "tr",
476
+ "sv",
477
+ "da",
478
+ "fi",
479
+ "no",
480
+ "cs",
481
+ "el",
482
+ "he",
483
+ "th",
484
+ "vi",
485
+ "id",
486
+ "uk",
487
+ "ro",
488
+ "hu",
489
+ "ms",
490
+ // Malay
491
+ "tl",
492
+ // Tagalog
493
+ "tw",
494
+ // locale-style variant (zh-TW shorthand on some sites)
495
+ "hi",
496
+ // Hindi
497
+ "bn",
498
+ // Bengali
499
+ "sk",
500
+ // Slovak
501
+ "bg",
502
+ // Bulgarian
503
+ "sr",
504
+ "sl",
505
+ "et",
506
+ "lv",
507
+ "lt",
508
+ "fa",
509
+ "ur",
510
+ "ta"
511
+ // NOTE: deliberately NOT adding 'hr' — too often means human-resources as a
512
+ // content section, not Croatian. Erring toward keeping content over dropping it.
513
+ ]);
514
+ function extOf(pathname) {
515
+ const last = pathname.split("/").pop() ?? "";
516
+ const dot = last.lastIndexOf(".");
517
+ if (dot <= 0) return null;
518
+ return last.slice(dot + 1).toLowerCase();
519
+ }
520
+ __name(extOf, "extOf");
521
+ function normalizeUrl(raw) {
522
+ let u;
523
+ try {
524
+ u = new URL(raw);
525
+ } catch {
526
+ try {
527
+ u = new URL(`https://${raw}`);
528
+ } catch {
529
+ return null;
530
+ }
531
+ }
532
+ if (u.protocol !== "http:" && u.protocol !== "https:") return null;
533
+ u.hash = "";
534
+ u.hostname = u.hostname.toLowerCase();
535
+ const TRACKING = /^(utm_|gclid|fbclid|mc_|ref$|ref_|_hs|igshid|si$)/i;
536
+ const params = [...u.searchParams.entries()].filter(([k]) => !TRACKING.test(k));
537
+ params.sort(([a], [b]) => a.localeCompare(b));
538
+ u.search = "";
539
+ for (const [k, v] of params) u.searchParams.append(k, v);
540
+ if (u.pathname.length > 1 && u.pathname.endsWith("/")) {
541
+ u.pathname = u.pathname.replace(/\/+$/, "");
542
+ }
543
+ return u.toString();
544
+ }
545
+ __name(normalizeUrl, "normalizeUrl");
546
+ var ROUTE_TEMPLATE = /(\[[^\]]*\]|\{[^}]*\}|\/:[^/]+|\/\*(?:\/|$))/;
547
+ function isRouteTemplate(url) {
548
+ try {
549
+ return ROUTE_TEMPLATE.test(decodeURIComponent(new URL(url).pathname));
550
+ } catch {
551
+ return ROUTE_TEMPLATE.test(url);
552
+ }
553
+ }
554
+ __name(isRouteTemplate, "isRouteTemplate");
555
+ function isAllowedContent(url) {
556
+ let pathname;
557
+ try {
558
+ pathname = new URL(url).pathname;
559
+ } catch {
560
+ return false;
561
+ }
562
+ if (isRouteTemplate(url)) return false;
563
+ const ext = extOf(pathname);
564
+ if (ext == null) return true;
565
+ if (KEEP_EXT.has(ext)) return true;
566
+ if (DROP_EXT.has(ext)) return false;
567
+ return true;
568
+ }
569
+ __name(isAllowedContent, "isAllowedContent");
570
+ function sectionOf(url) {
571
+ let segs;
572
+ try {
573
+ segs = new URL(url).pathname.split("/").filter(Boolean);
574
+ } catch {
575
+ return "";
576
+ }
577
+ for (const s of segs) {
578
+ if (isLocaleSegment(s)) continue;
579
+ return s.toLowerCase();
580
+ }
581
+ return "";
582
+ }
583
+ __name(sectionOf, "sectionOf");
584
+ function isLocaleSegment(seg) {
585
+ if (!LOCALE_SEG.test(seg)) return false;
586
+ const lang = seg.slice(0, 2).toLowerCase();
587
+ return KNOWN_LANGS.has(lang);
588
+ }
589
+ __name(isLocaleSegment, "isLocaleSegment");
590
+ function localeAgnosticKey(url) {
591
+ try {
592
+ const u = new URL(url);
593
+ const segs = u.pathname.split("/").filter(Boolean);
594
+ while (segs.length && isLocaleSegment(segs[0])) segs.shift();
595
+ return `${u.hostname}/${segs.join("/")}`;
596
+ } catch {
597
+ return url;
598
+ }
599
+ }
600
+ __name(localeAgnosticKey, "localeAgnosticKey");
601
+ function isEnglishOrNeutral(url) {
602
+ try {
603
+ const segs = new URL(url).pathname.split("/").filter(Boolean);
604
+ const first = segs[0];
605
+ if (!first || !isLocaleSegment(first)) return true;
606
+ return first.slice(0, 2).toLowerCase() === "en";
607
+ } catch {
608
+ return true;
609
+ }
610
+ }
611
+ __name(isEnglishOrNeutral, "isEnglishOrNeutral");
612
+
613
+ // src/discovery/detect-site-type.ts
614
+ function matchesGlob(url, glob) {
615
+ let path2;
616
+ try {
617
+ path2 = new URL(url).pathname;
618
+ } catch {
619
+ return false;
620
+ }
621
+ const re = new RegExp(
622
+ "^" + glob.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
623
+ );
624
+ return re.test(path2);
625
+ }
626
+ __name(matchesGlob, "matchesGlob");
627
+ function isShopifyProductsPayload(payload) {
628
+ if (!payload || typeof payload !== "object") return false;
629
+ const products = payload.products;
630
+ if (!Array.isArray(products)) return false;
631
+ if (products.length === 0) return true;
632
+ const first = products[0];
633
+ return first != null && typeof first === "object" && "handle" in first && typeof first.handle === "string" && "variants" in first && Array.isArray(first.variants);
634
+ }
635
+ __name(isShopifyProductsPayload, "isShopifyProductsPayload");
636
+ var SHOPIFY_EXCLUDES = [
637
+ "/products/*",
638
+ // individual SKU pages
639
+ "*/products/*",
640
+ // products nested under a collection
641
+ "/cart*",
642
+ "/account*",
643
+ "/checkout*"
644
+ ];
645
+ var SHOPIFY_INCLUDE_HINTS = ["/collections", "/pages", "/policies", "/pages/faq", "/pages/contact"];
646
+ async function detectSiteType(siteUrl, baseHost, fetchJsonFn = fetchJson) {
647
+ const origin = originOf(siteUrl);
648
+ if (!origin) return unknownType();
649
+ const payload = await fetchJsonFn(`${origin}/products.json?limit=1`, baseHost);
650
+ if (isShopifyProductsPayload(payload)) {
651
+ return { type: "shopify", excludePatterns: [...SHOPIFY_EXCLUDES], includeHints: [...SHOPIFY_INCLUDE_HINTS] };
652
+ }
653
+ return unknownType();
654
+ }
655
+ __name(detectSiteType, "detectSiteType");
656
+ function unknownType() {
657
+ return { type: "unknown", excludePatterns: [], includeHints: [] };
658
+ }
659
+ __name(unknownType, "unknownType");
660
+ function originOf(url) {
661
+ try {
662
+ return new URL(url).origin;
663
+ } catch {
664
+ try {
665
+ return new URL(`https://${url}`).origin;
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+ }
671
+ __name(originOf, "originOf");
672
+ function applyExcludes(urls, patterns) {
673
+ if (patterns.length === 0) return urls;
674
+ return urls.filter((u) => !patterns.some((p) => matchesGlob(u, p)));
675
+ }
676
+ __name(applyExcludes, "applyExcludes");
677
+
678
+ // src/discovery/score.ts
679
+ var TOP = [
680
+ "docs",
681
+ "doc",
682
+ "documentation",
683
+ "help",
684
+ "support",
685
+ "faq",
686
+ "faqs",
687
+ "guide",
688
+ "guides",
689
+ "kb",
690
+ "knowledge-base",
691
+ "knowledgebase",
692
+ "pricing",
693
+ "plans",
694
+ "integrations",
695
+ "integration",
696
+ "api",
697
+ "apis",
698
+ "features",
699
+ "feature",
700
+ "getting-started",
701
+ "get-started",
702
+ "how-to",
703
+ "howto",
704
+ "tutorial",
705
+ "tutorials",
706
+ "contact",
707
+ "contact-us"
708
+ ];
709
+ var MID = [
710
+ "product",
711
+ "products",
712
+ "solutions",
713
+ "solution",
714
+ "use-cases",
715
+ "use-case",
716
+ "about",
717
+ "about-us",
718
+ "security",
719
+ "company",
720
+ "platform",
721
+ "services",
722
+ "service"
723
+ ];
724
+ var LOW = [
725
+ "blog",
726
+ "news",
727
+ "press",
728
+ "newsroom",
729
+ "events",
730
+ "event",
731
+ "case-studies",
732
+ "case-study",
733
+ "customers",
734
+ "webinar",
735
+ "webinars",
736
+ "resources",
737
+ "resource",
738
+ "library",
739
+ "podcast",
740
+ "stories"
741
+ ];
742
+ var DROP = [
743
+ "tag",
744
+ "tags",
745
+ "author",
746
+ "authors",
747
+ "category",
748
+ "categories",
749
+ "archive",
750
+ "archives",
751
+ "login",
752
+ "signin",
753
+ "sign-in",
754
+ "account",
755
+ "cart",
756
+ "checkout",
757
+ "register",
758
+ "signup",
759
+ "sign-up",
760
+ "wishlist",
761
+ "search"
762
+ ];
763
+ function tierWeights() {
764
+ const m = {};
765
+ for (const s of TOP) m[s] = 10;
766
+ for (const s of MID) m[s] = 5;
767
+ for (const s of LOW) m[s] = 1.5;
768
+ for (const s of DROP) m[s] = -8;
769
+ return m;
770
+ }
771
+ __name(tierWeights, "tierWeights");
772
+ var DEFAULT_WEIGHTS = {
773
+ sectionWeights: tierWeights(),
774
+ defaultSectionWeight: 3,
775
+ // unknown section: neutral-positive (homepage-ish)
776
+ depthPenalty: 0.6,
777
+ depthWaivedSections: ["docs", "doc", "documentation", "help", "support", "kb", "guide", "guides", "api"],
778
+ recencyBonus: 3,
779
+ priorityBonus: 2,
780
+ multiSourceBonus: 1.5,
781
+ llmsBonus: 8,
782
+ queryParamPenalty: 1.5
783
+ };
784
+ var DATED_PATH = /\/(?:19|20)\d{2}(?:[/-](?:0?[1-9]|1[0-2]))?(?:[/-]\d{1,2})?(?=\/|$)/;
785
+ var SITEMAP_BOOST = ["help", "support", "docs", "doc", "kb", "faq", "guide", "api", "pricing", "product-doc"];
786
+ var SITEMAP_PENALTY = ["pdp", "product", "gridwall", "snkrs", "locator", "article", "blog", "news", "image", "video"];
787
+ function scoreSitemapUrl(sitemapUrl) {
788
+ const lower = sitemapUrl.toLowerCase();
789
+ let score = 1;
790
+ for (const k of SITEMAP_BOOST) if (lower.includes(k)) score += 6;
791
+ for (const k of SITEMAP_PENALTY) if (lower.includes(k)) score -= 5;
792
+ if (/\/sitemap(-index)?\.xml$/.test(lower)) score += 2;
793
+ return score;
794
+ }
795
+ __name(scoreSitemapUrl, "scoreSitemapUrl");
796
+ function pathSegments(url) {
797
+ try {
798
+ return new URL(url).pathname.split("/").filter(Boolean);
799
+ } catch {
800
+ return [];
801
+ }
802
+ }
803
+ __name(pathSegments, "pathSegments");
804
+ var HIGH_VALUE_SUB_TOKENS = /* @__PURE__ */ new Set([
805
+ "help",
806
+ "support",
807
+ "docs",
808
+ "doc",
809
+ "kb",
810
+ "faq",
811
+ "knowledgebase",
812
+ "developer",
813
+ "developers"
814
+ ]);
815
+ function highValueSubdomain(url) {
816
+ let host;
817
+ try {
818
+ host = new URL(url).hostname.toLowerCase();
819
+ } catch {
820
+ return null;
821
+ }
822
+ const label = host.split(".")[0];
823
+ if (!label) return null;
824
+ for (const tok of label.split(/[-_]/)) {
825
+ if (HIGH_VALUE_SUB_TOKENS.has(tok)) return tok;
826
+ }
827
+ return null;
828
+ }
829
+ __name(highValueSubdomain, "highValueSubdomain");
830
+ function recencyScore(lastmod, now, bonus) {
831
+ if (!lastmod) return 0;
832
+ const t = Date.parse(lastmod);
833
+ if (!Number.isFinite(t)) return 0;
834
+ const years = (now - t) / (365.25 * 24 * 3600 * 1e3);
835
+ if (years <= 0) return bonus;
836
+ return Math.max(0, bonus * (1 - years / 2));
837
+ }
838
+ __name(recencyScore, "recencyScore");
839
+ function scoreUrl(url, meta = { sources: [] }, weights = DEFAULT_WEIGHTS, now = Date.parse("2026-06-30")) {
840
+ const reasons = [];
841
+ let score = 0;
842
+ const section = sectionOf(url);
843
+ const segs = pathSegments(url);
844
+ if (section === "") {
845
+ score += 6;
846
+ reasons.push("homepage (+6)");
847
+ } else if (section in weights.sectionWeights) {
848
+ const w = weights.sectionWeights[section];
849
+ score += w;
850
+ reasons.push(`section:${section} (${w >= 0 ? "+" : ""}${w})`);
851
+ } else {
852
+ score += weights.defaultSectionWeight;
853
+ reasons.push(`section:${section} default (+${weights.defaultSectionWeight})`);
854
+ }
855
+ const sub = highValueSubdomain(url);
856
+ if (sub) {
857
+ score += 8;
858
+ reasons.push(`subdomain:${sub} (+8)`);
859
+ }
860
+ for (const seg of segs) {
861
+ if (DROP.includes(seg.toLowerCase()) && seg.toLowerCase() !== section) {
862
+ score += -4;
863
+ reasons.push(`drop-segment:${seg} (-4)`);
864
+ break;
865
+ }
866
+ }
867
+ const depth = Math.max(0, segs.length - 1);
868
+ if (depth > 0) {
869
+ const waived = weights.depthWaivedSections.includes(section);
870
+ const perLevel = waived ? weights.depthPenalty * 0.25 : weights.depthPenalty;
871
+ const pen = -(perLevel * depth);
872
+ if (pen !== 0) {
873
+ score += pen;
874
+ reasons.push(`depth:${depth}${waived ? " (waived section)" : ""} (${pen.toFixed(2)})`);
875
+ }
876
+ }
877
+ try {
878
+ if (DATED_PATH.test(new URL(url).pathname)) {
879
+ score += -3;
880
+ reasons.push("dated-path (-3)");
881
+ }
882
+ } catch {
883
+ }
884
+ const rec = recencyScore(meta.lastmod, now, weights.recencyBonus);
885
+ if (rec > 0) {
886
+ score += rec;
887
+ reasons.push(`recency (+${rec.toFixed(2)})`);
888
+ }
889
+ if (typeof meta.priority === "number") {
890
+ const p = weights.priorityBonus * meta.priority;
891
+ score += p;
892
+ reasons.push(`priority:${meta.priority} (+${p.toFixed(2)})`);
893
+ }
894
+ if (meta.sources.includes("llms")) {
895
+ score += weights.llmsBonus;
896
+ reasons.push(`llms.txt curated (+${weights.llmsBonus})`);
897
+ }
898
+ const extra = meta.sources.length - 1;
899
+ if (extra > 0) {
900
+ const b = weights.multiSourceBonus * extra;
901
+ score += b;
902
+ reasons.push(`multi-source x${meta.sources.length} (+${b.toFixed(2)})`);
903
+ }
904
+ try {
905
+ const q = new URL(url).searchParams;
906
+ const n = [...q.keys()].length;
907
+ if (n > 0) {
908
+ const pen = -(weights.queryParamPenalty * n);
909
+ score += pen;
910
+ reasons.push(`query-params:${n} (${pen.toFixed(2)})`);
911
+ }
912
+ } catch {
913
+ }
914
+ return { url, score, reasons, section, meta };
915
+ }
916
+ __name(scoreUrl, "scoreUrl");
917
+
918
+ // src/discovery/build-tree.ts
919
+ function newNode(segment) {
920
+ return { segment, children: [], childMap: /* @__PURE__ */ new Map(), score: -Infinity, count: 0 };
921
+ }
922
+ __name(newNode, "newNode");
923
+ function buildTree(scored) {
924
+ const root = newNode("");
925
+ for (const item of scored) {
926
+ let segs;
927
+ try {
928
+ segs = new URL(item.url).pathname.split("/").filter(Boolean);
929
+ } catch {
930
+ continue;
931
+ }
932
+ if (segs.length > 1 && isLocaleSegment(segs[0])) segs = segs.slice(1);
933
+ let node = root;
934
+ for (const seg of segs) {
935
+ let child = node.childMap.get(seg);
936
+ if (!child) {
937
+ child = newNode(seg);
938
+ node.childMap.set(seg, child);
939
+ node.children.push(child);
940
+ }
941
+ node = child;
942
+ }
943
+ if (node.url == null || item.score > node.score) {
944
+ node.url = item.url;
945
+ node.meta = item.meta;
946
+ node.reasons = item.reasons;
947
+ }
948
+ if (item.score > node.score) node.score = item.score;
949
+ }
950
+ finalize(root);
951
+ return strip(root);
952
+ }
953
+ __name(buildTree, "buildTree");
954
+ function finalize(node) {
955
+ let count = node.url ? 1 : 0;
956
+ let maxScore = node.url ? node.score : -Infinity;
957
+ for (const child of node.children) {
958
+ finalize(child);
959
+ count += child.count;
960
+ if (child.score > maxScore) maxScore = child.score;
961
+ }
962
+ node.count = count;
963
+ if (!Number.isFinite(node.score) || maxScore > node.score) node.score = maxScore;
964
+ if (!Number.isFinite(node.score)) node.score = 0;
965
+ node.children.sort((a, b) => b.score - a.score || a.segment.localeCompare(b.segment));
966
+ }
967
+ __name(finalize, "finalize");
968
+ function strip(node) {
969
+ return {
970
+ segment: node.segment,
971
+ ...node.url ? { url: node.url } : {},
972
+ ...node.meta ? { meta: node.meta } : {},
973
+ ...node.reasons ? { reasons: node.reasons } : {},
974
+ score: node.score,
975
+ count: node.count,
976
+ children: node.children.map((c) => strip(c))
977
+ };
978
+ }
979
+ __name(strip, "strip");
980
+ function* leafUrls(node) {
981
+ if (node.url) yield node;
982
+ for (const c of node.children) yield* leafUrls(c);
983
+ }
984
+ __name(leafUrls, "leafUrls");
985
+
986
+ // src/discovery/discover.ts
987
+ var DEFAULTS = {
988
+ budgetMs: 12e3,
989
+ maxUrls: 1e5,
990
+ maxSitemapFanout: 4,
991
+ // extra discoverUrls calls on high-value sitemaps
992
+ maxChildSitemaps: 12,
993
+ // child sitemaps to parse for metadata
994
+ enoughUrls: 200,
995
+ // re-discover until at least this many (or rounds exhausted)
996
+ maxRounds: 3
997
+ };
998
+ async function discover(website, opts = {}, deps) {
999
+ const d = deps;
1000
+ const host = hostOf(website);
1001
+ if (!host) {
1002
+ return emptyMap("unknown");
1003
+ }
1004
+ const origin = siteOrigin(website) ?? `https://${host}`;
1005
+ const cfg = { ...DEFAULTS, ...stripUndefined(opts) };
1006
+ const deadline = Date.now() + cfg.budgetMs;
1007
+ const aborted = /* @__PURE__ */ __name(() => opts.signal?.aborted === true || Date.now() >= deadline, "aborted");
1008
+ const remaining = /* @__PURE__ */ __name(() => Math.max(500, deadline - Date.now()), "remaining");
1009
+ const steps = [];
1010
+ const log = /* @__PURE__ */ __name((s) => {
1011
+ steps.push(s);
1012
+ opts.onActivity?.(s);
1013
+ }, "log");
1014
+ const byUrl = /* @__PURE__ */ new Map();
1015
+ const add = /* @__PURE__ */ __name((rawUrl, source, extra = {}) => {
1016
+ if (byUrl.size >= cfg.maxUrls) return;
1017
+ const norm = normalizeUrl(rawUrl);
1018
+ if (!norm || hostOf(norm) == null) return;
1019
+ if (isNonEnglishLocalePath(norm)) return;
1020
+ const m = byUrl.get(norm);
1021
+ if (m) {
1022
+ if (!m.sources.includes(source)) m.sources.push(source);
1023
+ if (extra.lastmod && !m.lastmod) m.lastmod = extra.lastmod;
1024
+ if (extra.priority != null && m.priority == null) m.priority = extra.priority;
1025
+ if (extra.changefreq && !m.changefreq) m.changefreq = extra.changefreq;
1026
+ if (extra.title && !m.title) m.title = extra.title;
1027
+ if (extra.searchRank != null && m.searchRank == null) m.searchRank = extra.searchRank;
1028
+ } else {
1029
+ byUrl.set(norm, { sources: [source], ...extra });
1030
+ }
1031
+ }, "add");
1032
+ let sitemapUrls = [];
1033
+ if (!aborted()) {
1034
+ const robotsTxt = await d.fetchText(`${origin}/robots.txt`, host, remaining()).catch(() => null);
1035
+ const robots = robotsTxt ? parseRobots(robotsTxt, `${origin}/robots.txt`) : { sitemaps: [], disallow: [] };
1036
+ sitemapUrls = robots.sitemaps.length ? robots.sitemaps : [`${origin}/sitemap.xml`];
1037
+ log({
1038
+ kind: "robots",
1039
+ detail: robotsTxt ? `${robots.sitemaps.length} sitemap(s) declared` : "no robots.txt",
1040
+ count: robots.sitemaps.length
1041
+ });
1042
+ }
1043
+ const rankedSitemaps = rankSitemaps(sitemapUrls);
1044
+ const fanoutTargets = rankedSitemaps.slice(0, cfg.maxSitemapFanout);
1045
+ const branches = [];
1046
+ branches.push(
1047
+ discoverUrlsWithRetry(d, host).then((r) => {
1048
+ r.urls.forEach((u) => add(u, "discoverUrls"));
1049
+ log({ kind: "discoverUrls", detail: `domain ${host}`, count: r.urls.length });
1050
+ }).catch(() => log({ kind: "discoverUrls", detail: `domain ${host} failed`, count: 0 }))
1051
+ );
1052
+ for (const sm of fanoutTargets) {
1053
+ if (aborted()) break;
1054
+ branches.push(
1055
+ d.discoverUrls({ url: sm, count: 1e3, onlyHttps: true }).then((r) => {
1056
+ r.urls.forEach((u) => add(u, "discoverUrls"));
1057
+ log({ kind: "discoverUrls", detail: `sitemap ${sm}`, count: r.urls.length });
1058
+ }).catch(() => log({ kind: "discoverUrls", detail: `sitemap ${sm} failed`, count: 0 }))
1059
+ );
1060
+ }
1061
+ branches.push(
1062
+ enrichFromSitemaps(rankedSitemaps.slice(0, 5), host, d, add, log, {
1063
+ maxChildren: cfg.maxChildSitemaps,
1064
+ remaining,
1065
+ aborted
1066
+ })
1067
+ );
1068
+ branches.push(
1069
+ Promise.all([
1070
+ d.fetchText(`${origin}/llms.txt`, host, remaining()).catch(() => null),
1071
+ d.fetchText(`${origin}/llms-full.txt`, host, remaining()).catch(() => null)
1072
+ ]).then(([a, b]) => {
1073
+ let n = 0;
1074
+ for (const text2 of [a, b]) {
1075
+ if (!text2) continue;
1076
+ for (const link of parseLlms(text2)) {
1077
+ add(link.url, "llms", link.title ? { title: link.title } : {});
1078
+ n++;
1079
+ }
1080
+ }
1081
+ if (n) log({ kind: "llms", detail: "llms.txt curated links", count: n });
1082
+ })
1083
+ );
1084
+ const queries = buildSearchQueries(host, opts.topics ?? []);
1085
+ branches.push(
1086
+ Promise.all(
1087
+ queries.map(
1088
+ (query) => d.webSearch({ query, includeSites: [host], count: 15 }).then((r) => {
1089
+ r.results.forEach((res, i) => add(res.url, "search", { searchRank: i + 1 }));
1090
+ log({ kind: "search", detail: query, count: r.results.length });
1091
+ return r.results;
1092
+ }).catch(() => [])
1093
+ )
1094
+ )
1095
+ );
1096
+ const siteTypeP = detectSiteType(origin, host, d.fetchJson).then((st) => {
1097
+ if (st.type !== "unknown") log({ kind: "site-type", detail: st.type });
1098
+ return st;
1099
+ }).catch(() => ({ type: "unknown", excludePatterns: [], includeHints: [] }));
1100
+ await Promise.allSettled(branches);
1101
+ const siteType = await siteTypeP;
1102
+ let round = 1;
1103
+ while (byUrl.size < cfg.enoughUrls && round < cfg.maxRounds && !aborted()) {
1104
+ round++;
1105
+ const subdomainTargets = pickRediscoverTargets(byUrl, host);
1106
+ if (subdomainTargets.length === 0) break;
1107
+ log({ kind: "note", detail: `re-discover round ${round}: ${subdomainTargets.length} target(s)` });
1108
+ await Promise.allSettled(
1109
+ subdomainTargets.map(
1110
+ (t) => d.discoverUrls({ url: t, count: 1e3, onlyHttps: true }).then((r) => {
1111
+ r.urls.forEach((u) => add(u, "discoverUrls"));
1112
+ log({ kind: "discoverUrls", detail: `re-discover ${t}`, count: r.urls.length });
1113
+ }).catch(() => {
1114
+ })
1115
+ )
1116
+ );
1117
+ }
1118
+ const allowedDomains = computeAllowedDomains(byUrl, host);
1119
+ log({ kind: "note", detail: `same-site domains: ${[...allowedDomains].join(", ")}` });
1120
+ let entries = [...byUrl.entries()].filter(([u]) => {
1121
+ const h = hostOf(u);
1122
+ return h != null && allowedDomains.has(registrableDomain3(h)) && isAllowedContent(u);
1123
+ });
1124
+ if (siteType.excludePatterns.length) {
1125
+ const before = entries.length;
1126
+ const keep = new Set(
1127
+ applyExcludes(
1128
+ entries.map(([u]) => u),
1129
+ siteType.excludePatterns
1130
+ )
1131
+ );
1132
+ entries = entries.filter(([u]) => keep.has(u));
1133
+ log({ kind: "site-type", detail: `${siteType.type} excludes`, count: before - entries.length });
1134
+ }
1135
+ {
1136
+ const before = entries.length;
1137
+ entries = dedupeLocales(entries);
1138
+ if (before !== entries.length) log({ kind: "note", detail: "locale-dedup", count: before - entries.length });
1139
+ }
1140
+ const scored = entries.map(([url, meta]) => scoreUrl(url, meta, void 0, opts.now));
1141
+ const tree = buildTree(scored);
1142
+ return {
1143
+ tree,
1144
+ total: scored.length,
1145
+ steps,
1146
+ aborted: aborted() && byUrl.size < cfg.enoughUrls,
1147
+ siteType: siteType.type
1148
+ };
1149
+ }
1150
+ __name(discover, "discover");
1151
+ function emptyMap(siteType) {
1152
+ return { tree: { segment: "", children: [], score: 0, count: 0 }, total: 0, steps: [], aborted: false, siteType };
1153
+ }
1154
+ __name(emptyMap, "emptyMap");
1155
+ function stripUndefined(o) {
1156
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
1157
+ }
1158
+ __name(stripUndefined, "stripUndefined");
1159
+ function rankSitemaps(sitemaps) {
1160
+ return sitemaps.filter((u) => {
1161
+ const seg = firstSegment(u);
1162
+ return !(seg && isLocaleSegment(seg));
1163
+ }).map((u) => ({ u, s: scoreSitemapUrl(u) })).sort((a, b) => b.s - a.s).map((x) => x.u);
1164
+ }
1165
+ __name(rankSitemaps, "rankSitemaps");
1166
+ function firstSegment(url) {
1167
+ try {
1168
+ return new URL(url).pathname.split("/").filter(Boolean)[0];
1169
+ } catch {
1170
+ return void 0;
1171
+ }
1172
+ }
1173
+ __name(firstSegment, "firstSegment");
1174
+ function registrableDomain3(host) {
1175
+ return host.split(".").slice(-2).join(".");
1176
+ }
1177
+ __name(registrableDomain3, "registrableDomain");
1178
+ function computeAllowedDomains(byUrl, inputHost) {
1179
+ const counts = /* @__PURE__ */ new Map();
1180
+ for (const url of byUrl.keys()) {
1181
+ const h = hostOf(url);
1182
+ if (!h) continue;
1183
+ const d = registrableDomain3(h);
1184
+ counts.set(d, (counts.get(d) ?? 0) + 1);
1185
+ }
1186
+ const allowed = /* @__PURE__ */ new Set([registrableDomain3(inputHost)]);
1187
+ let top = null;
1188
+ let topN = 0;
1189
+ for (const [d, n] of counts) {
1190
+ if (n > topN) {
1191
+ topN = n;
1192
+ top = d;
1193
+ }
1194
+ }
1195
+ if (top) allowed.add(top);
1196
+ return allowed;
1197
+ }
1198
+ __name(computeAllowedDomains, "computeAllowedDomains");
1199
+ function isNonEnglishLocalePath(url) {
1200
+ const seg = firstSegment(url);
1201
+ if (!seg || !isLocaleSegment(seg)) return false;
1202
+ return !isEnglishOrNeutral(url);
1203
+ }
1204
+ __name(isNonEnglishLocalePath, "isNonEnglishLocalePath");
1205
+ async function discoverUrlsWithRetry(d, host) {
1206
+ const call = /* @__PURE__ */ __name(() => d.discoverUrls({ url: host, count: 1e3, onlyHttps: true }), "call");
1207
+ try {
1208
+ const r = await call();
1209
+ if (r.urls.length > 0) return r;
1210
+ } catch {
1211
+ }
1212
+ return call().catch(() => ({ urls: [], stopReason: "error" }));
1213
+ }
1214
+ __name(discoverUrlsWithRetry, "discoverUrlsWithRetry");
1215
+ function localePrefix(url) {
1216
+ const seg = firstSegment(url);
1217
+ return seg && isLocaleSegment(seg) ? seg.toLowerCase() : "";
1218
+ }
1219
+ __name(localePrefix, "localePrefix");
1220
+ function dedupeLocales(entries) {
1221
+ const best = /* @__PURE__ */ new Map();
1222
+ const rank = /* @__PURE__ */ __name((url) => {
1223
+ const p = localePrefix(url);
1224
+ if (p === "") return 0;
1225
+ if (p.slice(0, 2) === "en") return 1;
1226
+ return 2;
1227
+ }, "rank");
1228
+ for (const entry of entries) {
1229
+ const key = localeAgnosticKey(entry[0]);
1230
+ const cur = best.get(key);
1231
+ if (!cur) {
1232
+ best.set(key, entry);
1233
+ continue;
1234
+ }
1235
+ const better = rank(entry[0]) < rank(cur[0]) || rank(entry[0]) === rank(cur[0]) && entry[1].sources.length > cur[1].sources.length;
1236
+ if (better) best.set(key, entry);
1237
+ }
1238
+ const seen = /* @__PURE__ */ new Set();
1239
+ const out = [];
1240
+ for (const entry of entries) {
1241
+ const key = localeAgnosticKey(entry[0]);
1242
+ if (seen.has(key)) continue;
1243
+ seen.add(key);
1244
+ out.push(best.get(key));
1245
+ }
1246
+ return out;
1247
+ }
1248
+ __name(dedupeLocales, "dedupeLocales");
1249
+ function buildSearchQueries(host, topics) {
1250
+ const queries = [`${host} help support documentation`];
1251
+ const topic = topics.find((t) => t.trim().length > 0);
1252
+ if (topic) queries.push(`${host} ${topic.slice(0, 40)}`);
1253
+ return queries;
1254
+ }
1255
+ __name(buildSearchQueries, "buildSearchQueries");
1256
+ function pickRediscoverTargets(byUrl, host) {
1257
+ const baseDomain = host.split(".").slice(-2).join(".");
1258
+ const subroots = /* @__PURE__ */ new Set();
1259
+ for (const url of byUrl.keys()) {
1260
+ const h = hostOf(url);
1261
+ if (!h || h === host) continue;
1262
+ if (!h.endsWith(baseDomain)) continue;
1263
+ if (/^(help|support|docs|kb|developer|developers)\./.test(h)) {
1264
+ subroots.add(`https://${h}`);
1265
+ }
1266
+ }
1267
+ return [...subroots].slice(0, 3);
1268
+ }
1269
+ __name(pickRediscoverTargets, "pickRediscoverTargets");
1270
+ async function enrichFromSitemaps(seeds, host, deps, add, log, cfg) {
1271
+ let childBudget = cfg.maxChildren;
1272
+ const seen = /* @__PURE__ */ new Set();
1273
+ const fetchAndParse = /* @__PURE__ */ __name(async (url, depth = 0) => {
1274
+ if (seen.has(url) || cfg.aborted() || depth > 3) return;
1275
+ seen.add(url);
1276
+ const xml = await deps.fetchText(url, host, cfg.remaining()).catch(() => null);
1277
+ if (!xml) return;
1278
+ const redirect = htmlSitemapRedirect(xml, url);
1279
+ if (redirect && !seen.has(redirect)) {
1280
+ log({ kind: "redirect", detail: `${url} \u2192 ${redirect}` });
1281
+ await fetchAndParse(redirect, depth + 1);
1282
+ return;
1283
+ }
1284
+ const parsed = parseSitemap(xml);
1285
+ if (parsed.kind === "index") {
1286
+ const children = prioritizeSitemapChildren(parsed.sitemaps).slice(0, Math.max(0, childBudget));
1287
+ childBudget -= children.length;
1288
+ log({ kind: "sitemap-index", detail: url, count: parsed.sitemaps.length });
1289
+ await Promise.allSettled(children.map((c) => fetchAndParse(c, depth + 1)));
1290
+ } else if (parsed.kind === "urlset") {
1291
+ for (const e of parsed.urls) {
1292
+ add(e.loc, "sitemap", {
1293
+ ...e.lastmod ? { lastmod: e.lastmod } : {},
1294
+ ...e.priority != null ? { priority: e.priority } : {},
1295
+ ...e.changefreq ? { changefreq: e.changefreq } : {}
1296
+ });
1297
+ }
1298
+ log({ kind: "sitemap", detail: url, count: parsed.urls.length });
1299
+ }
1300
+ }, "fetchAndParse");
1301
+ await Promise.allSettled(seeds.map((s) => fetchAndParse(s, 0)));
1302
+ }
1303
+ __name(enrichFromSitemaps, "enrichFromSitemaps");
1304
+ function htmlSitemapRedirect(body, base) {
1305
+ const head = body.slice(0, 1e3);
1306
+ if (!/^\s*<!doctype html|^\s*<html|<meta/i.test(head)) return null;
1307
+ const refresh = /http-equiv=["']?refresh["']?[^>]*content=["'][^"']*url=([^"'>\s]+)/i.exec(head);
1308
+ const canonical = /<link[^>]+rel=["']?canonical["']?[^>]*href=["']([^"']+)["']/i.exec(head);
1309
+ const target = refresh?.[1] ?? canonical?.[1];
1310
+ if (!target) return null;
1311
+ try {
1312
+ const abs = new URL(target, base).toString();
1313
+ return /sitemap/i.test(abs) ? abs : null;
1314
+ } catch {
1315
+ return null;
1316
+ }
1317
+ }
1318
+ __name(htmlSitemapRedirect, "htmlSitemapRedirect");
1319
+ function prioritizeSitemapChildren(children) {
1320
+ const neutral = [];
1321
+ const english = [];
1322
+ for (const url of children) {
1323
+ const seg = firstSegment(url);
1324
+ if (seg && isLocaleSegment(seg)) {
1325
+ if (seg.slice(0, 2).toLowerCase() === "en") english.push(url);
1326
+ } else {
1327
+ neutral.push(url);
1328
+ }
1329
+ }
1330
+ return [...neutral, ...english];
1331
+ }
1332
+ __name(prioritizeSitemapChildren, "prioritizeSitemapChildren");
1333
+
1334
+ // src/selection/llm-select.ts
1335
+ var import_zui = require("@bpinternal/zui");
1336
+
1337
+ // src/selection/candidate-tree.ts
1338
+ var DEFAULTS2 = { maxLines: 1e3 };
1339
+ var HIGH = [
1340
+ "help",
1341
+ "support",
1342
+ "faq",
1343
+ "faqs",
1344
+ "hc",
1345
+ "kb",
1346
+ "knowledge-base",
1347
+ "knowledgebase",
1348
+ "docs",
1349
+ "documentation",
1350
+ "pricing",
1351
+ "plans",
1352
+ "price",
1353
+ "prices",
1354
+ "contact",
1355
+ "contact-us",
1356
+ "contacto",
1357
+ "contatti",
1358
+ "shipping",
1359
+ "returns",
1360
+ "return",
1361
+ "refund",
1362
+ "refunds",
1363
+ "track",
1364
+ "trackorder",
1365
+ "order",
1366
+ "orders",
1367
+ "delivery",
1368
+ "warranty",
1369
+ "account",
1370
+ "myaccount",
1371
+ "sizing",
1372
+ "size-guide",
1373
+ "size-chart",
1374
+ "how-it-works",
1375
+ "get-started",
1376
+ "getting-started",
1377
+ "guide",
1378
+ "guides",
1379
+ "policy",
1380
+ "policies",
1381
+ "terms",
1382
+ "privacy",
1383
+ "about",
1384
+ "about-us",
1385
+ "integrations",
1386
+ "api",
1387
+ "gift-cards",
1388
+ "gift-card",
1389
+ // multilingual high-value
1390
+ "tarifas",
1391
+ "cuotas",
1392
+ "precios",
1393
+ "orari",
1394
+ "ayuda",
1395
+ "aiuto",
1396
+ "contacto",
1397
+ "envios",
1398
+ "devoluciones"
1399
+ ];
1400
+ var LOW2 = [
1401
+ "blog",
1402
+ "news",
1403
+ "press",
1404
+ "press-releases",
1405
+ "investor",
1406
+ "investors",
1407
+ "investor-home",
1408
+ "financials-filings",
1409
+ "events-and-presentations",
1410
+ "stock-information",
1411
+ "esg",
1412
+ "tag",
1413
+ "tags",
1414
+ "category",
1415
+ "author",
1416
+ "sitemap",
1417
+ "archive"
1418
+ ];
1419
+ var HIGH_SET = new Set(HIGH);
1420
+ var LOW_SET = new Set(LOW2);
1421
+ var DATED = /(^|[-/])(19|20)\d{2}([-/]|$)/;
1422
+ function importanceOf(url) {
1423
+ let path2;
1424
+ try {
1425
+ path2 = new URL(url).pathname.toLowerCase();
1426
+ } catch {
1427
+ return 0;
1428
+ }
1429
+ const segs = path2.split("/").filter(Boolean);
1430
+ if (segs.length === 0) return 100;
1431
+ let score = 10 - Math.min(segs.length, 6);
1432
+ for (const s of segs) {
1433
+ if (HIGH_SET.has(s)) score += 40;
1434
+ if (LOW_SET.has(s)) score -= 25;
1435
+ }
1436
+ if (/help|support|faq|\/hc\/|pricing|contact|shipping|return|track|refund|docs?|guide|policy/.test(path2)) score += 15;
1437
+ if (/\/c\//.test(path2)) score += 8;
1438
+ if (/\/p\//.test(path2)) score -= 15;
1439
+ if (DATED.test(path2)) score -= 15;
1440
+ return score;
1441
+ }
1442
+ __name(importanceOf, "importanceOf");
1443
+ function renderNumberedTree(candidates, host, opts = {}) {
1444
+ const index = [];
1445
+ const text2 = renderCandidateTree(candidates, host, opts, (url) => {
1446
+ const i = index.length;
1447
+ index.push(url);
1448
+ return `[${i}] `;
1449
+ });
1450
+ return { text: text2, index };
1451
+ }
1452
+ __name(renderNumberedTree, "renderNumberedTree");
1453
+ function renderCandidateTree(candidates, host, opts = {}, numberPrefix) {
1454
+ const o = { ...DEFAULTS2, ...opts };
1455
+ const seen = /* @__PURE__ */ new Set();
1456
+ const pages = [];
1457
+ for (const c of candidates) {
1458
+ if (seen.has(c.url)) continue;
1459
+ seen.add(c.url);
1460
+ let segs;
1461
+ try {
1462
+ segs = new URL(c.url).pathname.split("/").filter(Boolean);
1463
+ } catch {
1464
+ continue;
1465
+ }
1466
+ pages.push({ url: c.url, meta: c.meta, segs, imp: importanceOf(c.url) });
1467
+ }
1468
+ const totalPages = pages.length;
1469
+ const byImp = [...pages].sort((a, b) => b.imp - a.imp || a.segs.length - b.segs.length || a.url.localeCompare(b.url));
1470
+ const headerLines = 1;
1471
+ const LOCALE = /^[a-z]{2}([-_][a-z]{2})?$/i;
1472
+ const MARKET = /* @__PURE__ */ new Set(["us", "ca", "mx", "uk", "eu", "intl", "en", "fr", "es", "de", "it"]);
1473
+ const groupKey = /* @__PURE__ */ __name((segs) => {
1474
+ if (segs.length <= 1) return "";
1475
+ let i = 0;
1476
+ while (i < segs.length - 1 && (LOCALE.test(segs[i]) || MARKET.has(segs[i].toLowerCase()))) i++;
1477
+ return segs.slice(0, i + 1).join("/");
1478
+ }, "groupKey");
1479
+ const totalByGroup = /* @__PURE__ */ new Map();
1480
+ for (const p of pages) totalByGroup.set(groupKey(p.segs), (totalByGroup.get(groupKey(p.segs)) ?? 0) + 1);
1481
+ const selectWithCap = /* @__PURE__ */ __name((cap) => {
1482
+ const shownByGroup2 = /* @__PURE__ */ new Map();
1483
+ let shownCount = 0;
1484
+ for (const p of byImp) {
1485
+ const g = groupKey(p.segs);
1486
+ const arr = shownByGroup2.get(g) ?? shownByGroup2.set(g, []).get(g);
1487
+ if (arr.length >= cap) continue;
1488
+ arr.push(p);
1489
+ shownCount++;
1490
+ }
1491
+ let foldLines = 0;
1492
+ for (const [g, tot] of totalByGroup) if (tot > (shownByGroup2.get(g)?.length ?? 0)) foldLines++;
1493
+ return { shownByGroup: shownByGroup2, lines: headerLines + shownCount + foldLines };
1494
+ }, "selectWithCap");
1495
+ let chosen = selectWithCap(1);
1496
+ for (let cap = 2; cap <= 1e3; cap++) {
1497
+ const next = selectWithCap(cap);
1498
+ if (next.lines > o.maxLines) break;
1499
+ chosen = next;
1500
+ }
1501
+ const shownByGroup = chosen.shownByGroup;
1502
+ const num = /* @__PURE__ */ __name((url) => numberPrefix ? numberPrefix(url) : "", "num");
1503
+ const metaOf = /* @__PURE__ */ __name((m) => {
1504
+ if (!m) return "";
1505
+ const bits = [];
1506
+ if (m.sources?.includes("llms")) bits.push("llms");
1507
+ if (m.lastmod && recentish(m.lastmod)) bits.push("recent");
1508
+ return bits.length ? ` {${bits.join(",")}}` : "";
1509
+ }, "metaOf");
1510
+ const groups = [.../* @__PURE__ */ new Set([...totalByGroup.keys()])].sort((a, b) => {
1511
+ const ia = Math.max(-999, ...shownByGroup.get(a)?.map((p) => p.imp) ?? []);
1512
+ const ib = Math.max(-999, ...shownByGroup.get(b)?.map((p) => p.imp) ?? []);
1513
+ return ib - ia || a.localeCompare(b);
1514
+ });
1515
+ const lines = [`${host} \u2014 ${totalPages} pages:`];
1516
+ for (const g of groups) {
1517
+ const shownHere = (shownByGroup.get(g) ?? []).sort((a, b) => b.imp - a.imp || a.url.localeCompare(b.url));
1518
+ for (const p of shownHere) lines.push(`${num(p.url)}${pathStr(p.segs)}${metaOf(p.meta)}`);
1519
+ const omitted = (totalByGroup.get(g) ?? 0) - shownHere.length;
1520
+ if (omitted > 0) {
1521
+ const under = g === "" ? "/" : `/${g}/`;
1522
+ lines.push(`(+${omitted} more pages under ${under})`);
1523
+ }
1524
+ }
1525
+ return lines.join("\n");
1526
+ }
1527
+ __name(renderCandidateTree, "renderCandidateTree");
1528
+ function pathStr(segs) {
1529
+ return "/" + segs.join("/");
1530
+ }
1531
+ __name(pathStr, "pathStr");
1532
+ var TWO_YEARS_MS = 2 * 365.25 * 24 * 3600 * 1e3;
1533
+ function recentish(lastmod) {
1534
+ const t = Date.parse(lastmod);
1535
+ if (!Number.isFinite(t)) return false;
1536
+ const ageMs = Date.now() - t;
1537
+ return ageMs >= 0 && ageMs < TWO_YEARS_MS;
1538
+ }
1539
+ __name(recentish, "recentish");
1540
+
1541
+ // src/selection/llm-select.ts
1542
+ var SELECT_PROMPT_VERSION = 3;
1543
+ async function llmSelect(candidates, host, limit, ctx, deps) {
1544
+ const { text: tree, index } = renderNumberedTree(candidates, host);
1545
+ const run = /* @__PURE__ */ __name(() => deps.extract(
1546
+ `Site map for ${ctx.website ?? host} \u2014 a compact tree of the site's pages. Each pickable page is prefixed with a number [n]; folders show page counts; huge sections are folded to "+N more pages\u2026":
1547
+
1548
+ ${tree}`,
1549
+ import_zui.z.object({
1550
+ picks: import_zui.z.array(import_zui.z.number().int()).describe(`The \u2264${limit} most useful page NUMBERS, BEST FIRST. Use the [n] numbers from the tree.`),
1551
+ rationale: import_zui.z.string().optional()
1552
+ }),
1553
+ {
1554
+ instructions: `You are building a knowledge base for an AI SUPPORT agent${ctx.company ? ` for ${ctx.company}` : ""}. Pick the \u2264${limit} pages, BEST FIRST, that best let the agent answer real customer/visitor questions. Return their [n] NUMBERS.
1555
+
1556
+ ${ctx.overview ? `ABOUT THE BUSINESS: ${ctx.overview}
1557
+ ` : ""}${ctx.prompt ? `USE-CASE FOCUS: ${ctx.prompt}
1558
+ ` : ""}
1559
+ How to choose (judgment about THIS business, not rules):
1560
+ - Prioritize pages that answer questions: what the business offers, pricing/plans, how it works, how to buy/apply/book, FAQ/help/support articles, policies (returns/shipping/privacy), contact, locations/hours, key product/service pages.
1561
+ - A page's URL slug tells you what it's about \u2014 use it (any language: Spanish tarifas/cuotas, Italian orari/contatti \u2014 judge by MEANING).
1562
+ - Judge whether this site's value is in its SERVICE/PRODUCT pages or its ARTICLES: a news org / non-profit's articles ARE the product (include many); a shop or club with a peripheral blog should mostly SKIP the blog and take the service pages.
1563
+ - Do NOT spend slots on near-identical BULK: individual product SKUs, every staff bio, every location, every dated promo/event, blog archives. Take a few representative ones at most.
1564
+ - Skip non-content: cart/checkout/login/account, tag/category/author archives, search/sitemap pages, dated marketing campaigns, thin utility/landing pages.
1565
+ - Prefer ONE language (the site's default) \u2014 don't pick the same page in multiple locales.
1566
+
1567
+ Return \u2264${limit} page NUMBERS, best first.`
1568
+ }
1569
+ ), "run");
1570
+ const cache = deps.cache ?? (async (_kind, _keyParts, fn) => fn());
1571
+ const raw = await cache("url-selection.select", { v: SELECT_PROMPT_VERSION, tree, limit, ctx }, async () => {
1572
+ try {
1573
+ return await run();
1574
+ } catch {
1575
+ return await run();
1576
+ }
1577
+ }).catch(() => null);
1578
+ if (!raw || !Array.isArray(raw.picks)) return { urls: [], rationale: "llm-select failed" };
1579
+ const seen = /* @__PURE__ */ new Set();
1580
+ const urls = [];
1581
+ for (const p of raw.picks) {
1582
+ const i = typeof p === "number" ? p : NaN;
1583
+ if (!Number.isInteger(i) || i < 0 || i >= index.length) continue;
1584
+ const url = index[i];
1585
+ if (seen.has(url)) continue;
1586
+ seen.add(url);
1587
+ urls.push(url);
1588
+ if (urls.length >= limit) break;
1589
+ }
1590
+ return { urls, rationale: raw.rationale };
1591
+ }
1592
+ __name(llmSelect, "llmSelect");
1593
+
1594
+ // src/selection/select.ts
1595
+ var DEFAULT_LIMIT = 100;
1596
+ async function select(map, opts = {}, deps) {
1597
+ const limit = opts.limit ?? DEFAULT_LIMIT;
1598
+ if (map.unsupported) return { urls: [], useful: 0 };
1599
+ const candidates = [...leafUrls(map.tree)].filter((n) => !!n.url).map((n) => ({ url: n.url, meta: n.meta ?? { sources: [] } }));
1600
+ if (candidates.length === 0) return { urls: [], useful: 0 };
1601
+ const host = hostOf(candidates[0].url) ?? "";
1602
+ const picked = await llmSelect(
1603
+ candidates,
1604
+ host,
1605
+ limit,
1606
+ {
1607
+ ...opts.context?.company ? { company: opts.context.company } : {},
1608
+ ...opts.context?.website ? { website: opts.context.website } : {},
1609
+ ...opts.context?.overview ? { overview: opts.context.overview } : {},
1610
+ ...opts.prompt ? { prompt: opts.prompt } : {}
1611
+ },
1612
+ deps
1613
+ );
1614
+ return { urls: picked.urls, useful: candidates.length };
1615
+ }
1616
+ __name(select, "select");
1617
+
1618
+ // src/http-cache.ts
1619
+ var import_node_fs = __toESM(require("fs"), 1);
1620
+ var import_node_path = __toESM(require("path"), 1);
1621
+ function fastHash(str) {
1622
+ let hash = 0;
1623
+ for (let i = 0; i < str.length; i++) {
1624
+ hash = (hash << 5) - hash + str.charCodeAt(i);
1625
+ hash |= 0;
1626
+ }
1627
+ return (hash >>> 0).toString(16);
1628
+ }
1629
+ __name(fastHash, "fastHash");
1630
+ function sortKeys(input) {
1631
+ if (Array.isArray(input)) return input.map(sortKeys);
1632
+ if (input && typeof input === "object" && input.constructor === Object) {
1633
+ return Object.keys(input).sort().reduce(
1634
+ (acc, key) => {
1635
+ acc[key] = sortKeys(input[key]);
1636
+ return acc;
1637
+ },
1638
+ {}
1639
+ );
1640
+ }
1641
+ return input;
1642
+ }
1643
+ __name(sortKeys, "sortKeys");
1644
+ function stableStringify(obj) {
1645
+ return JSON.stringify(sortKeys(obj));
1646
+ }
1647
+ __name(stableStringify, "stableStringify");
1648
+ function httpCacheMode() {
1649
+ if (process.env.URL_FIXTURE_RECORD) return "record";
1650
+ if (process.env.NODE_ENV === "test" || process.env.BUN_TEST || process.env.VITEST || process.env.URL_FIXTURE_REPLAY) {
1651
+ return "replay";
1652
+ }
1653
+ return "passthrough";
1654
+ }
1655
+ __name(httpCacheMode, "httpCacheMode");
1656
+ var FIXTURE_DIR = import_node_path.default.resolve(__dirname, "./__fixtures__");
1657
+ var loaded = /* @__PURE__ */ new Map();
1658
+ function groupPath(group) {
1659
+ return import_node_path.default.join(FIXTURE_DIR, `${group}.jsonl`);
1660
+ }
1661
+ __name(groupPath, "groupPath");
1662
+ function loadGroup(group) {
1663
+ const existing = loaded.get(group);
1664
+ if (existing) return existing;
1665
+ const map = /* @__PURE__ */ new Map();
1666
+ try {
1667
+ const content = import_node_fs.default.readFileSync(groupPath(group), "utf-8");
1668
+ for (const line of content.split(/\r?\n/).filter(Boolean)) {
1669
+ try {
1670
+ const entry = JSON.parse(line);
1671
+ map.set(entry.key, entry);
1672
+ } catch {
1673
+ }
1674
+ }
1675
+ } catch {
1676
+ }
1677
+ loaded.set(group, map);
1678
+ return map;
1679
+ }
1680
+ __name(loadGroup, "loadGroup");
1681
+ function appendEntry(entry) {
1682
+ const map = loadGroup(entry.group);
1683
+ map.set(entry.key, entry);
1684
+ try {
1685
+ import_node_fs.default.mkdirSync(FIXTURE_DIR, { recursive: true });
1686
+ } catch {
1687
+ }
1688
+ import_node_fs.default.appendFileSync(groupPath(entry.group), JSON.stringify(entry) + "\n");
1689
+ }
1690
+ __name(appendEntry, "appendEntry");
1691
+ var FixtureMissError = class extends Error {
1692
+ static {
1693
+ __name(this, "FixtureMissError");
1694
+ }
1695
+ constructor(kind, group, input) {
1696
+ super(
1697
+ `[http-cache] no recorded fixture for kind="${kind}" group="${group}".
1698
+ Re-record with: URL_FIXTURE_RECORD=1 bun run scripts/record-fixtures.ts
1699
+ input: ${input.slice(0, 300)}`
1700
+ );
1701
+ this.name = "FixtureMissError";
1702
+ }
1703
+ };
1704
+ var stats = { hits: 0, misses: 0, recorded: 0 };
1705
+ async function cachedHttp(group, kind, args, fn) {
1706
+ const mode = httpCacheMode();
1707
+ if (mode === "passthrough") return fn();
1708
+ const key = fastHash(`${kind}:${stableStringify(args)}`);
1709
+ const map = loadGroup(group);
1710
+ const hit = map.get(key);
1711
+ if (hit) {
1712
+ stats.hits++;
1713
+ return hit.value;
1714
+ }
1715
+ if (mode === "replay") {
1716
+ stats.misses++;
1717
+ throw new FixtureMissError(kind, group, stableStringify(args));
1718
+ }
1719
+ const value = await fn();
1720
+ appendEntry({ key, kind, group, input: stableStringify(args), value });
1721
+ stats.recorded++;
1722
+ return value;
1723
+ }
1724
+ __name(cachedHttp, "cachedHttp");
1725
+
1726
+ // src/discovery/cached-deps.ts
1727
+ function cachedDeps(group, real) {
1728
+ return {
1729
+ // extract/cache pass through untouched — the LLM side has its own
1730
+ // record/replay via the injected cache dep.
1731
+ ...real,
1732
+ fetchText: /* @__PURE__ */ __name((url, baseHost, timeoutMs) => cachedHttp(group, "fetchText", { url }, () => real.fetchText(url, baseHost, timeoutMs)), "fetchText"),
1733
+ fetchJson: /* @__PURE__ */ __name((url, baseHost, timeoutMs) => cachedHttp(group, "fetchJson", { url }, () => real.fetchJson(url, baseHost, timeoutMs)), "fetchJson"),
1734
+ discoverUrls: /* @__PURE__ */ __name((args) => cachedHttp(group, "discoverUrls", args, () => real.discoverUrls(args)), "discoverUrls"),
1735
+ webSearch: /* @__PURE__ */ __name((args) => cachedHttp(group, "webSearch", args, () => real.webSearch(args)), "webSearch")
1736
+ };
1737
+ }
1738
+ __name(cachedDeps, "cachedDeps");
1739
+
1740
+ // src/index.ts
1741
+ async function findKnowledgeUrls(input, deps, extra = {}) {
1742
+ if (isUnsupportedSite(input.website)) {
1743
+ return {
1744
+ urls: [],
1745
+ useful: 0,
1746
+ discovered: 0,
1747
+ aborted: false,
1748
+ siteType: "unknown",
1749
+ stopReason: "unsupported_site"
1750
+ };
1751
+ }
1752
+ if (!hostOf(input.website)) {
1753
+ return {
1754
+ urls: [],
1755
+ useful: 0,
1756
+ discovered: 0,
1757
+ aborted: false,
1758
+ siteType: "unknown",
1759
+ stopReason: "no_sources"
1760
+ };
1761
+ }
1762
+ const map = await discover(
1763
+ input.website,
1764
+ {
1765
+ ...input.topics ? { topics: input.topics } : {},
1766
+ ...input.signal ? { signal: input.signal } : {},
1767
+ ...input.budgetMs != null ? { budgetMs: input.budgetMs } : {},
1768
+ ...input.onActivity ? { onActivity: input.onActivity } : {},
1769
+ ...extra.now != null ? { now: extra.now } : {}
1770
+ },
1771
+ deps
1772
+ );
1773
+ const selection = await select(
1774
+ map,
1775
+ {
1776
+ ...input.limit != null ? { limit: input.limit } : {},
1777
+ ...input.prompt ? { prompt: input.prompt } : {},
1778
+ ...input.context ? { context: input.context } : {}
1779
+ },
1780
+ deps
1781
+ );
1782
+ const stopReason = map.aborted ? "time_limit_reached" : map.total === 0 ? "no_sources" : "ok";
1783
+ return { ...selection, discovered: map.total, aborted: map.aborted, siteType: map.siteType, stopReason };
1784
+ }
1785
+ __name(findKnowledgeUrls, "findKnowledgeUrls");
1786
+ // Annotate the CommonJS export names for ESM import in node:
1787
+ 0 && (module.exports = {
1788
+ cachedDeps,
1789
+ discover,
1790
+ fetchJson,
1791
+ fetchText,
1792
+ findKnowledgeUrls,
1793
+ hostOf,
1794
+ select,
1795
+ siteOrigin
1796
+ });
1797
+ //# sourceMappingURL=index.cjs.map