@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.
@@ -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: ok ? "reachable" : "dead",
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(combinedSources) {
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 citationUrls = await checkCitationUrls(combinedSources, {
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: qualityScore >= Math.min(qualityThreshold, 8),
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 = 3,
2215
- iterations = 2,
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 = typeof breadth === "number";
2230
- const userSpecifiedIterations = typeof iterations === "number";
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
- combinedSources,
2480
- Math.min(remainingFetchBudget, combinedSources.length),
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(combinedSources);
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(combinedSources);
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: { error: "Perplexity input not found — page may be blocked or in unexpected state" },
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: perplexity and bing only");
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
- const data = JSON.parse(readFileSync(outFile, "utf8"));
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`.