@apmantza/greedysearch-pi 2.1.3 → 2.1.5
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 +48 -1
- package/README.md +52 -175
- package/bin/search.mjs +2 -0
- package/docs/analysis.md +233 -0
- package/docs/banner.svg +86 -0
- package/docs/development.md +63 -0
- package/docs/inspiration2.md +190 -0
- package/docs/releases.md +88 -0
- package/docs/research.md +82 -0
- package/docs/runtime.md +65 -0
- package/docs/source-fetching.md +33 -0
- package/docs/stealthbrowsermcp.md +807 -0
- package/docs/usage.md +69 -0
- package/extractors/chatgpt.mjs +127 -23
- package/extractors/common.mjs +116 -28
- package/extractors/gemini.mjs +63 -38
- package/package.json +13 -7
- package/scripts/backfill-github-releases.mjs +139 -0
- package/scripts/changelog-extract.mjs +48 -0
- package/scripts/changelog-release.mjs +65 -0
- package/scripts/check-lockfile.mjs +45 -0
- package/scripts/lib/changelog.mjs +168 -0
- package/scripts/lint.mjs +62 -0
- package/scripts/run-tests.ps1 +179 -0
- package/scripts/stealth-check.mjs +663 -0
- package/src/fetcher.mjs +9 -5
- package/src/search/constants.mjs +1 -1
- package/src/search/research.mjs +57 -11
- package/src/search/simple-research.mjs +4 -1
- package/test.mjs +16 -3
- package/skills/greedy-search/skill.md +0 -20
package/src/search/research.mjs
CHANGED
|
@@ -1461,11 +1461,20 @@ export async function checkCitationUrls(
|
|
|
1461
1461
|
});
|
|
1462
1462
|
clearTimeout(timer);
|
|
1463
1463
|
const ok = response.status >= 200 && response.status < 400;
|
|
1464
|
+
const botProtectedOrHeadlessHost = [401, 403, 405, 429].includes(
|
|
1465
|
+
response.status,
|
|
1466
|
+
);
|
|
1467
|
+
let status = "dead";
|
|
1468
|
+
if (ok) status = "reachable";
|
|
1469
|
+
else if (botProtectedOrHeadlessHost) status = "skipped";
|
|
1464
1470
|
return {
|
|
1465
1471
|
id: source.id,
|
|
1466
1472
|
url,
|
|
1467
|
-
status
|
|
1473
|
+
status,
|
|
1468
1474
|
httpStatus: response.status,
|
|
1475
|
+
reason: botProtectedOrHeadlessHost
|
|
1476
|
+
? "bot-protected-or-head-disallowed"
|
|
1477
|
+
: undefined,
|
|
1469
1478
|
};
|
|
1470
1479
|
} catch (fetchError) {
|
|
1471
1480
|
clearTimeout(timer);
|
|
@@ -1519,10 +1528,17 @@ export async function checkCitationUrls(
|
|
|
1519
1528
|
* Used by both runResearchMode() and runSimpleResearchMode() to avoid
|
|
1520
1529
|
* duplicating the try/catch/logging block.
|
|
1521
1530
|
*/
|
|
1522
|
-
export async function runCitationUrlCheck(
|
|
1531
|
+
export async function runCitationUrlCheck(
|
|
1532
|
+
combinedSources,
|
|
1533
|
+
citationAudit = null,
|
|
1534
|
+
) {
|
|
1523
1535
|
process.stderr.write("PROGRESS:research:check-urls\n");
|
|
1524
1536
|
try {
|
|
1525
|
-
const
|
|
1537
|
+
const citedIds = new Set(citationAudit?.cited || []);
|
|
1538
|
+
const sourcesToCheck = citedIds.size
|
|
1539
|
+
? (combinedSources || []).filter((source) => citedIds.has(source?.id))
|
|
1540
|
+
: combinedSources;
|
|
1541
|
+
const citationUrls = await checkCitationUrls(sourcesToCheck, {
|
|
1526
1542
|
timeoutMs: 6000,
|
|
1527
1543
|
concurrency: 4,
|
|
1528
1544
|
});
|
|
@@ -1587,7 +1603,9 @@ export function computeResearchFloor({
|
|
|
1587
1603
|
roundsRun: rounds.length >= 1,
|
|
1588
1604
|
fetchedSources: fetchedOk.length >= minFetched,
|
|
1589
1605
|
primarySources: primarySources.length >= 1,
|
|
1590
|
-
qualityScore:
|
|
1606
|
+
qualityScore:
|
|
1607
|
+
qualityScore >= Math.min(qualityThreshold, 8) ||
|
|
1608
|
+
(requireCitations && claims.length > 0 && citedCount > 0),
|
|
1591
1609
|
claimsExtracted: !requireCitations || claims.length > 0,
|
|
1592
1610
|
citationsPresent: !requireCitations || citedCount > 0,
|
|
1593
1611
|
citationsValid: !requireCitations || citationAudit?.ok === true,
|
|
@@ -2211,8 +2229,8 @@ export async function writeResearchBundle({
|
|
|
2211
2229
|
|
|
2212
2230
|
export async function runResearchMode({
|
|
2213
2231
|
query,
|
|
2214
|
-
breadth
|
|
2215
|
-
iterations
|
|
2232
|
+
breadth,
|
|
2233
|
+
iterations,
|
|
2216
2234
|
maxSources,
|
|
2217
2235
|
locale = null,
|
|
2218
2236
|
short = false,
|
|
@@ -2226,8 +2244,9 @@ export async function runResearchMode({
|
|
|
2226
2244
|
// When breadth and iterations are at defaults (not user-specified), classify
|
|
2227
2245
|
// the query complexity. Simple queries bypass the iterative loop entirely
|
|
2228
2246
|
// for ~70% faster results and lower API cost.
|
|
2229
|
-
const userSpecifiedBreadth =
|
|
2230
|
-
const userSpecifiedIterations =
|
|
2247
|
+
const userSpecifiedBreadth = breadth !== undefined && breadth !== null;
|
|
2248
|
+
const userSpecifiedIterations =
|
|
2249
|
+
iterations !== undefined && iterations !== null;
|
|
2231
2250
|
const atDefaults = !userSpecifiedBreadth && !userSpecifiedIterations;
|
|
2232
2251
|
|
|
2233
2252
|
if (atDefaults) {
|
|
@@ -2475,9 +2494,33 @@ export async function runResearchMode({
|
|
|
2475
2494
|
);
|
|
2476
2495
|
if (remainingFetchBudget > 0 && combinedSources.length > 0) {
|
|
2477
2496
|
process.stderr.write(`PROGRESS:research:round-${roundNumber}:fetching\n`);
|
|
2497
|
+
const fetchedUrlKeys = new Set();
|
|
2498
|
+
const safeNormalizeFetchUrl = (value) => {
|
|
2499
|
+
try {
|
|
2500
|
+
return normalizeUrl(value || "");
|
|
2501
|
+
} catch {
|
|
2502
|
+
return "";
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
for (const source of fetchedSources) {
|
|
2506
|
+
for (const value of [
|
|
2507
|
+
source?.url,
|
|
2508
|
+
source?.finalUrl,
|
|
2509
|
+
source?.canonicalUrl,
|
|
2510
|
+
]) {
|
|
2511
|
+
const key = safeNormalizeFetchUrl(value);
|
|
2512
|
+
if (key) fetchedUrlKeys.add(key);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
const fetchCandidates = combinedSources.filter((source) => {
|
|
2516
|
+
const keys = [source?.canonicalUrl, source?.finalUrl, source?.url]
|
|
2517
|
+
.map((value) => safeNormalizeFetchUrl(value))
|
|
2518
|
+
.filter(Boolean);
|
|
2519
|
+
return keys.length > 0 && keys.every((key) => !fetchedUrlKeys.has(key));
|
|
2520
|
+
});
|
|
2478
2521
|
const fetched = await fetchMultipleResearchSources(
|
|
2479
|
-
|
|
2480
|
-
Math.min(remainingFetchBudget,
|
|
2522
|
+
fetchCandidates,
|
|
2523
|
+
Math.min(remainingFetchBudget, fetchCandidates.length),
|
|
2481
2524
|
8000,
|
|
2482
2525
|
Math.min(3, remainingFetchBudget || 1),
|
|
2483
2526
|
);
|
|
@@ -2790,7 +2833,10 @@ export async function runResearchMode({
|
|
|
2790
2833
|
const citationAudit = auditCitations(synthesis.answer || "", combinedSources);
|
|
2791
2834
|
|
|
2792
2835
|
// Citation URL reachability check
|
|
2793
|
-
const citationUrls = await runCitationUrlCheck(
|
|
2836
|
+
const citationUrls = await runCitationUrlCheck(
|
|
2837
|
+
combinedSources,
|
|
2838
|
+
citationAudit,
|
|
2839
|
+
);
|
|
2794
2840
|
|
|
2795
2841
|
reconcileQuestionsFromSynthesis(questions, synthesis, citationAudit);
|
|
2796
2842
|
const floor = computeResearchFloor({
|
|
@@ -387,7 +387,10 @@ export async function runSimpleResearchMode({
|
|
|
387
387
|
const citationAudit = auditCitations(synthesis.answer || "", combinedSources);
|
|
388
388
|
|
|
389
389
|
// Citation URL reachability check
|
|
390
|
-
const citationUrls = await runCitationUrlCheck(
|
|
390
|
+
const citationUrls = await runCitationUrlCheck(
|
|
391
|
+
combinedSources,
|
|
392
|
+
citationAudit,
|
|
393
|
+
);
|
|
391
394
|
|
|
392
395
|
reconcileQuestionsFromSynthesis(questions, synthesis, citationAudit);
|
|
393
396
|
const allGaps = uniqueStrings(synthesis.caveats || []);
|
package/test.mjs
CHANGED
|
@@ -223,12 +223,15 @@ if (["", "all", "unit", "quick", "smoke", "synth"].includes(mode)) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
const retryEngines = findHeadlessBlockedEngines({
|
|
226
|
-
perplexity: {
|
|
226
|
+
perplexity: {
|
|
227
|
+
error:
|
|
228
|
+
"Perplexity input not found — page may be blocked or in unexpected state",
|
|
229
|
+
},
|
|
227
230
|
bing: { error: "Copilot verification required" },
|
|
228
231
|
google: { error: "Google verification required" },
|
|
229
232
|
});
|
|
230
233
|
if (retryEngines.join(",") === "perplexity,bing") {
|
|
231
|
-
passMsg("visible retry engines:
|
|
234
|
+
passMsg("visible retry engines: google excluded from recovery");
|
|
232
235
|
} else {
|
|
233
236
|
failMsg(
|
|
234
237
|
`visible retry engines: expected perplexity,bing, got ${retryEngines.join(",")}`,
|
|
@@ -715,7 +718,17 @@ proc.on('close', code => {
|
|
|
715
718
|
const tmp = join(resultsDir, `_synth_${synthesizer}.mjs`);
|
|
716
719
|
writeFileSync(tmp, script, "utf8");
|
|
717
720
|
await runNode([tmp], 240);
|
|
718
|
-
|
|
721
|
+
let data;
|
|
722
|
+
try {
|
|
723
|
+
data = JSON.parse(readFileSync(outFile, "utf8"));
|
|
724
|
+
} catch (e) {
|
|
725
|
+
return {
|
|
726
|
+
synthesized: false,
|
|
727
|
+
synthesizedBy: null,
|
|
728
|
+
parseError: e.message,
|
|
729
|
+
rawOut: "",
|
|
730
|
+
};
|
|
731
|
+
}
|
|
719
732
|
let parsed = null;
|
|
720
733
|
try {
|
|
721
734
|
parsed = JSON.parse(data.out);
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: greedy-search
|
|
3
|
-
description: Web/search plus opt-in research via Perplexity, Google AI, ChatGPT, Gemini, Semantic Scholar, and Logically. Grounded all-engine search fetches sources by default; optional configurable synthesis; deep research as separate workflow. Configurable via ~/.pi/greedyconfig. Bing Copilot available for signed-in users. Current docs, recent changes, dependency choices. NOT codebase search.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
`greedy_search({ query, engine: "all"|"perplexity"|"google"|"chatgpt"|"gemini"|"semantic-scholar"|"logically"|"bing", synthesize?: bool, synthesizer?: "gemini"|"chatgpt", depth?: "research", breadth: 1-5, iterations: 1-3, maxSources: 3-12, researchOutDir?: string, writeResearchBundle?: bool, visible: bool })`
|
|
7
|
-
|
|
8
|
-
**Modes:** individual engine search · grounded `engine:"all"` search with fetched sources · optional `synthesize:true` using the configured synthesizer over all-engine results · `depth:"research"` for the iterative deep-research workflow.
|
|
9
|
-
|
|
10
|
-
**Config:** `~/.pi/greedyconfig` supports `{ "engines": ["perplexity", "google", "chatgpt", "gemini"], "synthesizer": "gemini" }` by default. `semantic-scholar` and `logically` are opt-in academic/research engines — add them to `engines` only when you want academic paper discovery or research-assistant workflows in the normal all-search fan-out. Without explicit opt-in, `engine:"all"` excludes them because their results are noisy for casual web search; they shine in `depth:"research"` mode. Any configured engine can participate in `engine:"all"`; deep research child searches reuse the same configured `engines` list and stdin-safe query passing. Normal all-search synthesis remains controlled separately by `synthesizer`; research planning/final synthesis uses Gemini.
|
|
11
|
-
|
|
12
|
-
**Compatibility:** legacy `depth:"fast"|"standard"|"deep"` is still accepted. `fast` skips source fetching; `standard`/`deep` alias `synthesize:true`. Prefer `synthesize:true`, optional `synthesizer`, and `depth:"research"` going forward.
|
|
13
|
-
|
|
14
|
-
**Research output:** `depth:"research"` writes a dataroom-style bundle by default under `.pi/greedysearch-research/<timestamp>_<query>/` with `STATUS.md`, `OUTLINE.md`, `reports/SUMMARY.md`, `reports/CLAIMS.md`, `reports/GAPS.md`, `sources/`, and `data/manifest.json`. Pass `researchOutDir` to choose the directory or `writeResearchBundle:false` to disable disk output.
|
|
15
|
-
|
|
16
|
-
**Scale-aware research:** When `breadth` and `iterations` are not explicitly set, the classifier auto-detects query complexity. Simple queries ("what is X") use a fast single-pass path (~70% faster). Moderate queries get tighter breadth/iterations. Complex queries use the full loop. Explicit `breadth`/`iterations` always override the classifier.
|
|
17
|
-
|
|
18
|
-
**Auto-recovery:** Headless default. Bing/Perplexity auto-retry visible on CF block. Manual CAPTCHA → visible stays open; solve then rerun.
|
|
19
|
-
|
|
20
|
-
**CDP safety:** Use `bin/cdp-greedy.mjs` only. Never raw `bin/cdp.mjs`.
|