mcp-scraper 0.79.1 → 0.79.3
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 +25 -1
- package/README.md +1 -1
- package/dist/{analytics-repository-VEUDEPWI.js → analytics-repository-YOY5KMOD.js} +3 -3
- package/dist/bin/api-server.js +4 -4
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-core.js +7 -7
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +7 -7
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-LB2GNACG.js → chunk-3423R23T.js} +3 -3
- package/dist/{chunk-NGLZ6UJR.js → chunk-5UAFBKJR.js} +1 -1
- package/dist/{chunk-B6L73AO4.js → chunk-6HAV7LCE.js} +3 -1
- package/dist/{chunk-NH3EUCZL.js → chunk-7UZAVM4J.js} +138 -11
- package/dist/{chunk-Q5L2OCOQ.js → chunk-C5Z4OFKW.js} +1 -1
- package/dist/{chunk-KAGHJQFO.js → chunk-EQGTEHLZ.js} +1 -1
- package/dist/{chunk-BTQ23KCT.js → chunk-F5GQJWZU.js} +1 -1
- package/dist/{chunk-DUWZQSSJ.js → chunk-HEUMDX4I.js} +1 -1
- package/dist/{chunk-OIGM6VKQ.js → chunk-IHYG52ZW.js} +1 -1
- package/dist/{chunk-OUBH6XMY.js → chunk-K3Z5AQYE.js} +1 -1
- package/dist/{chunk-UFJJGXVO.js → chunk-KNGTQD6U.js} +396 -67
- package/dist/{chunk-R4YRH5PG.js → chunk-MVB25X55.js} +1 -1
- package/dist/{chunk-47ZCUBM5.js → chunk-NVUKO5NN.js} +12 -1
- package/dist/{chunk-QVW5S6BD.js → chunk-TCYZ7HMU.js} +3 -3
- package/dist/{chunk-SL7YVA4S.js → chunk-YBR32LOD.js} +1 -1
- package/dist/{chunk-OU4FV2RP.js → chunk-YXNDOQXN.js} +49 -0
- package/dist/{db-TA567JLA.js → db-Z34LPZNR.js} +5 -1
- package/dist/{extract-bundle-I2GNC25L.js → extract-bundle-S3ZGNFML.js} +3 -3
- package/dist/{gmail-service-GGQVJUC3.js → gmail-service-YFS2OV4S.js} +4 -4
- package/dist/index.cjs +394 -65
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +3 -3
- package/dist/{lead-list-enrichment-repository-5LEWW4X7.js → lead-list-enrichment-repository-36RPVV6N.js} +2 -2
- package/dist/{location-data-repository-AHKDXTEQ.js → location-data-repository-WPRG62GE.js} +2 -2
- package/dist/{server-MNSYIX42.js → server-MDWFOUTS.js} +73 -28
- package/dist/{site-extract-repository-KF736IR6.js → site-extract-repository-WAJMIDPJ.js} +2 -2
- package/dist/{worker-VU6JCCLG.js → worker-3LUFQWWJ.js} +5 -5
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
currentCostContext,
|
|
9
9
|
recordKernelSession,
|
|
10
10
|
runWithCostContext
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5UAFBKJR.js";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_PROXY_MODE,
|
|
14
14
|
HarvestOptionsSchema,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
RECAPTCHA_INSTRUCTIONS,
|
|
25
25
|
RequestAbortedError,
|
|
26
26
|
sanitizeVendorName
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-YXNDOQXN.js";
|
|
28
28
|
|
|
29
29
|
// src/driver/BrowserDriver.ts
|
|
30
30
|
import { chromium } from "playwright-extra";
|
|
@@ -2054,6 +2054,93 @@ async function resolveGoogleGotoUrls(rawUrls, options = {}) {
|
|
|
2054
2054
|
await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
|
|
2055
2055
|
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
2056
2056
|
}
|
|
2057
|
+
function resolvedSite(link, fallback) {
|
|
2058
|
+
try {
|
|
2059
|
+
return new URL(link.resolvedUrl ?? link.url).hostname.replace(/^www\./, "");
|
|
2060
|
+
} catch {
|
|
2061
|
+
return fallback;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
|
|
2065
|
+
const treeNodes = [];
|
|
2066
|
+
const visit = (nodes) => {
|
|
2067
|
+
for (const node of nodes) {
|
|
2068
|
+
treeNodes.push(node);
|
|
2069
|
+
visit(node.children ?? []);
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
visit(result.tree ?? []);
|
|
2073
|
+
const candidates = [
|
|
2074
|
+
...result.flat.flatMap((row) => row.sources ?? []),
|
|
2075
|
+
...treeNodes.flatMap((node) => node.sources),
|
|
2076
|
+
...result.organicResults ?? [],
|
|
2077
|
+
...(result.aiOverview?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
|
|
2078
|
+
...(result.aiMode?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
|
|
2079
|
+
];
|
|
2080
|
+
const unique = new Map(candidates.filter((link) => Boolean(link?.rawUrl)).map((link) => [link.rawUrl, link]));
|
|
2081
|
+
const gotoUrls = [...unique.values()].filter((link) => link.linkType === "google_goto_redirect" && link.resolutionStatus !== "resolved").map((link) => link.rawUrl);
|
|
2082
|
+
if (gotoUrls.length > 0) {
|
|
2083
|
+
const batch = await resolveGoogleGotoUrls(gotoUrls, options);
|
|
2084
|
+
for (const resolution2 of batch.resolutions) {
|
|
2085
|
+
const link = unique.get(resolution2.rawUrl);
|
|
2086
|
+
if (!link) continue;
|
|
2087
|
+
unique.set(resolution2.rawUrl, resolution2.resolutionStatus === "resolved" && resolution2.resolvedUrl ? { ...link, url: resolution2.resolvedUrl, resolvedUrl: resolution2.resolvedUrl, resolutionStatus: "resolved" } : { ...link, resolutionStatus: resolution2.resolutionStatus });
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
const replace = (link) => ({
|
|
2091
|
+
...link,
|
|
2092
|
+
...unique.get(link.rawUrl) ?? link
|
|
2093
|
+
});
|
|
2094
|
+
const replaceSources = (sources) => sources.map((source) => {
|
|
2095
|
+
const link = replace(source);
|
|
2096
|
+
return { ...link, site: resolvedSite(link, source.site) };
|
|
2097
|
+
});
|
|
2098
|
+
for (const row of result.flat) {
|
|
2099
|
+
row.sources = replaceSources(row.sources ?? []);
|
|
2100
|
+
const primary = row.sources[0];
|
|
2101
|
+
row.source_title = primary?.title ?? row.source_title;
|
|
2102
|
+
row.source_site = primary?.site ?? row.source_site;
|
|
2103
|
+
row.source_cite = primary?.url ?? row.source_cite;
|
|
2104
|
+
}
|
|
2105
|
+
for (const node of treeNodes) {
|
|
2106
|
+
node.sources = replaceSources(node.sources ?? []);
|
|
2107
|
+
const primary = node.sources[0];
|
|
2108
|
+
node.sourceTitle = primary?.title ?? node.sourceTitle;
|
|
2109
|
+
node.sourceSite = primary?.site ?? node.sourceSite;
|
|
2110
|
+
node.sourceCite = primary?.url ?? node.sourceCite;
|
|
2111
|
+
}
|
|
2112
|
+
for (let index = 0; index < (result.organicResults?.length ?? 0); index++) {
|
|
2113
|
+
const link = replace(result.organicResults[index]);
|
|
2114
|
+
result.organicResults[index] = { ...link, domain: resolvedSite(link, result.organicResults[index].domain) ?? "" };
|
|
2115
|
+
}
|
|
2116
|
+
const replaceCitation = (citation) => {
|
|
2117
|
+
const link = replace({
|
|
2118
|
+
url: citation.href,
|
|
2119
|
+
rawUrl: citation.rawUrl,
|
|
2120
|
+
resolvedUrl: citation.resolvedUrl,
|
|
2121
|
+
linkType: citation.linkType,
|
|
2122
|
+
resolutionStatus: citation.resolutionStatus
|
|
2123
|
+
});
|
|
2124
|
+
const { url, ...metadata } = link;
|
|
2125
|
+
return { ...citation, ...metadata, href: url };
|
|
2126
|
+
};
|
|
2127
|
+
if (result.aiOverview) result.aiOverview.citations = (result.aiOverview.citations ?? []).map(replaceCitation);
|
|
2128
|
+
if (result.aiMode) result.aiMode.citations = (result.aiMode.citations ?? []).map(replaceCitation);
|
|
2129
|
+
const values = [...unique.values()];
|
|
2130
|
+
result.diagnostics.links = {
|
|
2131
|
+
linkCandidatesObserved: unique.size,
|
|
2132
|
+
plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
|
|
2133
|
+
googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
|
|
2134
|
+
googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
|
|
2135
|
+
redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
|
|
2136
|
+
redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
|
|
2137
|
+
redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
|
|
2138
|
+
resolverNonGoogleRequestsObserved: 0,
|
|
2139
|
+
paaSourcesPreserved: result.flat.reduce((total, row) => total + row.sources.length, 0),
|
|
2140
|
+
aiOverviewLabelValidated: result.aiOverview?.detected ?? false
|
|
2141
|
+
};
|
|
2142
|
+
return result;
|
|
2143
|
+
}
|
|
2057
2144
|
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2058
2145
|
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2059
2146
|
for (const value of values) byRawUrl.set(value.rawUrl, value);
|
|
@@ -2174,15 +2261,24 @@ async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
|
2174
2261
|
}
|
|
2175
2262
|
|
|
2176
2263
|
// src/extractor/PAAExtractor.ts
|
|
2264
|
+
var PAA_ANSWER_PLACEHOLDER_PATTERN = String.raw`^(?:an error has occurred(?:[.!…]\s*)?(?:please try again later[.!…]?)?|loading|please wait|try again|something went wrong)[.!…\s]*$`;
|
|
2265
|
+
var PAA_CLICK_EARLIEST_CONFIRMATION_MS = 700;
|
|
2266
|
+
var PAA_CLICK_MID_CONFIRMATION_MS = 1e3;
|
|
2267
|
+
var PAA_CLICK_ATTEMPT_BUDGET_MS = 1400;
|
|
2268
|
+
var PAA_CLICK_DISPATCH_TIMEOUT_MS = 900;
|
|
2269
|
+
var PAA_FRONTIER_BATCH_SIZE = 3;
|
|
2270
|
+
var MAX_RETAINED_INTERACTION_OBSERVATIONS = 250;
|
|
2177
2271
|
var PAAExtractor = class {
|
|
2178
|
-
constructor(driver, reporter, onPaaProgress) {
|
|
2272
|
+
constructor(driver, reporter, onPaaProgress, onProviderSession) {
|
|
2179
2273
|
this.driver = driver;
|
|
2180
2274
|
this.reporter = reporter;
|
|
2181
2275
|
this.onPaaProgress = onPaaProgress;
|
|
2276
|
+
this.onProviderSession = onProviderSession;
|
|
2182
2277
|
}
|
|
2183
2278
|
driver;
|
|
2184
2279
|
reporter;
|
|
2185
2280
|
onPaaProgress;
|
|
2281
|
+
onProviderSession;
|
|
2186
2282
|
completeness = {
|
|
2187
2283
|
paaWithoutAnswer: 0,
|
|
2188
2284
|
paaWithoutSource: 0,
|
|
@@ -2197,6 +2293,7 @@ var PAAExtractor = class {
|
|
|
2197
2293
|
clickedQuestions = /* @__PURE__ */ new Set();
|
|
2198
2294
|
paaLifecycle = this.emptyPaaLifecycle(0);
|
|
2199
2295
|
checkpointMaterial = null;
|
|
2296
|
+
interactionObservations = [];
|
|
2200
2297
|
emptyPaaLifecycle(requestedQuestions) {
|
|
2201
2298
|
return {
|
|
2202
2299
|
requestedQuestions,
|
|
@@ -2209,7 +2306,12 @@ var PAAExtractor = class {
|
|
|
2209
2306
|
knownQuestions: 0,
|
|
2210
2307
|
processedQuestions: 0,
|
|
2211
2308
|
failedInteractions: 0,
|
|
2212
|
-
noGrowthConfirmations: 0
|
|
2309
|
+
noGrowthConfirmations: 0,
|
|
2310
|
+
visibleControls: 0,
|
|
2311
|
+
eligibleControls: 0,
|
|
2312
|
+
dispatchedInteractions: 0,
|
|
2313
|
+
confirmedInteractions: 0,
|
|
2314
|
+
unconfirmedInteractions: 0
|
|
2213
2315
|
};
|
|
2214
2316
|
}
|
|
2215
2317
|
emptyLinkDiagnostics() {
|
|
@@ -2318,7 +2420,7 @@ var PAAExtractor = class {
|
|
|
2318
2420
|
}
|
|
2319
2421
|
async extractVisibleItems(page, onlyQuestion) {
|
|
2320
2422
|
const sels = PAASelectors;
|
|
2321
|
-
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2 }) => {
|
|
2423
|
+
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2, answerPlaceholderPattern }) => {
|
|
2322
2424
|
function cleanText(el) {
|
|
2323
2425
|
if (!el) return "";
|
|
2324
2426
|
const parts = [];
|
|
@@ -2345,12 +2447,13 @@ var PAAExtractor = class {
|
|
|
2345
2447
|
const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
|
|
2346
2448
|
const answerElement = aiAnswerBody ?? answerContainer;
|
|
2347
2449
|
const answerText = cleanText(answerElement);
|
|
2450
|
+
const answerIsPlaceholder = new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2348
2451
|
const citationCandidates = [
|
|
2349
2452
|
...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
|
|
2350
2453
|
...Array.from(pair.querySelectorAll(selectors.aiCitation)),
|
|
2351
2454
|
...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
|
|
2352
2455
|
];
|
|
2353
|
-
const hasLoadedAnswer = expanded
|
|
2456
|
+
const hasLoadedAnswer = !answerIsPlaceholder && (expanded && answerText.length >= 10 || answerText.length > 30 && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href)));
|
|
2354
2457
|
let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
|
|
2355
2458
|
let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
|
|
2356
2459
|
const sources = [];
|
|
@@ -2374,7 +2477,7 @@ var PAAExtractor = class {
|
|
|
2374
2477
|
sources
|
|
2375
2478
|
};
|
|
2376
2479
|
});
|
|
2377
|
-
}, { selectors: sels, onlyQuestion });
|
|
2480
|
+
}, { selectors: sels, onlyQuestion, answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN });
|
|
2378
2481
|
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
2379
2482
|
return raw.flatMap((item) => {
|
|
2380
2483
|
const sources = [];
|
|
@@ -2470,7 +2573,11 @@ var PAAExtractor = class {
|
|
|
2470
2573
|
phase,
|
|
2471
2574
|
records: this.collectedRows.map((row) => ({ ...row })),
|
|
2472
2575
|
lifecycle: { ...this.paaLifecycle },
|
|
2473
|
-
...this.checkpointMaterial ? { material: structuredClone(this.checkpointMaterial) } : {}
|
|
2576
|
+
...this.checkpointMaterial ? { material: structuredClone(this.checkpointMaterial) } : {},
|
|
2577
|
+
interactions: this.interactionObservations.map((observation) => ({
|
|
2578
|
+
...observation,
|
|
2579
|
+
confirmationSignals: [...observation.confirmationSignals]
|
|
2580
|
+
}))
|
|
2474
2581
|
};
|
|
2475
2582
|
try {
|
|
2476
2583
|
await this.onPaaProgress(snapshot);
|
|
@@ -2551,6 +2658,7 @@ var PAAExtractor = class {
|
|
|
2551
2658
|
const seenQs = /* @__PURE__ */ new Set();
|
|
2552
2659
|
const orderedQs = [];
|
|
2553
2660
|
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
2661
|
+
this.interactionObservations = [];
|
|
2554
2662
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
2555
2663
|
const clickAttempts = /* @__PURE__ */ new Map();
|
|
2556
2664
|
const processedQuestions = /* @__PURE__ */ new Set();
|
|
@@ -2558,6 +2666,7 @@ var PAAExtractor = class {
|
|
|
2558
2666
|
const capturedItems = /* @__PURE__ */ new Map();
|
|
2559
2667
|
const MAX_CLICK_ATTEMPTS = 2;
|
|
2560
2668
|
const EXHAUSTION_CONFIRMATIONS = 3;
|
|
2669
|
+
let batchNumber = 0;
|
|
2561
2670
|
const itemArgs = {
|
|
2562
2671
|
sel: PAASelectors.item,
|
|
2563
2672
|
dataQ: PAASelectors.itemDataQ,
|
|
@@ -2565,46 +2674,171 @@ var PAAExtractor = class {
|
|
|
2565
2674
|
questionEl: PAASelectors.itemQuestionEl,
|
|
2566
2675
|
answerSel: PAASelectors.answerContainer,
|
|
2567
2676
|
expandedClass: PAASelectors.expandedClass,
|
|
2568
|
-
clickTarget: PAASelectors.clickTarget
|
|
2677
|
+
clickTarget: PAASelectors.clickTarget,
|
|
2678
|
+
answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN,
|
|
2679
|
+
questionsOnly: options.questionsOnly,
|
|
2680
|
+
sourceSel: "a[href]"
|
|
2569
2681
|
};
|
|
2570
2682
|
const readItemStates = () => page.evaluate(
|
|
2571
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2683
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel, answerPlaceholderPattern, questionsOnly }) => {
|
|
2684
|
+
const cleanVisibleMaterial = (element) => {
|
|
2685
|
+
if (!element) return "";
|
|
2686
|
+
const parts = [];
|
|
2687
|
+
for (const node of element.childNodes) {
|
|
2688
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2689
|
+
const text2 = node.textContent?.trim();
|
|
2690
|
+
if (text2) parts.push(text2);
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
2694
|
+
const child = node;
|
|
2695
|
+
if (child.tagName === "STYLE" || child.tagName === "SCRIPT" || child.getAttribute("aria-hidden") === "true") continue;
|
|
2696
|
+
const text = cleanVisibleMaterial(child);
|
|
2697
|
+
if (text) parts.push(text);
|
|
2698
|
+
}
|
|
2699
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
2700
|
+
};
|
|
2701
|
+
return Array.from(document.querySelectorAll(sel)).map((el, index) => {
|
|
2702
|
+
const answerText = cleanVisibleMaterial(el.querySelector(answerSel)).replace(/An AI Overview is not available for this search/gi, " ").replace(/Can't generate an AI overview right now\.?\s*Try again later\.?/gi, " ").replace(/\bAI Overview\b/gi, " ").replace(/\s+/g, " ").trim();
|
|
2703
|
+
const expanded = el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true";
|
|
2704
|
+
const answerReady = answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2705
|
+
return {
|
|
2706
|
+
q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
|
|
2707
|
+
done: expanded && (questionsOnly || answerReady),
|
|
2708
|
+
expanded,
|
|
2709
|
+
index
|
|
2710
|
+
};
|
|
2711
|
+
}).filter((s) => s.q);
|
|
2576
2712
|
},
|
|
2577
2713
|
itemArgs
|
|
2578
2714
|
);
|
|
2579
|
-
const expandOneItemSerially = async (q, beforeCount) => {
|
|
2715
|
+
const expandOneItemSerially = async (q, beforeCount, itemIndex, wasExpanded, controlAttempt, batchNumber2) => {
|
|
2580
2716
|
const clickStartedMs = Date.now();
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2717
|
+
const baseObservation = {
|
|
2718
|
+
id: `${batchNumber2}:${questionIdFor(q)}:${controlAttempt}`,
|
|
2719
|
+
questionId: questionIdFor(q),
|
|
2720
|
+
question: q,
|
|
2721
|
+
batchNumber: batchNumber2,
|
|
2722
|
+
controlAttempt,
|
|
2723
|
+
observedAt: new Date(clickStartedMs).toISOString(),
|
|
2724
|
+
beforeQuestionCount: beforeCount,
|
|
2725
|
+
expandedBefore: wasExpanded
|
|
2726
|
+
};
|
|
2727
|
+
const clickTarget = itemIndex >= 0 ? page.locator(itemArgs.sel).nth(itemIndex).locator(itemArgs.clickTarget) : null;
|
|
2728
|
+
let dispatchErrorCode = null;
|
|
2587
2729
|
const clicked = clickTarget ? await (async () => {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2730
|
+
if (wasExpanded) {
|
|
2731
|
+
await clickTarget.click({ timeout: PAA_CLICK_DISPATCH_TIMEOUT_MS, force: true });
|
|
2732
|
+
await page.waitForTimeout(120);
|
|
2733
|
+
}
|
|
2734
|
+
await clickTarget.click({ timeout: PAA_CLICK_DISPATCH_TIMEOUT_MS, force: true });
|
|
2592
2735
|
return true;
|
|
2593
|
-
})().catch(() =>
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
const
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2736
|
+
})().catch((error) => {
|
|
2737
|
+
dispatchErrorCode = error instanceof Error && (error.name === "TimeoutError" || /timeout/i.test(error.message)) ? "click_ack_timeout" : "click_failed";
|
|
2738
|
+
return false;
|
|
2739
|
+
}) : false;
|
|
2740
|
+
const dispatchDurationMs = Date.now() - clickStartedMs;
|
|
2741
|
+
if (!clicked && dispatchErrorCode !== "click_ack_timeout") {
|
|
2742
|
+
const observation2 = {
|
|
2743
|
+
...baseObservation,
|
|
2744
|
+
dispatched: false,
|
|
2745
|
+
outcome: "dispatch_failed",
|
|
2746
|
+
confirmationSignals: [],
|
|
2747
|
+
confirmationCheckpointMs: null,
|
|
2748
|
+
confirmedAtMs: null,
|
|
2749
|
+
dispatchDurationMs,
|
|
2750
|
+
totalDurationMs: dispatchDurationMs,
|
|
2751
|
+
afterQuestionCount: beforeCount,
|
|
2752
|
+
newQuestionCount: 0,
|
|
2753
|
+
expandedAfter: wasExpanded,
|
|
2754
|
+
answerObserved: false,
|
|
2755
|
+
sourceCount: 0,
|
|
2756
|
+
errorCode: itemIndex < 0 ? "control_missing" : dispatchErrorCode ?? "click_failed"
|
|
2757
|
+
};
|
|
2758
|
+
return { dispatched: false, confirmed: false, observation: observation2 };
|
|
2759
|
+
}
|
|
2760
|
+
let latest = {
|
|
2761
|
+
exists: true,
|
|
2762
|
+
expanded: wasExpanded,
|
|
2763
|
+
answerReady: false,
|
|
2764
|
+
sourceCount: 0,
|
|
2765
|
+
questionCount: beforeCount
|
|
2766
|
+
};
|
|
2767
|
+
let signals = [];
|
|
2768
|
+
let confirmationCheckpointMs = null;
|
|
2769
|
+
let confirmedAtMs = null;
|
|
2770
|
+
let confirmed = false;
|
|
2771
|
+
for (const checkpointMs of [PAA_CLICK_EARLIEST_CONFIRMATION_MS, PAA_CLICK_MID_CONFIRMATION_MS, PAA_CLICK_ATTEMPT_BUDGET_MS]) {
|
|
2772
|
+
const remainingMs = clickStartedMs + checkpointMs - Date.now();
|
|
2773
|
+
if (remainingMs > 0) await page.waitForTimeout(remainingMs);
|
|
2774
|
+
latest = await page.evaluate(
|
|
2775
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, answerPlaceholderPattern, sourceSel, target }) => {
|
|
2776
|
+
const cleanVisibleMaterial = (element) => {
|
|
2777
|
+
if (!element) return "";
|
|
2778
|
+
const parts = [];
|
|
2779
|
+
for (const node of element.childNodes) {
|
|
2780
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2781
|
+
const text2 = node.textContent?.trim();
|
|
2782
|
+
if (text2) parts.push(text2);
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
2786
|
+
const child = node;
|
|
2787
|
+
if (child.tagName === "STYLE" || child.tagName === "SCRIPT" || child.getAttribute("aria-hidden") === "true") continue;
|
|
2788
|
+
const text = cleanVisibleMaterial(child);
|
|
2789
|
+
if (text) parts.push(text);
|
|
2790
|
+
}
|
|
2791
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
2792
|
+
};
|
|
2793
|
+
const items = Array.from(document.querySelectorAll(sel));
|
|
2794
|
+
const item = items.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
|
|
2795
|
+
if (!item) return { exists: false, expanded: false, answerReady: false, sourceCount: 0, questionCount: items.length };
|
|
2796
|
+
const expanded = item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true";
|
|
2797
|
+
const answerElement = item.querySelector(answerSel);
|
|
2798
|
+
const answerText = cleanVisibleMaterial(answerElement);
|
|
2799
|
+
const confirmationText = answerText.replace(/An AI Overview is not available for this search/gi, " ").replace(/Can't generate an AI overview right now\.?\s*Try again later\.?/gi, " ").replace(/\bAI Overview\b/gi, " ").replace(/\s+/g, " ").trim();
|
|
2800
|
+
const answerReady = confirmationText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(confirmationText);
|
|
2801
|
+
return {
|
|
2802
|
+
exists: true,
|
|
2803
|
+
expanded,
|
|
2804
|
+
answerReady,
|
|
2805
|
+
sourceCount: answerReady ? answerElement?.querySelectorAll(sourceSel).length ?? 0 : 0,
|
|
2806
|
+
questionCount: items.length
|
|
2807
|
+
};
|
|
2808
|
+
},
|
|
2809
|
+
{ ...itemArgs, target: q }
|
|
2810
|
+
).catch(() => latest);
|
|
2811
|
+
signals = [
|
|
2812
|
+
...latest.expanded ? ["expanded"] : [],
|
|
2813
|
+
...latest.answerReady ? ["answer"] : [],
|
|
2814
|
+
...latest.sourceCount > 0 ? ["source"] : [],
|
|
2815
|
+
...latest.questionCount > beforeCount ? ["question_growth"] : []
|
|
2816
|
+
];
|
|
2817
|
+
if (latest.answerReady || latest.sourceCount > 0 || latest.questionCount > beforeCount) {
|
|
2818
|
+
confirmed = true;
|
|
2819
|
+
confirmationCheckpointMs = checkpointMs;
|
|
2820
|
+
confirmedAtMs = Date.now() - clickStartedMs;
|
|
2821
|
+
break;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
const totalDurationMs = Date.now() - clickStartedMs;
|
|
2825
|
+
const observation = {
|
|
2826
|
+
...baseObservation,
|
|
2827
|
+
dispatched: clicked,
|
|
2828
|
+
outcome: confirmed ? "confirmed" : clicked ? "unconfirmed" : "dispatch_failed",
|
|
2829
|
+
confirmationSignals: signals,
|
|
2830
|
+
confirmationCheckpointMs,
|
|
2831
|
+
confirmedAtMs,
|
|
2832
|
+
dispatchDurationMs,
|
|
2833
|
+
totalDurationMs,
|
|
2834
|
+
afterQuestionCount: latest.questionCount,
|
|
2835
|
+
newQuestionCount: Math.max(0, latest.questionCount - beforeCount),
|
|
2836
|
+
expandedAfter: latest.expanded,
|
|
2837
|
+
answerObserved: latest.answerReady,
|
|
2838
|
+
sourceCount: latest.sourceCount,
|
|
2839
|
+
errorCode: !clicked ? dispatchErrorCode : confirmed ? null : latest.exists ? "confirmation_timeout" : "control_missing"
|
|
2840
|
+
};
|
|
2841
|
+
return { dispatched: clicked, confirmed, observation };
|
|
2608
2842
|
};
|
|
2609
2843
|
let round = 0;
|
|
2610
2844
|
expansion: while (true) {
|
|
@@ -2635,7 +2869,8 @@ var PAAExtractor = class {
|
|
|
2635
2869
|
targetStatus: orderedQs.length >= options.maxQuestions ? "met" : "not_met",
|
|
2636
2870
|
knownQuestions: orderedQs.length,
|
|
2637
2871
|
processedQuestions: processedQuestions.size,
|
|
2638
|
-
failedInteractions: failedInteractions.size
|
|
2872
|
+
failedInteractions: failedInteractions.size,
|
|
2873
|
+
visibleControls: states.length
|
|
2639
2874
|
};
|
|
2640
2875
|
if (discovered) await this.emitProgress("discovered");
|
|
2641
2876
|
if (options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
@@ -2647,10 +2882,44 @@ var PAAExtractor = class {
|
|
|
2647
2882
|
};
|
|
2648
2883
|
break;
|
|
2649
2884
|
}
|
|
2885
|
+
if (!options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
2886
|
+
let materialChanged = false;
|
|
2887
|
+
const keptQuestions = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2888
|
+
for (const item of await this.extractVisibleItems(page)) {
|
|
2889
|
+
if (!keptQuestions.has(item.question)) continue;
|
|
2890
|
+
const existing = capturedItems.get(item.question);
|
|
2891
|
+
const merged = {
|
|
2892
|
+
...item,
|
|
2893
|
+
answer: item.answer ?? existing?.answer,
|
|
2894
|
+
sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
|
|
2895
|
+
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
2896
|
+
sourceCite: item.sourceCite ?? existing?.sourceCite,
|
|
2897
|
+
sources: this.mergePaaSources(existing?.sources, item.sources)
|
|
2898
|
+
};
|
|
2899
|
+
capturedItems.set(item.question, merged);
|
|
2900
|
+
materialChanged = this.upsertCollectedRow(this.toFlatRow(merged, 1, null, options.query)) || materialChanged;
|
|
2901
|
+
}
|
|
2902
|
+
if (materialChanged) await this.emitProgress("answer_captured");
|
|
2903
|
+
if ([...keptQuestions].every((question) => {
|
|
2904
|
+
const item = capturedItems.get(question);
|
|
2905
|
+
return Boolean(item?.answer?.trim() && item.sources.length > 0);
|
|
2906
|
+
})) {
|
|
2907
|
+
failedInteractions.clear();
|
|
2908
|
+
this.paaLifecycle = {
|
|
2909
|
+
...this.paaLifecycle,
|
|
2910
|
+
discoveryStatus: "target_reached",
|
|
2911
|
+
targetStatus: "met",
|
|
2912
|
+
failedInteractions: 0,
|
|
2913
|
+
completionReason: "The requested unique-question count and its answer/source material were observed."
|
|
2914
|
+
};
|
|
2915
|
+
break;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2650
2918
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2651
2919
|
const clickable = states.filter((state) => kept.has(state.q) && !state.done && (clickAttempts.get(state.q) ?? 0) < MAX_CLICK_ATTEMPTS && !abandonedNeverReclick.has(state.q));
|
|
2652
|
-
const
|
|
2653
|
-
|
|
2920
|
+
const targets = clickable.slice(-PAA_FRONTIER_BATCH_SIZE);
|
|
2921
|
+
this.paaLifecycle = { ...this.paaLifecycle, eligibleControls: clickable.length };
|
|
2922
|
+
if (targets.length === 0) {
|
|
2654
2923
|
if (seenQs.size >= options.maxQuestions) {
|
|
2655
2924
|
this.paaLifecycle = {
|
|
2656
2925
|
...this.paaLifecycle,
|
|
@@ -2698,37 +2967,64 @@ var PAAExtractor = class {
|
|
|
2698
2967
|
};
|
|
2699
2968
|
break;
|
|
2700
2969
|
}
|
|
2701
|
-
this.reporter.onDepth(++round);
|
|
2702
2970
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
2703
|
-
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2971
|
+
batchNumber += 1;
|
|
2972
|
+
for (const target of targets) {
|
|
2973
|
+
this.reporter.onDepth(++round);
|
|
2974
|
+
if ((clickAttempts.get(target.q) ?? 0) > 0) {
|
|
2975
|
+
this.paaLifecycle = {
|
|
2976
|
+
...this.paaLifecycle,
|
|
2977
|
+
automaticRetries: this.paaLifecycle.automaticRetries + 1
|
|
2978
|
+
};
|
|
2711
2979
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2980
|
+
clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
|
|
2981
|
+
}
|
|
2982
|
+
const expansionResults = await Promise.all(targets.map(async (target) => ({
|
|
2983
|
+
target,
|
|
2984
|
+
result: await expandOneItemSerially(
|
|
2985
|
+
target.q,
|
|
2986
|
+
states.length,
|
|
2987
|
+
target.index,
|
|
2988
|
+
target.expanded,
|
|
2989
|
+
clickAttempts.get(target.q) ?? 1,
|
|
2990
|
+
batchNumber
|
|
2991
|
+
)
|
|
2992
|
+
})));
|
|
2993
|
+
for (const { target, result } of expansionResults) {
|
|
2994
|
+
if (result.dispatched || result.confirmed) this.clickedQuestions.add(target.q);
|
|
2995
|
+
this.interactionObservations.push(result.observation);
|
|
2996
|
+
}
|
|
2997
|
+
if (this.interactionObservations.length > MAX_RETAINED_INTERACTION_OBSERVATIONS) {
|
|
2998
|
+
this.interactionObservations.splice(0, this.interactionObservations.length - MAX_RETAINED_INTERACTION_OBSERVATIONS);
|
|
2999
|
+
}
|
|
3000
|
+
if (!options.questionsOnly) {
|
|
3001
|
+
const targetQuestions = new Set(targets.map((target) => target.q));
|
|
3002
|
+
for (const clickedItem of await this.extractVisibleItems(page)) {
|
|
3003
|
+
if (!targetQuestions.has(clickedItem.question)) continue;
|
|
3004
|
+
capturedItems.set(clickedItem.question, clickedItem);
|
|
2714
3005
|
if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
|
|
2715
3006
|
await this.emitProgress("answer_captured");
|
|
2716
3007
|
}
|
|
2717
3008
|
}
|
|
2718
3009
|
}
|
|
2719
|
-
const
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
3010
|
+
for (const { target, result } of expansionResults) {
|
|
3011
|
+
const captured = capturedItems.get(target.q);
|
|
3012
|
+
if (result.confirmed || Boolean(captured?.answer?.trim())) {
|
|
3013
|
+
processedQuestions.add(target.q);
|
|
3014
|
+
failedInteractions.delete(target.q);
|
|
3015
|
+
} else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
|
|
3016
|
+
failedInteractions.add(target.q);
|
|
3017
|
+
abandonedNeverReclick.add(target.q);
|
|
3018
|
+
}
|
|
2726
3019
|
}
|
|
2727
3020
|
this.paaLifecycle = {
|
|
2728
3021
|
...this.paaLifecycle,
|
|
2729
3022
|
processedQuestions: processedQuestions.size,
|
|
2730
3023
|
failedInteractions: failedInteractions.size,
|
|
2731
|
-
noGrowthConfirmations: 0
|
|
3024
|
+
noGrowthConfirmations: 0,
|
|
3025
|
+
dispatchedInteractions: this.interactionObservations.filter((value) => value.dispatched).length,
|
|
3026
|
+
confirmedInteractions: this.interactionObservations.filter((value) => value.outcome === "confirmed").length,
|
|
3027
|
+
unconfirmedInteractions: this.interactionObservations.filter((value) => value.outcome !== "confirmed").length
|
|
2732
3028
|
};
|
|
2733
3029
|
await this.emitProgress("click_observed");
|
|
2734
3030
|
}
|
|
@@ -3322,6 +3618,18 @@ var PAAExtractor = class {
|
|
|
3322
3618
|
try {
|
|
3323
3619
|
this.throwIfAborted(signal);
|
|
3324
3620
|
await this.driver.launch(config);
|
|
3621
|
+
const providerSession = this.driver.getProviderSessionMetadata?.();
|
|
3622
|
+
if (providerSession?.providerSessionId && this.onProviderSession) {
|
|
3623
|
+
try {
|
|
3624
|
+
await this.onProviderSession(providerSession);
|
|
3625
|
+
} catch (err) {
|
|
3626
|
+
console.warn(JSON.stringify({
|
|
3627
|
+
event: "paa_provider_session_sink_failed",
|
|
3628
|
+
provider: providerSession.provider,
|
|
3629
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3630
|
+
}));
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3325
3633
|
this.throwIfAborted(signal);
|
|
3326
3634
|
const locationResolution = options.location ? resolveLocation(options.location) : null;
|
|
3327
3635
|
const canonicalLocation = options.location ? locationResolution?.canonical ?? options.location : null;
|
|
@@ -4430,6 +4738,17 @@ function stripInternalDebug(result, keepDebug) {
|
|
|
4430
4738
|
delete diagnostics.debug;
|
|
4431
4739
|
return { ...result, diagnostics };
|
|
4432
4740
|
}
|
|
4741
|
+
async function resolveSurvivingGotoLinks(result) {
|
|
4742
|
+
try {
|
|
4743
|
+
return await resolveHarvestResultGoogleGotoLinks(result, { totalBudgetMs: 1e4 });
|
|
4744
|
+
} catch (error) {
|
|
4745
|
+
console.warn(JSON.stringify({
|
|
4746
|
+
event: "harvest_goto_resolution_failed",
|
|
4747
|
+
message: errorMessage(error)
|
|
4748
|
+
}));
|
|
4749
|
+
return result;
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4433
4752
|
async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
4434
4753
|
if (!kernelApiKey || !proxyId) return;
|
|
4435
4754
|
try {
|
|
@@ -4446,10 +4765,10 @@ async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
|
4446
4765
|
}));
|
|
4447
4766
|
}
|
|
4448
4767
|
}
|
|
4449
|
-
async function extractOnce(options, signal, forceManagedSerp = false, onPaaProgress) {
|
|
4768
|
+
async function extractOnce(options, signal, forceManagedSerp = false, onPaaProgress, onProviderSession) {
|
|
4450
4769
|
const driver = createSerpDriver(options, { forceManaged: forceManagedSerp });
|
|
4451
4770
|
const reporter = new ProgressReporter();
|
|
4452
|
-
const extractor = new PAAExtractor(driver, reporter, onPaaProgress);
|
|
4771
|
+
const extractor = new PAAExtractor(driver, reporter, onPaaProgress, onProviderSession);
|
|
4453
4772
|
if (signal?.aborted) {
|
|
4454
4773
|
return {
|
|
4455
4774
|
result: null,
|
|
@@ -4594,12 +4913,20 @@ async function harvest(rawOptions) {
|
|
|
4594
4913
|
max_questions: attemptOptions.maxQuestions
|
|
4595
4914
|
}));
|
|
4596
4915
|
const baseCtx = currentCostContext();
|
|
4916
|
+
const onProviderSession = (metadata) => emitAttemptEvent(onAttemptEvent, {
|
|
4917
|
+
type: "provider_session_started",
|
|
4918
|
+
attemptNumber,
|
|
4919
|
+
maxAttempts,
|
|
4920
|
+
provider: metadata.provider,
|
|
4921
|
+
providerSessionId: metadata.providerSessionId,
|
|
4922
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4923
|
+
});
|
|
4597
4924
|
const allowHeadlessTest = baseCtx?.harvestBrowserMode === "headless-test" && process.env[HEADLESS_HARVEST_TEST_ENV] === "true";
|
|
4598
4925
|
const attempt = baseCtx ? await runWithCostContext({
|
|
4599
4926
|
...baseCtx,
|
|
4600
4927
|
forceHeadless: allowHeadlessTest,
|
|
4601
4928
|
forceHeadful: !allowHeadlessTest
|
|
4602
|
-
}, () => extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress)) : await extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress);
|
|
4929
|
+
}, () => extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress, onProviderSession)) : await extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress, onProviderSession);
|
|
4603
4930
|
if (attempt.error) {
|
|
4604
4931
|
const err = attempt.error;
|
|
4605
4932
|
const outcome2 = classifyAttemptError(err);
|
|
@@ -4644,7 +4971,8 @@ async function harvest(rawOptions) {
|
|
|
4644
4971
|
lastError = err;
|
|
4645
4972
|
if (willRetry2) continue;
|
|
4646
4973
|
if (attempt.result) {
|
|
4647
|
-
|
|
4974
|
+
const resolvedResult2 = await resolveSurvivingGotoLinks(attempt.result);
|
|
4975
|
+
return withResultQuality(stripInternalDebug(resolvedResult2, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
4648
4976
|
}
|
|
4649
4977
|
break;
|
|
4650
4978
|
}
|
|
@@ -4679,7 +5007,8 @@ async function harvest(rawOptions) {
|
|
|
4679
5007
|
if (willRetry2) continue;
|
|
4680
5008
|
break;
|
|
4681
5009
|
}
|
|
4682
|
-
const
|
|
5010
|
+
const resolvedResult = await resolveSurvivingGotoLinks(result);
|
|
5011
|
+
const finalResult = withResultQuality(stripInternalDebug(resolvedResult, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
4683
5012
|
const outcome = classifyAttemptResult(finalResult);
|
|
4684
5013
|
const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
|
|
4685
5014
|
const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// release-message.json
|
|
2
2
|
var release_message_default = {
|
|
3
|
-
message: "PAA harvests now distinguish
|
|
3
|
+
message: "PAA harvests now distinguish click acknowledgement from confirmed Google DOM effects, preserve every observed interaction at 0.7, 1.0, and 1.4-second checkpoints, reject hidden or scripted placeholder text as answers, correlate attempts to Bright Data sessions immediately, and recover interrupted work without claiming the question frontier ended."
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
// src/install-terminal.ts
|