smart-web-mcp 0.44.0 → 1.0.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/CHANGELOG.md +352 -1
- package/README.md +33 -566
- package/dist/assessment.d.ts +10 -3
- package/dist/assessment.js +21 -6
- package/dist/assessment.js.map +1 -1
- package/dist/body-export.d.ts +10 -0
- package/dist/body-export.js +28 -0
- package/dist/body-export.js.map +1 -0
- package/dist/browser-session.d.ts +32 -1
- package/dist/browser-session.js +373 -3
- package/dist/browser-session.js.map +1 -1
- package/dist/cli-fetch-options.d.ts +7 -0
- package/dist/cli-fetch-options.js +41 -0
- package/dist/cli-fetch-options.js.map +1 -0
- package/dist/cli.js +31 -12
- package/dist/cli.js.map +1 -1
- package/dist/comment-export.d.ts +10 -0
- package/dist/comment-export.js +39 -0
- package/dist/comment-export.js.map +1 -0
- package/dist/composition.d.ts +1 -22
- package/dist/composition.js +0 -16
- package/dist/composition.js.map +1 -1
- package/dist/extraction/budget.d.ts +3 -0
- package/dist/extraction/budget.js +88 -7
- package/dist/extraction/budget.js.map +1 -1
- package/dist/extraction/document.js +134 -9
- package/dist/extraction/document.js.map +1 -1
- package/dist/korea/coupang-filters.d.ts +5 -0
- package/dist/korea/coupang-filters.js +87 -0
- package/dist/korea/coupang-filters.js.map +1 -0
- package/dist/korea/coupang.d.ts +27 -6
- package/dist/korea/coupang.js +202 -82
- package/dist/korea/coupang.js.map +1 -1
- package/dist/korea/proxy-client.d.ts +1 -0
- package/dist/korea/proxy-client.js +6 -2
- package/dist/korea/proxy-client.js.map +1 -1
- package/dist/korea/smartvertical.d.ts +1 -0
- package/dist/korea/smartvertical.js +39 -25
- package/dist/korea/smartvertical.js.map +1 -1
- package/dist/lib.d.ts +4 -2
- package/dist/lib.js +2 -1
- package/dist/lib.js.map +1 -1
- package/dist/mcp-server.js +39 -15
- package/dist/mcp-server.js.map +1 -1
- package/dist/operator-browser-fixture.d.ts +7 -0
- package/dist/operator-browser-fixture.js +19 -0
- package/dist/operator-browser-fixture.js.map +1 -0
- package/dist/operator-canary-cli.d.ts +6 -0
- package/dist/operator-canary-cli.js +190 -0
- package/dist/operator-canary-cli.js.map +1 -0
- package/dist/operator-canary.d.ts +74 -0
- package/dist/operator-canary.js +135 -0
- package/dist/operator-canary.js.map +1 -0
- package/dist/operator-cdp.d.ts +15 -0
- package/dist/operator-cdp.js +195 -0
- package/dist/operator-cdp.js.map +1 -0
- package/dist/operator-frame.d.ts +3 -0
- package/dist/operator-frame.js +55 -0
- package/dist/operator-frame.js.map +1 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -1
- package/dist/shared.d.ts +47 -10
- package/dist/shared.js +15 -9
- package/dist/shared.js.map +1 -1
- package/dist/smartbrowser.d.ts +169 -0
- package/dist/smartbrowser.js +288 -0
- package/dist/smartbrowser.js.map +1 -0
- package/dist/smartcrawl.js +16 -21
- package/dist/smartcrawl.js.map +1 -1
- package/dist/smartfetch/academic-fallback.js +3 -5
- package/dist/smartfetch/academic-fallback.js.map +1 -1
- package/dist/smartfetch/archive-fallback.js +2 -5
- package/dist/smartfetch/archive-fallback.js.map +1 -1
- package/dist/smartfetch/assets.d.ts +1 -1
- package/dist/smartfetch/assets.js +28 -1
- package/dist/smartfetch/assets.js.map +1 -1
- package/dist/smartfetch/jina-reader.d.ts +1 -0
- package/dist/smartfetch/jina-reader.js +17 -3
- package/dist/smartfetch/jina-reader.js.map +1 -1
- package/dist/smartfetch/pipeline.js +15 -25
- package/dist/smartfetch/pipeline.js.map +1 -1
- package/dist/smartfetch/provider-policy.d.ts +3 -0
- package/dist/smartfetch/provider-policy.js +45 -2
- package/dist/smartfetch/provider-policy.js.map +1 -1
- package/dist/smartfetch/provider-types.d.ts +8 -0
- package/dist/smartfetch/providers/article.js +76 -10
- package/dist/smartfetch/providers/article.js.map +1 -1
- package/dist/smartfetch/providers/blind-post.d.ts +2 -0
- package/dist/smartfetch/providers/blind-post.js +167 -0
- package/dist/smartfetch/providers/blind-post.js.map +1 -0
- package/dist/smartfetch/providers/clien.d.ts +2 -0
- package/dist/smartfetch/providers/clien.js +157 -0
- package/dist/smartfetch/providers/clien.js.map +1 -0
- package/dist/smartfetch/providers/commerce.d.ts +15 -0
- package/dist/smartfetch/providers/commerce.js +123 -7
- package/dist/smartfetch/providers/commerce.js.map +1 -1
- package/dist/smartfetch/providers/dcinside.js +313 -20
- package/dist/smartfetch/providers/dcinside.js.map +1 -1
- package/dist/smartfetch/providers/hackernews-html.d.ts +21 -0
- package/dist/smartfetch/providers/hackernews-html.js +148 -0
- package/dist/smartfetch/providers/hackernews-html.js.map +1 -0
- package/dist/smartfetch/providers/hackernews.js +76 -14
- package/dist/smartfetch/providers/hackernews.js.map +1 -1
- package/dist/smartfetch/providers/index.js +7 -1
- package/dist/smartfetch/providers/index.js.map +1 -1
- package/dist/smartfetch/providers/linkedin.js +2 -1
- package/dist/smartfetch/providers/linkedin.js.map +1 -1
- package/dist/smartfetch/providers/naver-blog.d.ts +2 -0
- package/dist/smartfetch/providers/naver-blog.js +87 -45
- package/dist/smartfetch/providers/naver-blog.js.map +1 -1
- package/dist/smartfetch/providers/naver-cafe-native.d.ts +7 -0
- package/dist/smartfetch/providers/naver-cafe-native.js +188 -0
- package/dist/smartfetch/providers/naver-cafe-native.js.map +1 -0
- package/dist/smartfetch/providers/naver-cafe.js +165 -4
- package/dist/smartfetch/providers/naver-cafe.js.map +1 -1
- package/dist/smartfetch/providers/naver-map-search.js +1 -1
- package/dist/smartfetch/providers/naver-map-search.js.map +1 -1
- package/dist/smartfetch/providers/ppomppu.d.ts +2 -0
- package/dist/smartfetch/providers/ppomppu.js +289 -0
- package/dist/smartfetch/providers/ppomppu.js.map +1 -0
- package/dist/smartfetch/providers/reddit.js +24 -3
- package/dist/smartfetch/providers/reddit.js.map +1 -1
- package/dist/smartfetch/providers/threads.js +1 -1
- package/dist/smartfetch/providers/threads.js.map +1 -1
- package/dist/smartfetch/providers/wikipedia.js +138 -0
- package/dist/smartfetch/providers/wikipedia.js.map +1 -1
- package/dist/smartfetch.d.ts +11 -2
- package/dist/smartfetch.js +369 -78
- package/dist/smartfetch.js.map +1 -1
- package/dist/smartsearch-brave.d.ts +2 -0
- package/dist/smartsearch-brave.js +43 -0
- package/dist/smartsearch-brave.js.map +1 -0
- package/dist/smartsearch-browser.d.ts +5 -0
- package/dist/smartsearch-browser.js +116 -0
- package/dist/smartsearch-browser.js.map +1 -0
- package/dist/smartsearch-ddg.d.ts +2 -0
- package/dist/smartsearch-ddg.js +46 -0
- package/dist/smartsearch-ddg.js.map +1 -0
- package/dist/smartsearch-node.d.ts +6 -0
- package/dist/smartsearch-node.js +21 -0
- package/dist/smartsearch-node.js.map +1 -0
- package/dist/smartsearch-postgresql.d.ts +7 -0
- package/dist/smartsearch-postgresql.js +44 -0
- package/dist/smartsearch-postgresql.js.map +1 -0
- package/dist/smartsearch-python.d.ts +8 -0
- package/dist/smartsearch-python.js +87 -0
- package/dist/smartsearch-python.js.map +1 -0
- package/dist/smartsearch-quality.d.ts +6 -0
- package/dist/smartsearch-quality.js +47 -0
- package/dist/smartsearch-quality.js.map +1 -0
- package/dist/smartsearch-youtube.d.ts +7 -0
- package/dist/smartsearch-youtube.js +76 -0
- package/dist/smartsearch-youtube.js.map +1 -0
- package/dist/smartsearch.d.ts +3 -0
- package/dist/smartsearch.js +617 -132
- package/dist/smartsearch.js.map +1 -1
- package/package.json +8 -5
- package/python/requirements-undetected-chromedriver.txt +2 -2
package/dist/smartsearch.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { configEnabled, fetchText,
|
|
1
|
+
import { configEnabled, fetchText, smartWebProfile, stripTags, uniqueResults, validateOutboundUrl } from "./shared.js";
|
|
2
2
|
import { createAssessment, createHandoffSuggestion, detectInteractiveLikeText, firstMeaningfulBlockedReason } from "./assessment.js";
|
|
3
3
|
import { configText } from "./settings.js";
|
|
4
4
|
import { runSmartcrawl } from "./smartcrawl.js";
|
|
5
5
|
import { budgetText, projectRetrievalAssessment } from "./extraction/budget.js";
|
|
6
6
|
import { parseFeed } from "./smartfetch/generic-fallbacks.js";
|
|
7
|
+
import { runBrowserSession } from "./browser-session.js";
|
|
8
|
+
import { browserSearchMatchesQuery, parseNaverBrowserSearch, searchResultMatchesSite } from "./smartsearch-browser.js";
|
|
9
|
+
import { parseBraveSearch } from "./smartsearch-brave.js";
|
|
10
|
+
import { parseHTML } from "linkedom";
|
|
11
|
+
import { parseDuckDuckGoSearch } from "./smartsearch-ddg.js";
|
|
12
|
+
import { parsePythonDocsSearch, pythonDocsSearchUrl, unscopedPythonDocsTerms } from "./smartsearch-python.js";
|
|
13
|
+
import { postgresqlDocsBodyHasSymbol, postgresqlDocsCandidates, postgresqlDocsIntent } from "./smartsearch-postgresql.js";
|
|
14
|
+
import { nodeDocsIntent, nodeDocsOriginal } from "./smartsearch-node.js";
|
|
15
|
+
import { parseYoutubeSearch, youtubeSearchUrl } from "./smartsearch-youtube.js";
|
|
16
|
+
import { discoveryEvidenceScore, hasDiscoveryYield, isSearchTrackingRelay, koreanDiscoveryTerms, rankDiscoveryResults } from "./smartsearch-quality.js";
|
|
7
17
|
const DEFAULT_TIMEOUT_MS = 12000;
|
|
8
18
|
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
|
|
9
19
|
const SMARTSEARCH_BUDGET_PRESETS = {
|
|
@@ -94,21 +104,46 @@ export function projectSmartsearchOutput(response, preset = "compact") {
|
|
|
94
104
|
metadata[path] = projected.metadata;
|
|
95
105
|
return projected.value;
|
|
96
106
|
};
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
// A clipped URL is not an original the caller can open; omit it rather than
|
|
108
|
+
// returning a plausible-looking but invalid link, and fill from later rows.
|
|
109
|
+
const usableResults = response.results.filter((item) => item.url.length <= policy.maxUrlChars);
|
|
110
|
+
const omittedUrls = response.results.length - usableResults.length;
|
|
111
|
+
const results = budgetList(usableResults, policy.maxResults);
|
|
112
|
+
if (omittedUrls > 0 || results.metadata.truncated) {
|
|
113
|
+
metadata.results = {
|
|
114
|
+
truncated: true,
|
|
115
|
+
original_items: response.results.length,
|
|
116
|
+
returned_items: results.items.length,
|
|
117
|
+
omitted_items: response.results.length - results.items.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
99
120
|
const projectedResults = results.items.map((item, index) => ({
|
|
100
121
|
...item,
|
|
101
122
|
title: projectText(item.title, policy.maxTitleChars, `results.${index}.title`),
|
|
102
|
-
url:
|
|
123
|
+
url: item.url,
|
|
103
124
|
snippet: projectText(item.snippet || "", policy.maxSnippetChars, `results.${index}.snippet`),
|
|
104
125
|
engine: projectText(item.engine, policy.maxEngineChars, `results.${index}.engine`),
|
|
105
126
|
}));
|
|
106
127
|
const notes = budgetList(response.notes, policy.maxNotes);
|
|
107
128
|
recordListMetadata(metadata, "notes", notes.metadata);
|
|
108
129
|
const projectedNotes = notes.items.map((note, index) => projectText(note, policy.maxNoteChars, `notes.${index}`));
|
|
130
|
+
if (omittedUrls > 0)
|
|
131
|
+
projectedNotes.push(`${omittedUrls} original URL(s) omitted because they exceed the ${policy.maxUrlChars}-character output limit.`);
|
|
109
132
|
const fallbacks = budgetList(response.fallbacks_tried, policy.maxFallbacks);
|
|
110
133
|
recordListMetadata(metadata, "fallbacks_tried", fallbacks.metadata);
|
|
111
134
|
const projectedFallbacks = fallbacks.items.map((fallback, index) => projectText(fallback, policy.maxFallbackChars, `fallbacks_tried.${index}`));
|
|
135
|
+
const execution = response.execution ? (() => {
|
|
136
|
+
const attempts = budgetList(response.execution.attempts, policy.maxFallbacks);
|
|
137
|
+
recordListMetadata(metadata, "execution.attempts", attempts.metadata);
|
|
138
|
+
return {
|
|
139
|
+
...response.execution,
|
|
140
|
+
attempts: attempts.items.map((attempt, index) => ({
|
|
141
|
+
...attempt,
|
|
142
|
+
provider: projectText(attempt.provider, policy.maxEngineChars, `execution.attempts.${index}.provider`),
|
|
143
|
+
...(attempt.error === undefined ? {} : { error: projectText(attempt.error, policy.maxNoteChars, `execution.attempts.${index}.error`) }),
|
|
144
|
+
})),
|
|
145
|
+
};
|
|
146
|
+
})() : undefined;
|
|
112
147
|
const handoff = response.handoff ? (() => {
|
|
113
148
|
const items = budgetList(response.handoff.items, policy.maxHandoffItems);
|
|
114
149
|
recordListMetadata(metadata, "handoff.items", items.metadata);
|
|
@@ -139,6 +174,8 @@ export function projectSmartsearchOutput(response, preset = "compact") {
|
|
|
139
174
|
maxUrlChars: policy.maxUrlChars,
|
|
140
175
|
}, metadata)
|
|
141
176
|
: undefined;
|
|
177
|
+
if (omittedUrls > 0 && assessment)
|
|
178
|
+
assessment.confidence = "low";
|
|
142
179
|
const engine = projectText(response.engine, policy.maxEngineChars, "engine");
|
|
143
180
|
const query = projectText(response.query, policy.maxQueryChars, "query");
|
|
144
181
|
const truncated = Object.values(metadata).some((item) => item.truncated);
|
|
@@ -157,6 +194,7 @@ export function projectSmartsearchOutput(response, preset = "compact") {
|
|
|
157
194
|
results: projectedResults,
|
|
158
195
|
fallbacks_tried: projectedFallbacks,
|
|
159
196
|
notes: projectedNotes,
|
|
197
|
+
...(execution ? { execution } : {}),
|
|
160
198
|
...(handoff ? { handoff } : {}),
|
|
161
199
|
...(assessment ? { assessment } : {}),
|
|
162
200
|
budget: {
|
|
@@ -166,6 +204,21 @@ export function projectSmartsearchOutput(response, preset = "compact") {
|
|
|
166
204
|
},
|
|
167
205
|
};
|
|
168
206
|
}
|
|
207
|
+
export function isSmartsearchExecutionFailure(response) {
|
|
208
|
+
return response.execution?.status === "failed";
|
|
209
|
+
}
|
|
210
|
+
function completedProviderSearch(results, note) {
|
|
211
|
+
return { ok: results.length > 0, status: "succeeded", results, note };
|
|
212
|
+
}
|
|
213
|
+
function failedProviderSearch(note) {
|
|
214
|
+
return { ok: false, status: "failed", results: [], note };
|
|
215
|
+
}
|
|
216
|
+
function smartsearchExecution(attempts) {
|
|
217
|
+
return {
|
|
218
|
+
status: attempts.some((attempt) => attempt.status === "succeeded") ? "succeeded" : "failed",
|
|
219
|
+
attempts,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
169
222
|
function localOnlyMode() {
|
|
170
223
|
return configEnabled("network.localOnly", false, { private: true });
|
|
171
224
|
}
|
|
@@ -329,9 +382,13 @@ function buildSearchResponse(response, handoff) {
|
|
|
329
382
|
return handoff ? { ...response, handoff, assessment } : { ...response, assessment };
|
|
330
383
|
}
|
|
331
384
|
function assessSearchResponse(response, handoff) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
385
|
+
// A failed earlier engine remains in notes, but does not block an exact
|
|
386
|
+
// watch-page original recovered from the site's own search.
|
|
387
|
+
const blockedReason = response.engine === "youtube_browser" && response.results.length > 0 ? "" : firstMeaningfulBlockedReason(response.notes);
|
|
388
|
+
const publicHtmlEngine = ["duckduckgo", "brave_html", "naver_browser", "youtube_browser", "mixed"].includes(response.engine);
|
|
389
|
+
const weakDiscovery = (publicHtmlEngine || response.engine === "searxng")
|
|
390
|
+
&& !hasDiscoveryYield(response.results, koreanDiscoveryTerms(response.query), response.results.length || 1);
|
|
391
|
+
const confidence = weakDiscovery ? "low" : response.results.length === 0
|
|
335
392
|
? (handoff?.performed ? "medium" : "low")
|
|
336
393
|
: (response.results.length >= 3 && !publicHtmlEngine ? "high" : "medium");
|
|
337
394
|
const interactiveFlowLikely = Boolean(handoff && !handoff.performed && detectInteractiveLikeText(handoff.reason));
|
|
@@ -379,6 +436,15 @@ async function maybeRunCrawlHandoff(response, options, runtime) {
|
|
|
379
436
|
}
|
|
380
437
|
const crawlQuery = stripSiteOperators(options.query);
|
|
381
438
|
const mode = inferCrawlMode(startUrl, options.crawlMode);
|
|
439
|
+
const siteTokens = searchQueryTokens(options.query).filter(token => /^site:[^"']+$/i.test(token));
|
|
440
|
+
if (siteTokens.length === 1 && extractSiteScope(siteTokens[0])?.host === "news.ycombinator.com" && parseHnDateQuery(options.query).bounded) {
|
|
441
|
+
const reason = "site_crawl not performed: HN date constraints require native publication timestamps that crawl items do not verify";
|
|
442
|
+
const declined = buildSearchResponse({ ...response, notes: [...response.notes, reason] }, {
|
|
443
|
+
tool: "smartcrawl", performed: false, reason, start_url: startUrl, query: options.query, mode,
|
|
444
|
+
items: [], pages_attempted: 0, pages_loaded: 0, errors: [reason],
|
|
445
|
+
});
|
|
446
|
+
return { ...declined, assessment: createAssessment({ ...declined.assessment, recommended_handoff: null }) };
|
|
447
|
+
}
|
|
382
448
|
const crawl = await (runtime.runSmartcrawl || runSmartcrawl)({
|
|
383
449
|
url: startUrl,
|
|
384
450
|
...(crawlQuery ? { query: crawlQuery } : {}),
|
|
@@ -429,7 +495,7 @@ async function searchVelog(query, numResults, timeoutMs) {
|
|
|
429
495
|
}),
|
|
430
496
|
});
|
|
431
497
|
if (!result.ok)
|
|
432
|
-
return
|
|
498
|
+
return failedProviderSearch(result.error || `Velog search error ${result.status}`);
|
|
433
499
|
try {
|
|
434
500
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
435
501
|
const raw = Array.isArray(data?.data?.searchPosts?.posts) ? data.data.searchPosts.posts : [];
|
|
@@ -439,11 +505,11 @@ async function searchVelog(query, numResults, timeoutMs) {
|
|
|
439
505
|
snippet: String(item?.short_description || "").trim(),
|
|
440
506
|
engine: "velog",
|
|
441
507
|
})).filter((item) => item.url)).slice(0, numResults);
|
|
442
|
-
return
|
|
508
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "Velog search returned no results");
|
|
443
509
|
}
|
|
444
510
|
catch (error) {
|
|
445
511
|
const message = error instanceof Error ? error.message : String(error);
|
|
446
|
-
return
|
|
512
|
+
return failedProviderSearch(`Velog search parse error: ${message}`);
|
|
447
513
|
}
|
|
448
514
|
}
|
|
449
515
|
async function searchStackExchange(query, numResults, timeoutMs) {
|
|
@@ -463,7 +529,7 @@ async function searchStackExchange(query, numResults, timeoutMs) {
|
|
|
463
529
|
},
|
|
464
530
|
});
|
|
465
531
|
if (!result.ok)
|
|
466
|
-
return
|
|
532
|
+
return failedProviderSearch(result.error || `StackExchange error ${result.status}`);
|
|
467
533
|
try {
|
|
468
534
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
469
535
|
const raw = Array.isArray(data?.items) ? data.items : [];
|
|
@@ -473,27 +539,71 @@ async function searchStackExchange(query, numResults, timeoutMs) {
|
|
|
473
539
|
snippet: String(item?.tags?.join(", ") || "").trim(),
|
|
474
540
|
engine: "stackexchange",
|
|
475
541
|
})).filter((item) => item.url)).slice(0, numResults);
|
|
476
|
-
return
|
|
542
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "StackExchange search returned no results");
|
|
477
543
|
}
|
|
478
544
|
catch (error) {
|
|
479
545
|
const message = error instanceof Error ? error.message : String(error);
|
|
480
|
-
return
|
|
546
|
+
return failedProviderSearch(`StackExchange parse error: ${message}`);
|
|
481
547
|
}
|
|
482
548
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
549
|
+
function searchQueryTokens(query) {
|
|
550
|
+
return query.match(/"(?:\\.|[^"\\])*"|'[^']*'|[()]|[^\s()]+/g) || [];
|
|
551
|
+
}
|
|
552
|
+
function parseHnDateQuery(query) {
|
|
553
|
+
const tokens = searchQueryTokens(query).filter(token => !/^site:[^"']+$/i.test(token));
|
|
554
|
+
const parsed = { text: "", bounded: false };
|
|
555
|
+
const terms = [];
|
|
556
|
+
for (const token of tokens) {
|
|
557
|
+
const operator = /^(after|before):(.*)$/i.exec(token);
|
|
558
|
+
if (!operator) {
|
|
559
|
+
terms.push(token);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
parsed.bounded = true;
|
|
563
|
+
const date = operator[2];
|
|
564
|
+
const timestamp = Date.parse(`${date}T00:00:00Z`);
|
|
565
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== date) {
|
|
566
|
+
parsed.error = `Invalid HN date constraint: ${token}; use a real UTC calendar date YYYY-MM-DD`;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const seconds = timestamp / 1000;
|
|
570
|
+
if (operator[1].toLowerCase() === "after")
|
|
571
|
+
parsed.after = Math.max(parsed.after ?? -Infinity, seconds);
|
|
572
|
+
else
|
|
573
|
+
parsed.before = Math.min(parsed.before ?? Infinity, seconds);
|
|
574
|
+
}
|
|
575
|
+
parsed.text = terms.join(" ");
|
|
576
|
+
if (parsed.bounded && terms.some(term => /^(?:AND|OR|NOT)$/.test(term) || /[()]/.test(term.replace(/^(["']).*\1$/, ""))))
|
|
577
|
+
parsed.error = "HN date constraints with Boolean/grouped expressions are unsupported; refusing to change their scope";
|
|
578
|
+
if (parsed.after !== undefined && parsed.before !== undefined && parsed.after >= parsed.before)
|
|
579
|
+
parsed.error = "HN date constraints have an empty or reversed UTC interval";
|
|
580
|
+
return parsed;
|
|
581
|
+
}
|
|
582
|
+
async function searchHackerNews(query, numResults, timeoutMs, dates) {
|
|
583
|
+
const q = dates.bounded ? dates.text : stripSiteOperators(query) || query;
|
|
584
|
+
const params = new URLSearchParams({ query: q, tags: "story", hitsPerPage: String(Math.min(numResults, 20)) });
|
|
585
|
+
const filters = [dates.after === undefined ? "" : `created_at_i>=${dates.after}`, dates.before === undefined ? "" : `created_at_i<${dates.before}`].filter(Boolean);
|
|
586
|
+
if (filters.length)
|
|
587
|
+
params.set("numericFilters", filters.join(","));
|
|
588
|
+
const result = await fetchText(`https://hn.algolia.com/api/v1/search?${params}`, timeoutMs, {
|
|
486
589
|
headers: {
|
|
487
590
|
accept: "application/json,text/plain,*/*",
|
|
488
591
|
"user-agent": DEFAULT_USER_AGENT,
|
|
489
592
|
},
|
|
490
593
|
});
|
|
491
594
|
if (!result.ok)
|
|
492
|
-
return
|
|
595
|
+
return failedProviderSearch(result.error || `Hacker News error ${result.status}`);
|
|
493
596
|
try {
|
|
494
597
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
495
|
-
|
|
496
|
-
|
|
598
|
+
if (!Array.isArray(data?.hits))
|
|
599
|
+
return failedProviderSearch("Hacker News returned an invalid hits payload");
|
|
600
|
+
const raw = data.hits;
|
|
601
|
+
const valid = dates.bounded ? raw.filter((item) => item && typeof item.objectID === "string" && /^\d+$/.test(item.objectID)
|
|
602
|
+
&& typeof item.title === "string" && item.title.trim() && Number.isSafeInteger(item.created_at_i) && Math.abs(item.created_at_i) <= 8640000000000
|
|
603
|
+
&& (dates.after === undefined || item.created_at_i >= dates.after) && (dates.before === undefined || item.created_at_i < dates.before)) : raw;
|
|
604
|
+
if (dates.bounded && raw.length && !valid.length)
|
|
605
|
+
return failedProviderSearch("Hacker News returned no verifiable stories satisfying the requested date constraints");
|
|
606
|
+
const results = uniqueResults(valid.map((item) => {
|
|
497
607
|
const objectId = String(item?.objectID || "").trim();
|
|
498
608
|
let linkedHost = "";
|
|
499
609
|
try {
|
|
@@ -506,19 +616,20 @@ async function searchHackerNews(query, numResults, timeoutMs) {
|
|
|
506
616
|
title: String(item?.title || item?.story_title || `HN #${objectId}`).trim(),
|
|
507
617
|
url: objectId ? `https://news.ycombinator.com/item?id=${objectId}` : "",
|
|
508
618
|
snippet: [
|
|
619
|
+
dates.bounded ? new Date(item.created_at_i * 1000).toISOString() : "",
|
|
509
620
|
item?.points ? `▲ ${item.points}` : "",
|
|
510
621
|
item?.author ? `by ${item.author}` : "",
|
|
511
|
-
item?.created_at ? String(item.created_at).slice(0, 10) : "",
|
|
622
|
+
!dates.bounded && item?.created_at ? String(item.created_at).slice(0, 10) : "",
|
|
512
623
|
linkedHost ? `→ ${linkedHost}` : "",
|
|
513
624
|
].filter(Boolean).join(" · "),
|
|
514
625
|
engine: "hackernews",
|
|
515
626
|
};
|
|
516
627
|
}).filter((item) => item.url)).slice(0, numResults);
|
|
517
|
-
return {
|
|
628
|
+
return { ...completedProviderSearch(results, results.length > 0 ? "" : "Hacker News search returned no results"), hnDateBoundsVerified: dates.bounded };
|
|
518
629
|
}
|
|
519
630
|
catch (error) {
|
|
520
631
|
const message = error instanceof Error ? error.message : String(error);
|
|
521
|
-
return
|
|
632
|
+
return failedProviderSearch(`Hacker News parse error: ${message}`);
|
|
522
633
|
}
|
|
523
634
|
}
|
|
524
635
|
function redditSubredditScope(query) {
|
|
@@ -552,11 +663,12 @@ async function searchReddit(query, numResults, timeoutMs) {
|
|
|
552
663
|
const subreddit = redditSubredditScope(query);
|
|
553
664
|
const notes = [];
|
|
554
665
|
let collected = [];
|
|
666
|
+
let completed = false;
|
|
555
667
|
const collect = (results) => {
|
|
556
668
|
collected = uniqueResults([...collected, ...results]).slice(0, numResults);
|
|
557
669
|
return collected.length >= numResults;
|
|
558
670
|
};
|
|
559
|
-
const success = () => (
|
|
671
|
+
const success = () => completedProviderSearch(collected, "");
|
|
560
672
|
const keepScope = (results) => {
|
|
561
673
|
if (!subreddit)
|
|
562
674
|
return results;
|
|
@@ -581,6 +693,7 @@ async function searchReddit(query, numResults, timeoutMs) {
|
|
|
581
693
|
},
|
|
582
694
|
});
|
|
583
695
|
if (rss.ok) {
|
|
696
|
+
completed = true;
|
|
584
697
|
const feed = parseFeed(rss.text, rssUrl);
|
|
585
698
|
const results = keepScope(uniqueResults((feed?.entries || []).map((entry) => {
|
|
586
699
|
let compatibleUrl = "";
|
|
@@ -631,6 +744,7 @@ async function searchReddit(query, numResults, timeoutMs) {
|
|
|
631
744
|
else {
|
|
632
745
|
notes.push("Arctic Shift returned no matching Reddit posts");
|
|
633
746
|
}
|
|
747
|
+
completed = true;
|
|
634
748
|
}
|
|
635
749
|
catch (error) {
|
|
636
750
|
notes.push(`Arctic Shift parse error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -659,6 +773,7 @@ async function searchReddit(query, numResults, timeoutMs) {
|
|
|
659
773
|
else {
|
|
660
774
|
notes.push("PullPush returned no matching historical Reddit posts");
|
|
661
775
|
}
|
|
776
|
+
completed = true;
|
|
662
777
|
}
|
|
663
778
|
catch (error) {
|
|
664
779
|
notes.push(`PullPush parse error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -687,6 +802,7 @@ async function searchReddit(query, numResults, timeoutMs) {
|
|
|
687
802
|
else {
|
|
688
803
|
notes.push("Reddit JSON returned no matching posts");
|
|
689
804
|
}
|
|
805
|
+
completed = true;
|
|
690
806
|
}
|
|
691
807
|
catch (error) {
|
|
692
808
|
notes.push(`Reddit JSON parse error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -697,7 +813,8 @@ async function searchReddit(query, numResults, timeoutMs) {
|
|
|
697
813
|
}
|
|
698
814
|
if (collected.length > 0)
|
|
699
815
|
return success();
|
|
700
|
-
|
|
816
|
+
const note = notes.filter(Boolean).join("; ") || "Reddit search returned no results";
|
|
817
|
+
return completed ? completedProviderSearch([], note) : failedProviderSearch(note);
|
|
701
818
|
}
|
|
702
819
|
async function searchGitHub(query, numResults, timeoutMs) {
|
|
703
820
|
const q = stripSiteOperators(query) || query;
|
|
@@ -708,7 +825,7 @@ async function searchGitHub(query, numResults, timeoutMs) {
|
|
|
708
825
|
},
|
|
709
826
|
});
|
|
710
827
|
if (!result.ok)
|
|
711
|
-
return
|
|
828
|
+
return failedProviderSearch(result.error || `GitHub error ${result.status}`);
|
|
712
829
|
try {
|
|
713
830
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
714
831
|
const raw = Array.isArray(data?.items) ? data.items : [];
|
|
@@ -722,17 +839,17 @@ async function searchGitHub(query, numResults, timeoutMs) {
|
|
|
722
839
|
].filter(Boolean).join(" · "),
|
|
723
840
|
engine: "github",
|
|
724
841
|
}))).slice(0, numResults);
|
|
725
|
-
return
|
|
842
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "GitHub search returned no results");
|
|
726
843
|
}
|
|
727
844
|
catch (error) {
|
|
728
845
|
const message = error instanceof Error ? error.message : String(error);
|
|
729
|
-
return
|
|
846
|
+
return failedProviderSearch(`GitHub parse error: ${message}`);
|
|
730
847
|
}
|
|
731
848
|
}
|
|
732
849
|
async function searchGitHubRepoDocs(query, numResults, timeoutMs) {
|
|
733
850
|
const repoScope = parseGitHubRepoScope(query);
|
|
734
851
|
if (!repoScope)
|
|
735
|
-
return
|
|
852
|
+
return failedProviderSearch("GitHub repository docs fallback skipped without owner/repo site scope");
|
|
736
853
|
const repoResult = await fetchText(`https://api.github.com/repos/${encodeURIComponent(repoScope.owner)}/${encodeURIComponent(repoScope.repo)}`, timeoutMs, {
|
|
737
854
|
headers: {
|
|
738
855
|
accept: "application/vnd.github+json",
|
|
@@ -740,7 +857,7 @@ async function searchGitHubRepoDocs(query, numResults, timeoutMs) {
|
|
|
740
857
|
},
|
|
741
858
|
});
|
|
742
859
|
if (!repoResult.ok)
|
|
743
|
-
return
|
|
860
|
+
return failedProviderSearch(repoResult.error || `GitHub repository metadata error ${repoResult.status}`);
|
|
744
861
|
let defaultBranch = "main";
|
|
745
862
|
try {
|
|
746
863
|
const repoData = repoResult.text ? JSON.parse(repoResult.text) : {};
|
|
@@ -756,7 +873,7 @@ async function searchGitHubRepoDocs(query, numResults, timeoutMs) {
|
|
|
756
873
|
},
|
|
757
874
|
});
|
|
758
875
|
if (!result.ok)
|
|
759
|
-
return
|
|
876
|
+
return failedProviderSearch(result.error || `GitHub repository docs error ${result.status}`);
|
|
760
877
|
try {
|
|
761
878
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
762
879
|
const raw = Array.isArray(data?.tree) ? data.tree : [];
|
|
@@ -782,15 +899,18 @@ async function searchGitHubRepoDocs(query, numResults, timeoutMs) {
|
|
|
782
899
|
snippet: `GitHub repository document · path:${item.path}`,
|
|
783
900
|
engine: "github_repo_docs",
|
|
784
901
|
}))).slice(0, numResults);
|
|
785
|
-
return
|
|
902
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "GitHub repository docs search returned no matching documents");
|
|
786
903
|
}
|
|
787
904
|
catch (error) {
|
|
788
905
|
const message = error instanceof Error ? error.message : String(error);
|
|
789
|
-
return
|
|
906
|
+
return failedProviderSearch(`GitHub repository docs parse error: ${message}`);
|
|
790
907
|
}
|
|
791
908
|
}
|
|
792
909
|
async function searchMdn(query, numResults, timeoutMs) {
|
|
793
910
|
const q = stripSiteOperators(query) || query;
|
|
911
|
+
const quoted = searchQueryTokens(query)
|
|
912
|
+
.filter(token => token.startsWith('"') && token.endsWith('"') && token.length > 2 && !token.includes("\\"))
|
|
913
|
+
.map(token => token.slice(1, -1).toLocaleLowerCase());
|
|
794
914
|
const result = await fetchText(`https://developer.mozilla.org/api/v1/search?q=${encodeURIComponent(q)}&locale=en-US`, timeoutMs, {
|
|
795
915
|
headers: {
|
|
796
916
|
accept: "application/json,text/plain,*/*",
|
|
@@ -798,11 +918,13 @@ async function searchMdn(query, numResults, timeoutMs) {
|
|
|
798
918
|
},
|
|
799
919
|
});
|
|
800
920
|
if (!result.ok)
|
|
801
|
-
return
|
|
921
|
+
return failedProviderSearch(result.error || `MDN error ${result.status}`);
|
|
802
922
|
try {
|
|
803
923
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
804
|
-
|
|
805
|
-
|
|
924
|
+
if (!Array.isArray(data?.documents))
|
|
925
|
+
return failedProviderSearch("MDN returned an invalid documents payload");
|
|
926
|
+
const raw = data.documents;
|
|
927
|
+
const candidates = uniqueResults(raw.map((item) => {
|
|
806
928
|
const mdnUrl = String(item?.mdn_url || item?.url || "").trim();
|
|
807
929
|
return {
|
|
808
930
|
title: String(item?.title || item?.slug || mdnUrl).trim(),
|
|
@@ -810,12 +932,18 @@ async function searchMdn(query, numResults, timeoutMs) {
|
|
|
810
932
|
snippet: String(item?.summary || item?.excerpt || "").trim(),
|
|
811
933
|
engine: "mdn",
|
|
812
934
|
};
|
|
813
|
-
}).filter((item) => item.url && item.url !== "https://developer.mozilla.org/"))
|
|
814
|
-
|
|
935
|
+
}).filter((item) => item.url && item.url !== "https://developer.mozilla.org/"));
|
|
936
|
+
// Native index neighbors do not satisfy an explicit quote by proximity.
|
|
937
|
+
const results = quoted.length ? candidates.filter(item => quoted.every(phrase => item.title.toLocaleLowerCase().includes(phrase) || item.snippet.toLocaleLowerCase().includes(phrase))) : candidates;
|
|
938
|
+
if (quoted.length && candidates.length && !results.length)
|
|
939
|
+
return failedProviderSearch("MDN index has no title/summary evidence for the quoted phrase; continuing to full-query search");
|
|
940
|
+
return completedProviderSearch(results.slice(0, numResults), quoted.length && results.length
|
|
941
|
+
? "MDN quoted phrase matched index title/summary only; original body remains unverified"
|
|
942
|
+
: results.length ? "" : "MDN search returned no results");
|
|
815
943
|
}
|
|
816
944
|
catch (error) {
|
|
817
945
|
const message = error instanceof Error ? error.message : String(error);
|
|
818
|
-
return
|
|
946
|
+
return failedProviderSearch(`MDN parse error: ${message}`);
|
|
819
947
|
}
|
|
820
948
|
}
|
|
821
949
|
async function searchCrates(query, numResults, timeoutMs) {
|
|
@@ -827,10 +955,12 @@ async function searchCrates(query, numResults, timeoutMs) {
|
|
|
827
955
|
},
|
|
828
956
|
});
|
|
829
957
|
if (!result.ok)
|
|
830
|
-
return
|
|
958
|
+
return failedProviderSearch(result.error || `crates.io error ${result.status}`);
|
|
831
959
|
try {
|
|
832
960
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
833
|
-
|
|
961
|
+
if (!Array.isArray(data?.crates))
|
|
962
|
+
return failedProviderSearch("crates.io returned an invalid crates payload");
|
|
963
|
+
const raw = data.crates;
|
|
834
964
|
const results = uniqueResults(raw.map((item) => {
|
|
835
965
|
const name = String(item?.name || item?.id || "").trim();
|
|
836
966
|
const latest = String(item?.max_version || item?.newest_version || "").trim();
|
|
@@ -845,14 +975,14 @@ async function searchCrates(query, numResults, timeoutMs) {
|
|
|
845
975
|
engine: "crates",
|
|
846
976
|
};
|
|
847
977
|
}).filter((item) => item.url)).slice(0, numResults);
|
|
848
|
-
return
|
|
978
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "crates.io search returned no results");
|
|
849
979
|
}
|
|
850
980
|
catch (error) {
|
|
851
981
|
const message = error instanceof Error ? error.message : String(error);
|
|
852
|
-
return
|
|
982
|
+
return failedProviderSearch(`crates.io parse error: ${message}`);
|
|
853
983
|
}
|
|
854
984
|
}
|
|
855
|
-
async function searchNpmPackage(packageName, timeoutMs) {
|
|
985
|
+
async function searchNpmPackage(packageName, timeoutMs, quoted = []) {
|
|
856
986
|
const result = await fetchText(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, timeoutMs, {
|
|
857
987
|
headers: {
|
|
858
988
|
accept: "application/json,text/plain,*/*",
|
|
@@ -860,28 +990,37 @@ async function searchNpmPackage(packageName, timeoutMs) {
|
|
|
860
990
|
},
|
|
861
991
|
});
|
|
862
992
|
if (!result.ok)
|
|
863
|
-
return
|
|
993
|
+
return failedProviderSearch(result.error || `npm package error ${result.status}`);
|
|
864
994
|
try {
|
|
865
995
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
866
996
|
const record = data;
|
|
867
|
-
|
|
997
|
+
// The requested URL is not evidence that a successful response describes
|
|
998
|
+
// that package. An empty or mismatched registry payload must not invent it.
|
|
999
|
+
if (typeof record.name !== "string" || record.name !== packageName) {
|
|
1000
|
+
return failedProviderSearch("npm package metadata did not identify the requested package");
|
|
1001
|
+
}
|
|
1002
|
+
const name = record.name;
|
|
1003
|
+
const description = typeof record.description === "string" ? record.description : "";
|
|
1004
|
+
if (quoted.length && !quoted.every(phrase => name.toLocaleLowerCase().includes(phrase) || description.toLocaleLowerCase().includes(phrase))) {
|
|
1005
|
+
return failedProviderSearch("npm package metadata has no evidence for the quoted phrase; continuing to full-query search");
|
|
1006
|
+
}
|
|
868
1007
|
const latest = String(record["dist-tags"]?.latest || record.version || "").trim();
|
|
869
1008
|
const maintainer = Array.isArray(record.maintainers) ? String(record.maintainers[0]?.name || "").trim() : "";
|
|
870
1009
|
const results = uniqueResults([{
|
|
871
1010
|
title: name,
|
|
872
1011
|
url: `https://www.npmjs.com/package/${name}`,
|
|
873
1012
|
snippet: [
|
|
874
|
-
|
|
1013
|
+
description.trim(),
|
|
875
1014
|
latest ? `v${latest}` : "",
|
|
876
1015
|
maintainer ? `by ${maintainer}` : "",
|
|
877
1016
|
].filter(Boolean).join(" · "),
|
|
878
1017
|
engine: "npm",
|
|
879
1018
|
}]);
|
|
880
|
-
return
|
|
1019
|
+
return completedProviderSearch(results, quoted.length ? "npm quoted phrase matched package metadata only; original body remains unverified" : "");
|
|
881
1020
|
}
|
|
882
1021
|
catch (error) {
|
|
883
1022
|
const message = error instanceof Error ? error.message : String(error);
|
|
884
|
-
return
|
|
1023
|
+
return failedProviderSearch(`npm package parse error: ${message}`);
|
|
885
1024
|
}
|
|
886
1025
|
}
|
|
887
1026
|
async function searchNpm(query, numResults, timeoutMs) {
|
|
@@ -893,10 +1032,12 @@ async function searchNpm(query, numResults, timeoutMs) {
|
|
|
893
1032
|
},
|
|
894
1033
|
});
|
|
895
1034
|
if (!result.ok)
|
|
896
|
-
return
|
|
1035
|
+
return failedProviderSearch(result.error || `npm error ${result.status}`);
|
|
897
1036
|
try {
|
|
898
1037
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
899
|
-
|
|
1038
|
+
if (!Array.isArray(data?.objects))
|
|
1039
|
+
return failedProviderSearch("npm search returned an invalid objects payload");
|
|
1040
|
+
const raw = data.objects;
|
|
900
1041
|
const results = uniqueResults(raw.map((item) => {
|
|
901
1042
|
const pkg = item?.package || {};
|
|
902
1043
|
const name = String(pkg?.name || "").trim();
|
|
@@ -911,11 +1052,11 @@ async function searchNpm(query, numResults, timeoutMs) {
|
|
|
911
1052
|
engine: "npm",
|
|
912
1053
|
};
|
|
913
1054
|
}).filter((item) => item.url)).slice(0, numResults);
|
|
914
|
-
return
|
|
1055
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "npm search returned no results");
|
|
915
1056
|
}
|
|
916
1057
|
catch (error) {
|
|
917
1058
|
const message = error instanceof Error ? error.message : String(error);
|
|
918
|
-
return
|
|
1059
|
+
return failedProviderSearch(`npm parse error: ${message}`);
|
|
919
1060
|
}
|
|
920
1061
|
}
|
|
921
1062
|
function braveHtmlEnabled() {
|
|
@@ -935,10 +1076,12 @@ async function searchWikipedia(query, numResults, timeoutMs) {
|
|
|
935
1076
|
},
|
|
936
1077
|
});
|
|
937
1078
|
if (!result.ok)
|
|
938
|
-
return
|
|
1079
|
+
return failedProviderSearch(result.error || `Wikipedia error ${result.status}`);
|
|
939
1080
|
try {
|
|
940
1081
|
const data = result.text ? JSON.parse(result.text) : {};
|
|
941
|
-
|
|
1082
|
+
if (!Array.isArray(data?.query?.search))
|
|
1083
|
+
return failedProviderSearch("Wikipedia returned an invalid search payload");
|
|
1084
|
+
const raw = data.query.search;
|
|
942
1085
|
const results = uniqueResults(raw.map((item) => {
|
|
943
1086
|
const title = String(item?.title || "").trim();
|
|
944
1087
|
const pageId = item?.pageid;
|
|
@@ -950,11 +1093,24 @@ async function searchWikipedia(query, numResults, timeoutMs) {
|
|
|
950
1093
|
engine: "wikipedia",
|
|
951
1094
|
};
|
|
952
1095
|
}).filter((item) => item.url)).slice(0, numResults);
|
|
953
|
-
|
|
1096
|
+
// Native API ordering can fill a Korean two-term query with articles whose
|
|
1097
|
+
// excerpt only contains one incidental word (e.g. 기계식 계산기 for a keyboard
|
|
1098
|
+
// query). Do not hide all leads if none has enough visible evidence.
|
|
1099
|
+
const terms = wikiLang === "ko" ? koreanDiscoveryTerms(query) : [];
|
|
1100
|
+
const topical = terms.length >= 2 ? results.filter(row => discoveryEvidenceScore(row, terms) >= 2) : results;
|
|
1101
|
+
const phrase = terms.join("");
|
|
1102
|
+
const folded = (value) => value.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
1103
|
+
const phraseScore = (row) => !phrase ? 0
|
|
1104
|
+
: folded(row.title).includes(phrase) ? 2 : folded(row.snippet).includes(phrase) ? 1 : 0;
|
|
1105
|
+
const selected = topical.length ? topical.sort((a, b) => phraseScore(b) - phraseScore(a)) : results;
|
|
1106
|
+
const note = selected.length < results.length
|
|
1107
|
+
? `Excluded ${results.length - selected.length} Wikipedia result(s) without both query terms in title/snippet`
|
|
1108
|
+
: selected.length ? "" : "Wikipedia search returned no results";
|
|
1109
|
+
return completedProviderSearch(selected, note);
|
|
954
1110
|
}
|
|
955
1111
|
catch (error) {
|
|
956
1112
|
const message = error instanceof Error ? error.message : String(error);
|
|
957
|
-
return
|
|
1113
|
+
return failedProviderSearch(`Wikipedia parse error: ${message}`);
|
|
958
1114
|
}
|
|
959
1115
|
}
|
|
960
1116
|
async function searchDuckDuckGo(query, numResults, timeoutMs) {
|
|
@@ -964,24 +1120,12 @@ async function searchDuckDuckGo(query, numResults, timeoutMs) {
|
|
|
964
1120
|
},
|
|
965
1121
|
});
|
|
966
1122
|
if (!result.ok)
|
|
967
|
-
return
|
|
1123
|
+
return failedProviderSearch(result.error || `DuckDuckGo error ${result.status}`);
|
|
968
1124
|
if (/anomaly\.js|sending automated queries|captcha/i.test(result.text)) {
|
|
969
|
-
return
|
|
1125
|
+
return failedProviderSearch("DuckDuckGo blocked the automated HTML fallback");
|
|
970
1126
|
}
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
const anchor = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
|
|
974
|
-
const snippet = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i)
|
|
975
|
-
|| block.match(/<div[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/div>/i);
|
|
976
|
-
const rawUrl = anchor?.[1] || "";
|
|
977
|
-
return {
|
|
978
|
-
title: stripTags(anchor?.[2] || ""),
|
|
979
|
-
url: resolveDuckDuckGoUrl(rawUrl),
|
|
980
|
-
snippet: stripTags(snippet?.[1] || ""),
|
|
981
|
-
engine: "duckduckgo",
|
|
982
|
-
};
|
|
983
|
-
}).filter((item) => item.url)).slice(0, numResults);
|
|
984
|
-
return { ok: results.length > 0, results, note: results.length > 0 ? "" : "DuckDuckGo returned no results" };
|
|
1127
|
+
const results = parseDuckDuckGoSearch(result.text);
|
|
1128
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "DuckDuckGo returned no results");
|
|
985
1129
|
}
|
|
986
1130
|
async function searchBraveHtml(query, numResults, timeoutMs) {
|
|
987
1131
|
const result = await fetchText(`https://search.brave.com/search?q=${encodeURIComponent(query)}&source=web`, timeoutMs, {
|
|
@@ -991,21 +1135,16 @@ async function searchBraveHtml(query, numResults, timeoutMs) {
|
|
|
991
1135
|
},
|
|
992
1136
|
});
|
|
993
1137
|
if (!result.ok)
|
|
994
|
-
return
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
engine: "brave_html",
|
|
1003
|
-
});
|
|
1004
|
-
if (results.length >= numResults)
|
|
1005
|
-
break;
|
|
1138
|
+
return failedProviderSearch(result.error || `Brave HTML error ${result.status}`);
|
|
1139
|
+
const unique = parseBraveSearch(result.text);
|
|
1140
|
+
if (unique.length === 0 && result.text.trim()) {
|
|
1141
|
+
const { document } = parseHTML(result.text);
|
|
1142
|
+
const visible = `${document.title} ${document.body?.textContent || ""}`.slice(0, 10000);
|
|
1143
|
+
if (/verify you are human|captcha|unusual traffic|automated quer(?:y|ies)|비정상적인 접근|자동입력 방지/i.test(visible)) {
|
|
1144
|
+
return failedProviderSearch("Brave HTML stopped at a challenge");
|
|
1145
|
+
}
|
|
1006
1146
|
}
|
|
1007
|
-
|
|
1008
|
-
return { ok: unique.length > 0, results: unique, note: unique.length > 0 ? "" : "Brave HTML returned no results" };
|
|
1147
|
+
return completedProviderSearch(unique, unique.length > 0 ? "" : "Brave HTML returned no results");
|
|
1009
1148
|
}
|
|
1010
1149
|
async function searchSearxng(baseUrl, query, numResults, timeoutMs) {
|
|
1011
1150
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
@@ -1016,44 +1155,205 @@ async function searchSearxng(baseUrl, query, numResults, timeoutMs) {
|
|
|
1016
1155
|
},
|
|
1017
1156
|
});
|
|
1018
1157
|
if (!result.ok)
|
|
1019
|
-
return
|
|
1158
|
+
return failedProviderSearch(result.error || `SearXNG error ${result.status}`);
|
|
1020
1159
|
try {
|
|
1021
|
-
const data =
|
|
1022
|
-
|
|
1160
|
+
const data = JSON.parse(result.text);
|
|
1161
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.results)) {
|
|
1162
|
+
return failedProviderSearch("SearXNG invalid result envelope: expected a results array");
|
|
1163
|
+
}
|
|
1164
|
+
const raw = data.results;
|
|
1023
1165
|
const results = uniqueResults(raw.map((item) => ({
|
|
1024
1166
|
title: String(item?.title || item?.url || "").trim(),
|
|
1025
1167
|
url: String(item?.url || "").trim(),
|
|
1026
1168
|
snippet: String(item?.content || item?.description || item?.text || "").trim(),
|
|
1027
1169
|
engine: "searxng",
|
|
1028
|
-
}))).slice(0, numResults);
|
|
1029
|
-
return
|
|
1170
|
+
}))).slice(0, Math.max(100, numResults));
|
|
1171
|
+
return completedProviderSearch(results, results.length > 0 ? "" : "SearXNG returned no results");
|
|
1030
1172
|
}
|
|
1031
1173
|
catch (error) {
|
|
1032
1174
|
const message = error instanceof Error ? error.message : String(error);
|
|
1033
|
-
return
|
|
1175
|
+
return failedProviderSearch(`SearXNG parse error: ${message}`);
|
|
1034
1176
|
}
|
|
1035
1177
|
}
|
|
1036
1178
|
export async function runSmartsearch(options, runtime = {}) {
|
|
1179
|
+
const startedAt = Date.now();
|
|
1037
1180
|
const query = options.query.trim();
|
|
1038
1181
|
const numResults = Math.max(1, Math.min(20, Math.floor(Number(options.numResults || 8))));
|
|
1039
1182
|
const searchType = options.searchType || "auto";
|
|
1040
1183
|
const contextMaxCharacters = Math.max(1000, Math.floor(Number(options.contextMaxCharacters || 10000)));
|
|
1041
1184
|
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs || DEFAULT_TIMEOUT_MS)));
|
|
1185
|
+
const cdpUrl = configText("fetch.browserCdpUrl");
|
|
1186
|
+
const browserEnabled = Boolean(cdpUrl && /[가-힣a-z]/iu.test(stripSiteOperators(query)) && !localOnlyMode() && !privateProfileMode() && configEnabled("search.enableBrowserFallback", true));
|
|
1187
|
+
const attemptTimeout = () => Math.max(1, Math.floor((timeoutMs - (Date.now() - startedAt)) / (browserEnabled ? 4 : 1)));
|
|
1042
1188
|
const searxngBaseUrl = (options.searxngBaseUrl || configText("search.searxngBaseUrl") || "").trim();
|
|
1043
1189
|
const fallbacks = [];
|
|
1044
1190
|
const notes = [];
|
|
1191
|
+
const attempts = [];
|
|
1192
|
+
// A result outside an explicit site/path is not a useful provider success.
|
|
1193
|
+
// Multiple positive site operators are left to the general engine; native
|
|
1194
|
+
// first-site routing cannot preserve their Boolean semantics.
|
|
1195
|
+
const tokens = searchQueryTokens(query);
|
|
1196
|
+
const siteMatches = tokens.filter((token) => /^site:[^"']+$/i.test(token));
|
|
1197
|
+
const scope = siteMatches.length === 1 ? extractSiteScope(siteMatches[0]) : null;
|
|
1198
|
+
const unscopedDocsTerms = !siteMatches.length ? unscopedPythonDocsTerms(query) : undefined;
|
|
1199
|
+
const pythonSearchUrl = browserEnabled ? pythonDocsSearchUrl(unscopedDocsTerms ? { host: "docs.python.org", path: "3" } : scope, unscopedDocsTerms || tokens.filter(token => token !== siteMatches[0]).join(" ")) : undefined;
|
|
1200
|
+
const nativeYoutubeUrl = browserEnabled ? youtubeSearchUrl(scope, tokens.filter(token => token !== siteMatches[0]).join(" ")) : undefined;
|
|
1201
|
+
const hnDates = scope?.host === "news.ycombinator.com" ? parseHnDateQuery(query) : undefined;
|
|
1202
|
+
const nativeCandidateLimit = scope ? 20 : numResults;
|
|
1203
|
+
const constrain = (outcome, limit = numResults) => {
|
|
1204
|
+
if (hnDates?.bounded && outcome.status === "succeeded" && !outcome.hnDateBoundsVerified)
|
|
1205
|
+
return failedProviderSearch("HN date constraints require verified native publication timestamps; undated search snippets were not accepted");
|
|
1206
|
+
if (!outcome.ok)
|
|
1207
|
+
return outcome;
|
|
1208
|
+
const originals = outcome.results.filter((item) => !isSearchTrackingRelay(item));
|
|
1209
|
+
if (originals.length !== outcome.results.length) {
|
|
1210
|
+
notes.push(`Excluded ${outcome.results.length - originals.length} outbound tracking relay(s); discovery requires original pages`);
|
|
1211
|
+
outcome = completedProviderSearch(originals, outcome.note);
|
|
1212
|
+
if (!originals.length)
|
|
1213
|
+
return failedProviderSearch("Search returned tracking relays without original results");
|
|
1214
|
+
}
|
|
1215
|
+
if (!scope)
|
|
1216
|
+
return { ...outcome, results: outcome.results.slice(0, limit) };
|
|
1217
|
+
const scoped = outcome.results.filter((item) => searchResultMatchesSite(item.url, scope.host, scope.path));
|
|
1218
|
+
if (scoped.length !== outcome.results.length)
|
|
1219
|
+
notes.push(`Filtered ${outcome.results.length - scoped.length} result(s) outside site:${scope.host}${scope.path ? `/${scope.path}` : ""}`);
|
|
1220
|
+
const wikiArticleIntent = scope.host === "github.com" && /(?:^|\/)wiki(?:\/|$)/i.test(scope.path)
|
|
1221
|
+
&& !/\b(?:history|revisions?|edit)\b/i.test(stripSiteOperators(query));
|
|
1222
|
+
const results = wikiArticleIntent
|
|
1223
|
+
? scoped.filter((item) => !/\/wiki\/(?:[^?#]+\/)?_(?:history|edit)(?:[/?#]|$)/i.test(new URL(item.url).pathname))
|
|
1224
|
+
: scoped;
|
|
1225
|
+
if (results.length !== scoped.length)
|
|
1226
|
+
notes.push(`Excluded ${scoped.length - results.length} GitHub wiki history/edit page(s); original articles were requested`);
|
|
1227
|
+
return results.length ? completedProviderSearch(results.slice(0, limit), outcome.note) : failedProviderSearch("No results satisfied the explicit site constraint");
|
|
1228
|
+
};
|
|
1229
|
+
const recordAttempt = (provider, outcome) => {
|
|
1230
|
+
attempts.push({
|
|
1231
|
+
provider,
|
|
1232
|
+
status: outcome.status,
|
|
1233
|
+
result_count: outcome.results.length,
|
|
1234
|
+
...(outcome.status === "failed" && outcome.note ? { error: outcome.note } : {}),
|
|
1235
|
+
});
|
|
1236
|
+
};
|
|
1237
|
+
if (hnDates?.error)
|
|
1238
|
+
return buildSearchResponse({ engine: "error", query, results: [], fallbacks_tried: [], notes: [hnDates.error], execution: { status: "failed", attempts: [{ provider: "query_constraints", status: "failed", result_count: 0, error: hnDates.error }] } });
|
|
1239
|
+
const discoveryTerms = koreanDiscoveryTerms(query);
|
|
1240
|
+
let discoveryResults = [];
|
|
1241
|
+
const collectDiscovery = (outcome) => {
|
|
1242
|
+
if (!outcome.ok)
|
|
1243
|
+
return false;
|
|
1244
|
+
discoveryResults = rankDiscoveryResults([...discoveryResults, ...outcome.results], discoveryTerms);
|
|
1245
|
+
if (hasDiscoveryYield(discoveryResults, discoveryTerms, numResults))
|
|
1246
|
+
return true;
|
|
1247
|
+
notes.push("Low discovery yield in title/snippet evidence; continuing within the existing search budget. Unmatched originals are retained, not declared irrelevant.");
|
|
1248
|
+
return false;
|
|
1249
|
+
};
|
|
1250
|
+
const finishDiscovery = () => {
|
|
1251
|
+
const results = discoveryResults.slice(0, numResults);
|
|
1252
|
+
const engines = new Set(results.map((row) => row.engine));
|
|
1253
|
+
if (!hasDiscoveryYield(results, discoveryTerms, numResults))
|
|
1254
|
+
notes.push("Limited topical evidence in discovery metadata; original-page relevance remains unverified");
|
|
1255
|
+
return maybeRunCrawlHandoff({ engine: engines.size > 1 ? "mixed" : results[0]?.engine || "none", query, results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1256
|
+
};
|
|
1257
|
+
const budgetExhausted = () => {
|
|
1258
|
+
if (Date.now() - startedAt < timeoutMs)
|
|
1259
|
+
return false;
|
|
1260
|
+
notes.push("Search budget exhausted; no further discovery engine was started");
|
|
1261
|
+
return true;
|
|
1262
|
+
};
|
|
1045
1263
|
try {
|
|
1046
|
-
|
|
1264
|
+
// A full site:path can itself be the requested original. Check its authored
|
|
1265
|
+
// body before relying on search indexes that only expose titles/summaries.
|
|
1266
|
+
// Only quote-only queries are safe here: extra terms/operators need a real
|
|
1267
|
+
// search engine rather than being silently discarded.
|
|
1268
|
+
const remainder = tokens.filter(token => !/^site:[^"']+$/i.test(token));
|
|
1269
|
+
const quoted = remainder.filter(token => /^"[^"\\]+"$/.test(token)).map(token => token.slice(1, -1));
|
|
1270
|
+
if (scope?.path.includes("/") && !siteNativeFallbackBlockedReason("Scoped original") && !parseNpmPackageScope(query)
|
|
1271
|
+
&& quoted.length > 0 && quoted.length === remainder.length && !budgetExhausted()) {
|
|
1272
|
+
const url = siteScopeToStartUrl(scope);
|
|
1273
|
+
const validation = await validateOutboundUrl(url);
|
|
1274
|
+
if (validation.ok && !budgetExhausted()) {
|
|
1275
|
+
const original = await fetchText(url, attemptTimeout(), { redirect: "error", headers: { "user-agent": DEFAULT_USER_AGENT } });
|
|
1276
|
+
if (original.ok && !budgetExhausted()) {
|
|
1277
|
+
const document = parseHTML(original.text).document;
|
|
1278
|
+
const article = document.querySelector("main article") || document.querySelector("main") || document.querySelector("article");
|
|
1279
|
+
if (article) {
|
|
1280
|
+
for (const node of article.querySelectorAll("script, style, nav, aside, header, footer, [hidden], [inert], [aria-hidden='true'], [style*='display:none'], [style*='display: none']"))
|
|
1281
|
+
node.remove();
|
|
1282
|
+
const body = (article.textContent || "").replace(/\s+/g, " ").trim();
|
|
1283
|
+
const heading = (document.title || article.querySelector("h1")?.textContent || "").replace(/\s+/g, " ").trim();
|
|
1284
|
+
const lower = body.toLocaleLowerCase();
|
|
1285
|
+
if (body.length >= 40 && heading.length >= 3 && quoted.every(phrase => lower.includes(phrase.toLocaleLowerCase()))
|
|
1286
|
+
&& !detectInteractiveLikeText(`${heading} ${body.slice(0, 300)}`)) {
|
|
1287
|
+
const position = lower.indexOf(quoted[0].toLocaleLowerCase());
|
|
1288
|
+
const snippet = body.slice(Math.max(0, position - 90), position + quoted[0].length + 110);
|
|
1289
|
+
const outcome = constrain(completedProviderSearch([{ title: heading, url, snippet, engine: "scoped_original" }], ""));
|
|
1290
|
+
if (outcome.ok) {
|
|
1291
|
+
fallbacks.push("scoped_original");
|
|
1292
|
+
recordAttempt("scoped_original", outcome);
|
|
1293
|
+
return maybeRunCrawlHandoff({ engine: "scoped_original", query, results: outcome.results, fallbacks_tried: fallbacks, notes: ["Quoted phrase verified in the exact scoped original body"], execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const nodeIntent = !siteMatches.length ? nodeDocsIntent(query) : undefined;
|
|
1301
|
+
if (nodeIntent && !budgetExhausted()) {
|
|
1302
|
+
fallbacks.push("node_docs");
|
|
1303
|
+
const original = await fetchText(nodeIntent.url, attemptTimeout(), { redirect: "error", headers: { "user-agent": DEFAULT_USER_AGENT } });
|
|
1304
|
+
const result = original.ok && !budgetExhausted() ? nodeDocsOriginal(original.text, nodeIntent.url, nodeIntent.symbol) : undefined;
|
|
1305
|
+
const outcome = result ? completedProviderSearch([result], "") : failedProviderSearch(original.ok
|
|
1306
|
+
? "Node.js 24 official filesystem original did not verify the requested symbol"
|
|
1307
|
+
: `Node.js 24 official original unavailable: ${original.error || original.status}`);
|
|
1308
|
+
recordAttempt("node_docs", outcome);
|
|
1309
|
+
if (result) {
|
|
1310
|
+
notes.push("Official Node.js 24 documentation section verified; complete page extraction remains a separate read");
|
|
1311
|
+
collectDiscovery(outcome);
|
|
1312
|
+
return finishDiscovery();
|
|
1313
|
+
}
|
|
1314
|
+
notes.push(outcome.note);
|
|
1315
|
+
}
|
|
1316
|
+
const postgresIntent = !siteMatches.length ? postgresqlDocsIntent(query) : undefined;
|
|
1317
|
+
if (postgresIntent && !budgetExhausted()) {
|
|
1318
|
+
fallbacks.push("postgresql_docs");
|
|
1319
|
+
const searchUrl = new URL("https://www.postgresql.org/search/");
|
|
1320
|
+
searchUrl.searchParams.set("q", `${postgresIntent.version} ${postgresIntent.symbol}`);
|
|
1321
|
+
const index = await fetchText(searchUrl.href, attemptTimeout(), { redirect: "error", headers: { "user-agent": DEFAULT_USER_AGENT } });
|
|
1322
|
+
let verified;
|
|
1323
|
+
if (index.ok && !budgetExhausted()) {
|
|
1324
|
+
for (const candidate of postgresqlDocsCandidates(index.text, postgresIntent.version)) {
|
|
1325
|
+
if (budgetExhausted())
|
|
1326
|
+
break;
|
|
1327
|
+
const original = await fetchText(candidate.url, attemptTimeout(), { redirect: "error", headers: { "user-agent": DEFAULT_USER_AGENT } });
|
|
1328
|
+
if (original.ok && postgresqlDocsBodyHasSymbol(original.text, postgresIntent.symbol) && !budgetExhausted()) {
|
|
1329
|
+
verified = candidate;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const outcome = verified ? completedProviderSearch([verified], "") : failedProviderSearch(index.ok
|
|
1335
|
+
? "PostgreSQL search did not verify the requested symbol in a version-matched original"
|
|
1336
|
+
: `PostgreSQL official search unavailable: ${index.error || index.status}`);
|
|
1337
|
+
recordAttempt("postgresql_docs", outcome);
|
|
1338
|
+
if (verified) {
|
|
1339
|
+
notes.push("Official versioned documentation original contains the requested symbol; complete page extraction remains a separate read");
|
|
1340
|
+
collectDiscovery(outcome);
|
|
1341
|
+
return finishDiscovery();
|
|
1342
|
+
}
|
|
1343
|
+
notes.push(outcome.note);
|
|
1344
|
+
}
|
|
1345
|
+
const siteOperator = siteMatches.length !== 1 || tokens.some(token => token === "(" || token === ")") ? "" : extractSiteOperator(siteMatches[0]);
|
|
1047
1346
|
if (siteOperator === "velog.io") {
|
|
1048
1347
|
const blockedReason = siteNativeFallbackBlockedReason("Velog");
|
|
1049
1348
|
if (blockedReason) {
|
|
1050
1349
|
notes.push(blockedReason);
|
|
1051
1350
|
}
|
|
1052
1351
|
else {
|
|
1053
|
-
const velog = await searchVelog(query,
|
|
1352
|
+
const velog = constrain(await searchVelog(query, nativeCandidateLimit, attemptTimeout()));
|
|
1054
1353
|
fallbacks.push("velog");
|
|
1354
|
+
recordAttempt("velog", velog);
|
|
1055
1355
|
if (velog.ok)
|
|
1056
|
-
return maybeRunCrawlHandoff({ engine: "velog", query, results: velog.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1356
|
+
return maybeRunCrawlHandoff({ engine: "velog", query, results: velog.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1057
1357
|
if (velog.note)
|
|
1058
1358
|
notes.push(velog.note);
|
|
1059
1359
|
}
|
|
@@ -1064,10 +1364,11 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1064
1364
|
notes.push(blockedReason);
|
|
1065
1365
|
}
|
|
1066
1366
|
else {
|
|
1067
|
-
const stackexchange = await searchStackExchange(query,
|
|
1367
|
+
const stackexchange = constrain(await searchStackExchange(query, nativeCandidateLimit, attemptTimeout()));
|
|
1068
1368
|
fallbacks.push("stackexchange");
|
|
1369
|
+
recordAttempt("stackexchange", stackexchange);
|
|
1069
1370
|
if (stackexchange.ok)
|
|
1070
|
-
return maybeRunCrawlHandoff({ engine: "stackexchange", query, results: stackexchange.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1371
|
+
return maybeRunCrawlHandoff({ engine: "stackexchange", query, results: stackexchange.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1071
1372
|
if (stackexchange.note)
|
|
1072
1373
|
notes.push(stackexchange.note);
|
|
1073
1374
|
}
|
|
@@ -1078,10 +1379,11 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1078
1379
|
notes.push(blockedReason);
|
|
1079
1380
|
}
|
|
1080
1381
|
else {
|
|
1081
|
-
const hackernews = await searchHackerNews(query,
|
|
1382
|
+
const hackernews = constrain(await searchHackerNews(query, nativeCandidateLimit, attemptTimeout(), hnDates));
|
|
1082
1383
|
fallbacks.push("hackernews");
|
|
1083
|
-
|
|
1084
|
-
|
|
1384
|
+
recordAttempt("hackernews", hackernews);
|
|
1385
|
+
if (hackernews.ok || (hnDates?.bounded && hackernews.status === "succeeded"))
|
|
1386
|
+
return maybeRunCrawlHandoff({ engine: "hackernews", query, results: hackernews.results, fallbacks_tried: fallbacks, notes: hackernews.note ? [...notes, hackernews.note] : notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1085
1387
|
if (hackernews.note)
|
|
1086
1388
|
notes.push(hackernews.note);
|
|
1087
1389
|
}
|
|
@@ -1092,24 +1394,32 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1092
1394
|
notes.push(blockedReason);
|
|
1093
1395
|
}
|
|
1094
1396
|
else {
|
|
1095
|
-
|
|
1397
|
+
// Reddit already preserves subreddit scope while filling this count;
|
|
1398
|
+
// oversampling here would trigger unnecessary secondary-provider reads.
|
|
1399
|
+
const reddit = constrain(await searchReddit(query, numResults, attemptTimeout()));
|
|
1096
1400
|
fallbacks.push("reddit");
|
|
1401
|
+
recordAttempt("reddit", reddit);
|
|
1097
1402
|
if (reddit.ok)
|
|
1098
|
-
return maybeRunCrawlHandoff({ engine: "reddit", query, results: reddit.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1403
|
+
return maybeRunCrawlHandoff({ engine: "reddit", query, results: reddit.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1099
1404
|
if (reddit.note)
|
|
1100
1405
|
notes.push(reddit.note);
|
|
1101
1406
|
}
|
|
1102
1407
|
}
|
|
1103
1408
|
if (siteOperator === "github.com") {
|
|
1104
1409
|
const blockedReason = siteNativeFallbackBlockedReason("GitHub");
|
|
1410
|
+
const repoScope = parseGitHubRepoScope(query);
|
|
1105
1411
|
if (blockedReason) {
|
|
1106
1412
|
notes.push(blockedReason);
|
|
1107
1413
|
}
|
|
1108
|
-
else if (
|
|
1109
|
-
|
|
1414
|
+
else if (repoScope?.scopePath && !repoScope.scopePath.startsWith("blob/")) {
|
|
1415
|
+
notes.push("GitHub repository tree lookup cannot search this URL section; preserving the full query for general search");
|
|
1416
|
+
}
|
|
1417
|
+
else if (repoScope) {
|
|
1418
|
+
const githubRepoDocs = constrain(await searchGitHubRepoDocs(query, nativeCandidateLimit, attemptTimeout()));
|
|
1110
1419
|
fallbacks.push("github_repo_docs");
|
|
1420
|
+
recordAttempt("github_repo_docs", githubRepoDocs);
|
|
1111
1421
|
if (githubRepoDocs.ok)
|
|
1112
|
-
return maybeRunCrawlHandoff({ engine: "github_repo_docs", query, results: githubRepoDocs.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1422
|
+
return maybeRunCrawlHandoff({ engine: "github_repo_docs", query, results: githubRepoDocs.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1113
1423
|
if (githubRepoDocs.note)
|
|
1114
1424
|
notes.push(githubRepoDocs.note);
|
|
1115
1425
|
}
|
|
@@ -1117,27 +1427,40 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1117
1427
|
notes.push("GitHub native repo fallback skipped for non-repository intent");
|
|
1118
1428
|
}
|
|
1119
1429
|
else {
|
|
1120
|
-
const github = await searchGitHub(query,
|
|
1430
|
+
const github = constrain(await searchGitHub(query, nativeCandidateLimit, attemptTimeout()));
|
|
1121
1431
|
fallbacks.push("github");
|
|
1432
|
+
recordAttempt("github", github);
|
|
1122
1433
|
if (github.ok)
|
|
1123
|
-
return maybeRunCrawlHandoff({ engine: "github", query, results: github.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1434
|
+
return maybeRunCrawlHandoff({ engine: "github", query, results: github.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1124
1435
|
if (github.note)
|
|
1125
1436
|
notes.push(github.note);
|
|
1126
1437
|
}
|
|
1127
1438
|
}
|
|
1128
1439
|
if (siteOperator === "npmjs.com") {
|
|
1129
1440
|
const blockedReason = siteNativeFallbackBlockedReason("npm");
|
|
1441
|
+
const exactPackage = parseNpmPackageScope(query);
|
|
1442
|
+
const remainder = searchQueryTokens(query).filter(token => !/^site:[^"']+$/i.test(token));
|
|
1443
|
+
const quoted = remainder.filter(token => token.startsWith('"') && token.endsWith('"') && token.length > 2 && !token.includes("\\"))
|
|
1444
|
+
.map(token => token.slice(1, -1).toLocaleLowerCase());
|
|
1445
|
+
const nativeExact = Boolean(exactPackage && (remainder.length === 0 || (quoted.length === remainder.length && quoted.length > 0)));
|
|
1130
1446
|
if (blockedReason) {
|
|
1131
1447
|
notes.push(blockedReason);
|
|
1132
1448
|
}
|
|
1449
|
+
else if (exactPackage && !nativeExact) {
|
|
1450
|
+
// Metadata cannot evaluate free words, exclusions, dates or Boolean
|
|
1451
|
+
// expressions. Only standalone quotes can be checked in its own fields.
|
|
1452
|
+
notes.push("npm package metadata cannot evaluate the remaining query constraints; preserving the full query for general search");
|
|
1453
|
+
}
|
|
1133
1454
|
else {
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
: await searchNpm(query, numResults, timeoutMs);
|
|
1455
|
+
const npm = constrain(exactPackage
|
|
1456
|
+
? await searchNpmPackage(exactPackage, attemptTimeout(), quoted)
|
|
1457
|
+
: await searchNpm(query, nativeCandidateLimit, attemptTimeout()));
|
|
1138
1458
|
fallbacks.push("npm");
|
|
1459
|
+
recordAttempt("npm", npm);
|
|
1460
|
+
if (npm.ok && npm.note)
|
|
1461
|
+
notes.push(npm.note);
|
|
1139
1462
|
if (npm.ok)
|
|
1140
|
-
return maybeRunCrawlHandoff({ engine: "npm", query, results: npm.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1463
|
+
return maybeRunCrawlHandoff({ engine: "npm", query, results: npm.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1141
1464
|
if (npm.note)
|
|
1142
1465
|
notes.push(npm.note);
|
|
1143
1466
|
}
|
|
@@ -1148,10 +1471,13 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1148
1471
|
notes.push(blockedReason);
|
|
1149
1472
|
}
|
|
1150
1473
|
else {
|
|
1151
|
-
const mdn = await searchMdn(query,
|
|
1474
|
+
const mdn = constrain(await searchMdn(query, nativeCandidateLimit, attemptTimeout()));
|
|
1152
1475
|
fallbacks.push("mdn");
|
|
1476
|
+
recordAttempt("mdn", mdn);
|
|
1477
|
+
if (mdn.ok && mdn.note)
|
|
1478
|
+
notes.push(mdn.note);
|
|
1153
1479
|
if (mdn.ok)
|
|
1154
|
-
return maybeRunCrawlHandoff({ engine: "mdn", query, results: mdn.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1480
|
+
return maybeRunCrawlHandoff({ engine: "mdn", query, results: mdn.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1155
1481
|
if (mdn.note)
|
|
1156
1482
|
notes.push(mdn.note);
|
|
1157
1483
|
}
|
|
@@ -1162,10 +1488,11 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1162
1488
|
notes.push(blockedReason);
|
|
1163
1489
|
}
|
|
1164
1490
|
else {
|
|
1165
|
-
const crates = await searchCrates(query,
|
|
1491
|
+
const crates = constrain(await searchCrates(query, nativeCandidateLimit, attemptTimeout()));
|
|
1166
1492
|
fallbacks.push("crates");
|
|
1493
|
+
recordAttempt("crates", crates);
|
|
1167
1494
|
if (crates.ok)
|
|
1168
|
-
return maybeRunCrawlHandoff({ engine: "crates", query, results: crates.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1495
|
+
return maybeRunCrawlHandoff({ engine: "crates", query, results: crates.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1169
1496
|
if (crates.note)
|
|
1170
1497
|
notes.push(crates.note);
|
|
1171
1498
|
}
|
|
@@ -1176,24 +1503,28 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1176
1503
|
notes.push(blockedReason);
|
|
1177
1504
|
}
|
|
1178
1505
|
else {
|
|
1179
|
-
const wikipedia = await searchWikipedia(query,
|
|
1506
|
+
const wikipedia = constrain(await searchWikipedia(query, nativeCandidateLimit, attemptTimeout()));
|
|
1180
1507
|
fallbacks.push("wikipedia");
|
|
1508
|
+
recordAttempt("wikipedia", wikipedia);
|
|
1181
1509
|
if (wikipedia.ok)
|
|
1182
|
-
return maybeRunCrawlHandoff({ engine: "wikipedia", query, results: wikipedia.results, fallbacks_tried: fallbacks, notes }, options, runtime);
|
|
1510
|
+
return maybeRunCrawlHandoff({ engine: "wikipedia", query, results: wikipedia.results, fallbacks_tried: fallbacks, notes, execution: smartsearchExecution(attempts) }, options, runtime);
|
|
1183
1511
|
if (wikipedia.note)
|
|
1184
1512
|
notes.push(wikipedia.note);
|
|
1185
1513
|
}
|
|
1186
1514
|
}
|
|
1515
|
+
if (budgetExhausted())
|
|
1516
|
+
return finishDiscovery();
|
|
1187
1517
|
if (searxngEnabled() && searxngBaseUrl) {
|
|
1188
1518
|
const privacySensitive = localOnlyMode() || privateProfileMode();
|
|
1189
1519
|
if (privacySensitive && !await privacySafeSearxngBase(searxngBaseUrl)) {
|
|
1190
1520
|
notes.push("SearXNG disabled by privacy mode because the configured base URL is not local or private");
|
|
1191
1521
|
}
|
|
1192
1522
|
else {
|
|
1193
|
-
const searxng = await searchSearxng(searxngBaseUrl, query, numResults,
|
|
1523
|
+
const searxng = constrain(await searchSearxng(searxngBaseUrl, query, numResults, attemptTimeout()), 100);
|
|
1194
1524
|
fallbacks.push("searxng");
|
|
1195
|
-
|
|
1196
|
-
|
|
1525
|
+
recordAttempt("searxng", searxng);
|
|
1526
|
+
if (collectDiscovery(searxng))
|
|
1527
|
+
return finishDiscovery();
|
|
1197
1528
|
if (searxng.note)
|
|
1198
1529
|
notes.push(searxng.note);
|
|
1199
1530
|
}
|
|
@@ -1201,33 +1532,190 @@ export async function runSmartsearch(options, runtime = {}) {
|
|
|
1201
1532
|
else if (!searxngEnabled()) {
|
|
1202
1533
|
notes.push("SearXNG disabled by configuration");
|
|
1203
1534
|
}
|
|
1535
|
+
if (budgetExhausted())
|
|
1536
|
+
return finishDiscovery();
|
|
1204
1537
|
if (duckduckgoEnabled()) {
|
|
1205
|
-
const ddg = await searchDuckDuckGo(query, numResults,
|
|
1538
|
+
const ddg = constrain(await searchDuckDuckGo(query, numResults, attemptTimeout()), 100);
|
|
1206
1539
|
fallbacks.push("duckduckgo");
|
|
1207
|
-
|
|
1208
|
-
|
|
1540
|
+
recordAttempt("duckduckgo", ddg);
|
|
1541
|
+
if (collectDiscovery(ddg))
|
|
1542
|
+
return finishDiscovery();
|
|
1209
1543
|
if (ddg.note)
|
|
1210
1544
|
notes.push(ddg.note);
|
|
1211
1545
|
}
|
|
1212
1546
|
else {
|
|
1213
1547
|
notes.push("DuckDuckGo disabled by configuration");
|
|
1214
1548
|
}
|
|
1549
|
+
if (budgetExhausted())
|
|
1550
|
+
return finishDiscovery();
|
|
1215
1551
|
if (braveHtmlEnabled()) {
|
|
1216
|
-
const braveHtml = await searchBraveHtml(query, numResults,
|
|
1552
|
+
const braveHtml = constrain(await searchBraveHtml(query, numResults, attemptTimeout()), 100);
|
|
1217
1553
|
fallbacks.push("brave_html");
|
|
1218
|
-
|
|
1219
|
-
|
|
1554
|
+
recordAttempt("brave_html", braveHtml);
|
|
1555
|
+
if (collectDiscovery(braveHtml))
|
|
1556
|
+
return finishDiscovery();
|
|
1220
1557
|
if (braveHtml.note)
|
|
1221
1558
|
notes.push(braveHtml.note);
|
|
1222
1559
|
}
|
|
1223
1560
|
else {
|
|
1224
1561
|
notes.push("Brave HTML disabled by configuration");
|
|
1225
1562
|
}
|
|
1226
|
-
|
|
1563
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
1564
|
+
let canContinueNative = false;
|
|
1565
|
+
if (browserEnabled) {
|
|
1566
|
+
fallbacks.push("naver_browser");
|
|
1567
|
+
let outcome;
|
|
1568
|
+
if (remaining <= 0)
|
|
1569
|
+
outcome = failedProviderSearch("Search budget exhausted before operator-browser continuation");
|
|
1570
|
+
else {
|
|
1571
|
+
try {
|
|
1572
|
+
// Native search sometimes treats a site path as text. A second,
|
|
1573
|
+
// host-only discovery query may recover the same originals; the
|
|
1574
|
+
// original path remains a hard result constraint below.
|
|
1575
|
+
const searchQueries = scope?.path && siteMatches.length === 1 && /[가-힣]/u.test(query)
|
|
1576
|
+
&& ["clien.net", "dcinside.com", "ppomppu.co.kr"].includes(scope.host)
|
|
1577
|
+
? [query, query.replace(siteMatches[0], `site:${scope.host}`)] : [query];
|
|
1578
|
+
outcome = failedProviderSearch("Browser search exposed no verified result headlines satisfying supported query constraints");
|
|
1579
|
+
for (const [index, searchQuery] of searchQueries.entries()) {
|
|
1580
|
+
const budget = timeoutMs - (Date.now() - startedAt);
|
|
1581
|
+
if (budget <= 0)
|
|
1582
|
+
break;
|
|
1583
|
+
const url = `https://search.naver.com/search.naver?query=${encodeURIComponent(searchQuery)}`;
|
|
1584
|
+
const page = await (runtime.runBrowserSession || runBrowserSession)({ url, cdpUrl, timeoutMs: Math.max(1, Math.floor(budget / ((pythonSearchUrl || nativeYoutubeUrl ? 1 : 0) + searchQueries.length - index))), allowPrivateHosts: false });
|
|
1585
|
+
if (!page.ok) {
|
|
1586
|
+
outcome = failedProviderSearch(`Approved browser: ${page.error.code}: ${page.error.message}`);
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
const final = new URL(page.final_url);
|
|
1590
|
+
const sameQuery = final.origin === "https://search.naver.com" && !final.username && !final.password
|
|
1591
|
+
&& final.pathname === "/search.naver" && final.searchParams.getAll("query").length === 1 && final.searchParams.get("query") === searchQuery;
|
|
1592
|
+
if (!sameQuery || /captcha|verify you are human|unusual traffic|자동입력 방지|비정상적인 접근/i.test(page.text.slice(0, 1000))) {
|
|
1593
|
+
outcome = failedProviderSearch("Browser search stopped at a challenge or changed query; user interaction may be required");
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
canContinueNative = true;
|
|
1597
|
+
const safeResults = [];
|
|
1598
|
+
for (const item of rankDiscoveryResults(parseNaverBrowserSearch(page), discoveryTerms)) {
|
|
1599
|
+
if (Date.now() - startedAt >= timeoutMs)
|
|
1600
|
+
break;
|
|
1601
|
+
if (!browserSearchMatchesQuery(item, query))
|
|
1602
|
+
continue;
|
|
1603
|
+
if (isSearchTrackingRelay(item))
|
|
1604
|
+
continue;
|
|
1605
|
+
if (scope && !searchResultMatchesSite(item.url, scope.host, scope.path))
|
|
1606
|
+
continue;
|
|
1607
|
+
let timer;
|
|
1608
|
+
const validation = await Promise.race([
|
|
1609
|
+
validateOutboundUrl(item.url, { allowPrivateHosts: false }),
|
|
1610
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve({ ok: false }), Math.max(1, timeoutMs - (Date.now() - startedAt))); }),
|
|
1611
|
+
]).finally(() => { if (timer)
|
|
1612
|
+
clearTimeout(timer); });
|
|
1613
|
+
if (Date.now() - startedAt >= timeoutMs)
|
|
1614
|
+
break;
|
|
1615
|
+
if (validation.ok)
|
|
1616
|
+
safeResults.push(item);
|
|
1617
|
+
if (safeResults.length >= numResults)
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1620
|
+
outcome = safeResults.length ? constrain(completedProviderSearch(safeResults, "")) : failedProviderSearch("Browser search exposed no verified result headlines satisfying supported query constraints");
|
|
1621
|
+
if (outcome.ok) {
|
|
1622
|
+
if (index > 0)
|
|
1623
|
+
notes.push("Host-only discovery preserved the requested site path on every returned original");
|
|
1624
|
+
notes.push("Recovered through the approved local browser. Search snippets are discovery evidence, not verification of article bodies or all comments.");
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
outcome = failedProviderSearch(`Approved browser search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
recordAttempt("naver_browser", outcome);
|
|
1634
|
+
if (outcome.ok) {
|
|
1635
|
+
collectDiscovery(outcome);
|
|
1636
|
+
if (!unscopedDocsTerms)
|
|
1637
|
+
return finishDiscovery();
|
|
1638
|
+
}
|
|
1639
|
+
if (outcome.note)
|
|
1640
|
+
notes.push(outcome.note);
|
|
1641
|
+
}
|
|
1642
|
+
if (nativeYoutubeUrl && canContinueNative && !budgetExhausted()) {
|
|
1643
|
+
fallbacks.push("youtube_browser");
|
|
1644
|
+
let outcome;
|
|
1645
|
+
try {
|
|
1646
|
+
const page = await (runtime.runBrowserSession || runBrowserSession)({
|
|
1647
|
+
url: nativeYoutubeUrl, cdpUrl, timeoutMs: Math.max(1, timeoutMs - (Date.now() - startedAt)), allowPrivateHosts: false,
|
|
1648
|
+
waitForSelector: "ytd-video-renderer a#video-title",
|
|
1649
|
+
});
|
|
1650
|
+
if (Date.now() - startedAt >= timeoutMs)
|
|
1651
|
+
outcome = failedProviderSearch("YouTube native search exceeded the shared search budget");
|
|
1652
|
+
else if (!page.ok)
|
|
1653
|
+
outcome = failedProviderSearch(`YouTube native browser: ${page.error.code}: ${page.error.message}`);
|
|
1654
|
+
else {
|
|
1655
|
+
const rows = parseYoutubeSearch(page, nativeYoutubeUrl, query);
|
|
1656
|
+
outcome = Date.now() - startedAt >= timeoutMs ? failedProviderSearch("YouTube native search exceeded the shared search budget")
|
|
1657
|
+
: rows.length ? constrain(completedProviderSearch(rows, "")) : failedProviderSearch("YouTube native search exposed no exact watch-page headlines satisfying the quoted query");
|
|
1658
|
+
if (outcome.ok)
|
|
1659
|
+
notes.push("Recovered watch-page discovery through the approved local browser. Video title evidence does not verify the full video or transcript.");
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
catch (error) {
|
|
1663
|
+
outcome = failedProviderSearch(`YouTube native search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1664
|
+
}
|
|
1665
|
+
recordAttempt("youtube_browser", outcome);
|
|
1666
|
+
if (outcome.ok) {
|
|
1667
|
+
collectDiscovery(outcome);
|
|
1668
|
+
return finishDiscovery();
|
|
1669
|
+
}
|
|
1670
|
+
if (outcome.note)
|
|
1671
|
+
notes.push(outcome.note);
|
|
1672
|
+
}
|
|
1673
|
+
if (pythonSearchUrl && canContinueNative && !budgetExhausted()) {
|
|
1674
|
+
fallbacks.push("python_docs_browser");
|
|
1675
|
+
let outcome;
|
|
1676
|
+
try {
|
|
1677
|
+
const page = await (runtime.runBrowserSession || runBrowserSession)({
|
|
1678
|
+
url: pythonSearchUrl, cdpUrl, timeoutMs: Math.max(1, timeoutMs - (Date.now() - startedAt)), allowPrivateHosts: false,
|
|
1679
|
+
waitForSelector: "#search-results .search-summary:not(:empty)",
|
|
1680
|
+
});
|
|
1681
|
+
if (Date.now() - startedAt >= timeoutMs)
|
|
1682
|
+
outcome = failedProviderSearch("Python native search exceeded the shared search budget");
|
|
1683
|
+
else if (!page.ok)
|
|
1684
|
+
outcome = failedProviderSearch(`Python native browser: ${page.error.code}: ${page.error.message}`);
|
|
1685
|
+
else {
|
|
1686
|
+
const rows = parsePythonDocsSearch(page, pythonSearchUrl, unscopedDocsTerms || query);
|
|
1687
|
+
outcome = Date.now() - startedAt >= timeoutMs ? failedProviderSearch("Python native search exceeded the shared search budget")
|
|
1688
|
+
: rows.length ? constrain(completedProviderSearch(rows, "")) : failedProviderSearch("Python native search exposed no completed results satisfying the original query");
|
|
1689
|
+
if (outcome.ok)
|
|
1690
|
+
notes.push("Recovered through official Python documentation search in the approved browser. Native index/headline evidence is discovery, not complete original-body verification.");
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
catch (error) {
|
|
1694
|
+
outcome = failedProviderSearch(`Python native search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1695
|
+
}
|
|
1696
|
+
recordAttempt("python_docs_browser", outcome);
|
|
1697
|
+
if (outcome.ok) {
|
|
1698
|
+
// Explicit documentation intent: a verified official index supersedes
|
|
1699
|
+
// adjacent general-web candidates; retain them if the native lane fails.
|
|
1700
|
+
if (unscopedDocsTerms)
|
|
1701
|
+
discoveryResults = [];
|
|
1702
|
+
collectDiscovery(outcome);
|
|
1703
|
+
}
|
|
1704
|
+
if (outcome.note)
|
|
1705
|
+
notes.push(outcome.note);
|
|
1706
|
+
}
|
|
1707
|
+
return finishDiscovery();
|
|
1227
1708
|
}
|
|
1228
1709
|
catch (error) {
|
|
1229
1710
|
const message = error instanceof Error ? error.message : String(error);
|
|
1230
|
-
return maybeRunCrawlHandoff({
|
|
1711
|
+
return maybeRunCrawlHandoff({
|
|
1712
|
+
engine: "error",
|
|
1713
|
+
query,
|
|
1714
|
+
results: [],
|
|
1715
|
+
fallbacks_tried: fallbacks,
|
|
1716
|
+
notes: [...notes, message],
|
|
1717
|
+
execution: { status: "failed", attempts: [...attempts, { provider: "smartsearch", status: "failed", result_count: 0, error: message }] },
|
|
1718
|
+
}, options, runtime);
|
|
1231
1719
|
}
|
|
1232
1720
|
}
|
|
1233
1721
|
export function renderSmartsearch(response) {
|
|
@@ -1267,9 +1755,6 @@ export function renderSmartsearch(response) {
|
|
|
1267
1755
|
lines.push(item.snippet);
|
|
1268
1756
|
parts.push(lines.join("\n"));
|
|
1269
1757
|
});
|
|
1270
|
-
if (assessment.recommended_handoff && assessment.recommended_handoff.tool !== "smartcrawl") {
|
|
1271
|
-
parts.push(`Recommended handoff:\n- tool: ${assessment.recommended_handoff.tool}\n- reason: ${assessment.recommended_handoff.reason}\n- url: ${assessment.recommended_handoff.url}`);
|
|
1272
|
-
}
|
|
1273
1758
|
if (response.notes.length > 0)
|
|
1274
1759
|
parts.push(`Notes:\n${response.notes.map((item) => `- ${item}`).join("\n")}`);
|
|
1275
1760
|
return parts.join("\n\n").trim();
|