@duckmind/dm-windows-x64 0.60.6 → 0.60.8

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.
Files changed (197) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +70 -123
  3. package/extensions/dm-9router-ext/src/index.js +410 -5
  4. package/extensions/dm-caveman/extensions/caveman.js +283 -12
  5. package/extensions/dm-cliproxy/index.js +182 -2
  6. package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
  7. package/extensions/dm-cliproxy/src/apply.js +228 -1
  8. package/extensions/dm-cliproxy/src/cache.js +42 -1
  9. package/extensions/dm-cliproxy/src/commands.js +50 -2
  10. package/extensions/dm-cliproxy/src/compat.js +81 -1
  11. package/extensions/dm-cliproxy/src/config.js +192 -2
  12. package/extensions/dm-cliproxy/src/conflicts.js +46 -1
  13. package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
  14. package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
  15. package/extensions/dm-cliproxy/src/log.js +23 -1
  16. package/extensions/dm-cliproxy/src/status-quota.js +77 -1
  17. package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
  18. package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
  19. package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
  20. package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
  21. package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
  22. package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
  23. package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
  24. package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
  25. package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
  26. package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
  27. package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
  28. package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
  29. package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
  30. package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
  31. package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
  32. package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
  33. package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
  34. package/extensions/dm-context/src/context.js +144 -1
  35. package/extensions/dm-context/src/index.js +339 -7
  36. package/extensions/dm-context/src/utils.js +9 -1
  37. package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
  38. package/extensions/dm-cua/index.js +75 -6
  39. package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
  40. package/extensions/dm-cua/src/browser-install.mjs +331 -2
  41. package/extensions/dm-fff/src/index.js +688 -12
  42. package/extensions/dm-fff/src/query.js +60 -1
  43. package/extensions/dm-goal/src/goal.js +894 -23
  44. package/extensions/dm-image2/index.js +103 -9
  45. package/extensions/dm-image2/src/image-lib.mjs +1275 -8
  46. package/extensions/dm-subagents/install.mjs +62 -8
  47. package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
  48. package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
  49. package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
  50. package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
  51. package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
  52. package/extensions/dm-subagents/src/agents/agents.js +1139 -11
  53. package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
  54. package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
  55. package/extensions/dm-subagents/src/agents/identity.js +29 -1
  56. package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
  57. package/extensions/dm-subagents/src/agents/skills.js +614 -6
  58. package/extensions/dm-subagents/src/extension/config.js +35 -2
  59. package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
  60. package/extensions/dm-subagents/src/extension/doctor.js +172 -15
  61. package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
  62. package/extensions/dm-subagents/src/extension/index.js +521 -359
  63. package/extensions/dm-subagents/src/extension/rpc.js +266 -7
  64. package/extensions/dm-subagents/src/extension/schemas.js +275 -1
  65. package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
  66. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
  67. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
  68. package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
  69. package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
  70. package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
  71. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
  72. package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
  73. package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
  74. package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
  75. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
  76. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
  77. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
  78. package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
  79. package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
  80. package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
  81. package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
  82. package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
  83. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
  84. package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
  85. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
  86. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
  87. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
  88. package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
  89. package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
  90. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
  91. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
  92. package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
  93. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
  94. package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
  95. package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
  96. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
  97. package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
  98. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
  99. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
  100. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
  101. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
  102. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
  103. package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
  104. package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
  105. package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
  106. package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
  107. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
  108. package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
  109. package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
  110. package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
  111. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
  112. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
  113. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
  114. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
  115. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
  116. package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
  117. package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
  118. package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
  119. package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
  120. package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
  121. package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
  122. package/extensions/dm-subagents/src/shared/formatters.js +98 -7
  123. package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
  124. package/extensions/dm-subagents/src/shared/model-info.js +62 -1
  125. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
  126. package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
  127. package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
  128. package/extensions/dm-subagents/src/shared/settings.js +198 -11
  129. package/extensions/dm-subagents/src/shared/status-format.js +53 -1
  130. package/extensions/dm-subagents/src/shared/types.js +184 -6
  131. package/extensions/dm-subagents/src/shared/utils.js +462 -2
  132. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
  133. package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
  134. package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
  135. package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
  136. package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
  137. package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
  138. package/extensions/dm-subagents/src/tui/render.js +1542 -4
  139. package/extensions/dm-usage/index.js +1294 -9
  140. package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
  141. package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
  142. package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
  143. package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
  144. package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
  145. package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
  146. package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
  147. package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
  148. package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
  149. package/extensions/greedysearch-dm/bin/search.mjs +620 -540
  150. package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
  151. package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
  152. package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
  153. package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
  154. package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
  155. package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
  156. package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
  157. package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
  158. package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
  159. package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
  160. package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
  161. package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
  162. package/extensions/greedysearch-dm/index.js +123 -23
  163. package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
  164. package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
  165. package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
  166. package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
  167. package/extensions/greedysearch-dm/src/github.mjs +222 -7
  168. package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
  169. package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
  170. package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
  171. package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
  172. package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
  173. package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
  174. package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
  175. package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
  176. package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
  177. package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
  178. package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
  179. package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
  180. package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
  181. package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
  182. package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
  183. package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
  184. package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
  185. package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
  186. package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
  187. package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
  188. package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
  189. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
  190. package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
  191. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
  192. package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
  193. package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
  194. package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
  195. package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
  196. package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
  197. package/package.json +1 -1
@@ -1,486 +1,2255 @@
1
- import{createRequire as lp}from"node:module";var pp=Object.defineProperty;var ap=(t)=>t;function gp(t,n){this[t]=ap.bind(null,n)}var dt=(t,n)=>{for(var f in n)pp(t,f,{get:n[f],enumerable:!0,configurable:!0,set:gp.bind(n,f)})};var qt=(t,n)=>()=>(t&&(n=t(t=0)),n);var Ft=lp(import.meta.url);var gf={};dt(gf,{writeSourcesToFiles:()=>mt});import{mkdirSync as Wp,writeFileSync as mp}from"node:fs";import{join as af}from"node:path";function mt(t,n=Vp){return Wp(n,{recursive:!0}),t.map((f)=>{if(!f.content||f.content.length<10)return f;let p=String(f.id||"unknown").replace(/[^a-zA-Z0-9_-]/g,""),a=(f.canonicalUrl||f.url||"").replace(/^https?:\/\//,"").replace(/[^a-zA-Z0-9]/g,"-").slice(0,40),l=`${p}-${a}.md`,g=af(n,l),r=`---
2
- url: ${f.finalUrl||f.url}
3
- title: ${f.title||""}
4
- source: ${f.source||"unknown"}
5
- status: ${f.status||""}
6
- chars: ${f.contentChars||f.content.length}
7
- ---
8
-
9
- `;mp(g,r+f.content,"utf8");let{content:w,...$}=f;return{...$,contentPath:g,contentChars:f.contentChars||w.length}})}var Vp;var gn=qt(()=>{Vp=af(process.cwd(),".dm","greedysearch-sources")});var Nn={};dt(Nn,{fetchTopSource:()=>ua,fetchSourceContent:()=>Wf,fetchMultipleSources:()=>On});import{createRequire as Jp}from"node:module";import{createRequire as Zp}from"node:module";import{spawn as ra}from"node:child_process";import{basename as wa}from"node:path";import{dirname as $a,join as _a}from"node:path";import{fileURLToPath as ea}from"node:url";import{fileURLToPath as ha}from"node:url";import{existsSync as Jt,mkdirSync as Ea,readFileSync as Tf,writeFileSync as Da}from"node:fs";import{homedir as ba}from"node:os";import{join as Af}from"node:path";import{tmpdir as Ca}from"node:os";import{existsSync as Zt,mkdirSync as va,readFileSync as kf,writeFileSync as ja}from"node:fs";import{homedir as Pa}from"node:os";import{join as Hf}from"node:path";import{tmpdir as Ra}from"node:os";import{existsSync as Kt,mkdirSync as Ja,readFileSync as If,writeFileSync as Za}from"node:fs";import{homedir as Ka}from"node:os";import{join as Ff}from"node:path";import{tmpdir as Xa}from"node:os";async function Kp(){if(Vt)return Vt;let[{Readability:t},{JSDOM:n},{default:f}]=await Promise.all([import("@mozilla/readability"),import("jsdom"),import("turndown")]),p=new f({headingStyle:"atx",bulletListMarker:"-",codeBlockStyle:"fenced"});return p.addRule("removeDataUrls",{filter:(a)=>a.tagName==="IMG"&&a.getAttribute("src")?.startsWith("data:"),replacement:()=>""}),Vt={Readability:t,JSDOM:n,turndown:p},Vt}function Up(t){if(!t||t.includes(":"))return null;let n=t.split(".");if(n.length<1||n.length>4)return null;if(!n.every((a)=>Bp.test(a)))return null;let f=n.map((a)=>{if(/^0x/i.test(a))return parseInt(a,16);if(/^0[0-7]+$/.test(a))return parseInt(a,8);return parseInt(a,10)});if(f.some((a)=>!Number.isFinite(a)||a<0))return null;let p;if(f.length===1){if(f[0]>4294967295)return null;p=[f[0]>>>24&255,f[0]>>>16&255,f[0]>>>8&255,f[0]&255]}else if(f.length===2){if(f[0]>255||f[1]>16777215)return null;p=[f[0],f[1]>>>16&255,f[1]>>>8&255,f[1]&255]}else if(f.length===3){if(f[0]>255||f[1]>255||f[2]>65535)return null;p=[f[0],f[1],f[2]>>>8&255,f[2]&255]}else{if(f.some((a)=>a>255))return null;p=f}return p.join(".")}function Qp(t){let n=t.match(Xp);if(n)return`${n[1]}.${n[2]}.${n[3]}.${n[4]}`;let f=t.match(op);if(f){let p=parseInt(f[1],16),a=parseInt(f[2],16);if(p>65535||a>65535)return null;return[p>>>8&255,p&255,a>>>8&255,a&255].join(".")}return null}function lf(t){return jf.some((n)=>n.test(t))}function xp(t={}){return{...vf,...t}}function $n(t){try{if(typeof t!=="string"||!t.trim())return{blocked:!0,reason:"URL must be a non-empty string"};let n=new URL(t);if(n.protocol!=="http:"&&n.protocol!=="https:")return{blocked:!0,reason:`Protocol not allowed: ${n.protocol}`};let f=n.hostname.toLowerCase();for(let a of jf)if(a.test(f))return{blocked:!0,reason:`Private/internal address: ${f}`};if(f.startsWith("[")&&f.endsWith("]")){let a=f.slice(1,-1),l=Qp(a);if(l&&lf(l))return{blocked:!0,reason:`Private/internal address: ${f} (maps to ${l})`}}let p=Up(f);if(p&&lf(p))return{blocked:!0,reason:`Private/internal address: ${f} (normalizes to ${p})`};return{blocked:!1}}catch(n){return{blocked:!0,reason:`Invalid URL: ${n.message}`}}}function zp(t){try{let n=new URL(t);if(!(n.hostname==="github.com"||n.hostname.endsWith(".github.com")))return t;let f=n.pathname.split("/").filter(Boolean);if(f.length<5)return t;let[p,a,l,g,...r]=f;if(l!=="blob")return t;let w=r.join("/");return`https://raw.githubusercontent.com/${p}/${a}/${g}/${w}`}catch{return t}}async function Sp(t,n={}){let f=$n(t);if(f.blocked)return{ok:!1,url:t,finalUrl:t,status:403,error:`Blocked: ${f.reason}`,needsBrowser:!1};let p=t;if(t=zp(t),t!==p)console.error(`[fetcher] Rewrote GitHub URL: ${p.slice(0,60)}... → raw.githubusercontent.com`);let{timeoutMs:a=15000,userAgent:l,signal:g}=n,r=new AbortController,w=setTimeout(()=>r.abort(),a);if(g)g.addEventListener("abort",()=>r.abort(),{once:!0});try{let $=await fetch(t,{method:"GET",headers:{...vf,"user-agent":l||Cf},redirect:"follow",signal:r.signal});clearTimeout(w);let _=$.headers.get("content-type")||"",h=$.url,E=$.headers.get("last-modified")||"",e=$n(h);if(e.blocked)return{ok:!1,url:t,finalUrl:h,status:$.status,error:`Blocked: ${e.reason}`,needsBrowser:!1};let b=!1;try{b=new URL(h).hostname.toLowerCase()==="raw.githubusercontent.com"}catch{}if(_.includes("text/plain")&&b){let y=await $.text();return{ok:!0,url:p,finalUrl:h,status:$.status,title:h.split("/").pop()||"GitHub File",byline:"",siteName:"GitHub",lang:"",publishedTime:E,lastModified:E,markdown:y,contentLength:y.length,excerpt:y.slice(0,300).replaceAll(/\n/g," "),needsBrowser:!1}}if(!_.includes("text/html")&&!_.includes("application/xhtml"))return{ok:!1,url:t,finalUrl:h,status:$.status,error:`Unsupported content type: ${_}`,needsBrowser:!1};let D=await $.text(),v=Pf($.status,D,h,t);if(v.blocked)return{ok:!1,url:t,finalUrl:h,status:$.status,error:`Blocked: ${v.reason}`,needsBrowser:!0};let P=await Rf(D,h),R=yf(P);if(!R.ok)return{ok:!1,url:t,finalUrl:h,status:$.status,error:`Low quality content: ${R.reason}`,needsBrowser:!0};return{ok:!0,url:t,finalUrl:h,status:$.status,title:P.title,byline:P.byline,siteName:P.siteName,lang:P.lang,publishedTime:P.publishedTime||E,lastModified:E,markdown:P.markdown,excerpt:P.excerpt,contentLength:P.markdown.length,needsBrowser:!1}}catch($){clearTimeout(w);let _=up($);return{ok:!1,url:t,finalUrl:t,status:0,error:$.message,needsBrowser:_}}}function Pf(t,n,f,p){let a=n.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.toLowerCase()||"",l=n.slice(0,30000).toLowerCase(),g=`${a} ${l}`;if(t===403||t===429||t===503)return{blocked:!0,reason:`HTTP ${t}`};let r=[{pattern:/class=["'][^"']*captcha["']|<div[^>]*id=["']captcha/i,reason:"captcha"},{pattern:/g-recaptcha|data-sitekey|i['"]m not a robot/i,reason:"captcha"},{pattern:/checking your browser.{0,100}please wait|cf-browser-verification/i,reason:"cloudflare challenge"},{pattern:/just a moment.{0,50}security check|ddos protection by cloudflare/i,reason:"cloudflare challenge"},{pattern:/unusual traffic.{0,50}from your computer network/i,reason:"unusual traffic"},{pattern:/bot detected|automated.{0,20}request/i,reason:"bot detection"},{pattern:/enable\s+javascript\s+to\s+view|javascript\s+is\s+required.{0,50}enabled/i,reason:"requires javascript"},{pattern:/access denied|accessdenied/i,reason:"access denied"},{pattern:/protected by anubis|anubis uses a proof-of-work/i,reason:"anubis challenge"}];for(let $ of r)if($.pattern.test(g))return{blocked:!0,reason:$.reason};let w=sp(p,f,n);if(w)return{blocked:!0,reason:w};return{blocked:!1}}function sp(t,n,f){try{let p=new URL(t),a=new URL(n);if(p.hostname.toLowerCase()===a.hostname.toLowerCase())return;let l=a.hostname.toLowerCase();if(cp.some((r)=>l===r||l.endsWith(`.${r}`)))return`redirected to login (${a.hostname})`;if(dp.some((r)=>l.startsWith(r)))return`redirected to login (${a.hostname})`;let g=f.slice(0,20000).toLowerCase();if(qp.some((r)=>g.includes(r)))return`redirected to login page (${a.hostname})`}catch{}return}function up(t){let n=t.message.toLowerCase();return n.includes("fetch failed")||n.includes("unable to verify")||n.includes("certificate")||n.includes("timeout")}function rf(t){let n=['meta[property="article:published_time"]','meta[name="article:published_time"]','meta[property="og:published_time"]','meta[name="publication_date"]','meta[name="date"]','meta[itemprop="datePublished"]','time[itemprop="datePublished"]','meta[name="DC.date"]'];for(let f of n){let p=t.querySelector(f),a=p?.getAttribute("content")||p?.getAttribute("datetime")||"";if(a)return a}return""}async function Rf(t,n){let{Readability:f,JSDOM:p,turndown:a}=await Kp(),l=new p(t,{url:n});try{let g=l.window.document,r=new f(g).parse();if(r&&r.content){let $=a.turndown(r.content).replaceAll(/\n{3,}/g,`
10
-
11
- `).trim(),_=r.publishedTime||rf(g)||"";return{title:r.title||g.title||n,byline:r.byline||"",siteName:r.siteName||"",lang:r.lang||"",publishedTime:_,markdown:$,excerpt:$.slice(0,300).replaceAll(/\n/g," ")}}let w=g.body;if(w){let $=w.cloneNode(!0);$.querySelectorAll("script, style, nav, footer, header, aside").forEach((h)=>h.remove());let _=($.textContent||"").replaceAll(/\s+/g," ").trim();return{title:g.title||n,byline:"",siteName:"",lang:"",publishedTime:rf(g),markdown:_,excerpt:_.slice(0,300)}}return{title:n,byline:"",siteName:"",lang:"",publishedTime:"",markdown:"",excerpt:""}}finally{l.window.close()}}function yf(t){let n=t.markdown.trim().toLowerCase(),f=(t.title||"").toLowerCase();if(t.markdown.trim().length<100)return{ok:!1,reason:"content too short (< 100 chars)"};let p=n.toLowerCase(),a=[{check:()=>p.includes("loading")&&p.includes("please wait"),desc:"loading page"},{check:()=>p.includes("please ensure javascript is enabled"),desc:"requires javascript"},{check:()=>p.includes("enable javascript to view"),desc:"requires javascript"},{check:()=>p.includes("just a moment"),desc:"cloudflare challenge detected in content"},{check:()=>p.includes("verify you are human"),desc:"human verification"},{check:()=>p.includes("captcha required"),desc:"captcha in extracted content"},{check:()=>p.includes("access denied"),desc:"access denied in content"},{check:()=>/^\s{0,10}sign\s{1,5}in\s{0,10}$|^\s{0,10}log\s{1,5}in\s{0,10}$/im.test(n),desc:"login form only"}];for(let{check:l,desc:g}of a)if(l())return{ok:!1,reason:g};if(f.includes("just a moment")||f.includes("checking your browser"))return{ok:!1,reason:"cloudflare challenge page detected in title"};return{ok:!0}}function _n(t){try{let n=new URL(t);if(!(n.hostname==="github.com"||n.hostname.endsWith(".github.com")))return null;let f=n.pathname.split("/").filter(Boolean);if(f.length<2)return null;let[p,a]=f;if(f.length===2)return{owner:p,repo:a,type:"root"};if(f.length>=4&&(f[2]==="blob"||f[2]==="tree")){let l=f[2],g=f[3],r=f.slice(4).join("/");return{owner:p,repo:a,type:l,ref:g,path:r}}return null}catch{return null}}async function B(t,n=1e4){let f=new AbortController,p=setTimeout(()=>f.abort(),n);try{let a=await fetch(`https://api.github.com${t}`,{headers:Of,signal:f.signal});if(clearTimeout(p),!a.ok)throw Error(`GitHub API ${a.status}: ${t}`);return await a.json()}catch(a){throw clearTimeout(p),a}}async function ta(t,n){try{let f=await B(`/repos/${t}/${n}/readme`);if(f.content&&f.encoding==="base64")return Buffer.from(f.content,"base64").toString("utf8");return""}catch{return""}}async function wf(t,n,f="HEAD",p="",a){try{let l;if(f==="HEAD")if(a)l=await B(`/repos/${t}/${n}/git/ref/heads/${a}`).catch(()=>null);else l=await Promise.any([B(`/repos/${t}/${n}/git/ref/heads/main`),B(`/repos/${t}/${n}/git/ref/heads/master`)]).catch(()=>null);else l=await B(`/repos/${t}/${n}/git/ref/heads/${f}`).catch(()=>B(`/repos/${t}/${n}/git/ref/heads/master`).catch(()=>null));if(!l?.object?.sha)return[];let g=(await B(`/repos/${t}/${n}/git/commits/${l.object.sha}`)).tree.sha,r=(await B(`/repos/${t}/${n}/git/trees/${g}`)).tree||[];if(p)r=r.filter((w)=>w.path.startsWith(p));return r.slice(0,50).map((w)=>({path:w.path,type:w.type==="tree"?"dir":"file",size:w.size}))}catch{return[]}}async function na(t,n,f,p,a=1e4,l){let g=async(w)=>{let $=new AbortController,_=setTimeout(()=>$.abort(),a);try{let h=await fetch(w,{headers:{"user-agent":Of["user-agent"]},signal:$.signal});if(clearTimeout(_),h.ok)return await h.text();throw Error("not ok")}catch{throw clearTimeout(_),Error("failed")}};if(!f||f==="HEAD"){if(l)try{return await g(`https://raw.githubusercontent.com/${t}/${n}/${l}/${p}`)}catch{return null}try{return await Promise.any([g(`https://raw.githubusercontent.com/${t}/${n}/main/${p}`),g(`https://raw.githubusercontent.com/${t}/${n}/master/${p}`)])}catch{return null}}let r=[`https://raw.githubusercontent.com/${t}/${n}/${f}/${p}`,`https://raw.githubusercontent.com/${t}/${n}/master/${p}`];for(let w of r)try{return await g(w)}catch{}return null}async function fa(t){let n=_n(t);if(!n)return{ok:!1,error:"Not a valid GitHub URL"};let{owner:f,repo:p,type:a,ref:l,path:g}=n;try{if(a==="root"||a==="tree"&&!g){let r=await B(`/repos/${f}/${p}`),[w,$]=await Promise.allSettled([ta(f,p),wf(f,p,l||"HEAD","",r.default_branch)]),_=w.status==="fulfilled"?w.value:"",h=$.status==="fulfilled"?$.value:[],E=r?.description?`
12
-
13
- > ${r.description}`:"",e=r?.stargazers_count==null?"":` ⭐ ${r.stargazers_count}`,b=r?.language?` · ${r.language}`:"",D=`# ${f}/${p}${e}${b}${E}
14
-
15
- `;if(_)D+=_.slice(0,6000);else D+=`[No README found]
16
-
17
- Files:
18
- ${h.map((v)=>` ${v.type==="dir"?"\uD83D\uDCC1":"\uD83D\uDCC4"} ${v.path}`).join(`
19
- `)}`;return{ok:!0,title:`${f}/${p}`,content:D,tree:h.slice(0,30)}}if(a==="blob"&&g){let r;if(!l||l==="HEAD")try{r=(await B(`/repos/${f}/${p}`)).default_branch}catch{r=void 0}let w=await na(f,p,l,g,1e4,r);if(w===null)return{ok:!1,error:`File not found: ${g}`};return{ok:!0,title:`${f}/${p}: ${g}`,content:w}}if(a==="tree"&&g){let r;if(!l||l==="HEAD")try{r=(await B(`/repos/${f}/${p}`)).default_branch}catch{r=void 0}let w=await wf(f,p,l||"HEAD",g,r),$=w.map((_)=>` ${_.type==="dir"?"\uD83D\uDCC1":"\uD83D\uDCC4"} ${_.path}`).join(`
20
- `);return{ok:!0,title:`${f}/${p}/${g}`,content:`[Directory: ${g}]
21
-
22
- Files:
23
- ${$}`,tree:w}}return{ok:!1,error:"Unsupported GitHub URL type"}}catch(r){return{ok:!1,error:r.message}}}function aa(t){try{let n=new URL(t),f=n.hostname.toLowerCase();if(!(f==="reddit.com"||f.endsWith(".reddit.com")))return null;let p=n.pathname;if(p.match(/^\/(u|user)\/[^/]+\/?$/i))return{type:"user",cleanUrl:$f(t)};if(p.match(/^\/r\/[^/]+\/comments\/[^/]+/i))return{type:"post",cleanUrl:$f(t)};return null}catch{return null}}function $f(t){try{let n=new URL(t);return`${n.protocol}//${n.hostname}${n.pathname}`}catch{return t}}async function ga(t,n=8000){let f=Date.now();try{let p=t.replace(/\/+$/,"")+".json",a=new AbortController,l=setTimeout(()=>a.abort(),15000),g=await fetch(p,{headers:pa,signal:a.signal});if(clearTimeout(l),!g.ok)throw Error(`Reddit API ${g.status}`);let r=await g.json();if(!Array.isArray(r)||r.length<1)throw Error("Invalid Reddit API response structure");let w=r[0],$=r[1],_=w?.data?.children?.[0]?.data;if(!_)throw Error("No post data in Reddit response");let h=la(_,$,n);return{ok:!0,url:t,finalUrl:t,status:200,contentType:"text/markdown",lastModified:"",title:_.title||"Reddit Post",byline:`u/${_.author}`,siteName:`r/${_.subreddit}`,lang:"en",publishedTime:new Date(_.created_utc*1000).toISOString(),excerpt:_.selftext?.slice(0,300).replace(/\n/g," ")||"",markdown:h,contentLength:h.length,needsBrowser:!1,duration:Date.now()-f}}catch(p){return{ok:!1,url:t,finalUrl:t,status:0,error:`Reddit fetch failed: ${p.message}`,needsBrowser:!1,duration:Date.now()-f}}}function la(t,n,f){let p="";if(p+=`# ${t.title}
24
-
25
- `,p+=`**Subreddit:** r/${t.subreddit} | **Author:** u/${t.author} | **Score:** ${t.score}
26
-
27
- `,t.selftext)p+=t.selftext,p+=`
28
-
29
- `;else if(t.url)try{let a=new URL(t.url).hostname.toLowerCase();if(a!=="reddit.com"&&!a.endsWith(".reddit.com"))p+=`**Link:** ${t.url}
30
-
31
- `}catch{p+=`**Link:** ${t.url}
32
-
33
- `}if(n?.data?.children?.length>0){p+=`---
34
-
35
- ## Comments
36
-
37
- `;let a=n.data.children.filter((l)=>l.kind==="t1").slice(0,10);for(let l of a)p+=Nf(l.data,0),p+=`
38
- `}if(p.length>f)p=p.slice(0,f).trim()+`
39
-
40
- ... (truncated)`;return p}function Nf(t,n){if(!t||t.body==="[deleted]"||t.body==="[removed]")return"";let f="> ".repeat(n),p="";if(p+=`${f}**u/${t.author}** (${t.score} pts)
41
- `,p+=`${f}${t.body.replaceAll(`
42
- `,`
43
- `+f)}
44
- `,n<3&&t.replies?.data?.children){let a=t.replies.data.children.filter((l)=>l.kind==="t1");for(let l of a.slice(0,5))p+=`
45
- `+Nf(l.data,n+1)}return p}function Rt(t,n=8000){if(!t||t.length<=n)return t;let f=`
46
-
47
- [...content trimmed...]
48
-
49
- `,p=n-f.length,a=Math.floor(p*0.75),l=p-a,g=a;while(g>a-100&&t[g]!==`
50
- `)g--;if(g<=a-100)g=a;let r=t.length-l;while(r<t.length-l+100&&t[r]!==`
51
- `)r++;if(r>=t.length-l+100)r=t.length-l;let w=t.slice(0,g).trimEnd(),$=t.slice(r).trimStart();return`${w}${f}${$}`}function ya(t=process.env,n=process.execPath){let f=t.GREEDY_SEARCH_NODE||t.NODE_BINARY||t.NODE;if(f?.trim())return f.trim();let p=wa(n||"").toLowerCase();if(p==="node"||p==="node.exe")return n;return"node"}function Ta(t){if(!Array.isArray(t)||t.length===0)throw Error("cdp: args must be a non-empty array");if(t[0]==="test")return t.map((n,f)=>_f(n,f));if(!ia.has(t[0]))throw Error(`cdp: unknown subcommand '${t[0]}'`);return t.map((n,f)=>_f(n,f))}function _f(t,n){if(typeof t!=="string")throw Error(`cdp: argv[${n}] must be a string (got ${typeof t})`);if(t.includes("\x00"))throw Error(`cdp: argv[${n}] contains a null byte`);return t}function Lf(t,n=30000){return Aa(t,null,n)}function Aa(t,n=null,f=30000){let p=Ta(t);return new Promise((a,l)=>{let g=ra(ya(),[Na,...p],{stdio:[n==null?"ignore":"pipe","pipe","pipe"]});if(n!=null)g.stdin.write(n),g.stdin.end();let r="",w="";g.stdout.on("data",(_)=>r+=_),g.stderr.on("data",(_)=>w+=_);let $=setTimeout(()=>{g.kill(),l(Error(`cdp timeout: ${t[0]}`))},f);g.on("close",(_)=>{if(clearTimeout($),_===0)a(r.trim());else l(Error(w.trim()||`cdp exit ${_}`))})})}async function ef(t){await Lf(["evalraw",t,"Page.addScriptToEvaluateOnNewDocument",JSON.stringify({source:`
52
- (function() {
53
- // ── Runtime.enable / CDP detection masking ──────────────
54
- try { delete window.__REBROWSER_RUNTIME_ENABLE; } catch(_) {}
55
- try { delete window.__REBROWSER_DEVTOOLS; } catch(_) {}
56
- try { delete window.__nightmare; } catch(_) {}
57
- try { delete window.__phantom; } catch(_) {}
58
- try { delete window.callPhantom; } catch(_) {}
59
- try { delete window._phantom; } catch(_) {}
60
- try { delete window.Buffer; } catch(_) {}
61
-
62
- // Real Chrome without automation should not expose navigator.webdriver at all.
63
- // A literal false or an own-property getter returning undefined is itself a
64
- // common stealth tell; remove both instance and prototype properties when the
65
- // descriptor is configurable (as it is with --disable-blink-features).
66
- try { delete navigator.webdriver; } catch(_) {}
67
- try { delete Navigator.prototype.webdriver; } catch(_) {}
68
- Object.defineProperty(navigator, 'vendor', { get: () => 'Google Inc.', configurable: true });
69
- Object.defineProperty(navigator, 'platform', { get: () => 'Win32', configurable: true });
70
- Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 0, configurable: true });
71
- Object.defineProperty(navigator, 'pdfViewerEnabled', { get: () => true, configurable: true });
72
- Object.defineProperty(navigator, 'productSub', { get: () => '20030107', configurable: true });
73
- Object.defineProperty(navigator, 'product', { get: () => 'Gecko', configurable: true });
74
- var __greedyMimeTypes = null;
75
- function __makeMimeTypes() {
76
- var pdf = { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: null };
77
- var textPdf = { type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: null };
78
- try { Object.setPrototypeOf(pdf, MimeType.prototype); } catch(_) {}
79
- try { Object.setPrototypeOf(textPdf, MimeType.prototype); } catch(_) {}
80
- var m = [pdf, textPdf];
81
- try { Object.setPrototypeOf(m, MimeTypeArray.prototype); } catch(_) {}
82
- m.item = function item(i) { return this[i] || null; };
83
- m.namedItem = function namedItem(name) { return Array.prototype.find.call(this, function(x) { return x && x.type === name; }) || null; };
84
- return m;
85
- }
86
- Object.defineProperty(navigator, 'plugins', {
87
- get: () => {
88
- __greedyMimeTypes = __greedyMimeTypes || __makeMimeTypes();
89
- var plugin0 = { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' };
90
- var plugin1 = { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' };
91
- var plugin2 = { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' };
92
- try { Object.setPrototypeOf(plugin0, Plugin.prototype); } catch(_) {}
93
- try { Object.setPrototypeOf(plugin1, Plugin.prototype); } catch(_) {}
94
- try { Object.setPrototypeOf(plugin2, Plugin.prototype); } catch(_) {}
95
- var p = [plugin0, plugin1, plugin2];
96
- p.item = function item(i) { return this[i] || null; };
97
- p.namedItem = function namedItem(name) { return Array.prototype.find.call(this, function(x) { return x && x.name === name; }) || null; };
98
- p.refresh = function refresh() {};
99
- try { Object.setPrototypeOf(p, PluginArray.prototype); } catch(_) {}
100
- try {
101
- __greedyMimeTypes[0].enabledPlugin = p[0];
102
- __greedyMimeTypes[1].enabledPlugin = p[0];
103
- } catch(_) {}
104
- return p;
1
+ import { spawn } from "node:child_process";
2
+ import { nodeRuntimeCommand } from "../utils/node-runtime.mjs";
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ buildSourceRegistry,
8
+ classifySourceType,
9
+ computeCompositeScore,
10
+ mergeFetchDataIntoSources,
11
+ normalizeUrl,
12
+ trimText
13
+ } from "./sources.mjs";
14
+ import { parseStructuredJson } from "./synthesis.mjs";
15
+ import { ALL_ENGINES, RESEARCH_ENGINES } from "./constants.mjs";
16
+ import { runGeminiPrompt } from "./synthesis-runner.mjs";
17
+ import { classifyResearchComplexity } from "./scale-aware.mjs";
18
+ import { runSimpleResearchMode } from "./simple-research.mjs";
19
+ import { createProgressTracker } from "./progress.mjs";
20
+ const __dir = fileURLToPath(new URL(".", import.meta.url)).replace(/^\/([A-Z]:)/, "$1");
21
+ const SEARCH_BIN = join(__dir, "..", "..", "bin", "search.mjs");
22
+ const DEFAULT_RESEARCH_BUNDLE_ROOT = join(process.cwd(), ".pi", "greedysearch-research");
23
+ const MAX_PROMPT_CHARS = 28000;
24
+ function slugifyResearchName(value) {
25
+ const slug = String(value || "research").toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 60);
26
+ return slug || "research";
27
+ }
28
+ function uniqueStrings(items, limit = 1 / 0) {
29
+ const seen = new Set;
30
+ const out = [];
31
+ for (const item of items || []) {
32
+ const clean = trimText(String(item || ""), 1000);
33
+ if (!clean || seen.has(clean))
34
+ continue;
35
+ seen.add(clean);
36
+ out.push(clean);
37
+ if (out.length >= limit)
38
+ break;
39
+ }
40
+ return out;
41
+ }
42
+ async function fetchMultipleResearchSources(...args) {
43
+ const { fetchMultipleSources } = await import("./fetch-source.mjs");
44
+ return fetchMultipleSources(...args);
45
+ }
46
+ async function writeResearchSourcesToFiles(...args) {
47
+ const { writeSourcesToFiles } = await import("./file-sources.mjs");
48
+ return writeSourcesToFiles(...args);
49
+ }
50
+ export function clampResearchOptions({
51
+ breadth = 3,
52
+ iterations = 2,
53
+ maxSources
54
+ }) {
55
+ const safeBreadth = clampInt(breadth, 1, 5, 3);
56
+ const safeIterations = clampInt(iterations, 1, 3, 2);
57
+ const safeMaxSources = clampInt(maxSources ?? Math.max(5, safeBreadth * safeIterations * 2), 3, 12, 8);
58
+ return {
59
+ breadth: safeBreadth,
60
+ iterations: safeIterations,
61
+ maxSources: safeMaxSources
62
+ };
63
+ }
64
+ function clampInt(value, min, max, fallback) {
65
+ const n = Number.parseInt(String(value ?? ""), 10);
66
+ if (!Number.isFinite(n))
67
+ return fallback;
68
+ return Math.min(max, Math.max(min, n));
69
+ }
70
+ export function normalizeResearchQueries(plan, originalQuery, breadth, { expand = true, includeOriginal = true, exclude = [] } = {}) {
71
+ const rawQueries = Array.isArray(plan?.queries) ? plan.queries : [];
72
+ const queries = [];
73
+ const excluded = new Set([...exclude].map((item) => sanitizeResearchQuery(item).toLowerCase()));
74
+ for (const item of rawQueries) {
75
+ const query = typeof item === "string" ? item : item?.query;
76
+ const researchGoal = typeof item === "string" ? "" : item?.researchGoal || "";
77
+ addResearchQuery(queries, query, researchGoal, { exclude: excluded });
78
+ }
79
+ if (includeOriginal) {
80
+ addResearchQuery(queries, originalQuery, "Original user query", {
81
+ prepend: true,
82
+ exclude: excluded
83
+ });
84
+ }
85
+ if (expand) {
86
+ const expansionQueries = [
87
+ {
88
+ query: `${originalQuery} official docs GitHub`,
89
+ researchGoal: "Find primary project docs, repository details, and maintainer claims."
90
+ },
91
+ {
92
+ query: `${originalQuery} benchmarks limitations compatibility`,
93
+ researchGoal: "Validate performance claims and uncover unsupported APIs or caveats."
94
+ },
95
+ {
96
+ query: `${originalQuery} alternatives comparison production use cases`,
97
+ researchGoal: "Compare against conventional headless browsers and identify when to choose it."
98
+ },
99
+ {
100
+ query: `${originalQuery} anti bot detection Cloudflare screenshots visual rendering`,
101
+ researchGoal: "Check automation risks, rendering gaps, screenshots, and bot-detection behavior."
102
+ }
103
+ ];
104
+ for (const item of expansionQueries) {
105
+ if (queries.length >= breadth)
106
+ break;
107
+ addResearchQuery(queries, item.query, item.researchGoal, {
108
+ exclude: excluded
109
+ });
110
+ }
111
+ }
112
+ return queries.slice(0, breadth);
113
+ }
114
+ function addResearchQuery(queries, query, researchGoal = "", { prepend = false, exclude = new Set } = {}) {
115
+ if (!query || typeof query !== "string")
116
+ return;
117
+ const clean = sanitizeResearchQuery(query);
118
+ if (!clean || exclude.has(clean.toLowerCase()) || queries.some((q) => q.query.toLowerCase() === clean.toLowerCase())) {
119
+ return;
120
+ }
121
+ const item = { query: clean, researchGoal: trimText(researchGoal, 320) };
122
+ if (prepend)
123
+ queries.unshift(item);
124
+ else
125
+ queries.push(item);
126
+ }
127
+ function sanitizeResearchQuery(query) {
128
+ return collapseWhitespace(stripMarkdownLinks(String(query)));
129
+ }
130
+ function stripMarkdownLinks(value) {
131
+ let output = "";
132
+ let index = 0;
133
+ while (index < value.length) {
134
+ const openLabel = value.indexOf("[", index);
135
+ if (openLabel === -1) {
136
+ output += value.slice(index);
137
+ break;
138
+ }
139
+ const closeLabel = value.indexOf("]", openLabel + 1);
140
+ if (closeLabel === -1 || value[closeLabel + 1] !== "(" || closeLabel === openLabel + 1) {
141
+ output += value.slice(index, openLabel + 1);
142
+ index = openLabel + 1;
143
+ continue;
144
+ }
145
+ const closeUrl = value.indexOf(")", closeLabel + 2);
146
+ if (closeUrl === -1) {
147
+ output += value.slice(index, openLabel + 1);
148
+ index = openLabel + 1;
149
+ continue;
150
+ }
151
+ const url = value.slice(closeLabel + 2, closeUrl).trimStart();
152
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
153
+ output += value.slice(index, openLabel + 1);
154
+ index = openLabel + 1;
155
+ continue;
156
+ }
157
+ output += value.slice(index, openLabel);
158
+ output += value.slice(openLabel + 1, closeLabel);
159
+ index = closeUrl + 1;
160
+ }
161
+ return output;
162
+ }
163
+ function collapseWhitespace(value) {
164
+ let output = "";
165
+ let previousWasWhitespace = false;
166
+ for (const char of value) {
167
+ if (char === " " || char === "\t" || char === `
168
+ ` || char === "\r") {
169
+ if (!previousWasWhitespace)
170
+ output += " ";
171
+ previousWasWhitespace = true;
172
+ } else {
173
+ output += char;
174
+ previousWasWhitespace = false;
175
+ }
176
+ }
177
+ return output.trim();
178
+ }
179
+ export function tokenSet(value) {
180
+ return new Set(String(value).toLowerCase().normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").split(/[^\w]+/).filter((t) => t.length > 1));
181
+ }
182
+ export function jaccardSimilarity(a, b) {
183
+ const tokensA = tokenSet(a);
184
+ const tokensB = tokenSet(b);
185
+ const unionSize = new Set([...tokensA, ...tokensB]).size;
186
+ if (unionSize === 0)
187
+ return 1;
188
+ let intersection = 0;
189
+ for (const t of tokensA) {
190
+ if (tokensB.has(t))
191
+ intersection++;
192
+ }
193
+ return intersection / unionSize;
194
+ }
195
+ export function isDuplicateQuery(query, usedQueries, { threshold = 0.75, roundIndex = 0, originalQuery = null } = {}) {
196
+ const normalized = sanitizeResearchQuery(query).toLowerCase();
197
+ if (usedQueries.has(normalized))
198
+ return true;
199
+ if (originalQuery && roundIndex > 0 && normalized === sanitizeResearchQuery(originalQuery).toLowerCase()) {
200
+ return true;
201
+ }
202
+ for (const used of usedQueries) {
203
+ if (jaccardSimilarity(normalized, used) >= threshold) {
204
+ return true;
205
+ }
206
+ }
207
+ return false;
208
+ }
209
+ function buildQualityEvaluationPrompt(originalQuery, rounds, allLearnings, allGaps) {
210
+ const roundSummaries = rounds.map((round) => ({
211
+ queries: round.queries?.map((q) => q.query || "") || [],
212
+ learnings: round.learnings || [],
213
+ gaps: round.gaps || []
214
+ }));
215
+ return [
216
+ "You are evaluating the quality of an iterative research run.",
217
+ "Assess coverage across: official sources, limitations/risks, benchmarks/performance, production usage, and counter-evidence.",
218
+ "Score each dimension 0-10. Overall score 0-10.",
219
+ "Identify remaining knowledge gaps.",
220
+ "Propose targeted next actions (search queries or direct URL fetches) that would most improve the research.",
221
+ "Decide whether to continue or stop.",
222
+ "terminationReason must be one of: quality_threshold | max_rounds | no_novel_actions | insufficient_evidence.",
223
+ "",
224
+ `Original research question: ${originalQuery}`,
225
+ `Rounds completed: ${JSON.stringify(roundSummaries, null, 2)}`,
226
+ `Accumulated learnings: ${JSON.stringify(allLearnings.slice(0, 12), null, 2)}`,
227
+ `Known gaps: ${JSON.stringify(allGaps.slice(0, 8), null, 2)}`,
228
+ "",
229
+ "Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:",
230
+ "BEGIN_JSON",
231
+ JSON.stringify({
232
+ score: 7.5,
233
+ coverage: {
234
+ officialSources: 8,
235
+ limitations: 5,
236
+ benchmarks: 7,
237
+ productionUseCases: 6,
238
+ counterEvidence: 4
239
+ },
240
+ knowledgeGaps: ["specific gap or missing evidence"],
241
+ shouldContinue: true,
242
+ terminationReason: "quality_threshold",
243
+ nextActions: [
244
+ { type: "search", query: "targeted search query" },
245
+ { type: "fetchUrl", url: "https://example.com/primary-doc" }
246
+ ]
247
+ }, null, 2),
248
+ "END_JSON"
249
+ ].join(`
250
+ `);
251
+ }
252
+ export function buildFallbackQueriesFromGaps(gaps, originalQuery, usedQueries, nextBreadth, roundIndex) {
253
+ const fallbacks = [];
254
+ const angles = [
255
+ {
256
+ template: (gap) => `${gap} official documentation`,
257
+ label: "official docs"
105
258
  },
106
- configurable: true,
107
- });
108
- Object.defineProperty(navigator, 'mimeTypes', {
109
- get: () => {
110
- __greedyMimeTypes = __greedyMimeTypes || __makeMimeTypes();
111
- return __greedyMimeTypes;
259
+ {
260
+ template: (gap) => `${gap} GitHub issues discussions`,
261
+ label: "community signals"
112
262
  },
113
- configurable: true,
114
- });
115
- Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'], configurable: true });
116
- try {
117
- Object.defineProperty(navigator, 'connection', { get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10, downlinkMax: Infinity, saveData: false }), configurable: true });
118
- } catch(_) {}
119
- if (!navigator.mediaDevices) {
120
- Object.defineProperty(navigator, 'mediaDevices', {
121
- get: () => ({
122
- enumerateDevices: () => Promise.resolve([
123
- { deviceId: 'default', kind: 'audioinput', label: '', groupId: 'default' },
124
- { deviceId: 'default', kind: 'audiooutput', label: '', groupId: 'default' },
125
- { deviceId: '', kind: 'videoinput', label: '', groupId: '' },
126
- ]),
127
- getUserMedia: () => Promise.reject(new DOMException('NotAllowedError')),
128
- getDisplayMedia: () => Promise.reject(new DOMException('NotAllowedError')),
129
- }),
130
- configurable: true,
131
- });
263
+ {
264
+ template: (gap) => `${gap} benchmarks performance comparison`,
265
+ label: "benchmarks"
266
+ },
267
+ {
268
+ template: (gap) => `${gap} limitations risks caveats`,
269
+ label: "limitations"
270
+ },
271
+ {
272
+ template: (gap) => `${gap} production deployment experience`,
273
+ label: "production usage"
274
+ },
275
+ {
276
+ template: (gap) => `${originalQuery} ${gap} counter evidence`,
277
+ label: "counter-evidence"
278
+ }
279
+ ];
280
+ for (let i = 0;i < gaps.length && fallbacks.length < nextBreadth; i++) {
281
+ const gap = gaps[i];
282
+ const angle = angles[i % angles.length];
283
+ const candidate = angle.template(gap);
284
+ if (!isDuplicateQuery(candidate, usedQueries, { roundIndex })) {
285
+ fallbacks.push({
286
+ query: candidate,
287
+ researchGoal: `Gap-driven: ${gap} (${angle.label})`
288
+ });
289
+ }
132
290
  }
133
- // ── Missing platform APIs (headless often lacks these) ─
291
+ return fallbacks;
292
+ }
293
+ async function evaluateResearchQuality(originalQuery, rounds, allLearnings, allGaps, qualityHistory) {
134
294
  try {
135
- if (!navigator.share) {
136
- navigator.share = function() { return Promise.reject(new Error('NotAllowedError')); };
295
+ const rawEvaluation = await runGeminiPrompt(buildQualityEvaluationPrompt(originalQuery, rounds, allLearnings, allGaps), { timeoutMs: 120000 });
296
+ const evaluation = parseGeminiJson(rawEvaluation, {});
297
+ const score = typeof evaluation.score === "number" ? Math.min(10, Math.max(0, evaluation.score)) : qualityHistory.length > 0 ? qualityHistory[qualityHistory.length - 1] : 5;
298
+ const gaps = Array.isArray(evaluation.knowledgeGaps) ? evaluation.knowledgeGaps.map((g) => String(g)).filter(Boolean).slice(0, 6) : [];
299
+ const nextActions = Array.isArray(evaluation.nextActions) ? evaluation.nextActions.slice(0, 5) : [];
300
+ const shouldContinue = typeof evaluation.shouldContinue === "boolean" ? evaluation.shouldContinue : score < 8;
301
+ const terminationReason = evaluation.terminationReason || null;
302
+ return {
303
+ score,
304
+ coverage: evaluation.coverage || {},
305
+ knowledgeGaps: gaps,
306
+ shouldContinue,
307
+ nextActions,
308
+ terminationReason: terminationReason || (score >= 8.5 ? "quality_threshold" : null),
309
+ evaluationError: ""
310
+ };
311
+ } catch (error) {
312
+ process.stderr.write(`[greedysearch] Quality evaluation failed: ${error.message}
313
+ `);
314
+ return {
315
+ score: qualityHistory.length > 0 ? qualityHistory[qualityHistory.length - 1] : 5,
316
+ coverage: {},
317
+ knowledgeGaps: [],
318
+ shouldContinue: true,
319
+ nextActions: [],
320
+ terminationReason: null,
321
+ evaluationError: error.message
322
+ };
323
+ }
324
+ }
325
+ function summarizeEngineAnswers(result) {
326
+ const summaries = {};
327
+ for (const engine of Object.keys(result || {}).filter((key) => !key.startsWith("_"))) {
328
+ const value = result?.[engine];
329
+ if (!value)
330
+ continue;
331
+ summaries[engine] = value.error ? { status: "error", error: String(value.error) } : {
332
+ status: "ok",
333
+ answer: trimText(value.answer || "", 1400),
334
+ sources: Array.isArray(value.sources) ? value.sources.slice(0, 5).map((s) => ({
335
+ title: trimText(s.title || "", 160),
336
+ url: s.url || ""
337
+ })) : []
338
+ };
339
+ }
340
+ return summaries;
341
+ }
342
+ function buildResearchActionPrompt(query, breadth, learnings = [], gaps = [], usedUrls = []) {
343
+ const gapSection = gaps.length > 0 ? `
344
+ Known knowledge gaps to target:
345
+ ${gaps.map((g) => `- ${g}`).join(`
346
+ `)}` : "";
347
+ const usedUrlSection = usedUrls.length > 0 ? `
348
+ Already fetched URLs (do not re-fetch):
349
+ ${usedUrls.map((u) => `- ${u}`).join(`
350
+ `)}` : "";
351
+ return [
352
+ "You are planning web research actions for a multi-engine search agent.",
353
+ "You can plan two types of actions:",
354
+ ' - "search": run a multi-engine SERP search query',
355
+ ' - "fetchUrl": directly fetch a specific URL (docs page, GitHub repo, specification, etc.)',
356
+ 'Prefer "fetchUrl" when a specific primary source URL is known or obvious.',
357
+ 'Use "search" for broad discovery or when specific URLs are unknown.',
358
+ `Return at most ${breadth} actions.`,
359
+ "Avoid near-duplicate search queries and already-fetched URLs.",
360
+ "",
361
+ `User topic: ${query}`,
362
+ learnings.length ? `
363
+ Prior learnings to build on:
364
+ ${learnings.map((l) => `- ${l}`).join(`
365
+ `)}` : "",
366
+ gapSection,
367
+ usedUrlSection,
368
+ "",
369
+ "Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:",
370
+ "BEGIN_JSON",
371
+ JSON.stringify({
372
+ actions: [
373
+ {
374
+ type: "search",
375
+ query: "specific search query",
376
+ researchGoal: "what this action should clarify"
377
+ },
378
+ {
379
+ type: "fetchUrl",
380
+ url: "https://example.com/docs/relevant-page",
381
+ researchGoal: "extract specific information from this page"
382
+ }
383
+ ]
384
+ }, null, 2),
385
+ "END_JSON"
386
+ ].join(`
387
+ `);
388
+ }
389
+ export function validateAction(action) {
390
+ if (!action || typeof action !== "object")
391
+ return null;
392
+ const type = action.type;
393
+ const researchGoal = trimText(action.researchGoal || "", 320);
394
+ if (type === "search") {
395
+ if (action.query == null)
396
+ return null;
397
+ const query = sanitizeResearchQuery(action.query);
398
+ return query ? { type: "search", query, researchGoal } : null;
399
+ }
400
+ if (type === "fetchUrl") {
401
+ if (action.url == null)
402
+ return null;
403
+ const url = normalizeUrl(action.url);
404
+ return url ? { type: "fetchUrl", url, researchGoal } : null;
405
+ }
406
+ return null;
407
+ }
408
+ async function executeResearchAction(action, { locale = null, short = true, usedQueries, usedUrls, maxChars = 8000 } = {}) {
409
+ if (action.type === "search") {
410
+ const normalizedQuery = sanitizeResearchQuery(action.query).toLowerCase();
411
+ usedQueries.add(normalizedQuery);
412
+ try {
413
+ const result = await runFastAllSearch(action.query, { locale, short });
414
+ const sources = buildSourceRegistry(result, action.query);
415
+ return {
416
+ ok: true,
417
+ action,
418
+ result,
419
+ sources
420
+ };
421
+ } catch (error) {
422
+ return {
423
+ ok: false,
424
+ action,
425
+ error: error.message,
426
+ sources: []
427
+ };
428
+ }
429
+ }
430
+ if (action.type === "fetchUrl") {
431
+ const normalizedUrl = normalizeUrl(action.url);
432
+ if (usedUrls.has(normalizedUrl)) {
433
+ return {
434
+ ok: false,
435
+ action,
436
+ error: `URL already fetched: ${normalizedUrl}`,
437
+ sources: []
438
+ };
439
+ }
440
+ try {
441
+ const fetchResult = await fetchSingleResearchSource(normalizedUrl, maxChars);
442
+ usedUrls.add(normalizedUrl);
443
+ const domain = getDomainFromUrl(normalizedUrl);
444
+ const source = {
445
+ id: "",
446
+ canonicalUrl: fetchResult.finalUrl || normalizedUrl,
447
+ displayUrl: fetchResult.url || normalizedUrl,
448
+ domain,
449
+ title: fetchResult.title || normalizedUrl,
450
+ engines: ["fetch"],
451
+ engineCount: 1,
452
+ perEngine: {},
453
+ sourceType: classifySourceType(domain, fetchResult.title || "", fetchResult.finalUrl || normalizedUrl),
454
+ isOfficial: false,
455
+ smartScore: 0,
456
+ fetch: {
457
+ attempted: true,
458
+ ok: !fetchResult.error && (fetchResult.contentChars || 0) > 100,
459
+ status: fetchResult.status || null,
460
+ finalUrl: fetchResult.finalUrl || normalizedUrl,
461
+ content: fetchResult.content || "",
462
+ contentChars: fetchResult.contentChars || 0,
463
+ snippet: fetchResult.snippet || "",
464
+ error: fetchResult.error || ""
465
+ }
466
+ };
467
+ return {
468
+ ok: true,
469
+ action,
470
+ result: null,
471
+ sources: [source],
472
+ fetchResult: {
473
+ id: source.id,
474
+ url: normalizedUrl,
475
+ finalUrl: fetchResult.finalUrl || normalizedUrl,
476
+ title: fetchResult.title || "",
477
+ content: fetchResult.content || "",
478
+ contentChars: fetchResult.contentChars || 0,
479
+ snippet: fetchResult.snippet || "",
480
+ status: fetchResult.status || null,
481
+ error: fetchResult.error || "",
482
+ source: fetchResult.source || "http",
483
+ duration: fetchResult.duration || 0
484
+ }
485
+ };
486
+ } catch (error) {
487
+ return {
488
+ ok: false,
489
+ action,
490
+ error: error.message,
491
+ sources: []
492
+ };
137
493
  }
138
- } catch(_) {}
494
+ }
495
+ return {
496
+ ok: false,
497
+ action,
498
+ error: `Unknown action type: ${action.type}`,
499
+ sources: []
500
+ };
501
+ }
502
+ async function fetchSingleResearchSource(url, maxChars) {
503
+ const { fetchSourceContent } = await import("./fetch-source.mjs");
504
+ return await fetchSourceContent(url, maxChars);
505
+ }
506
+ function getDomainFromUrl(rawUrl) {
139
507
  try {
140
- if (!navigator.contentIndex) {
141
- Object.defineProperty(navigator, 'contentIndex', { get: () => ({ add: function() {}, delete: function() {}, getAll: function() { return Promise.resolve([]); } }), configurable: true });
142
- }
143
- } catch(_) {}
144
-
145
- if (!window.chrome) {
146
- window.chrome = {
147
- app: { isInstalled: false, InstallState: {}, RunningState: {} },
148
- runtime: {
149
- OnInstalledReason: {}, OnRestartRequiredReason: {}, PlatformArch: {}, PlatformNaclArch: {}, PlatformOs: {}, RequestUpdateCheckStatus: {},
150
- connect: () => ({}), sendMessage: () => {}, onMessage: { addListener: () => {} }
151
- },
152
- loadTimes: function() { return { requestTime: 0, startLoadTime: Date.now() - 5000, commitLoadTime: Date.now() - 3000, finishDocumentLoadTime: Date.now() - 2000, finishLoadTime: Date.now() - 1000, firstPaintTime: Date.now() - 800, navigationType: 'Other', wasFetchedViaSpdy: true, wasNpnNegotiated: true, npnNegotiatedProtocol: 'h2', wasAlternateProtocolAvailable: false, connectionInfo: 'http/2' }; },
153
- csi: function() { var t = Date.now(); return { onloadT: t - 2000, startE: t - 5000, pageT: 'back', tran: 2 }; },
508
+ const domain = new URL(rawUrl).hostname.toLowerCase();
509
+ return domain.replace(/^www\./, "");
510
+ } catch {
511
+ return "";
512
+ }
513
+ }
514
+ async function normalizeGitHubFetchActions(actions, usedUrls) {
515
+ const normalized = [];
516
+ const { parseGitHubUrl } = await import("../github.mjs");
517
+ for (const action of actions) {
518
+ if (action.type !== "fetchUrl") {
519
+ normalized.push(action);
520
+ continue;
521
+ }
522
+ const parsed = parseGitHubUrl(action.url);
523
+ if (!parsed || parsed.type !== "root") {
524
+ normalized.push(action);
525
+ continue;
526
+ }
527
+ const { owner, repo } = parsed;
528
+ const base = `https://github.com/${owner}/${repo}`;
529
+ if (usedUrls.has(base)) {
530
+ continue;
531
+ }
532
+ const targets = [
533
+ base
534
+ ];
535
+ const candidatePaths = [
536
+ `${base}/blob/main/CONTRIBUTING.md`,
537
+ `${base}/blob/master/CONTRIBUTING.md`,
538
+ `${base}/blob/main/CHANGELOG.md`,
539
+ `${base}/blob/master/CHANGELOG.md`,
540
+ `${base}/blob/main/docs/README.md`
541
+ ];
542
+ for (const candidate of candidatePaths) {
543
+ if (targets.length >= 3)
544
+ break;
545
+ if (!usedUrls.has(candidate)) {
546
+ targets.push(candidate);
547
+ }
548
+ }
549
+ for (const url of targets) {
550
+ normalized.push({
551
+ type: "fetchUrl",
552
+ url,
553
+ researchGoal: action.researchGoal || `Fetch GitHub content for ${owner}/${repo}`
554
+ });
555
+ }
556
+ }
557
+ return normalized;
558
+ }
559
+ export function parseActionPlan(rawJson, breadth) {
560
+ const parsed = parseStructuredJson(rawJson?.answer || "") || {};
561
+ const rawActions = Array.isArray(parsed?.actions) ? parsed.actions : [];
562
+ const actions = [];
563
+ for (const item of rawActions) {
564
+ const action = validateAction(item);
565
+ if (action && actions.length < breadth) {
566
+ actions.push(action);
567
+ }
568
+ }
569
+ return actions;
570
+ }
571
+ export function queriesToActions(queries) {
572
+ return (queries || []).map((q) => ({
573
+ type: "search",
574
+ query: typeof q === "string" ? q : q.query,
575
+ researchGoal: typeof q === "string" ? "" : q.researchGoal || ""
576
+ })).filter((a) => a.query);
577
+ }
578
+ function sourceKey(source) {
579
+ return normalizeUrl(source?.finalUrl || source?.canonicalUrl || source?.url || "") || source?.id || "";
580
+ }
581
+ function normalizeEvidenceExtractions(payload, fetchedSources) {
582
+ const raw = Array.isArray(payload?.extractions) ? payload.extractions : [];
583
+ const byUrl = new Map;
584
+ const byId = new Map;
585
+ for (const source of fetchedSources || []) {
586
+ if (source?.id)
587
+ byId.set(String(source.id), source);
588
+ const key = sourceKey(source);
589
+ if (key)
590
+ byUrl.set(key, source);
591
+ }
592
+ return raw.map((item) => {
593
+ const source = byId.get(String(item?.sourceId || "")) || byUrl.get(normalizeUrl(item?.url || "") || "");
594
+ const sourceId = String(item?.sourceId || source?.id || "");
595
+ const url = normalizeUrl(item?.url || source?.finalUrl || source?.url || "");
596
+ const answers = Array.isArray(item?.answers) ? item.answers.map((answer) => ({
597
+ id: String(answer?.id || ""),
598
+ evidence: trimText(answer?.evidence || "", 500),
599
+ sourceIds: [sourceId].filter(Boolean)
600
+ })).filter((answer) => answer.id) : [];
601
+ return {
602
+ sourceId,
603
+ url,
604
+ title: source?.title || item?.title || "",
605
+ rational: trimText(item?.rational || "", 700),
606
+ evidence: trimText(item?.evidence || "", 1600),
607
+ summary: trimText(item?.summary || "", 700),
608
+ answers,
609
+ newQuestions: uniqueStrings(item?.newQuestions || [], 6)
154
610
  };
611
+ }).filter((item) => item.sourceId || item.url || item.summary || item.evidence);
612
+ }
613
+ function buildEvidenceExtractionPrompt(originalQuery, questions, fetchedSources, alreadyExtracted = new Set) {
614
+ const openQuestions = (questions || []).filter((q) => q.status !== "closed").slice(0, 12).map((q) => ({ id: q.id, question: q.question }));
615
+ const sourceSnippets = (fetchedSources || []).filter((source) => source?.content || source?.snippet).filter((source) => !alreadyExtracted.has(sourceKey(source))).slice(0, 6).map((source, index) => ({
616
+ id: source.id || `F${index + 1}`,
617
+ title: source.title || "",
618
+ url: source.finalUrl || source.url || source.canonicalUrl || "",
619
+ content: trimText(source.content || source.snippet || "", 5000)
620
+ }));
621
+ return [
622
+ "You are doing goal-based evidence extraction for an iterative research run.",
623
+ "For each source, extract only information that helps answer the open questions.",
624
+ "Use original wording/details where useful. Do not invent answers; leave questions open if evidence is insufficient.",
625
+ "If a source answers one or more tracked questions, identify those question IDs explicitly.",
626
+ "Also propose genuinely new sub-questions discovered from the evidence.",
627
+ "",
628
+ `Original research question: ${originalQuery}`,
629
+ `Open question ledger: ${JSON.stringify(openQuestions, null, 2)}`,
630
+ `Fetched sources: ${JSON.stringify(sourceSnippets, null, 2)}`,
631
+ "",
632
+ "Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:",
633
+ "BEGIN_JSON",
634
+ JSON.stringify({
635
+ extractions: [
636
+ {
637
+ sourceId: "S1",
638
+ url: "https://example.com/source",
639
+ rational: "why this source matters for the goal",
640
+ evidence: "specific quoted/paraphrased evidence with numbers, dates, caveats",
641
+ summary: "concise contribution to the research question",
642
+ answers: [
643
+ {
644
+ id: "Q1",
645
+ evidence: "brief evidence that closes the question"
646
+ }
647
+ ],
648
+ newQuestions: ["new sub-question raised by this source"]
649
+ }
650
+ ]
651
+ }, null, 2),
652
+ "END_JSON"
653
+ ].join(`
654
+ `);
655
+ }
656
+ export async function extractEvidenceFromSources({
657
+ query,
658
+ questions,
659
+ fetchedSources,
660
+ extractedSourceKeys
661
+ }) {
662
+ const pending = (fetchedSources || []).filter((source) => (source?.content || source?.snippet) && !extractedSourceKeys.has(sourceKey(source)));
663
+ if (pending.length === 0)
664
+ return { evidence: [], error: "" };
665
+ try {
666
+ const raw = await runGeminiPrompt(buildEvidenceExtractionPrompt(query, questions, pending, extractedSourceKeys), { timeoutMs: 120000 });
667
+ const parsed = parseGeminiJson(raw, { extractions: [] });
668
+ const evidence = normalizeEvidenceExtractions(parsed, pending);
669
+ for (const source of pending) {
670
+ const key = sourceKey(source);
671
+ if (key)
672
+ extractedSourceKeys.add(key);
673
+ }
674
+ return { evidence, error: "" };
675
+ } catch (error) {
676
+ return { evidence: [], error: error.message || String(error) };
155
677
  }
156
- var __greedyNativeFns = [];
157
- function __markNative(fn) { try { __greedyNativeFns.push(fn); } catch(_) {} return fn; }
158
-
159
- var origQuery = navigator.permissions?.query;
160
- if (origQuery) {
161
- navigator.permissions.query = __markNative(function query(params) {
162
- if (params && params.name === 'notifications') return Promise.resolve({ state: Notification.permission || 'default', onchange: null });
163
- return origQuery.apply(this, arguments);
678
+ }
679
+ export function buildEvidenceAndLearningPrompt(originalQuery, questions, roundQueries, searchSummaries, pendingSources, fetchedSources, evidenceItems = []) {
680
+ const openQuestions = (questions || []).filter((q) => q.status !== "closed").slice(0, 12).map((q) => ({ id: q.id, question: q.question }));
681
+ const extractionSources = (pendingSources || []).filter((source) => source?.content || source?.snippet);
682
+ const learningSources = (fetchedSources || []).filter((source) => source?.content || source?.snippet);
683
+ const assemblePrompt = ({
684
+ extractionCount,
685
+ extractionLimit,
686
+ learningCount,
687
+ learningLimit
688
+ }) => {
689
+ const extractionSourceSnippets = extractionSources.slice(0, extractionCount).map((source, index) => ({
690
+ id: source.id || `F${index + 1}`,
691
+ title: source.title || "",
692
+ url: source.finalUrl || source.url || source.canonicalUrl || "",
693
+ content: trimText(source.content || source.snippet || "", extractionLimit)
694
+ }));
695
+ const learningSourceSnippets = learningSources.slice(0, learningCount).map((source, index) => ({
696
+ id: `F${index + 1}`,
697
+ title: source.title || "",
698
+ url: source.finalUrl || source.url || "",
699
+ snippet: trimText(source.content || source.snippet || "", learningLimit)
700
+ }));
701
+ return [
702
+ "You are doing two combined research tasks for one round of an iterative research run. Perform BOTH tasks and return a single combined JSON object.",
703
+ "",
704
+ "TASK A — Goal-based evidence extraction:",
705
+ "For each source under 'Sources for evidence extraction', extract only information that helps answer the open questions.",
706
+ "Use original wording/details where useful. Do not invent answers; leave questions open if evidence is insufficient.",
707
+ "If a source answers one or more tracked questions, identify those question IDs explicitly.",
708
+ "Also propose genuinely new sub-questions discovered from the evidence.",
709
+ "",
710
+ "TASK B — Compact research-state learning extraction:",
711
+ "Using the round queries, question ledger, extracted source evidence, engine summaries, and fetched source snippets below, create dense, non-overlapping learnings with exact names, numbers, dates, limitations, and caveats where available.",
712
+ "Also propose follow-up search queries that would most improve confidence or fill gaps.",
713
+ "",
714
+ `Original research question: ${originalQuery}`,
715
+ `Open question ledger: ${JSON.stringify(openQuestions)}`,
716
+ `Round queries: ${JSON.stringify(roundQueries)}`,
717
+ `Question ledger: ${JSON.stringify(questions)}`,
718
+ `Extracted source evidence so far: ${JSON.stringify(evidenceItems.slice(-12))}`,
719
+ `Engine summaries: ${JSON.stringify(searchSummaries)}`,
720
+ `Sources for evidence extraction (Task A): ${JSON.stringify(extractionSourceSnippets)}`,
721
+ `Fetched source snippets (Task B context): ${JSON.stringify(learningSourceSnippets)}`,
722
+ "",
723
+ "Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers, combining both tasks into one object:",
724
+ "BEGIN_JSON",
725
+ JSON.stringify({
726
+ extractions: [
727
+ {
728
+ sourceId: "S1",
729
+ url: "https://example.com/source",
730
+ rational: "why this source matters for the goal",
731
+ evidence: "specific quoted/paraphrased evidence with numbers, dates, caveats",
732
+ summary: "concise contribution to the research question",
733
+ answers: [
734
+ {
735
+ id: "Q1",
736
+ evidence: "brief evidence that closes the question"
737
+ }
738
+ ],
739
+ newQuestions: ["new sub-question raised by this source"]
740
+ }
741
+ ],
742
+ learnings: ["concise, information-dense learning"],
743
+ answeredQuestions: [
744
+ {
745
+ id: "Q1",
746
+ evidence: "brief evidence that closes this question",
747
+ sourceIds: ["S1"]
748
+ }
749
+ ],
750
+ newQuestions: ["new sub-question discovered from the evidence"],
751
+ followUpQueries: ["specific next search query"],
752
+ gaps: ["important uncertainty or missing evidence"]
753
+ }, null, 2),
754
+ "END_JSON"
755
+ ].join(`
756
+ `);
757
+ };
758
+ let extractionCount = Math.min(4, extractionSources.length);
759
+ let extractionLimit = 3000;
760
+ let learningCount = Math.min(6, learningSources.length);
761
+ let learningLimit = 2000;
762
+ let prompt = assemblePrompt({
763
+ extractionCount,
764
+ extractionLimit,
765
+ learningCount,
766
+ learningLimit
767
+ });
768
+ let trimmedToFit = false;
769
+ const minimumSourceChars = 600;
770
+ while (prompt.length > MAX_PROMPT_CHARS) {
771
+ if (extractionLimit > minimumSourceChars || learningLimit > minimumSourceChars) {
772
+ extractionLimit = Math.max(minimumSourceChars, Math.floor(extractionLimit / 2));
773
+ learningLimit = Math.max(minimumSourceChars, Math.floor(learningLimit / 2));
774
+ } else if (learningCount > 1) {
775
+ learningCount -= 1;
776
+ } else if (extractionCount > 1) {
777
+ extractionCount -= 1;
778
+ } else if (extractionLimit > 1 || learningLimit > 1) {
779
+ extractionLimit = Math.max(1, Math.floor(extractionLimit / 2));
780
+ learningLimit = Math.max(1, Math.floor(learningLimit / 2));
781
+ } else {
782
+ throw new Error(`[greedysearch] evidence/learning prompt exceeds Gemini input cap after source trimming: ${prompt.length} chars`);
783
+ }
784
+ trimmedToFit = true;
785
+ prompt = assemblePrompt({
786
+ extractionCount,
787
+ extractionLimit,
788
+ learningCount,
789
+ learningLimit
164
790
  });
165
791
  }
792
+ if (trimmedToFit) {
793
+ console.error(`[greedysearch] evidence/learning prompt trimmed to fit Gemini input cap: ${prompt.length} chars`);
794
+ }
795
+ return prompt;
796
+ }
797
+ export async function extractEvidenceAndLearnings({
798
+ query,
799
+ questions,
800
+ fetchedSources,
801
+ extractedSourceKeys,
802
+ roundQueries,
803
+ searchSummaries,
804
+ evidenceItems = []
805
+ }) {
806
+ const pending = (fetchedSources || []).filter((source) => (source?.content || source?.snippet) && !extractedSourceKeys.has(sourceKey(source)));
807
+ let evidence = [];
808
+ let evidenceError = "";
809
+ let learningPayload = { learnings: [], followUpQueries: [], gaps: [] };
810
+ let learningError = "";
166
811
  try {
167
- var getParam = WebGLRenderingContext.prototype.getParameter;
168
- WebGLRenderingContext.prototype.getParameter = __markNative(function getParameter(p) {
169
- if (p === 37445) return 'Intel Inc.';
170
- if (p === 37446) return 'Intel Iris OpenGL Engine';
171
- return getParam.call(this, p);
812
+ const raw = await runGeminiPrompt(buildEvidenceAndLearningPrompt(query, questions, roundQueries, searchSummaries, pending, fetchedSources, evidenceItems), { timeoutMs: 180000 });
813
+ const parsed = parseGeminiJson(raw, {});
814
+ evidence = normalizeEvidenceExtractions(parsed, pending);
815
+ for (const source of pending) {
816
+ const key = sourceKey(source);
817
+ if (key)
818
+ extractedSourceKeys.add(key);
819
+ }
820
+ learningPayload = { ...learningPayload, ...parsed };
821
+ } catch (error) {
822
+ const message = error.message || String(error);
823
+ evidenceError = message;
824
+ learningError = message;
825
+ }
826
+ return { evidence, evidenceError, learningPayload, learningError };
827
+ }
828
+ export function buildFinalReportPrompt(originalQuery, rounds, sources, questions = [], evidenceItems = []) {
829
+ const learnings = rounds.flatMap((round) => round.learnings || []);
830
+ const gaps = rounds.flatMap((round) => round.gaps || []);
831
+ const sourceRegistry = sources.slice(0, 12).map((source) => ({
832
+ id: source.id,
833
+ title: source.title,
834
+ domain: source.domain,
835
+ url: source.canonicalUrl,
836
+ type: source.sourceType,
837
+ engines: source.engines,
838
+ fetch: source.fetch?.attempted ? {
839
+ ok: source.fetch.ok,
840
+ snippet: trimText(source.fetch.snippet || "", 1200),
841
+ publishedTime: source.fetch.publishedTime || ""
842
+ } : undefined
843
+ }));
844
+ return [
845
+ "You are writing the final research report for an iterative deep-research run.",
846
+ "Produce a thorough markdown report organized into clear sections.",
847
+ "",
848
+ "Use the learnings and source registry below. Every substantive claim MUST be backed by an [S1] citation.",
849
+ 'Where engines disagree, surface the conflicting claims explicitly in the "differences" array.',
850
+ 'Include a "Key Claims" structure that maps each distinct claim to its supporting source IDs.',
851
+ "",
852
+ "Report structure:",
853
+ "1. ## Summary — A 2-4 sentence executive summary of findings",
854
+ "2. ## Key Findings — The main findings, organized by theme or question, each with inline citations",
855
+ "3. ## Areas of Disagreement — Where engines or sources conflict (if any)",
856
+ "4. ## Limitations & Caveats — Important qualifiers, gaps, or uncertainties",
857
+ "",
858
+ `Original research question: ${originalQuery}`,
859
+ `Learnings: ${JSON.stringify(learnings, null, 2)}`,
860
+ `Known gaps/caveats: ${JSON.stringify(gaps, null, 2)}`,
861
+ `Question ledger: ${JSON.stringify(questions, null, 2)}`,
862
+ `Goal-based extracted evidence: ${JSON.stringify(evidenceItems.slice(-20), null, 2)}`,
863
+ `Source registry: ${JSON.stringify(sourceRegistry, null, 2)}`,
864
+ "",
865
+ "Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:",
866
+ "BEGIN_JSON",
867
+ JSON.stringify({
868
+ answer: "markdown report with sections and inline [S1] citations",
869
+ agreement: {
870
+ level: "high|medium|low|mixed|conflicting",
871
+ summary: "one-sentence confidence summary"
872
+ },
873
+ differences: ["notable disagreement or conflict between sources"],
874
+ caveats: ["important caveat or qualification"],
875
+ claims: [
876
+ {
877
+ claim: "specific factual statement from the research",
878
+ support: "strong|moderate|weak|conflicting",
879
+ sourceIds: ["S1", "S2"]
880
+ }
881
+ ],
882
+ recommendedSources: ["S1", "S2"]
883
+ }, null, 2),
884
+ "END_JSON"
885
+ ].join(`
886
+ `);
887
+ }
888
+ export function buildSynthesisFromEvidencePrompt(originalQuery, sources = [], questions = [], evidenceItems = []) {
889
+ const sourceRegistry = sources.slice(0, 12).map((source) => ({
890
+ id: source.id,
891
+ title: source.title,
892
+ domain: source.domain,
893
+ url: source.canonicalUrl,
894
+ type: source.sourceType,
895
+ engines: source.engines
896
+ }));
897
+ const evidenceSlice = evidenceItems.slice(-20);
898
+ const answerableQuestionIds = new Set;
899
+ for (const item of evidenceSlice) {
900
+ for (const ans of item.answers || []) {
901
+ if (ans?.id)
902
+ answerableQuestionIds.add(ans.id);
903
+ }
904
+ }
905
+ const openQuestionSummary = (questions || []).filter((q) => q.status !== "closed").map((q) => ({ id: q.id, question: q.question }));
906
+ return [
907
+ "You are writing the final research report from goal-based extracted evidence.",
908
+ "Per-round learnings were not produced, but the per-source evidence extraction step succeeded.",
909
+ "Synthesize a thorough markdown report using ONLY the evidence below. Every substantive claim MUST be backed by an [S1] citation.",
910
+ "",
911
+ "Report structure:",
912
+ "1. ## Summary — A 2-4 sentence executive summary of findings",
913
+ "2. ## Key Findings — The main findings, organized by theme or question, each with inline citations",
914
+ "3. ## Limitations & Caveats — Important qualifiers, gaps, or uncertainties",
915
+ "",
916
+ `Original research question: ${originalQuery}`,
917
+ `Per-source extracted evidence: ${JSON.stringify(evidenceSlice, null, 2)}`,
918
+ `Source registry: ${JSON.stringify(sourceRegistry, null, 2)}`,
919
+ `Questions already answered by the evidence: ${JSON.stringify(Array.from(answerableQuestionIds))}`,
920
+ `Questions still open after this evidence: ${JSON.stringify(openQuestionSummary)}`,
921
+ "",
922
+ "Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:",
923
+ "BEGIN_JSON",
924
+ JSON.stringify({
925
+ answer: "markdown report with sections and inline [S1] citations",
926
+ agreement: {
927
+ level: "high|medium|low|mixed|conflicting",
928
+ summary: "one-sentence confidence summary"
929
+ },
930
+ differences: ["notable disagreement or conflict between sources"],
931
+ caveats: ["important caveat or qualification"],
932
+ claims: [
933
+ {
934
+ claim: "specific factual statement supported by the evidence",
935
+ support: "strong|moderate|weak|conflicting",
936
+ sourceIds: ["S1", "S2"]
937
+ }
938
+ ],
939
+ recommendedSources: ["S1", "S2"]
940
+ }, null, 2),
941
+ "END_JSON"
942
+ ].join(`
943
+ `);
944
+ }
945
+ async function runFastAllSearch(query, { locale = null, short = true } = {}) {
946
+ const args = [SEARCH_BIN, "all", "--inline", "--stdin", "--fast"];
947
+ if (!short)
948
+ args.push("--full");
949
+ if (locale)
950
+ args.push("--locale", locale);
951
+ return new Promise((resolve, reject) => {
952
+ const proc = spawn(nodeRuntimeCommand(), args, {
953
+ stdio: ["pipe", "pipe", "pipe"],
954
+ env: { ...process.env, GREEDY_SEARCH_RESEARCH_CHILD: "1" }
172
955
  });
173
- } catch(_) {}
174
- // ── WebGL readPixels noise ──────────────────────────
175
- // CreepJS and other fingerprinters draw content with WebGL and read back the
176
- // rendered pixels. Adding subtle noise breaks rendering-based fingerprinting.
177
- try {
178
- var origReadPixels = WebGLRenderingContext.prototype.readPixels;
179
- WebGLRenderingContext.prototype.readPixels = __markNative(function readPixels(x, y, width, height, format, type, pixels) {
180
- var result = origReadPixels.call(this, x, y, width, height, format, type, pixels);
181
- if (pixels && pixels.length > 0) {
182
- pixels[0] ^= 1;
956
+ proc.stdin.write(query);
957
+ proc.stdin.end();
958
+ let out = "";
959
+ let err = "";
960
+ let stderrBuffer = "";
961
+ proc.stdout.on("data", (d) => out += d);
962
+ proc.stderr.on("data", (d) => {
963
+ err += d;
964
+ stderrBuffer += d.toString();
965
+ const lines = stderrBuffer.split(`
966
+ `);
967
+ stderrBuffer = lines.pop() || "";
968
+ for (const line of lines) {
969
+ if (shouldForwardChildStderr(line)) {
970
+ process.stderr.write(`${line}
971
+ `);
972
+ }
183
973
  }
184
- return result;
185
974
  });
186
- } catch(_) {}
187
- Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8, configurable: true });
188
- Object.defineProperty(navigator, 'deviceMemory', { get: () => 8, configurable: true });
189
-
190
- // ── Canvas fingerprint noise ─────────────────────────
191
- // Headless rendering engines produce slightly different canvas output
192
- // than headed Chrome. Subtle noise breaks hash-based fingerprinting.
193
- try {
194
- var __canvasNoise = ((Date.now() & 0xFF) | 1);
195
- var origFill = CanvasRenderingContext2D.prototype.fillText;
196
- CanvasRenderingContext2D.prototype.fillText = __markNative(function fillText() {
197
- this.globalAlpha = 0.9995;
198
- return origFill.apply(this, arguments);
199
- });
200
- } catch(_) {}
201
- try {
202
- var origStroke = CanvasRenderingContext2D.prototype.strokeText;
203
- CanvasRenderingContext2D.prototype.strokeText = __markNative(function strokeText() {
204
- this.globalAlpha = 0.9995;
205
- return origStroke.apply(this, arguments);
975
+ const t = setTimeout(() => {
976
+ proc.kill();
977
+ reject(new Error(`research child search timed out for: ${query}`));
978
+ }, 140000);
979
+ proc.on("close", (code) => {
980
+ clearTimeout(t);
981
+ if (code !== 0) {
982
+ reject(new Error(err.trim() || `search child exited with code ${code}`));
983
+ return;
984
+ }
985
+ try {
986
+ resolve(JSON.parse(out.trim()));
987
+ } catch {
988
+ reject(new Error(`Invalid JSON from research child: ${out.slice(0, 200)}`));
989
+ }
206
990
  });
207
- } catch(_) {}
208
- try {
209
- var origToDataURL = HTMLCanvasElement.prototype.toDataURL;
210
- HTMLCanvasElement.prototype.toDataURL = __markNative(function toDataURL() {
211
- var ctx = this.getContext('2d');
212
- if (ctx) {
213
- // Spread noise across canvas to break hash-based fingerprinting.
214
- // Uses a deterministic pattern so it's consistent per page load
215
- // but varies between sessions.
216
- var w = this.width, h = this.height;
217
- if (w > 0 && h > 0) {
218
- var imgData = ctx.getImageData(0, 0, Math.min(w, 4), Math.min(h, 4));
219
- if (imgData && imgData.data) {
220
- for (var __i = 0; __i < imgData.data.length; __i += 4) {
221
- imgData.data[__i] ^= (__canvasNoise + __i) & 0xFF;
991
+ });
992
+ }
993
+ function dedupeSources(sourceLists) {
994
+ const seen = new Map;
995
+ for (const source of sourceLists.flat()) {
996
+ const canonicalUrl = normalizeUrl(source.canonicalUrl || source.url);
997
+ if (!canonicalUrl)
998
+ continue;
999
+ const existing = seen.get(canonicalUrl);
1000
+ if (!existing) {
1001
+ seen.set(canonicalUrl, { ...source, canonicalUrl });
1002
+ continue;
1003
+ }
1004
+ existing.engines = [
1005
+ ...new Set([...existing.engines || [], ...source.engines || []])
1006
+ ];
1007
+ existing.engineCount = existing.engines.length;
1008
+ existing.smartScore = Math.max(existing.smartScore || 0, source.smartScore || 0);
1009
+ }
1010
+ return Array.from(seen.values()).sort((a, b) => {
1011
+ const diff = computeCompositeScore(b) - computeCompositeScore(a);
1012
+ if (diff !== 0)
1013
+ return diff;
1014
+ return (a.domain || "").localeCompare(b.domain || "");
1015
+ }).slice(0, 12).map((source, index) => ({ ...source, id: `S${index + 1}` }));
1016
+ }
1017
+ const _enginePattern = ALL_ENGINES.join("|");
1018
+ const _engineRegex = new RegExp(`^\\[(${_enginePattern})\\]`);
1019
+ function shouldForwardChildStderr(line) {
1020
+ return /^PROGRESS:/.test(line) || /^\[greedysearch\]/.test(line) || _engineRegex.test(line) || /^GreedySearch Chrome/.test(line) || /^Launching GreedySearch Chrome/.test(line) || /^Headless mode/.test(line) || /^Ready\.?$/.test(line);
1021
+ }
1022
+ function parseGeminiJson(raw, fallback = {}) {
1023
+ return parseStructuredJson(raw?.answer || "") || fallback;
1024
+ }
1025
+ export function auditCitations(answer, sources) {
1026
+ if (!answer || !Array.isArray(sources)) {
1027
+ return {
1028
+ cited: [],
1029
+ missing: [],
1030
+ unfetched: [],
1031
+ ok: true
1032
+ };
1033
+ }
1034
+ const idPattern = /\b[SF](\d+)\b/g;
1035
+ const citedIds = new Set;
1036
+ let match;
1037
+ while ((match = idPattern.exec(answer)) !== null) {
1038
+ citedIds.add(`S${match[1]}`);
1039
+ citedIds.add(`F${match[1]}`);
1040
+ }
1041
+ const sourceMap = new Map;
1042
+ for (const source of sources) {
1043
+ const id = source?.id;
1044
+ if (id) {
1045
+ sourceMap.set(id, source);
1046
+ }
1047
+ }
1048
+ const cited = Array.from(citedIds);
1049
+ const missing = [];
1050
+ const unfetched = [];
1051
+ for (const id of cited) {
1052
+ const source = sourceMap.get(id);
1053
+ if (!source) {
1054
+ const indexMatch = id.match(/^(S|F)(\d+)$/);
1055
+ if (indexMatch) {
1056
+ const idx = parseInt(indexMatch[2], 10) - 1;
1057
+ if (idx >= 0 && idx < sources.length) {
1058
+ const matched = sources[idx];
1059
+ if (matched) {
1060
+ const fetchOk = matched.fetch?.ok || matched.content && matched.content.length > 100 || matched.contentChars && matched.contentChars > 100;
1061
+ if (!fetchOk) {
1062
+ unfetched.push(id);
222
1063
  }
223
- ctx.putImageData(imgData, 0, 0);
1064
+ continue;
224
1065
  }
225
1066
  }
226
1067
  }
227
- return origToDataURL.apply(this, arguments);
228
- });
229
- } catch(_) {}
230
-
231
- // ── AudioContext fingerprint noise ────────────────────
232
- // Headless Chrome's AudioContext produces slightly different output.
233
- // Subtle noise breaks audio-based fingerprinting.
234
- try {
235
- var __audioSeed = ((Date.now() & 0x1F) | 1);
236
- var origGetChannelData = AudioBuffer.prototype.getChannelData;
237
- AudioBuffer.prototype.getChannelData = __markNative(function getChannelData(channel) {
238
- var data = origGetChannelData.call(this, channel);
239
- for (var __i = 0; __i < data.length; __i += 64) {
240
- data[__i] *= 0.99999;
1068
+ missing.push(id);
1069
+ } else {
1070
+ const fetchOk = source.fetch?.ok || source.content && source.content.length > 100 || source.contentChars && source.contentChars > 100;
1071
+ if (!fetchOk) {
1072
+ unfetched.push(id);
241
1073
  }
242
- return data;
243
- });
244
- } catch(_) {}
245
-
246
- // ── window outer dimensions ──────────────────────────
247
- // outerWidth/Height = 0 in headless — a well-known bot signal.
248
- // Mirror innerWidth/Height (set by --window-size flag) so the ratio is sane.
249
- try {
250
- if (!window.outerWidth) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth || 1920, configurable: true });
251
- if (!window.outerHeight) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight || 1080, configurable: true });
252
- } catch(_) {}
253
-
254
- // ── screen properties ─────────────────────────────────
255
- // Headless Chrome often reports an 800x600 screen even when the viewport is
256
- // 1920x1080. Keep screen metrics internally consistent with our launch flags.
257
- try {
258
- Object.defineProperty(screen, 'width', { get: () => 1920, configurable: true });
259
- Object.defineProperty(screen, 'height', { get: () => 1080, configurable: true });
260
- Object.defineProperty(screen, 'availWidth', { get: () => 1920, configurable: true });
261
- Object.defineProperty(screen, 'availHeight', { get: () => 1040, configurable: true });
262
- Object.defineProperty(screen, 'colorDepth', { get: () => 24, configurable: true });
263
- Object.defineProperty(screen, 'pixelDepth', { get: () => 24, configurable: true });
264
- } catch(_) {}
265
-
266
- // ── navigator.userAgentData (UA Client Hints) ─────────
267
- // Derive version from the UA string already set by --user-agent flag so the
268
- // two APIs are always consistent. Removes any "HeadlessChrome" brand entry.
269
- try {
270
- var _uaMajor = (navigator.userAgent.match(new RegExp('Chrome/([0-9]+)')) || [])[1] || '136';
271
- var _uaFull = (navigator.userAgent.match(new RegExp('Chrome/([0-9.]+)')) || [])[1] || (_uaMajor + '.0.0.0');
272
- var _brands = [
273
- { brand: 'Not)A;Brand', version: '99' },
274
- { brand: 'Google Chrome', version: _uaMajor },
275
- { brand: 'Chromium', version: _uaMajor },
276
- ];
277
- Object.defineProperty(navigator, 'userAgentData', {
278
- get: function() {
279
- return {
280
- brands: _brands, mobile: false, platform: 'Windows',
281
- getHighEntropyValues: function() {
282
- return Promise.resolve({
283
- architecture: 'x86', bitness: '64',
284
- brands: _brands,
285
- fullVersionList: [
286
- { brand: 'Not)A;Brand', version: '99.0.0.0' },
287
- { brand: 'Google Chrome', version: _uaFull },
288
- { brand: 'Chromium', version: _uaFull },
289
- ],
290
- mobile: false, model: '', platform: 'Windows',
291
- platformVersion: '15.0.0', uaFullVersion: _uaFull, wow64: false,
1074
+ }
1075
+ }
1076
+ return {
1077
+ cited,
1078
+ missing,
1079
+ unfetched,
1080
+ ok: missing.length === 0
1081
+ };
1082
+ }
1083
+ export async function checkCitationUrls(sources, { timeoutMs = 6000, concurrency = 4 } = {}) {
1084
+ const safeConcurrency = Math.max(1, Math.floor(concurrency || 1));
1085
+ const citedSources = (sources || []).filter((s) => s?.id && (s?.canonicalUrl || s?.finalUrl || s?.url));
1086
+ if (citedSources.length === 0) {
1087
+ return { reachable: [], dead: [], skipped: [], ok: true };
1088
+ }
1089
+ const reachable = [];
1090
+ const dead = [];
1091
+ const skipped = [];
1092
+ const results = new Array(citedSources.length);
1093
+ let nextIndex = 0;
1094
+ async function worker() {
1095
+ while (true) {
1096
+ const i = nextIndex++;
1097
+ if (i >= citedSources.length)
1098
+ return;
1099
+ const source = citedSources[i];
1100
+ try {
1101
+ const url = source.fetch?.finalUrl || source.canonicalUrl || source.finalUrl || source.url;
1102
+ if (!url) {
1103
+ results[i] = { id: source.id, url: "", status: "skipped" };
1104
+ continue;
1105
+ }
1106
+ try {
1107
+ const parsed = new URL(url);
1108
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1109
+ results[i] = { id: source.id, url, status: "skipped" };
1110
+ continue;
1111
+ }
1112
+ } catch {
1113
+ results[i] = { id: source.id, url, status: "skipped" };
1114
+ continue;
1115
+ }
1116
+ try {
1117
+ const controller = new AbortController;
1118
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1119
+ try {
1120
+ const response = await fetch(url, {
1121
+ method: "HEAD",
1122
+ redirect: "follow",
1123
+ signal: controller.signal,
1124
+ headers: {
1125
+ "User-Agent": "Mozilla/5.0 (compatible; GreedySearch/2.0; +https://github.com/apmantza/greedysearch-dm)"
1126
+ }
292
1127
  });
293
- },
294
- toJSON: function() { return { brands: _brands, mobile: false, platform: 'Windows' }; },
1128
+ clearTimeout(timer);
1129
+ const ok = response.status >= 200 && response.status < 400;
1130
+ const botProtectedOrHeadlessHost = [401, 403, 405, 429].includes(response.status);
1131
+ let status = "dead";
1132
+ if (ok)
1133
+ status = "reachable";
1134
+ else if (botProtectedOrHeadlessHost)
1135
+ status = "skipped";
1136
+ results[i] = {
1137
+ id: source.id,
1138
+ url,
1139
+ status,
1140
+ httpStatus: response.status,
1141
+ reason: botProtectedOrHeadlessHost ? "bot-protected-or-head-disallowed" : undefined
1142
+ };
1143
+ } catch (fetchError) {
1144
+ clearTimeout(timer);
1145
+ results[i] = {
1146
+ id: source.id,
1147
+ url,
1148
+ status: "dead",
1149
+ error: fetchError.name === "AbortError" ? "timeout" : fetchError.message
1150
+ };
1151
+ }
1152
+ } catch (error) {
1153
+ results[i] = {
1154
+ id: source.id,
1155
+ url,
1156
+ status: "dead",
1157
+ error: error.message
1158
+ };
1159
+ }
1160
+ } catch (error) {
1161
+ results[i] = {
1162
+ id: "?",
1163
+ url: "",
1164
+ status: "dead",
1165
+ error: error?.message || "unknown"
295
1166
  };
296
- },
297
- configurable: true,
1167
+ }
1168
+ }
1169
+ }
1170
+ const workerCount = Math.min(citedSources.length, safeConcurrency);
1171
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
1172
+ for (const value of results) {
1173
+ if (value.status === "reachable")
1174
+ reachable.push(value);
1175
+ else if (value.status === "dead")
1176
+ dead.push(value);
1177
+ else
1178
+ skipped.push(value);
1179
+ }
1180
+ return {
1181
+ reachable,
1182
+ dead,
1183
+ skipped,
1184
+ ok: dead.length === 0
1185
+ };
1186
+ }
1187
+ export async function runCitationUrlCheck(combinedSources, citationAudit = null) {
1188
+ process.stderr.write(`PROGRESS:research:check-urls
1189
+ `);
1190
+ try {
1191
+ const citedIds = new Set(citationAudit?.cited || []);
1192
+ const sourcesToCheck = citedIds.size ? (combinedSources || []).filter((source) => citedIds.has(source?.id)) : combinedSources;
1193
+ const citationUrls = await checkCitationUrls(sourcesToCheck, {
1194
+ timeoutMs: 6000,
1195
+ concurrency: 4
1196
+ });
1197
+ if (!citationUrls.ok) {
1198
+ process.stderr.write(`[greedysearch] ${citationUrls.dead.length} dead citation URL(s) detected
1199
+ `);
1200
+ }
1201
+ return citationUrls;
1202
+ } catch (error) {
1203
+ process.stderr.write(`[greedysearch] URL reachability check failed: ${error.message}
1204
+ `);
1205
+ return null;
1206
+ }
1207
+ }
1208
+ export function computeResearchFloor({
1209
+ sources = [],
1210
+ fetchedSources = [],
1211
+ synthesis = {},
1212
+ citationAudit = null,
1213
+ gaps = [],
1214
+ questions = [],
1215
+ rounds = [],
1216
+ qualityScore = 0,
1217
+ qualityThreshold = 8.5,
1218
+ maxSources = 8,
1219
+ requireCitations = true,
1220
+ requireQuestions = true
1221
+ } = {}) {
1222
+ const fetchedOk = fetchedSources.filter((source) => source?.fetch?.ok || (source?.contentChars || 0) > 100 || String(source?.content || "").length > 100);
1223
+ const primarySources = sources.filter((source) => ["official-docs", "repo", "maintainer-blog", "academic"].includes(String(source?.sourceType || "")));
1224
+ const claims = Array.isArray(synthesis?.claims) ? synthesis.claims : [];
1225
+ const citedCount = citationAudit ? citationAudit.cited?.length || 0 : 0;
1226
+ const questionStats = questionProgress(questions);
1227
+ const requiredQuestions = (questions || []).filter((q) => !q.createdRound || q.reason === "Original research question");
1228
+ const requiredQuestionStats = questionProgress(requiredQuestions);
1229
+ const roundCount = (rounds || []).length;
1230
+ const baseMin = Math.min(4, Math.max(2, Number(maxSources) || 8));
1231
+ const minFetched = roundCount <= 1 ? Math.min(2, baseMin) : baseMin;
1232
+ const checks = {
1233
+ roundsRun: rounds.length >= 1,
1234
+ fetchedSources: fetchedOk.length >= minFetched,
1235
+ primarySources: primarySources.length >= 1,
1236
+ qualityScore: qualityScore >= Math.min(qualityThreshold, 8) || requireCitations && claims.length > 0 && citedCount > 0,
1237
+ claimsExtracted: !requireCitations || claims.length > 0,
1238
+ citationsPresent: !requireCitations || citedCount > 0,
1239
+ citationsValid: !requireCitations || citationAudit?.ok === true,
1240
+ unfetchedCitations: !requireCitations || (citationAudit?.unfetched || []).length === 0,
1241
+ requiredQuestionsClosed: !requireQuestions || requiredQuestionStats.open === 0
1242
+ };
1243
+ return {
1244
+ floorMet: Object.values(checks).every(Boolean),
1245
+ checks,
1246
+ metrics: {
1247
+ fetchedOk: fetchedOk.length,
1248
+ primarySources: primarySources.length,
1249
+ claims: claims.length,
1250
+ cited: citedCount,
1251
+ gaps: gaps.length,
1252
+ openQuestions: questionStats.open,
1253
+ closedQuestions: questionStats.closed,
1254
+ totalQuestions: questionStats.total,
1255
+ openRequiredQuestions: requiredQuestionStats.open,
1256
+ closedRequiredQuestions: requiredQuestionStats.closed,
1257
+ totalRequiredQuestions: requiredQuestionStats.total,
1258
+ qualityScore,
1259
+ minFetched
1260
+ }
1261
+ };
1262
+ }
1263
+ function annotateFetchedSourcesWithIds(fetchedSources, sources) {
1264
+ const byUrl = new Map;
1265
+ for (const source of sources || []) {
1266
+ const key = normalizeUrl(source?.canonicalUrl || source?.finalUrl || source?.url);
1267
+ if (key && source?.id)
1268
+ byUrl.set(key, source.id);
1269
+ }
1270
+ return (fetchedSources || []).map((source, index) => {
1271
+ const key = normalizeUrl(source?.finalUrl || source?.canonicalUrl || source?.url);
1272
+ return {
1273
+ ...source,
1274
+ id: source?.id || byUrl.get(key) || `F${index + 1}`
1275
+ };
1276
+ });
1277
+ }
1278
+ export function createQuestionLedger(query) {
1279
+ return [
1280
+ {
1281
+ id: "Q1",
1282
+ question: trimText(sanitizeResearchQuery(query), 320),
1283
+ status: "open",
1284
+ reason: "Original research question",
1285
+ evidence: [],
1286
+ sourceIds: []
1287
+ }
1288
+ ];
1289
+ }
1290
+ function nextQuestionId(questions) {
1291
+ let max = 0;
1292
+ for (const q of questions || []) {
1293
+ const n = Number.parseInt(String(q.id || "").replace(/^Q/i, ""), 10);
1294
+ if (Number.isFinite(n))
1295
+ max = Math.max(max, n);
1296
+ }
1297
+ return `Q${max + 1}`;
1298
+ }
1299
+ function findSimilarQuestion(questions, question) {
1300
+ const normalized = sanitizeResearchQuery(question).toLowerCase();
1301
+ return (questions || []).find((q) => q.question?.toLowerCase() === normalized || jaccardSimilarity(q.question || "", normalized) >= 0.82);
1302
+ }
1303
+ function addQuestion(questions, question, { reason = "", round = null } = {}) {
1304
+ const clean = trimText(sanitizeResearchQuery(question), 320);
1305
+ if (!clean)
1306
+ return null;
1307
+ const existing = findSimilarQuestion(questions, clean);
1308
+ if (existing)
1309
+ return existing;
1310
+ const item = {
1311
+ id: nextQuestionId(questions),
1312
+ question: clean,
1313
+ status: "open",
1314
+ reason: trimText(reason, 240),
1315
+ createdRound: round,
1316
+ evidence: [],
1317
+ sourceIds: []
1318
+ };
1319
+ questions.push(item);
1320
+ return item;
1321
+ }
1322
+ function closeQuestion(questions, idOrQuestion, { evidence = "", sourceIds = [], round = null } = {}) {
1323
+ const target = questions.find((q) => q.id === idOrQuestion) || findSimilarQuestion(questions, idOrQuestion);
1324
+ if (!target)
1325
+ return null;
1326
+ target.status = "closed";
1327
+ target.closedRound = target.closedRound || round;
1328
+ if (evidence)
1329
+ target.evidence = uniqueStrings([...target.evidence || [], evidence], 4);
1330
+ if (Array.isArray(sourceIds)) {
1331
+ target.sourceIds = uniqueStrings([...target.sourceIds || [], ...sourceIds], 8);
1332
+ }
1333
+ return target;
1334
+ }
1335
+ function questionProgress(questions) {
1336
+ const total = questions.length;
1337
+ const closed = questions.filter((q) => q.status === "closed").length;
1338
+ return { total, closed, open: Math.max(0, total - closed) };
1339
+ }
1340
+ export function updateQuestionLedger(questions, { roundNumber, actions = [], learningPayload = {} } = {}) {
1341
+ for (const run of actions) {
1342
+ const action = run?.action || run;
1343
+ const goal = action?.researchGoal && action.researchGoal !== "Original user query" ? action.researchGoal : action?.query || action?.url || "";
1344
+ if (goal) {
1345
+ addQuestion(questions, goal, {
1346
+ reason: "Planned research action",
1347
+ round: roundNumber
1348
+ });
1349
+ }
1350
+ }
1351
+ const MAX_OPEN_FOLLOWUPS = 5;
1352
+ const followupOpen = questions.filter((q) => q.status === "open" && q.reason === "Discovered gap/follow-up");
1353
+ if (followupOpen.length > MAX_OPEN_FOLLOWUPS) {
1354
+ const overflow = followupOpen.sort((a, b) => (a.createdRound || 0) - (b.createdRound || 0)).slice(0, followupOpen.length - MAX_OPEN_FOLLOWUPS);
1355
+ for (const q of overflow) {
1356
+ q.status = "resolved";
1357
+ q.closedRound = roundNumber;
1358
+ q.evidence = uniqueStrings([...q.evidence || [], "Auto-resolved to cap open-question ledger"], 4);
1359
+ }
1360
+ }
1361
+ const answered = Array.isArray(learningPayload.answeredQuestions) ? learningPayload.answeredQuestions : [];
1362
+ for (const item of answered) {
1363
+ if (typeof item === "string") {
1364
+ closeQuestion(questions, item, { round: roundNumber });
1365
+ continue;
1366
+ }
1367
+ const id = item?.id || item?.question;
1368
+ if (!id && item?.question) {
1369
+ const added = addQuestion(questions, item.question, {
1370
+ reason: "Answered during learning extraction",
1371
+ round: roundNumber
1372
+ });
1373
+ if (added)
1374
+ closeQuestion(questions, added.id, { round: roundNumber });
1375
+ continue;
1376
+ }
1377
+ closeQuestion(questions, id, {
1378
+ evidence: item?.evidence || item?.answer || "",
1379
+ sourceIds: Array.isArray(item?.sourceIds) ? item.sourceIds : [],
1380
+ round: roundNumber
1381
+ });
1382
+ }
1383
+ const newQuestions = Array.isArray(learningPayload.newQuestions) ? learningPayload.newQuestions : [];
1384
+ for (const question of newQuestions) {
1385
+ addQuestion(questions, question, {
1386
+ reason: "Discovered gap/follow-up",
1387
+ round: roundNumber
298
1388
  });
299
- } catch(_) {}
300
-
301
- // ── CDP Runtime serialization guard ──────────────────
302
- // Sites detect CDP by putting a getter on Error.prototype.stack
303
- // and checking if console.log triggers it (only happens when
304
- // Runtime domain is enabled). We monkey-patch console methods to
305
- // strip custom getters from arguments before they reach CDP.
1389
+ }
1390
+ return questions;
1391
+ }
1392
+ function pickAcademicFetchTargets(combinedSources, usedUrls) {
1393
+ if (!Array.isArray(combinedSources) || combinedSources.length === 0)
1394
+ return [];
1395
+ const ACADEMIC_HOSTS = ["arxiv.org", "semanticscholar.org", "doi.org"];
1396
+ const seen = new Set;
1397
+ const targets = [];
1398
+ for (const source of combinedSources) {
1399
+ const url = source?.canonicalUrl || source?.finalUrl || source?.url || "";
1400
+ if (!url)
1401
+ continue;
1402
+ let domain = "";
1403
+ try {
1404
+ domain = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
1405
+ } catch {
1406
+ continue;
1407
+ }
1408
+ if (!ACADEMIC_HOSTS.some((h) => domain === h || domain.endsWith(`.${h}`))) {
1409
+ continue;
1410
+ }
1411
+ if (usedUrls.has(url) || seen.has(url))
1412
+ continue;
1413
+ seen.add(url);
1414
+ const htmlUrl = url.includes("/pdf/") ? url.replace(/\/pdf\//, "/html/").replace(/\.pdf$/i, "") : url;
1415
+ targets.push({
1416
+ url: htmlUrl,
1417
+ label: source?.title || source?.id || domain
1418
+ });
1419
+ }
1420
+ return targets.slice(0, 2);
1421
+ }
1422
+ export function reconcileQuestionsFromSynthesis(questions, synthesis, citationAudit) {
1423
+ if (!synthesis?.answer || citationAudit?.ok !== true)
1424
+ return questions;
1425
+ const claims = Array.isArray(synthesis.claims) ? synthesis.claims : [];
1426
+ const citedIds = Array.isArray(citationAudit.cited) ? citationAudit.cited : [];
1427
+ if (claims.length === 0 || citedIds.length === 0)
1428
+ return questions;
1429
+ for (const question of questions) {
1430
+ if (question.status === "closed")
1431
+ continue;
1432
+ let bestClaim = null;
1433
+ let bestScore = 0;
1434
+ for (const claim of claims) {
1435
+ const score = jaccardSimilarity(question.question || "", claim.claim || "");
1436
+ if (score > bestScore) {
1437
+ bestScore = score;
1438
+ bestClaim = claim;
1439
+ }
1440
+ }
1441
+ if (question.id === "Q1" || bestScore >= 0.18) {
1442
+ closeQuestion(questions, question.id, {
1443
+ evidence: bestClaim?.claim || "Answered in final cited synthesis",
1444
+ sourceIds: Array.isArray(bestClaim?.sourceIds) ? bestClaim.sourceIds : citedIds.slice(0, 4)
1445
+ });
1446
+ }
1447
+ }
1448
+ return questions;
1449
+ }
1450
+ function renderQuestionStatus(questions) {
1451
+ if (!questions.length)
1452
+ return "No tracked questions.";
1453
+ return questions.map((q) => {
1454
+ const ids = q.sourceIds?.length ? ` (${q.sourceIds.join(", ")})` : "";
1455
+ return `- [${q.status === "closed" ? "x" : " "}] ${q.id}: ${q.question}${ids}`;
1456
+ }).join(`
1457
+ `);
1458
+ }
1459
+ function markdownList(items, fallback = "None recorded.") {
1460
+ const unique = uniqueStrings(items);
1461
+ return unique.length ? unique.map((item) => `- ${item}`).join(`
1462
+ `) : fallback;
1463
+ }
1464
+ export function writeProvenanceSidecar(dir, {
1465
+ query,
1466
+ rounds,
1467
+ sources,
1468
+ fetchedSources,
1469
+ citationAudit,
1470
+ citationUrls,
1471
+ floor,
1472
+ manifest
1473
+ }) {
1474
+ const fetchedOk = (fetchedSources || []).filter((s) => s?.contentChars > 100 || s?.fetch?.ok);
1475
+ const primarySources = (sources || []).filter((s) => ["official-docs", "repo", "maintainer-blog", "academic"].includes(String(s?.sourceType || "")));
1476
+ const citedIds = new Set(citationAudit?.cited || []);
1477
+ const citedSources = (sources || []).filter((s) => citedIds.has(s?.id));
1478
+ const lines = [
1479
+ `# Provenance: ${query}`,
1480
+ "",
1481
+ `- **Date:** ${manifest?.startedAt || new Date().toISOString()}`,
1482
+ `- **Duration:** ${manifest?.durationMs ? `${(manifest.durationMs / 1000).toFixed(1)}s` : "unknown"}`,
1483
+ `- **Mode:** ${manifest?.terminationReason === "simple_single_pass" ? "simple (single-pass)" : "iterative"}`,
1484
+ `- **Rounds:** ${manifest?.rounds || rounds?.length || 1}`,
1485
+ "",
1486
+ "## Sources",
1487
+ "",
1488
+ `- **Consulted:** ${sources?.length || 0}`,
1489
+ `- **Fetched successfully:** ${fetchedOk.length}`,
1490
+ `- **Primary sources:** ${primarySources.length}`,
1491
+ `- **Cited in report:** ${citedSources.length}`,
1492
+ ""
1493
+ ];
1494
+ if (citedSources.length > 0) {
1495
+ lines.push("### Cited sources", "");
1496
+ for (const source of citedSources) {
1497
+ const url = source.canonicalUrl || source.finalUrl || source.url || "";
1498
+ const fetched = source.fetch?.ok ? "✓" : "✗";
1499
+ lines.push(`- **${source.id}:** [${source.title || url}](${url}) (${source.sourceType || "unknown"}, fetched: ${fetched})`);
1500
+ }
1501
+ lines.push("");
1502
+ }
1503
+ if (citationUrls && (citationUrls.reachable.length > 0 || citationUrls.dead.length > 0)) {
1504
+ lines.push("## URL reachability", "");
1505
+ if (citationUrls.dead.length > 0) {
1506
+ lines.push("");
1507
+ lines.push("**Dead links:**");
1508
+ for (const d of citationUrls.dead) {
1509
+ lines.push(`- ${d.id}: ${d.url} (${d.httpStatus || d.error || "unknown"})`);
1510
+ }
1511
+ }
1512
+ if (citationUrls.reachable.length > 0) {
1513
+ lines.push("");
1514
+ lines.push(`**Reachable:** ${citationUrls.reachable.length}/${citationUrls.reachable.length + citationUrls.dead.length}`);
1515
+ }
1516
+ lines.push("");
1517
+ }
1518
+ const verificationStatus = !citationAudit ? "NOT CHECKED" : citationAudit.ok && (citationUrls?.ok ?? true) ? "PASS" : citationAudit.ok === false ? "FAIL (missing citations)" : "FAIL (dead links)";
1519
+ lines.push("## Verification", "", `- **Citations:** ${citationAudit?.ok ? "PASS" : `FAIL — missing: ${(citationAudit?.missing || []).join(", ")}`}`, `- **URL reachability:** ${citationUrls ? citationUrls.ok ? "PASS" : `FAIL — ${citationUrls.dead.length} dead` : "SKIPPED"}`, `- **Floor:** ${floor?.floorMet ? "PASS" : "PARTIAL"}`, `- **Overall:** ${verificationStatus}`, "");
1520
+ if (floor?.checks) {
1521
+ lines.push("## Floor checks", "");
1522
+ for (const [name, ok] of Object.entries(floor.checks)) {
1523
+ lines.push(`- [${ok ? "x" : " "}] ${name}`);
1524
+ }
1525
+ lines.push("");
1526
+ }
1527
+ writeFileSync(join(dir, "provenance.md"), lines.join(`
1528
+ `), "utf8");
1529
+ }
1530
+ export async function writeResearchBundle({
1531
+ query,
1532
+ rounds,
1533
+ sources,
1534
+ fetchedSources,
1535
+ evidenceItems = [],
1536
+ synthesis,
1537
+ citationAudit,
1538
+ floor,
1539
+ manifest,
1540
+ allGaps = [],
1541
+ questions = [],
1542
+ citationUrls = null,
1543
+ outDir = null
1544
+ }) {
1545
+ const stamp = new Date().toISOString().replaceAll(/[:.]/g, "-").slice(0, 19);
1546
+ const dir = outDir || join(DEFAULT_RESEARCH_BUNDLE_ROOT, `${stamp}_${slugifyResearchName(query)}`);
1547
+ const reportsDir = join(dir, "reports");
1548
+ const sourcesDir = join(dir, "sources");
1549
+ const dataDir = join(dir, "data");
1550
+ mkdirSync(reportsDir, { recursive: true });
1551
+ mkdirSync(sourcesDir, { recursive: true });
1552
+ mkdirSync(dataDir, { recursive: true });
1553
+ const sourceFiles = await writeResearchSourcesToFiles(fetchedSources, sourcesDir);
1554
+ const gaps = uniqueStrings([
1555
+ ...allGaps,
1556
+ ...rounds.flatMap((round) => round.gaps || [])
1557
+ ]);
1558
+ writeFileSync(join(dir, "STATUS.md"), [
1559
+ floor.floorMet ? "STATUS: DONE" : "STATUS: PARTIAL",
1560
+ "",
1561
+ `Query: ${query}`,
1562
+ `Stop reason: ${manifest.terminationReason || "max_rounds"}`,
1563
+ "",
1564
+ "## Deterministic floor checks",
1565
+ ...Object.entries(floor.checks).map(([name, ok]) => `- [${ok ? "x" : " "}] ${name}`),
1566
+ "",
1567
+ "## Questions",
1568
+ renderQuestionStatus(questions),
1569
+ "",
1570
+ "## Open gaps",
1571
+ markdownList(gaps),
1572
+ ""
1573
+ ].join(`
1574
+ `), "utf8");
1575
+ writeFileSync(join(dir, "OUTLINE.md"), [
1576
+ "# Research bundle outline",
1577
+ "",
1578
+ "- `reports/SUMMARY.md` — final cited report",
1579
+ "- `reports/CLAIMS.md` — extracted claims with support/source IDs",
1580
+ "- `reports/EVIDENCE.md` — goal-based source evidence",
1581
+ "- `reports/GAPS.md` — remaining caveats and uncertainties",
1582
+ "- `provenance.md` — human-readable run metadata and verification",
1583
+ "- `sources/` — fetched source markdown files",
1584
+ "- `data/manifest.json` — machine-readable run metadata",
1585
+ "- `data/rounds.json` — per-round actions/learnings/gaps",
1586
+ "- `data/sources.json` — ranked source registry",
1587
+ "- `data/questions.json` — open/closed question ledger",
1588
+ ""
1589
+ ].join(`
1590
+ `), "utf8");
1591
+ writeFileSync(join(reportsDir, "SUMMARY.md"), String(synthesis.answer || ""), "utf8");
1592
+ writeFileSync(join(reportsDir, "CLAIMS.md"), [
1593
+ "# Key claims",
1594
+ "",
1595
+ ...Array.isArray(synthesis.claims) && synthesis.claims.length ? synthesis.claims.map((claim) => {
1596
+ const ids = Array.isArray(claim.sourceIds) ? claim.sourceIds.join(", ") : "";
1597
+ return `- ${claim.claim || ""} (${claim.support || "support unknown"}${ids ? `; ${ids}` : ""})`;
1598
+ }) : ["No structured claims were extracted."],
1599
+ ""
1600
+ ].join(`
1601
+ `), "utf8");
1602
+ writeFileSync(join(reportsDir, "EVIDENCE.md"), [
1603
+ "# Extracted evidence",
1604
+ "",
1605
+ ...evidenceItems.length ? evidenceItems.map((item) => [
1606
+ `## ${item.sourceId || item.url || "Source"}`,
1607
+ item.url ? `<${item.url}>` : "",
1608
+ item.rational ? `**Rational:** ${item.rational}` : "",
1609
+ item.evidence ? `**Evidence:** ${item.evidence}` : "",
1610
+ item.summary ? `**Summary:** ${item.summary}` : "",
1611
+ ""
1612
+ ].filter(Boolean).join(`
1613
+ `)) : ["No goal-based evidence was extracted."],
1614
+ ""
1615
+ ].join(`
1616
+ `), "utf8");
1617
+ writeFileSync(join(reportsDir, "GAPS.md"), [
1618
+ "# Gaps and caveats",
1619
+ "",
1620
+ "## Caveats",
1621
+ markdownList(synthesis.caveats || []),
1622
+ "",
1623
+ "## Research gaps",
1624
+ markdownList(gaps),
1625
+ ""
1626
+ ].join(`
1627
+ `), "utf8");
1628
+ writeFileSync(join(dataDir, "manifest.json"), JSON.stringify({ ...manifest, floor, citationAudit }, null, 2), "utf8");
1629
+ writeFileSync(join(dataDir, "rounds.json"), JSON.stringify(rounds, null, 2), "utf8");
1630
+ writeFileSync(join(dataDir, "sources.json"), JSON.stringify(sources, null, 2), "utf8");
1631
+ writeFileSync(join(dataDir, "questions.json"), JSON.stringify(questions, null, 2), "utf8");
1632
+ writeFileSync(join(dataDir, "evidence.json"), JSON.stringify(evidenceItems, null, 2), "utf8");
1633
+ writeFileSync(join(sourcesDir, "index.md"), [
1634
+ "# Source index",
1635
+ "",
1636
+ ...sourceFiles.map((source) => {
1637
+ const label = source.title || source.url;
1638
+ const url = source.finalUrl || source.url;
1639
+ const path = source.contentPath ? ` — ${source.contentPath}` : "";
1640
+ return `- ${source.id || "?"}: [${label}](${url})${path}`;
1641
+ }),
1642
+ ""
1643
+ ].join(`
1644
+ `), "utf8");
306
1645
  try {
307
- var _origLog = console.log, _origError = console.error,
308
- _origWarn = console.warn, _origDebug = console.debug,
309
- _origInfo = console.info;
310
- var _safeArg = function(a) {
311
- if (a instanceof Error) {
312
- try { return new Error(a.message); } catch(_) { return a; }
1646
+ writeProvenanceSidecar(dir, {
1647
+ query,
1648
+ rounds,
1649
+ sources,
1650
+ fetchedSources,
1651
+ citationAudit,
1652
+ citationUrls,
1653
+ floor,
1654
+ manifest
1655
+ });
1656
+ } catch (sidecarError) {
1657
+ process.stderr.write(`[greedysearch] Provenance sidecar write failed (non-critical): ${sidecarError.message}
1658
+ `);
1659
+ }
1660
+ return {
1661
+ dir,
1662
+ statusPath: join(dir, "STATUS.md"),
1663
+ summaryPath: join(reportsDir, "SUMMARY.md"),
1664
+ manifestPath: join(dataDir, "manifest.json"),
1665
+ provenancePath: join(dir, "provenance.md"),
1666
+ sourceCount: sourceFiles.length,
1667
+ sourceFiles
1668
+ };
1669
+ }
1670
+ export async function runResearchMode({
1671
+ query,
1672
+ breadth,
1673
+ iterations,
1674
+ maxSources,
1675
+ locale = null,
1676
+ short = false,
1677
+ qualityThreshold = 8.5,
1678
+ writeBundle = process.env.GREEDY_RESEARCH_BUNDLE !== "0",
1679
+ researchOutDir = null
1680
+ } = {}) {
1681
+ const options = clampResearchOptions({ breadth, iterations, maxSources });
1682
+ const userSpecifiedBreadth = breadth !== undefined && breadth !== null;
1683
+ const userSpecifiedIterations = iterations !== undefined && iterations !== null;
1684
+ const atDefaults = !userSpecifiedBreadth && !userSpecifiedIterations;
1685
+ if (atDefaults) {
1686
+ try {
1687
+ const classification = await classifyResearchComplexity(query);
1688
+ process.stderr.write(`[greedysearch] Complexity: ${classification.complexity} (${classification.reasoning})
1689
+ `);
1690
+ if (classification.complexity === "simple") {
1691
+ process.stderr.write(`[greedysearch] Simple query detected — using fast single-pass path
1692
+ `);
1693
+ return runSimpleResearchMode({
1694
+ query,
1695
+ locale,
1696
+ maxSources: Math.min(maxSources ?? 5, 5),
1697
+ qualityThreshold,
1698
+ writeBundle,
1699
+ researchOutDir
1700
+ });
1701
+ }
1702
+ if (!userSpecifiedBreadth) {
1703
+ options.breadth = classification.suggestedBreadth;
1704
+ }
1705
+ if (!userSpecifiedIterations) {
1706
+ options.iterations = classification.suggestedIterations;
1707
+ }
1708
+ } catch (error) {
1709
+ process.stderr.write(`[greedysearch] Scale classification failed, using defaults: ${error.message}
1710
+ `);
1711
+ }
1712
+ }
1713
+ const rounds = [];
1714
+ let allLearnings = [];
1715
+ let allGaps = [];
1716
+ const questions = createQuestionLedger(query);
1717
+ let activeActions = null;
1718
+ let combinedSources = [];
1719
+ let fetchedSources = [];
1720
+ let evidenceItems = [];
1721
+ const extractedSourceKeys = new Set;
1722
+ const usedQueries = new Set;
1723
+ const usedUrls = new Set;
1724
+ const qualityHistory = [];
1725
+ let terminationReason = "max_rounds";
1726
+ const startedAt = new Date().toISOString();
1727
+ const startMs = Date.now();
1728
+ let totalActionsRun = 0;
1729
+ let totalSearches = 0;
1730
+ let totalFetches = 0;
1731
+ const engineFailures = [];
1732
+ const progressTracker = createProgressTracker({
1733
+ totalActions: options.iterations * options.breadth,
1734
+ totalRounds: options.iterations,
1735
+ totalFetches: options.iterations,
1736
+ silent: process.env.GREEDY_RESEARCH_QUIET === "1"
1737
+ });
1738
+ progressTracker.startRound(1);
1739
+ process.stderr.write(`[greedysearch] Research mode: breadth ${options.breadth}, iterations ${options.iterations}, qualityThreshold ${qualityThreshold}, engines ${RESEARCH_ENGINES.join(",")}, synthesizer gemini
1740
+ `);
1741
+ for (let roundIndex = 0;roundIndex < options.iterations; roundIndex++) {
1742
+ const roundNumber = roundIndex + 1;
1743
+ const roundBreadth = Math.max(1, Math.ceil(options.breadth / 2 ** roundIndex));
1744
+ process.stderr.write(`PROGRESS:research:round-${roundNumber}:planning
1745
+ `);
1746
+ if (!activeActions) {
1747
+ try {
1748
+ const rawPlan = await runGeminiPrompt(buildResearchActionPrompt(query, roundBreadth, allLearnings, allGaps, [...usedUrls]), { timeoutMs: 120000 });
1749
+ let planActions = parseActionPlan(rawPlan, roundBreadth);
1750
+ if (roundIndex === 0) {
1751
+ planActions.unshift({
1752
+ type: "search",
1753
+ query,
1754
+ researchGoal: "Original user query"
1755
+ });
1756
+ }
1757
+ planActions = await normalizeGitHubFetchActions(planActions, usedUrls);
1758
+ activeActions = planActions;
1759
+ } catch (error) {
1760
+ process.stderr.write(`[greedysearch] Action planning failed, using fallback queries: ${error.message}
1761
+ `);
1762
+ const fallbackQueries = normalizeResearchQueries(null, query, roundBreadth, {
1763
+ includeOriginal: roundIndex === 0,
1764
+ exclude: usedQueries
1765
+ });
1766
+ activeActions = queriesToActions(fallbackQueries);
1767
+ }
1768
+ }
1769
+ const noveltyFiltered = (activeActions || []).filter((action) => {
1770
+ if (action.type === "search") {
1771
+ const pass = !isDuplicateQuery(action.query, usedQueries, {
1772
+ roundIndex,
1773
+ originalQuery: query
1774
+ });
1775
+ if (!pass) {
1776
+ process.stderr.write(`[greedysearch] Novelty gate rejected search: ${action.query}
1777
+ `);
1778
+ }
1779
+ return pass;
1780
+ }
1781
+ if (action.type === "fetchUrl") {
1782
+ const pass = !usedUrls.has(action.url);
1783
+ if (!pass) {
1784
+ process.stderr.write(`[greedysearch] Novelty gate rejected fetch: ${action.url}
1785
+ `);
1786
+ }
1787
+ return pass;
1788
+ }
1789
+ return false;
1790
+ });
1791
+ const roundActions = noveltyFiltered.slice(0, roundBreadth);
1792
+ const academicTargets = pickAcademicFetchTargets(combinedSources, usedUrls);
1793
+ const hasFetch = roundActions.some((a) => a.type === "fetchUrl");
1794
+ if (!hasFetch && academicTargets.length > 0) {
1795
+ const injectTarget = academicTargets[0];
1796
+ roundActions.push({
1797
+ type: "fetchUrl",
1798
+ url: injectTarget.url,
1799
+ researchGoal: `Direct fetch of known academic source: ${injectTarget.label || injectTarget.url}`
1800
+ });
1801
+ process.stderr.write(`[greedysearch] Forced fetchUrl for academic source: ${injectTarget.url}
1802
+ `);
1803
+ }
1804
+ const actionResults = new Array(roundActions.length);
1805
+ const actionWorkerCount = Math.min(3, roundActions.length);
1806
+ let nextActionIndex = 0;
1807
+ async function actionWorker() {
1808
+ while (true) {
1809
+ const i = nextActionIndex++;
1810
+ if (i >= roundActions.length)
1811
+ return;
1812
+ const action = roundActions[i];
1813
+ process.stderr.write(`PROGRESS:research:round-${roundNumber}:action-${i + 1}/${roundActions.length}
1814
+ `);
1815
+ process.stderr.write(`[greedysearch] Action ${i + 1}/${roundActions.length} [${action.type}]: ${(action.query || action.url).slice(0, 80)}
1816
+ `);
1817
+ progressTracker.startAction(action.type, (action.query || action.url || "").slice(0, 60));
1818
+ const run = await executeResearchAction(action, {
1819
+ locale,
1820
+ short,
1821
+ usedQueries,
1822
+ usedUrls,
1823
+ maxChars: 8000
1824
+ });
1825
+ progressTracker.endAction();
1826
+ actionResults[i] = run;
1827
+ }
1828
+ }
1829
+ await Promise.all(Array.from({ length: actionWorkerCount }, () => actionWorker()));
1830
+ const actionRuns = [];
1831
+ for (let i = 0;i < roundActions.length; i++) {
1832
+ const action = roundActions[i];
1833
+ const run = actionResults[i];
1834
+ actionRuns.push(run);
1835
+ totalActionsRun++;
1836
+ if (action.type === "search")
1837
+ totalSearches++;
1838
+ if (action.type === "fetchUrl") {
1839
+ totalFetches++;
1840
+ progressTracker.endFetch(run.ok);
1841
+ }
1842
+ if (!run.ok) {
1843
+ engineFailures.push({
1844
+ round: roundNumber,
1845
+ type: action.type,
1846
+ target: action.query || action.url,
1847
+ error: run.error
1848
+ });
1849
+ process.stderr.write(`[greedysearch] Action failed: ${run.error}
1850
+ `);
1851
+ }
1852
+ }
1853
+ const searchActionRuns = actionRuns.filter((r) => r.action.type === "search");
1854
+ const fetchActionRuns = actionRuns.filter((r) => r.action.type === "fetchUrl");
1855
+ updateQuestionLedger(questions, { roundNumber, actions: actionRuns });
1856
+ combinedSources = dedupeSources([
1857
+ combinedSources,
1858
+ searchActionRuns.flatMap((run) => run.sources || []),
1859
+ fetchActionRuns.flatMap((run) => run.sources || [])
1860
+ ]);
1861
+ for (const fetchRun of fetchActionRuns) {
1862
+ if (fetchRun.fetchResult) {
1863
+ fetchedSources.push(fetchRun.fetchResult);
1864
+ }
1865
+ }
1866
+ fetchedSources = dedupeFetchedSources(fetchedSources);
1867
+ const remainingFetchBudget = Math.max(0, options.maxSources - fetchedSources.filter((source) => source?.content || source?.contentChars > 100).length);
1868
+ if (remainingFetchBudget > 0 && combinedSources.length > 0) {
1869
+ process.stderr.write(`PROGRESS:research:round-${roundNumber}:fetching
1870
+ `);
1871
+ const fetchedUrlKeys = new Set;
1872
+ const safeNormalizeFetchUrl = (value) => {
1873
+ try {
1874
+ return normalizeUrl(value || "");
1875
+ } catch {
1876
+ return "";
1877
+ }
1878
+ };
1879
+ for (const source of fetchedSources) {
1880
+ for (const value of [
1881
+ source?.url,
1882
+ source?.finalUrl,
1883
+ source?.canonicalUrl
1884
+ ]) {
1885
+ const key = safeNormalizeFetchUrl(value);
1886
+ if (key)
1887
+ fetchedUrlKeys.add(key);
1888
+ }
313
1889
  }
314
- return a;
1890
+ const fetchCandidates = combinedSources.filter((source) => {
1891
+ const keys = [source?.canonicalUrl, source?.finalUrl, source?.url].map((value) => safeNormalizeFetchUrl(value)).filter(Boolean);
1892
+ return keys.length > 0 && keys.every((key) => !fetchedUrlKeys.has(key));
1893
+ });
1894
+ const fetched = await fetchMultipleResearchSources(fetchCandidates, Math.min(remainingFetchBudget, fetchCandidates.length), 8000, Math.min(3, remainingFetchBudget || 1));
1895
+ fetchedSources = dedupeFetchedSources([...fetchedSources, ...fetched]);
1896
+ combinedSources = mergeFetchDataIntoSources(combinedSources, fetchedSources);
1897
+ }
1898
+ fetchedSources = annotateFetchedSourcesWithIds(fetchedSources, combinedSources);
1899
+ const roundQueries = actionRuns.map((run) => ({
1900
+ query: run.action.query || run.action.url || "",
1901
+ researchGoal: run.action.researchGoal || ""
1902
+ }));
1903
+ process.stderr.write(`PROGRESS:research:round-${roundNumber}:evidence
1904
+ `);
1905
+ process.stderr.write(`PROGRESS:research:round-${roundNumber}:learning
1906
+ `);
1907
+ const combinedExtraction = await extractEvidenceAndLearnings({
1908
+ query,
1909
+ questions,
1910
+ fetchedSources,
1911
+ extractedSourceKeys,
1912
+ roundQueries,
1913
+ searchSummaries: searchActionRuns.map((run) => ({
1914
+ query: run.action.query,
1915
+ researchGoal: run.action.researchGoal,
1916
+ error: run.error || "",
1917
+ engines: summarizeEngineAnswers(run.result)
1918
+ })),
1919
+ evidenceItems
1920
+ });
1921
+ const evidenceRun = {
1922
+ evidence: combinedExtraction.evidence,
1923
+ error: combinedExtraction.evidenceError
315
1924
  };
316
- console.log = __markNative(function log() { return _origLog.apply(console, Array.prototype.map.call(arguments, _safeArg)); });
317
- console.error = __markNative(function error() { return _origError.apply(console, Array.prototype.map.call(arguments, _safeArg)); });
318
- console.warn = __markNative(function warn() { return _origWarn.apply(console, Array.prototype.map.call(arguments, _safeArg)); });
319
- console.debug = __markNative(function debug() { return _origDebug.apply(console, Array.prototype.map.call(arguments, _safeArg)); });
320
- console.info = __markNative(function info() { return _origInfo.apply(console, Array.prototype.map.call(arguments, _safeArg)); });
321
- } catch(_) {}
322
-
323
- // ── Native function masking ──────────────────────────
324
- // Patched APIs should not stringify as user-defined stealth code.
325
- try {
326
- var __nativeToString = Function.prototype.toString;
327
- Function.prototype.toString = function toString() {
328
- if (__greedyNativeFns.indexOf(this) !== -1) {
329
- var name = this.name || '';
330
- return 'function ' + name + '() { [native code] }';
1925
+ if (evidenceRun.error) {
1926
+ process.stderr.write(`[greedysearch] Evidence extraction failed: ${evidenceRun.error}
1927
+ `);
1928
+ }
1929
+ evidenceItems = [...evidenceItems, ...evidenceRun.evidence];
1930
+ for (const evidence of evidenceRun.evidence) {
1931
+ updateQuestionLedger(questions, {
1932
+ roundNumber,
1933
+ learningPayload: {
1934
+ answeredQuestions: evidence.answers || [],
1935
+ newQuestions: evidence.newQuestions || []
1936
+ }
1937
+ });
1938
+ }
1939
+ const learningPayload = combinedExtraction.learningPayload;
1940
+ const learningError = combinedExtraction.learningError;
1941
+ if (learningError) {
1942
+ process.stderr.write(`[greedysearch] Learning extraction failed: ${learningError}
1943
+ `);
1944
+ }
1945
+ const learnings = Array.isArray(learningPayload.learnings) ? learningPayload.learnings.map((l) => String(l)).filter(Boolean).slice(0, 8) : [];
1946
+ const gaps = Array.isArray(learningPayload.gaps) ? learningPayload.gaps.map((g) => String(g)).filter(Boolean).slice(0, 6) : [];
1947
+ allLearnings = uniqueStrings([...allLearnings, ...learnings]);
1948
+ allGaps = uniqueStrings([...allGaps, ...gaps]);
1949
+ updateQuestionLedger(questions, {
1950
+ roundNumber,
1951
+ actions: [],
1952
+ learningPayload,
1953
+ gaps
1954
+ });
1955
+ rounds.push({
1956
+ round: roundNumber,
1957
+ actions: actionRuns.map((run) => ({
1958
+ type: run.action.type,
1959
+ query: run.action.query || "",
1960
+ url: run.action.url || "",
1961
+ researchGoal: run.action.researchGoal || "",
1962
+ error: run.error || "",
1963
+ sourceCount: run.sources?.length || 0
1964
+ })),
1965
+ learnings,
1966
+ gaps,
1967
+ evidence: evidenceRun.evidence,
1968
+ evidenceError: evidenceRun.error,
1969
+ learningError
1970
+ });
1971
+ process.stderr.write(`PROGRESS:research:round-${roundNumber}:evaluating
1972
+ `);
1973
+ progressTracker.endRound();
1974
+ if (roundNumber < options.iterations) {
1975
+ progressTracker.startRound(roundNumber + 1);
1976
+ }
1977
+ const isFinalRound = roundNumber === options.iterations;
1978
+ const evaluation = isFinalRound ? {
1979
+ score: qualityHistory.length > 0 ? qualityHistory[qualityHistory.length - 1] : 5,
1980
+ coverage: {},
1981
+ knowledgeGaps: [],
1982
+ shouldContinue: false,
1983
+ nextActions: [],
1984
+ terminationReason: null,
1985
+ evaluationError: ""
1986
+ } : await evaluateResearchQuality(query, rounds, allLearnings, allGaps, qualityHistory);
1987
+ qualityHistory.push(evaluation.score);
1988
+ allGaps = uniqueStrings([...allGaps, ...evaluation.knowledgeGaps || []]);
1989
+ updateQuestionLedger(questions, {
1990
+ roundNumber,
1991
+ gaps: evaluation.knowledgeGaps || []
1992
+ });
1993
+ const preliminaryFloor = computeResearchFloor({
1994
+ sources: combinedSources,
1995
+ fetchedSources,
1996
+ gaps: allGaps,
1997
+ questions,
1998
+ rounds,
1999
+ qualityScore: evaluation.score,
2000
+ qualityThreshold,
2001
+ maxSources: options.maxSources,
2002
+ requireCitations: false,
2003
+ requireQuestions: false
2004
+ });
2005
+ process.stderr.write(`[greedysearch] Quality score round ${roundNumber}: ${evaluation.score.toFixed(1)} (shouldContinue: ${evaluation.shouldContinue}, floor: ${preliminaryFloor.floorMet})
2006
+ `);
2007
+ if (evaluation.score >= qualityThreshold && preliminaryFloor.floorMet && (!evaluation.shouldContinue || evaluation.terminationReason === "quality_threshold")) {
2008
+ terminationReason = evaluation.terminationReason || "quality_threshold";
2009
+ process.stderr.write(`[greedysearch] Research floor reached (score: ${evaluation.score.toFixed(1)}). Terminating early.
2010
+ `);
2011
+ break;
2012
+ }
2013
+ const nextBreadth = Math.max(1, Math.ceil(roundBreadth / 2));
2014
+ const followUpActions = (learningPayload.followUpQueries || []).map((q) => ({
2015
+ type: "search",
2016
+ query: sanitizeResearchQuery(String(q)),
2017
+ researchGoal: "Follow-up from learning extraction"
2018
+ })).filter((a) => a.query && a.query.toLowerCase() !== query.toLowerCase()).slice(0, nextBreadth);
2019
+ let nextActiveActions = followUpActions;
2020
+ if (nextActiveActions.length < nextBreadth && evaluation.nextActions.length > 0) {
2021
+ const evaluatorActions = evaluation.nextActions.map((a) => validateAction(a)).filter(Boolean);
2022
+ const merged = [...nextActiveActions, ...evaluatorActions];
2023
+ nextActiveActions = merged.slice(0, nextBreadth);
2024
+ }
2025
+ if (nextActiveActions.length < nextBreadth && allGaps.length > 0) {
2026
+ const fallbacks = buildFallbackQueriesFromGaps(allGaps, query, usedQueries, nextBreadth - nextActiveActions.length, roundIndex + 1);
2027
+ const fallbackActions = fallbacks.map((f) => ({
2028
+ type: "search",
2029
+ query: f.query,
2030
+ researchGoal: f.researchGoal
2031
+ }));
2032
+ nextActiveActions = [...nextActiveActions, ...fallbackActions].slice(0, nextBreadth);
2033
+ if (fallbacks.length > 0) {
2034
+ process.stderr.write(`[greedysearch] Generated ${fallbacks.length} gap-driven fallback actions.
2035
+ `);
331
2036
  }
332
- return __nativeToString.call(this);
2037
+ }
2038
+ activeActions = nextActiveActions.length >= nextBreadth ? nextActiveActions : null;
2039
+ }
2040
+ process.stderr.write(`PROGRESS:research:final-report
2041
+ `);
2042
+ let synthesis = {
2043
+ answer: allLearnings.length ? allLearnings.map((learning) => `- ${learning}`).join(`
2044
+ `) : "Research completed, but no structured learnings were extracted.",
2045
+ agreement: { level: "mixed", summary: "Research synthesis fallback." },
2046
+ differences: [],
2047
+ caveats: [],
2048
+ claims: [],
2049
+ recommendedSources: combinedSources.slice(0, 4).map((source) => source.id),
2050
+ synthesized: false
2051
+ };
2052
+ try {
2053
+ const rawReport = await runGeminiPrompt(buildFinalReportPrompt(query, rounds, combinedSources, questions, evidenceItems), { timeoutMs: 180000 });
2054
+ const parsed = parseGeminiJson(rawReport, {});
2055
+ const hasClaims = Array.isArray(parsed?.claims) && parsed.claims.length > 0;
2056
+ synthesis = {
2057
+ ...synthesis,
2058
+ ...parsed,
2059
+ rawAnswer: rawReport.answer || "",
2060
+ geminiSources: rawReport.sources || [],
2061
+ synthesized: hasClaims
333
2062
  };
334
- } catch(_) {}
335
- })();
336
- `})])}function ka(t){return ha(new URL(".",t)).replace(/^\/([A-Z]:)/,"$1")}function Ha(t,n=9222){let f=Number.parseInt(String(t??""),10);return Number.isInteger(f)&&f>1024&&f<65535?f:n}function Ya(){try{if(Jt(pt)){let t=Tf(pt,"utf8"),n=JSON.parse(t);if(Array.isArray(n.engines)&&n.engines.length>0&&n.engines.every((f)=>typeof f==="string")){let f=n.engines.filter((a)=>ln[a]),p=n.engines.filter((a)=>!ln[a]);if(p.length>0)process.stderr.write(`[greedysearch] Warning: ignoring unknown engine(s) in ${pt}: ${p.join(", ")}
337
- [greedysearch] Available engines: ${Object.keys(ln).join(", ")}
338
- `);if(f.length>0)return f;process.stderr.write(`[greedysearch] Warning: no valid engines in ${pt}, falling back to defaults: ${hn.join(", ")}
339
- `)}}}catch{}return hn}function Ia(){try{if(!Jt(en))Ea(en,{recursive:!0});if(!Jt(pt))Da(pt,JSON.stringify({engines:hn,synthesizer:En},null,2)+`
340
- `,"utf8")}catch{}}function Fa(){try{if(Jt(pt)){let t=Tf(pt,"utf8"),n=JSON.parse(t);if(typeof n.synthesizer==="string"){let f=n.synthesizer.toLowerCase();if(hf.includes(f))return f;process.stderr.write(`[greedysearch] Warning: unknown synthesizer "${n.synthesizer}" in ${pt}
341
- [greedysearch] Available synthesizers: ${hf.join(", ")}
342
- [greedysearch] Falling back to default: ${En}
343
- `)}}}catch{}return En}function Ga(t,n=9222){let f=Number.parseInt(String(t??""),10);return Number.isInteger(f)&&f>1024&&f<65535?f:n}function Wa(){try{if(Zt(at)){let t=kf(at,"utf8"),n=JSON.parse(t);if(Array.isArray(n.engines)&&n.engines.length>0&&n.engines.every((f)=>typeof f==="string")){let f=n.engines.filter((a)=>rn[a]),p=n.engines.filter((a)=>!rn[a]);if(p.length>0)process.stderr.write(`[greedysearch] Warning: ignoring unknown engine(s) in ${at}: ${p.join(", ")}
344
- [greedysearch] Available engines: ${Object.keys(rn).join(", ")}
345
- `);if(f.length>0)return f;process.stderr.write(`[greedysearch] Warning: no valid engines in ${at}, falling back to defaults: ${bn.join(", ")}
346
- `)}}}catch{}return bn}function ma(){try{if(!Zt(Dn))va(Dn,{recursive:!0});if(!Zt(at))ja(at,JSON.stringify({engines:bn,synthesizer:Cn},null,2)+`
347
- `,"utf8")}catch{}}function Va(){try{if(Zt(at)){let t=kf(at,"utf8"),n=JSON.parse(t);if(typeof n.synthesizer==="string"){let f=n.synthesizer.toLowerCase();if(Ef.includes(f))return f;process.stderr.write(`[greedysearch] Warning: unknown synthesizer "${n.synthesizer}" in ${at}
348
- [greedysearch] Available synthesizers: ${Ef.join(", ")}
349
- [greedysearch] Falling back to default: ${Cn}
350
- `)}}}catch{}return Cn}async function Yf(){let t=(await M(["list"])).split(`
351
- `)[0];if(!t)throw Error("No Chrome tabs found");return t.slice(0,8)}async function Rn(t="about:blank"){let n=await Yf(),f=new URL(t).hostname;if(f==="copilot.microsoft.com"||f==="www.perplexity.ai"||f==="perplexity.ai"||f.endsWith(".perplexity.ai")){let l=await M(["evalraw",n,"Target.createTarget",JSON.stringify({url:"about:blank"})]),{targetId:g}=JSON.parse(l),r=g.slice(0,8);if(await M(["list"]).catch(()=>null),f==="copilot.microsoft.com")await ef(r);else ef(r).catch(()=>{});return await M(["list"]).catch(()=>null),g}let p=await M(["evalraw",n,"Target.createTarget",JSON.stringify({url:t})]),{targetId:a}=JSON.parse(p);return await M(["list"]).catch(()=>null),a}async function yn(t){try{let n=await Yf();await M(["evalraw",n,"Target.closeTarget",JSON.stringify({targetId:t})])}catch{}}function oa(t,n=9222){let f=Number.parseInt(String(t??""),10);return Number.isInteger(f)&&f>1024&&f<65535?f:n}function Ua(){try{if(Kt(gt)){let t=If(gt,"utf8"),n=JSON.parse(t);if(Array.isArray(n.engines)&&n.engines.length>0&&n.engines.every((f)=>typeof f==="string")){let f=n.engines.filter((a)=>wn[a]),p=n.engines.filter((a)=>!wn[a]);if(p.length>0)process.stderr.write(`[greedysearch] Warning: ignoring unknown engine(s) in ${gt}: ${p.join(", ")}
352
- [greedysearch] Available engines: ${Object.keys(wn).join(", ")}
353
- `);if(f.length>0)return f;process.stderr.write(`[greedysearch] Warning: no valid engines in ${gt}, falling back to defaults: ${jn.join(", ")}
354
- `)}}}catch{}return jn}function Qa(){try{if(!Kt(vn))Ja(vn,{recursive:!0});if(!Kt(gt))Za(gt,JSON.stringify({engines:jn,synthesizer:Pn},null,2)+`
355
- `,"utf8")}catch{}}function xa(){try{if(Kt(gt)){let t=If(gt,"utf8"),n=JSON.parse(t);if(typeof n.synthesizer==="string"){let f=n.synthesizer.toLowerCase();if(Df.includes(f))return f;process.stderr.write(`[greedysearch] Warning: unknown synthesizer "${n.synthesizer}" in ${gt}
356
- [greedysearch] Available synthesizers: ${Df.join(", ")}
357
- [greedysearch] Falling back to default: ${Pn}
358
- `)}}}catch{}return Pn}function za(){if(typeof globalThis.DOMMatrix>"u")globalThis.DOMMatrix=class{constructor(t=void 0){}multiplySelf(){return this}preMultiplySelf(){return this}translateSelf(){return this}scaleSelf(){return this}rotateSelf(){return this}};if(typeof globalThis.ImageData>"u")globalThis.ImageData=class{constructor(t=void 0,n=0,f=0){this.data=t,this.width=n,this.height=f}};if(typeof globalThis.Path2D>"u")globalThis.Path2D=class{constructor(t=void 0){}}}async function Sa(){za();let t=await import("pdf-parse"),n=t.PDFParse??t.default;if(!n)throw Error("pdf-parse did not export PDFParse");return n}async function ca(t,n){try{let f=new(await Sa())({data:new Uint8Array(t)});await f.load();let p=await f.getText(),a=p.text?.trim();if(!a)return null;return{title:new URL(n).pathname.split("/").pop()||"Document.pdf",content:`## PDF Content (${p.total} pages)
359
-
360
- ${a}`,pages:p.total}}catch(f){return{error:f.message||String(f)}}}function Gf(t="",n=240){let f=String(t).replaceAll(/\s+/g," ").trim();if(f.length<=n)return f;let p=f.slice(0,n),a=p.lastIndexOf(" ");return a>0?`${p.slice(0,a)}...`:`${p}...`}async function da(t,n,f=8000){let p=Date.now();try{let a=(await M(["evalraw",t,"Page.getFrameTree","{}"]).then((E)=>JSON.parse(E)).catch(()=>null))?.frameTree?.frame?.id||void 0,l=await M(["evalraw",t,"Network.loadNetworkResource",JSON.stringify({frameId:a,url:n,options:{disableCache:!0,includeCredentials:!1}})],20000),g=JSON.parse(l).resource;if(!g?.success||!g.httpStatusCode)return{url:n,error:g?.netErrorName||g?.netError||"loadNetworkResource failed",source:"chrome",duration:Date.now()-p,needsFallback:!0};let r="";if(g.stream)try{let E=await M(["evalraw",t,"IO.read",JSON.stringify({handle:g.stream})],1e4);r=JSON.parse(E).data||"",await M(["evalraw",t,"IO.close",JSON.stringify({handle:g.stream})]).catch(()=>{})}catch{}if(!r||r.length<100)return{url:n,error:"Empty response body from Network.loadNetworkResource",source:"chrome",duration:Date.now()-p,needsFallback:!0};let w=Pf(g.httpStatusCode,r,n,n);if(w.blocked)return{url:n,status:g.httpStatusCode,error:`Blocked: ${w.reason}`,source:"chrome",duration:Date.now()-p,needsBrowser:!0};let $=await Rf(r,n),_=yf($);if(!_.ok)return{url:n,status:g.httpStatusCode,error:`Low quality: ${_.reason}`,source:"chrome",duration:Date.now()-p,needsBrowser:!0};let h=Rt($.markdown,f);return{url:n,finalUrl:n,status:g.httpStatusCode,contentType:"text/markdown",lastModified:"",publishedTime:$.publishedTime||"",byline:$.byline||"",siteName:$.siteName||"",lang:$.lang||"",title:$.title||n,snippet:$.excerpt,content:h,contentChars:h.length,source:"chrome",duration:Date.now()-p}}catch(a){return{url:n,error:a.message,source:"chrome",duration:Date.now()-p,needsFallback:!0}}}function Mf(t){try{return new URL(t).pathname.toLowerCase().endsWith(".pdf")}catch{return!1}}async function qa(t,n=8000){let f=$n(t);if(f.blocked)return{url:t,finalUrl:t,status:403,error:`Blocked: ${f.reason}`,source:"pdf-http"};let p=new AbortController,a=setTimeout(()=>p.abort(),20000),l=Date.now();try{let g=await fetch(t,{method:"GET",redirect:"follow",signal:p.signal,headers:xp({accept:"application/pdf,application/octet-stream;q=0.9,*/*;q=0.5"})});clearTimeout(a);let r=g.headers.get("content-type")||"",w=g.url||t,$=Number.parseInt(g.headers.get("content-length")||"0",10);if(g.status>=400)return{url:t,finalUrl:w,status:g.status,error:`HTTP ${g.status}`,source:"pdf-http",duration:Date.now()-l};if(!r.toLowerCase().includes("application/pdf")&&!Mf(w))return null;if($>31457280)return{url:t,finalUrl:w,status:g.status,error:`PDF too large: ${$} bytes`,source:"pdf-http",duration:Date.now()-l};let _=Buffer.from(await g.arrayBuffer()),h=await ca(_,w);if(!h||h.error)return{url:t,finalUrl:w,status:g.status,error:h?.error||"PDF text extraction failed",source:"pdf-http",duration:Date.now()-l};let E=Rt(h.content,n);return{url:t,finalUrl:w,status:g.status,contentType:"application/pdf",lastModified:g.headers.get("last-modified")||"",title:h.title,snippet:Gf(E,320),content:E,contentChars:E.length,pages:h.pages,source:"pdf-http",duration:Date.now()-l}}catch(g){return clearTimeout(a),{url:t,finalUrl:t,error:g.message||String(g),source:"pdf-http",duration:Date.now()-l}}}async function Wf(t,n=8000){let f=Date.now();if(Mf(t)){let a=await qa(t,n);if(a?.content||a?.status===403)return a}if(_n(t)){let a=_n(t);if(a&&(a.type==="root"||a.type==="tree"||a.type==="blob"&&!a.path?.includes("."))){let l=await fa(t);if(l.ok){let g=Rt(l.content,n);return{url:t,finalUrl:t,status:200,contentType:"text/markdown",lastModified:"",title:l.title,snippet:g.slice(0,320),content:g,contentChars:g.length,source:"github-api",...l.tree&&{tree:l.tree},duration:Date.now()-f}}process.stderr.write(`[greedysearch] GitHub API fetch failed, trying HTTP: ${l.error}
361
- `)}}if(aa(t)?.type==="post"){process.stderr.write(`[greedysearch] Using Reddit JSON API for: ${t.slice(0,60)}...
362
- `);let a=await ga(t,n);if(a.ok){let l=Rt(a.markdown,n);return{url:t,finalUrl:a.finalUrl,status:a.status,contentType:"text/markdown",lastModified:a.lastModified||"",publishedTime:a.publishedTime||"",byline:a.byline||"",siteName:a.siteName||"",lang:a.lang||"",title:a.title,snippet:a.excerpt,content:l,contentChars:l.length,source:"reddit-api",duration:Date.now()-f}}process.stderr.write(`[greedysearch] Reddit API fetch failed, falling back to HTTP: ${a.error}
363
- `)}let p=await Sp(t,{timeoutMs:1e4});if(p.ok){let a=Rt(p.markdown,n);return{url:t,finalUrl:p.finalUrl,status:p.status,contentType:"text/markdown",lastModified:p.lastModified||"",publishedTime:p.publishedTime||"",byline:p.byline||"",siteName:p.siteName||"",lang:p.lang||"",title:p.title,snippet:p.excerpt,content:a,contentChars:a.length,source:"http",duration:Date.now()-f}}if(p.needsBrowser)try{let a=await Rn();try{let l=await da(a,t,n);if(l.content&&l.content.length>100)return l}finally{await yn(a)}}catch{}return process.stderr.write(`[greedysearch] HTTP failed for ${t.slice(0,60)}, trying browser...
364
- `),await sa(t,n)}async function mf(t,n=4000,f=200){let p=Date.now()+n;while(Date.now()<p){try{if((await M(["eval",t,'document.readyState === "complete" && !!document.body && document.body.innerText.length > 500'])).trim()==="true")return}catch{}await new Promise((a)=>setTimeout(a,f))}}async function sa(t,n=8000){let f=Date.now(),p;try{p=await Rn()}catch(a){return{url:t,title:"",content:null,snippet:"",contentChars:0,error:`openNewTab failed: ${a.message}`,source:"browser",duration:Date.now()-f}}try{await M(["nav",p,t],30000),await mf(p);let a=await M(["eval",p,String.raw`
365
- (function(){
366
- var el = document.querySelector('article, [role="main"], main, .post-content, .article-body, #content, .content');
367
- var text = (el || document.body).innerText;
368
- return JSON.stringify({
369
- title: document.title,
370
- content: text.replace(/\s+/g, ' ').trim(),
371
- url: location.href
372
- });
373
- })()
374
- `]),l=JSON.parse(a),g=Rt(l.content,n);return{url:t,finalUrl:l.url||t,status:200,contentType:"text/plain",lastModified:"",title:l.title,snippet:Gf(g,320),content:g,contentChars:g.length,source:"browser",duration:Date.now()-f}}catch(a){return{url:t,title:"",content:null,snippet:"",contentChars:0,error:a.message,source:"browser",duration:Date.now()-f}}finally{await yn(p)}}async function On(t,n=5,f=8000,p=bf){let a=t.slice(0,n);if(a.length===0)return[];let l=Math.min(a.length,Math.max(1,Number.parseInt(String(p),10)||bf));process.stderr.write(`[greedysearch] Fetching content from ${a.length} sources via HTTP (concurrency ${l})...
375
- `);let g=Array(a.length),r=0,w=0;async function $(){while(!0){let e=r++;if(e>=a.length)return;let b=a[e],D=b.canonicalUrl||b.url;process.stderr.write(`[greedysearch] [${e+1}/${a.length}] Fetching: ${D.slice(0,60)}...
376
- `);let v=await Wf(D,f).catch((P)=>({url:D,title:"",content:null,snippet:"",contentChars:0,error:P.message,source:"error",duration:0}));if(g[e]={id:b.id,...v},v.content&&v.content.length>100)process.stderr.write(`[greedysearch] ✓ ${v.source}: ${v.content.length} chars
377
- `);else if(v.error)process.stderr.write(`[greedysearch] ✗ ${v.error.slice(0,80)}
378
- `);w+=1,process.stderr.write(`PROGRESS:fetch:${w}/${a.length}
379
- `)}}await Promise.all(Array.from({length:l},()=>$()));let _=g.filter((e)=>e.content&&e.content.length>100),h=g.filter((e)=>e.source==="http").length,E=g.filter((e)=>e.source==="browser").length;return process.stderr.write(`[greedysearch] Fetched ${_.length}/${g.length} sources (HTTP: ${h}, Browser: ${E})
380
- `),g}async function ua(t){let n=await Rn();try{await M(["nav",n,t],30000),await mf(n);let f=await M(["eval",n,String.raw`
381
- (function(){
382
- var el = document.querySelector('article, [role="main"], main, .post-content, .article-body, #content, .content');
383
- var text = (el || document.body).innerText;
384
- return text.replace(/\s+/g, ' ').trim();
385
- })()
386
- `]);return{url:t,content:f}}catch(f){return{url:t,content:null,error:f.message}}finally{await yn(n)}}var cl,ql,Vt=null,Cf="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",vf,jf,Xp,op,Bp,cp,dp,qp,Of,pa,Oa,Na,ia,La,er,x,hr,Er,Dr,br,Cr,vr,jr,Pr,Rr,en,pt,hn,En="gemini",hf,ln,yr,Or,Nr,Ma,ir,z,Tr,Ar,kr,Hr,Lr,Yr,Ir,Fr,Gr,Dn,at,bn,Cn="gemini",Ef,rn,Mr,Wr,mr,Vr,Jr,Zr,Kr,M,Ba,Qr,S,xr,zr,Sr,cr,dr,qr,sr,ur,t0,vn,gt,jn,Pn="gemini",Df,wn,n0,f0,bf;var Xt=qt(()=>{cl=Jp(import.meta.url),ql=Zp(import.meta.url);vf={"user-agent":Cf,accept:"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8","accept-language":"en-US,en;q=0.9","accept-encoding":"gzip, deflate, br","cache-control":"no-cache",pragma:"no-cache","sec-ch-ua":'"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',"sec-ch-ua-mobile":"?0","sec-ch-ua-platform":'"Windows"',"sec-fetch-dest":"document","sec-fetch-mode":"navigate","sec-fetch-site":"none","sec-fetch-user":"?1","upgrade-insecure-requests":"1"},jf=[/^localhost$/i,/^127\.\d+\.\d+\.\d+$/,/^0\.0\.0\.0$/,/^\[::1\]$/,/^10\./,/^172\.(1[6-9]|2\d|3[01])\./,/^192\.168\./,/^169\.254\./,/^fc00:/i,/^fe80:/i,/\.local$/i,/\.internal$/i,/\.localhost$/i],Xp=/^::ffff:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/i,op=/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i,Bp=/^(?:0x[0-9a-f]+|0[0-7]*|[1-9][0-9]*)$/i;cp=["accounts.google.com","login.microsoftonline.com","login.live.com","auth0.com","okta.com","auth.mozilla.auth0.com","id.atlassian.com"],dp=["login.","signin.","auth.","sso.","accounts.","idp."],qp=["sign in to continue","log in to continue","authentication required","create an account to continue","subscribe to continue reading","members only"];Of={"user-agent":"GreedySearch/1.0",accept:"application/vnd.github+json","x-github-api-version":"2022-11-28"};pa={"user-agent":"GreedySearch/1.0 (Research Bot)",accept:"application/json"};Oa=$a(ea(import.meta.url)),Na=_a(Oa,"..","bin","cdp.mjs"),ia=new Set(["list","snap","eval","shot","html","nav","net","click","clickxy","type","loadall","evalraw","browse","stop","--tab"]);La=Ca().replaceAll("\\","/"),er=Ha(process.env.GREEDY_SEARCH_PORT),x=(process.env.GREEDY_SEARCH_PROFILE_DIR||process.env.CDP_PROFILE_DIR||`${La}/greedysearch-chrome-profile`).replaceAll("\\","/"),hr=`${x}/DevToolsActivePort`,Er=process.env.GREEDY_SEARCH_PID_FILE||`${x}/browser.pid`,Dr=process.env.CDP_PAGES_CACHE||`${x}/cdp-pages.json`,br=process.env.GREEDY_SEARCH_MODE_FILE||`${x}/browser-mode`,Cr=process.env.GREEDY_SEARCH_METADATA_FILE||`${x}/browser-metadata.json`,vr=process.env.GREEDY_SEARCH_LAUNCH_LOCK_FILE||`${x}/browser-launch.lock`,jr=process.env.GREEDY_SEARCH_ACTIVITY_FILE||`${x}/browser-last-activity`,Pr=(process.env.CDP_SOCKET_DIR||`${x}/cdp-sockets`).replaceAll("\\","/"),Rr=`${x}/visible-recovery.jsonl`,en=Af(ba(),".dm"),pt=Af(en,"greedyconfig"),hn=["perplexity","google","chatgpt","gemini"];Ia();hf=["gemini","chatgpt"];ln={perplexity:"perplexity.mjs",p:"perplexity.mjs",bing:"bing-copilot.mjs",b:"bing-copilot.mjs",google:"google-ai.mjs",g:"google-ai.mjs",gemini:"gemini.mjs",gem:"gemini.mjs",chatgpt:"chatgpt.mjs",gpt:"chatgpt.mjs","semantic-scholar":"semantic-scholar.mjs",semanticscholar:"semantic-scholar.mjs",s2:"semantic-scholar.mjs",logically:"logically.mjs",log:"logically.mjs"},yr=Ya(),Or=Fa(),Nr=Math.max(1,Number.parseInt(process.env.GREEDY_FETCH_CONCURRENCY||"5",10)||5);process.env.CDP_PROFILE_DIR=x;Ma=Ra().replaceAll("\\","/"),ir=Ga(process.env.GREEDY_SEARCH_PORT),z=(process.env.GREEDY_SEARCH_PROFILE_DIR||process.env.CDP_PROFILE_DIR||`${Ma}/greedysearch-chrome-profile`).replaceAll("\\","/"),Tr=`${z}/DevToolsActivePort`,Ar=process.env.GREEDY_SEARCH_PID_FILE||`${z}/browser.pid`,kr=process.env.CDP_PAGES_CACHE||`${z}/cdp-pages.json`,Hr=process.env.GREEDY_SEARCH_MODE_FILE||`${z}/browser-mode`,Lr=process.env.GREEDY_SEARCH_METADATA_FILE||`${z}/browser-metadata.json`,Yr=process.env.GREEDY_SEARCH_LAUNCH_LOCK_FILE||`${z}/browser-launch.lock`,Ir=process.env.GREEDY_SEARCH_ACTIVITY_FILE||`${z}/browser-last-activity`,Fr=(process.env.CDP_SOCKET_DIR||`${z}/cdp-sockets`).replaceAll("\\","/"),Gr=`${z}/visible-recovery.jsonl`,Dn=Hf(Pa(),".dm"),at=Hf(Dn,"greedyconfig"),bn=["perplexity","google","chatgpt","gemini"];ma();Ef=["gemini","chatgpt"];rn={perplexity:"perplexity.mjs",p:"perplexity.mjs",bing:"bing-copilot.mjs",b:"bing-copilot.mjs",google:"google-ai.mjs",g:"google-ai.mjs",gemini:"gemini.mjs",gem:"gemini.mjs",chatgpt:"chatgpt.mjs",gpt:"chatgpt.mjs","semantic-scholar":"semantic-scholar.mjs",semanticscholar:"semantic-scholar.mjs",s2:"semantic-scholar.mjs",logically:"logically.mjs",log:"logically.mjs"},Mr=Wa(),Wr=Va(),mr=Math.max(1,Number.parseInt(process.env.GREEDY_FETCH_CONCURRENCY||"5",10)||5);process.env.CDP_PROFILE_DIR=z;Vr=Number.parseInt(process.env.GREEDY_SEARCH_IDLE_TIMEOUT_MINUTES||"5",10)||5,Jr=ka(import.meta.url),Zr=Number.parseInt(process.env.GREEDY_SEARCH_IDLE_TIMEOUT_MINUTES||"5",10)||5,Kr=Number.parseInt(process.env.GREEDY_SEARCH_VISIBLE_IDLE_TIMEOUT_MINUTES||"60",10)||60,M=Lf;Ba=Xa().replaceAll("\\","/"),Qr=oa(process.env.GREEDY_SEARCH_PORT),S=(process.env.GREEDY_SEARCH_PROFILE_DIR||process.env.CDP_PROFILE_DIR||`${Ba}/greedysearch-chrome-profile`).replaceAll("\\","/"),xr=`${S}/DevToolsActivePort`,zr=process.env.GREEDY_SEARCH_PID_FILE||`${S}/browser.pid`,Sr=process.env.CDP_PAGES_CACHE||`${S}/cdp-pages.json`,cr=process.env.GREEDY_SEARCH_MODE_FILE||`${S}/browser-mode`,dr=process.env.GREEDY_SEARCH_METADATA_FILE||`${S}/browser-metadata.json`,qr=process.env.GREEDY_SEARCH_LAUNCH_LOCK_FILE||`${S}/browser-launch.lock`,sr=process.env.GREEDY_SEARCH_ACTIVITY_FILE||`${S}/browser-last-activity`,ur=(process.env.CDP_SOCKET_DIR||`${S}/cdp-sockets`).replaceAll("\\","/"),t0=`${S}/visible-recovery.jsonl`,vn=Ff(Ka(),".dm"),gt=Ff(vn,"greedyconfig"),jn=["perplexity","google","chatgpt","gemini"];Qa();Df=["gemini","chatgpt"];wn={perplexity:"perplexity.mjs",p:"perplexity.mjs",bing:"bing-copilot.mjs",b:"bing-copilot.mjs",google:"google-ai.mjs",g:"google-ai.mjs",gemini:"gemini.mjs",gem:"gemini.mjs",chatgpt:"chatgpt.mjs",gpt:"chatgpt.mjs","semantic-scholar":"semantic-scholar.mjs",semanticscholar:"semantic-scholar.mjs",s2:"semantic-scholar.mjs",logically:"logically.mjs",log:"logically.mjs"},n0=Ua(),f0=xa(),bf=Math.max(1,Number.parseInt(process.env.GREEDY_FETCH_CONCURRENCY||"5",10)||5);process.env.CDP_PROFILE_DIR=S});var Bf={};dt(Bf,{parseGitHubUrl:()=>of,fetchGitHubContent:()=>vg});function of(t){try{let n=new URL(t);if(!(n.hostname==="github.com"||n.hostname.endsWith(".github.com")))return null;let f=n.pathname.split("/").filter(Boolean);if(f.length<2)return null;let[p,a]=f;if(f.length===2)return{owner:p,repo:a,type:"root"};if(f.length>=4&&(f[2]==="blob"||f[2]==="tree")){let l=f[2],g=f[3],r=f.slice(4).join("/");return{owner:p,repo:a,type:l,ref:g,path:r}}return null}catch{return null}}async function U(t,n=1e4){let f=new AbortController,p=setTimeout(()=>f.abort(),n);try{let a=await fetch(`https://api.github.com${t}`,{headers:Xf,signal:f.signal});if(clearTimeout(p),!a.ok)throw Error(`GitHub API ${a.status}: ${t}`);return await a.json()}catch(a){throw clearTimeout(p),a}}async function bg(t,n){try{let f=await U(`/repos/${t}/${n}/readme`);if(f.content&&f.encoding==="base64")return Buffer.from(f.content,"base64").toString("utf8");return""}catch{return""}}async function Kf(t,n,f="HEAD",p="",a){try{let l;if(f==="HEAD")if(a)l=await U(`/repos/${t}/${n}/git/ref/heads/${a}`).catch(()=>null);else l=await Promise.any([U(`/repos/${t}/${n}/git/ref/heads/main`),U(`/repos/${t}/${n}/git/ref/heads/master`)]).catch(()=>null);else l=await U(`/repos/${t}/${n}/git/ref/heads/${f}`).catch(()=>U(`/repos/${t}/${n}/git/ref/heads/master`).catch(()=>null));if(!l?.object?.sha)return[];let g=(await U(`/repos/${t}/${n}/git/commits/${l.object.sha}`)).tree.sha,r=(await U(`/repos/${t}/${n}/git/trees/${g}`)).tree||[];if(p)r=r.filter((w)=>w.path.startsWith(p));return r.slice(0,50).map((w)=>({path:w.path,type:w.type==="tree"?"dir":"file",size:w.size}))}catch{return[]}}async function Cg(t,n,f,p,a=1e4,l){let g=async(w)=>{let $=new AbortController,_=setTimeout(()=>$.abort(),a);try{let h=await fetch(w,{headers:{"user-agent":Xf["user-agent"]},signal:$.signal});if(clearTimeout(_),h.ok)return await h.text();throw Error("not ok")}catch{throw clearTimeout(_),Error("failed")}};if(!f||f==="HEAD"){if(l)try{return await g(`https://raw.githubusercontent.com/${t}/${n}/${l}/${p}`)}catch{return null}try{return await Promise.any([g(`https://raw.githubusercontent.com/${t}/${n}/main/${p}`),g(`https://raw.githubusercontent.com/${t}/${n}/master/${p}`)])}catch{return null}}let r=[`https://raw.githubusercontent.com/${t}/${n}/${f}/${p}`,`https://raw.githubusercontent.com/${t}/${n}/master/${p}`];for(let w of r)try{return await g(w)}catch{}return null}async function vg(t){let n=of(t);if(!n)return{ok:!1,error:"Not a valid GitHub URL"};let{owner:f,repo:p,type:a,ref:l,path:g}=n;try{if(a==="root"||a==="tree"&&!g){let r=await U(`/repos/${f}/${p}`),[w,$]=await Promise.allSettled([bg(f,p),Kf(f,p,l||"HEAD","",r.default_branch)]),_=w.status==="fulfilled"?w.value:"",h=$.status==="fulfilled"?$.value:[],E=r?.description?`
387
-
388
- > ${r.description}`:"",e=r?.stargazers_count==null?"":` ⭐ ${r.stargazers_count}`,b=r?.language?` · ${r.language}`:"",D=`# ${f}/${p}${e}${b}${E}
389
-
390
- `;if(_)D+=_.slice(0,6000);else D+=`[No README found]
391
-
392
- Files:
393
- ${h.map((v)=>` ${v.type==="dir"?"\uD83D\uDCC1":"\uD83D\uDCC4"} ${v.path}`).join(`
394
- `)}`;return{ok:!0,title:`${f}/${p}`,content:D,tree:h.slice(0,30)}}if(a==="blob"&&g){let r;if(!l||l==="HEAD")try{r=(await U(`/repos/${f}/${p}`)).default_branch}catch{r=void 0}let w=await Cg(f,p,l,g,1e4,r);if(w===null)return{ok:!1,error:`File not found: ${g}`};return{ok:!0,title:`${f}/${p}: ${g}`,content:w}}if(a==="tree"&&g){let r;if(!l||l==="HEAD")try{r=(await U(`/repos/${f}/${p}`)).default_branch}catch{r=void 0}let w=await Kf(f,p,l||"HEAD",g,r),$=w.map((_)=>` ${_.type==="dir"?"\uD83D\uDCC1":"\uD83D\uDCC4"} ${_.path}`).join(`
395
- `);return{ok:!0,title:`${f}/${p}/${g}`,content:`[Directory: ${g}]
396
-
397
- Files:
398
- ${$}`,tree:w}}return{ok:!1,error:"Unsupported GitHub URL type"}}catch(r){return{ok:!1,error:r.message}}}var Xf;var Uf=qt(()=>{Xf={"user-agent":"GreedySearch/1.0",accept:"application/vnd.github+json","x-github-api-version":"2022-11-28"}});import{spawn as jg}from"node:child_process";import{basename as rp}from"node:path";function jt(t=process.env,n=process.execPath){let f=t.GREEDY_SEARCH_NODE||t.NODE_BINARY||t.NODE;if(f?.trim())return f.trim();let p=rp(n||"").toLowerCase();if(p==="node"||p==="node.exe")return n;return"node"}import{mkdirSync as Fn,writeFileSync as J}from"node:fs";import{join as Y}from"node:path";import{fileURLToPath as Pg}from"node:url";var wp=["fbclid","gclid","ref","ref_src","ref_url","source","utm_campaign","utm_content","utm_medium","utm_source","utm_term"],$p=["dev.to","hashnode.com","medium.com","reddit.com","stackoverflow.com","stackexchange.com","substack.com"],_p=["arstechnica.com","techcrunch.com","theverge.com","venturebeat.com","wired.com","zdnet.com"],dn=["facebook.com","instagram.com","linkedin.com","pinterest.com","tiktok.com","twitter.com","x.com"];function H(t="",n=240){let f=String(t).replaceAll(/\s+/g," ").trim();if(f.length<=n)return f;let p=f.slice(0,n),a=p.lastIndexOf(" ");return a>0?`${p.slice(0,a)}...`:`${p}...`}function st(t=""){let n=H(t,180);if(!n)return"";if(/^https?:\/\//i.test(n))return"";let f=n.split(/\s+/).filter(Boolean).length,p=/[A-Z]/.test(n),a=/\d/.test(n);return n===n.toLowerCase()&&f<=4&&!p&&!a?"":n}function ut(t="",n=""){let f=st(t),p=st(n);if(!p)return f;if(!f)return p;let a=/^https?:\/\//i.test(f),l=/^https?:\/\//i.test(p);if(a&&!l)return p;if(!a&&l)return f;return p.length>f.length?p:f}function m(t){if(!t)return null;try{let n=new URL(t);if(!["http:","https:"].includes(n.protocol))return null;if(n.hash="",n.hostname=n.hostname.toLowerCase(),n.protocol==="https:"&&n.port==="443"||n.protocol==="http:"&&n.port==="80")n.port="";for(let a of[...n.searchParams.keys()]){let l=a.toLowerCase();if(wp.includes(l)||l.startsWith("utm_"))n.searchParams.delete(a)}n.searchParams.sort();let f=n.pathname.replace(/\/{1,10}$/,"")||"/";n.pathname=f;let p=n.toString();return f==="/"?p.replace(/\/$/,""):p}catch{return null}}function ep(t){try{return new URL(t).hostname.toLowerCase().replace(/^www\./,"")}catch{return""}}function it(t,n){return n.some((f)=>t===f||t.endsWith(`.${f}`))}function tn(t,n="",f=""){let p=n.toLowerCase(),a=f.toLowerCase();if(t==="github.com"||t==="gitlab.com")return"repo";if(t==="arxiv.org"||t==="doi.org"||t==="semanticscholar.org"||t.endsWith(".semanticscholar.org")||a.includes("/paper/")||a.includes("/pdf/"))return"academic";if(it(t,dn))return"social";if(it(t,$p))return"community";if(it(t,_p))return"news";if(t.startsWith("docs.")||t.startsWith("developer.")||t.startsWith("developers.")||t.startsWith("api.")||p.includes("documentation")||p.includes("docs")||p.includes("reference")||a.includes("/docs/")||a.includes("/reference/")||a.includes("/api/"))return"official-docs";if(t.startsWith("blog.")||a.includes("/blog/"))return"maintainer-blog";return"website"}function hp(t){switch(t){case"official-docs":return 5;case"repo":return 4;case"academic":return 4;case"maintainer-blog":return 3;case"website":return 2;case"community":return 1;case"news":return 0;case"social":return-6;default:return 0}}function Ep(t){let n=Object.values(t.perEngine||{}).map((f)=>f?.rank||99);return n.length?Math.min(...n):99}var Dp=["reddit.com","news.ycombinator.com","lobste.rs"];function et(t){return t.smartScore*3+t.engineCount*5+hp(t.sourceType)*2+Math.max(0,7-Ep(t))}function bp(t){let n=t.toLowerCase(),f=[];if(n.includes("openai")||n.includes("gpt")||n.includes("chatgpt"))f.push("openai.com","platform.openai.com","help.openai.com");if(n.includes("anthropic")||n.includes("claude"))f.push("anthropic.com","docs.anthropic.com");if(n.includes("bun"))f.push("bun.sh","bun.com");if(n.includes("next.js")||n.includes("nextjs"))f.push("nextjs.org","vercel.com");if(n.includes("playwright"))f.push("playwright.dev");if(n.includes("supabase"))f.push("supabase.com","supabase.io");if(n.includes("prisma"))f.push("prisma.io");if(n.includes("tailwind"))f.push("tailwindcss.com");if(n.includes("vite"))f.push("vitejs.dev","vite.dev");if(n.includes("astro"))f.push("astro.build");if(n.includes("svelte"))f.push("svelte.dev");if(n.includes("solid"))f.push("solidjs.com");if(n.includes("vue")||n.includes("nuxt"))f.push("vuejs.org","nuxt.com");if(n.includes("react")||n.includes("react native"))f.push("react.dev","reactnative.dev");if(n.includes("angular"))f.push("angular.io","angular.dev");if(n.includes("node.js")||n.includes("nodejs"))f.push("nodejs.org","nodejs.dev","npmjs.com");if(/\bgo\b/.test(n)||n.includes("golang"))f.push("go.dev","golang.org","pkg.go.dev");if(n.includes("deno"))f.push("deno.land","deno.com");if(n.includes("fresh"))f.push("fresh.deno.dev");if(n.includes("typescript")||n.includes("ts"))f.push("typescriptlang.org");if(n.includes("python"))f.push("python.org","docs.python.org");if(n.includes("rust"))f.push("rust-lang.org","docs.rs","crates.io");if(n.includes("zig"))f.push("ziglang.org");if(n.includes("docker"))f.push("docker.com","docs.docker.com","hub.docker.com");if(n.includes("kubernetes")||n.includes("k8s"))f.push("kubernetes.io","k8s.io");if(n.includes("postgres")||n.includes("postgresql"))f.push("postgresql.org","neon.tech","supabase.com");if(n.includes("redis"))f.push("redis.io");if(n.includes("sqlite"))f.push("sqlite.org");if(n.includes("cloudflare"))f.push("developers.cloudflare.com","cloudflare.com");if(n.includes("vercel"))f.push("vercel.com","nextjs.org");if(n.includes("netlify"))f.push("netlify.com","docs.netlify.com");if(n.includes("stripe"))f.push("stripe.com","docs.stripe.com");if(n.includes("github"))f.push("github.com","docs.github.com");if(n.includes("gitlab"))f.push("gitlab.com","docs.gitlab.com");if(n.includes("aws"))f.push("aws.amazon.com","docs.aws.amazon.com");if(n.includes("azure"))f.push("azure.microsoft.com","learn.microsoft.com");if(n.includes("gcp")||n.includes("google cloud"))f.push("cloud.google.com","developers.google.com");if(n.includes("gemini")||n.includes("google ai"))f.push("ai.google.dev","developers.google.com");for(let p of dn){let a=p.replace(/\.com$/,"");if(n.includes(a))f.push(p)}return[...new Set(f)]}function cn(t,n){return t===n||t.endsWith(`.${n}`)}function Tt(t,n=""){let f=new Map,p=Object.keys(t||{}).filter(($)=>!$.startsWith("_")),a=bp(n);for(let $ of p){let _=t[$];if(!_?.sources)continue;for(let h=0;h<_.sources.length;h++){let E=_.sources[h],e=m(E.url);if(!e||e.length<10)continue;let b=st(E.title||""),D=ep(e),v=tn(D,b,e),P=0;if(a.some((I)=>cn(D,I)))P+=10;if(v==="official-docs")P+=3;let R=e.toLowerCase();if(/\/docs\/|\/documentation\/|\.dev\/|\/api\/|\/reference\//.test(R))P+=2;let y=a.some((I)=>cn(D,I));if(v==="social"&&!y)P-=20;if(a.length>0){if(it(D,Dp))P-=3;else if(v==="community"&&!it(D,["stackoverflow.com","stackexchange.com"]))P-=1}let j=f.get(e)||{id:"",canonicalUrl:e,displayUrl:E.url||e,domain:D,title:"",engines:[],engineCount:0,perEngine:{},sourceType:v,isOfficial:v==="official-docs",smartScore:0};if(j.title=ut(j.title,b),j.displayUrl=j.displayUrl||E.url||e,j.sourceType=j.sourceType||v,j.isOfficial=j.isOfficial||v==="official-docs",j.smartScore=Math.max(j.smartScore,P),!j.engines.includes($))j.engines.push($);j.perEngine[$]={rank:h+1,title:ut(j.perEngine[$]?.title||"",b)},f.set(e,j)}}let l=Array.from(f.values()).map(($)=>({...$,engineCount:$.engines.length})),g=l.filter(($)=>$.sourceType!=="social"),r=l.filter(($)=>$.sourceType==="social");return g.sort(($,_)=>{let h=et(_)-et($);if(h!==0)return h;return $.domain.localeCompare(_.domain)}),r.sort(($,_)=>{let h=et(_)-et($);if(h!==0)return h;return $.domain.localeCompare(_.domain)}),[...g,...r].slice(0,12).map(($,_)=>({...$,id:`S${_+1}`,title:$.title||$.domain||$.canonicalUrl}))}function Gt(t,n){let f=new Map(n.map((p)=>[p.id,p]));return t.map((p)=>{let a=f.get(p.id);if(!a)return p;let l=ut(p.title,a.title||"");return{...p,title:l||p.title,fetch:{attempted:!0,ok:!a.error&&a.contentChars>100,status:a.status||null,finalUrl:a.finalUrl||a.url||p.canonicalUrl,contentType:a.contentType||"",lastModified:a.lastModified||"",publishedTime:a.publishedTime||"",byline:a.byline||"",siteName:a.siteName||"",lang:a.lang||"",title:a.title||"",snippet:a.snippet||"",contentChars:a.contentChars||0,source:a.source||"unknown",duration:a.duration||0,error:a.error||""}}})}import{existsSync as Mt,mkdirSync as Cp,readFileSync as qn,writeFileSync as vp}from"node:fs";import{homedir as jp}from"node:os";import{join as sn}from"node:path";import{tmpdir as Pp}from"node:os";function Rp(t,n=9222){let f=Number.parseInt(String(t??""),10);return Number.isInteger(f)&&f>1024&&f<65535?f:n}var yp=Pp().replaceAll("\\","/"),El=Rp(process.env.GREEDY_SEARCH_PORT),K=(process.env.GREEDY_SEARCH_PROFILE_DIR||process.env.CDP_PROFILE_DIR||`${yp}/greedysearch-chrome-profile`).replaceAll("\\","/"),Dl=`${K}/DevToolsActivePort`,bl=process.env.GREEDY_SEARCH_PID_FILE||`${K}/browser.pid`,Cl=process.env.CDP_PAGES_CACHE||`${K}/cdp-pages.json`,vl=process.env.GREEDY_SEARCH_MODE_FILE||`${K}/browser-mode`,jl=process.env.GREEDY_SEARCH_METADATA_FILE||`${K}/browser-metadata.json`,Pl=process.env.GREEDY_SEARCH_LAUNCH_LOCK_FILE||`${K}/browser-launch.lock`,Rl=process.env.GREEDY_SEARCH_ACTIVITY_FILE||`${K}/browser-last-activity`,yl=(process.env.CDP_SOCKET_DIR||`${K}/cdp-sockets`).replaceAll("\\","/"),Ol=`${K}/visible-recovery.jsonl`,fn=sn(jp(),".dm"),nt=sn(fn,"greedyconfig"),pn=["perplexity","google","chatgpt","gemini"],an="gemini";function Op(){try{if(Mt(nt)){let t=qn(nt,"utf8"),n=JSON.parse(t);if(Array.isArray(n.engines)&&n.engines.length>0&&n.engines.every((f)=>typeof f==="string")){let f=n.engines.filter((a)=>nn[a]),p=n.engines.filter((a)=>!nn[a]);if(p.length>0)process.stderr.write(`[greedysearch] Warning: ignoring unknown engine(s) in ${nt}: ${p.join(", ")}
399
- [greedysearch] Available engines: ${Object.keys(nn).join(", ")}
400
- `);if(f.length>0)return f;process.stderr.write(`[greedysearch] Warning: no valid engines in ${nt}, falling back to defaults: ${pn.join(", ")}
401
- `)}}}catch{}return pn}function Np(){try{if(!Mt(fn))Cp(fn,{recursive:!0});if(!Mt(nt))vp(nt,JSON.stringify({engines:pn,synthesizer:an},null,2)+`
402
- `,"utf8")}catch{}}Np();var At=["gemini","chatgpt"];function ip(){try{if(Mt(nt)){let t=qn(nt,"utf8"),n=JSON.parse(t);if(typeof n.synthesizer==="string"){let f=n.synthesizer.toLowerCase();if(At.includes(f))return f;process.stderr.write(`[greedysearch] Warning: unknown synthesizer "${n.synthesizer}" in ${nt}
403
- [greedysearch] Available synthesizers: ${At.join(", ")}
404
- [greedysearch] Falling back to default: ${an}
405
- `)}}}catch{}return an}var nn={perplexity:"perplexity.mjs",p:"perplexity.mjs",bing:"bing-copilot.mjs",b:"bing-copilot.mjs",google:"google-ai.mjs",g:"google-ai.mjs",gemini:"gemini.mjs",gem:"gemini.mjs",chatgpt:"chatgpt.mjs",gpt:"chatgpt.mjs","semantic-scholar":"semantic-scholar.mjs",semanticscholar:"semantic-scholar.mjs",s2:"semantic-scholar.mjs",logically:"logically.mjs",log:"logically.mjs"},Pt=Op(),kt=Pt,Nl=ip(),il=Math.max(1,Number.parseInt(process.env.GREEDY_FETCH_CONCURRENCY||"5",10)||5);process.env.CDP_PROFILE_DIR=K;function Tp(t){let n="",f=!1,p=!1;for(let a of String(t)){if(p){n+=a,p=!1;continue}if(a==="\\"){n+=a,p=!0;continue}if(a==='"'){f=!f,n+=a;continue}if(f&&a===`
406
- `)n+="\\n";else if(f&&a==="\r")n+="\\r";else if(f&&a==="\t")n+="\\t";else n+=a}return n}function ft(t){if(!t)return null;let n=String(t).trim(),f=n.indexOf("BEGIN_JSON"),p=n.indexOf("END_JSON");if(f!==-1&&p!==-1&&f<p)n=n.slice(f+10,p).trim();else{let r=n.indexOf("{");if(r>0)n=n.slice(r)}let a=[n,n.replace(/^```json\s*/i,"").replace(/^```\s*/i,"").replace(/```$/i,"").trim()],l=n.indexOf("{"),g=n.lastIndexOf("}");if(l!==-1&&g!==-1&&l<g)a.push(n.slice(l,g+1));for(let r of[...a]){let w=Tp(r);if(w!==r)a.push(w)}for(let r of a)try{return JSON.parse(r)}catch{}return null}import{spawn as Lp}from"node:child_process";import{existsSync as un}from"node:fs";import{dirname as Ap,join as Wt}from"node:path";import{fileURLToPath as kp}from"node:url";function tf(t){return kp(new URL(".",t)).replace(/^\/([A-Z]:)/,"$1")}function Hp(t,n=un){let f=new Set;for(let p of t.filter(Boolean)){if(f.has(p))continue;if(f.add(p),n(p))return p}return t.filter(Boolean).at(-1)}function nf(t,{moduleDir:n,entrypoint:f=process.argv[1],env:p=process.env,exists:a=un}={}){let l=f?Ap(f):null,g=p.GREEDY_SEARCH_EXTENSION_DIR?.trim()||null;return Hp([n?Wt(n,"..","..","extractors",t):null,n?Wt(n,"..","extractors",t):null,g?Wt(g,"extractors",t):null,l?Wt(l,"..","extractors",t):null],a)}var Yp=tf(import.meta.url),Ip={gemini:"gemini.mjs",chatgpt:"chatgpt.mjs"};function Fp(t="gemini"){let n=String(t||"gemini").toLowerCase();if(n==="gem")return"gemini";if(n==="gpt")return"chatgpt";return n}async function Gp(t,n,{tabPrefix:f=null,timeoutMs:p=180000,visible:a=null}={}){let l=Fp(t),g=Ip[l];if(!g||!At.includes(l))throw Error(`Unsupported synthesizer "${t}". Supported: ${At.join(", ")}`);return new Promise((r,w)=>{let $=nf(g,{moduleDir:Yp}),_=f?["--tab",String(f)]:[],h={...process.env,CDP_PROFILE_DIR:K};if(a!==!0)delete h.GREEDY_SEARCH_VISIBLE,delete h.GREEDY_SEARCH_ALWAYS_VISIBLE;else h.GREEDY_SEARCH_VISIBLE="1",h.GREEDY_SEARCH_ALWAYS_VISIBLE="1";let E=Lp(jt(),[$,"--stdin",..._],{stdio:["pipe","pipe","pipe"],env:h});E.stdin.write(n),E.stdin.end();let e="",b="";E.stdout.on("data",(v)=>e+=v),E.stderr.on("data",(v)=>b+=v);let D=setTimeout(()=>{E.kill(),w(Error(`${l} prompt timed out after ${p/1000}s`))},p);E.on("close",(v)=>{if(clearTimeout(D),v!==0){w(Error(b.trim()||`${l} extractor failed`));return}try{r(JSON.parse(e.trim()))}catch{w(Error(`bad JSON from ${l}: ${e.slice(0,100)}`))}})})}async function X(t,n={}){return Gp("gemini",t,n)}var Mp=30000;function ff(t,n,f,p){let a=Number.parseInt(String(t??""),10);if(!Number.isFinite(a))return p;return Math.min(f,Math.max(n,a))}async function pf(t){let n=["You are a research complexity classifier.","Classify the following query by research complexity.","","- simple: A narrow factual question (what is X, define X, how does X work)."," Answerable with 1-3 search queries and a short synthesis. No sub-questions.","- moderate: A focused comparison, recent change, or best-practice lookup."," Needs 2-4 angles but stays within one domain.","- complex: Multi-faceted survey, landscape analysis, or cross-domain investigation."," Benefits from parallel research directions and iterative deepening.","","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:","BEGIN_JSON",JSON.stringify({complexity:"simple",reasoning:"narrow factual question",suggestedBreadth:1,suggestedIterations:1,needsAcademicSources:!1},null,2),"END_JSON","","Query: "+t].join(`
407
- `);try{let f=await X(n,{timeoutMs:Mp}),p=ft(f?.answer||"")||{},a=["simple","moderate","complex"].includes(p.complexity)?p.complexity:"moderate";return{complexity:a,reasoning:H(p.reasoning||"",200),suggestedBreadth:ff(p.suggestedBreadth,1,5,a==="simple"?1:3),suggestedIterations:ff(p.suggestedIterations,1,3,a==="simple"?1:2),needsAcademicSources:p.needsAcademicSources===!0}}catch(f){return process.stderr.write(`[greedysearch] Complexity classification failed, defaulting to moderate: ${f.message}
408
- `),{complexity:"moderate",reasoning:"classification failed",suggestedBreadth:3,suggestedIterations:2,needsAcademicSources:!1}}}gn();Xt();function tg(t){if(t<1000)return"0s";let n=Math.round(t/1000);if(n<60)return`${n}s`;let f=Math.floor(n/60),p=n%60;return`${f}m ${p}s`}function ng(t,n=20){let f=Math.round(t*n),p=n-f;return"["+"█".repeat(f)+"░".repeat(p)+"]"}function ot({totalActions:t=0,totalRounds:n=0,totalFetches:f=0,silent:p=!1}={}){let a=Date.now(),l=0,g=0,r=0,w=[],$=null,_=null,h=0,E=500;function e(P){if(w.push(P),w.length>5)w.shift()}function b(){if(w.length===0)return null;return w.reduce((P,R)=>P+R,0)/w.length}function D(P){let R=Date.now()-a,y=t+f+n,j=l+r+g,I=y>0?Math.min(1,j/y):0,F=ng(I),T=b(),k=Math.max(0,y-j),G=T?T*k:null,ht=G?tg(G):"—",d=_?` ${_}`:"";return`${F} ${j}/${y} (${P}${d}, ETA ${ht})`}function v(P){if(p)return;let R=Date.now();if(R-h<E&&P!=="done")return;h=R,process.stderr.write(`[greedysearch] ${D(P)}
409
- `)}return{startRound(P){g=P-1},endRound(){g++,v("round")},startAction(P,R){$=Date.now(),_=`${P}:${(R||"").slice(0,40)}`,v(P)},endAction(){if($)e(Date.now()-$),$=null;l++,v("action")},startFetch(P){$=Date.now(),_=`fetch:${(P||"").slice(0,40)}`,v("fetch")},endFetch(P=!0){if($)e(Date.now()-$),$=null;r++,v(P?"fetch":"fetch-failed")},print(){v("progress")},finish(){v("done")},getElapsedMs(){return Date.now()-a}}}import{spawn as fg}from"node:child_process";import{join as pg}from"node:path";import{fileURLToPath as ag}from"node:url";var gg=ag(new URL(".",import.meta.url)).replace(/^\/([A-Z]:)/,"$1"),lg=pg(gg,"..","..","bin","search.mjs");function Vf(t,n=1/0){let f=new Set,p=[];for(let a of t||[]){let l=H(String(a||""),1000);if(!l||f.has(l))continue;if(f.add(l),p.push(l),p.length>=n)break}return p}function rg(t){let n=String(t||"").trim();if(!n)return[];return[`${n} — definition and overview`,`${n} — how it works, mechanism, or key details`,`${n} — current usage, comparison, or best practices`]}function wg(t,n){let f=new Map;for(let p of t||[]){let a=p?.canonicalUrl||p?.finalUrl||p?.url;if(a)f.set(a,p)}for(let p of n||[]){let a=p?.canonicalUrl||p?.finalUrl||p?.url;if(!a)continue;if(f.has(a)){let l={...f.get(a),angles:[...f.get(a).angles||[f.get(a).query||""],p.query||""]};f.set(a,l)}else f.set(a,p)}return Array.from(f.values())}var $g=Pt.join("|"),_g=new RegExp(`^\\[(${$g})\\]`);function eg(t){return/^PROGRESS:/.test(t)||/^\[greedysearch\]/.test(t)||_g.test(t)||/^GreedySearch Chrome/.test(t)||/^Launching GreedySearch Chrome/.test(t)||/^Headless mode/.test(t)||/^Ready\.?$/.test(t)}async function hg(t,{locale:n=null,short:f=!0}={}){let p=[lg,"all","--inline","--stdin","--fast"];if(!f)p.push("--full");if(n)p.push("--locale",n);return new Promise((a,l)=>{let g=fg(jt(),p,{stdio:["pipe","pipe","pipe"],env:{...process.env,GREEDY_SEARCH_RESEARCH_CHILD:"1"}});g.stdin.write(t),g.stdin.end();let r="",w="",$="";g.stdout.on("data",(h)=>r+=h),g.stderr.on("data",(h)=>{w+=h,$+=h.toString();let E=$.split(`
410
- `);$=E.pop()||"";for(let e of E)if(eg(e))process.stderr.write(`${e}
411
- `)});let _=setTimeout(()=>{g.kill(),l(Error(`research child search timed out for: ${t}`))},140000);g.on("close",(h)=>{if(clearTimeout(_),h!==0){l(Error(w.trim()||`search child exited with code ${h}`));return}try{a(JSON.parse(r.trim()))}catch{l(Error(`Invalid JSON from research child: ${r.slice(0,200)}`))}})})}function Eg(t,n){let f=new Map;for(let p of n||[]){let a=m(p?.canonicalUrl||p?.finalUrl||p?.url);if(a&&p?.id)f.set(a,p.id)}return(t||[]).map((p,a)=>{let l=m(p?.finalUrl||p?.canonicalUrl||p?.url);return{...p,id:p?.id||f.get(l)||`F${a+1}`}})}function Dg(t){let n=t.length,f=t.filter((p)=>p.status==="closed").length;return{total:n,closed:f,open:Math.max(0,n-f)}}async function Jf({query:t,locale:n=null,maxSources:f=5,qualityThreshold:p=8.5,writeBundle:a=process.env.GREEDY_RESEARCH_BUNDLE!=="0",researchOutDir:l=null}={}){let g=new Date().toISOString(),r=Date.now(),w=Ln(t),$=new Set;process.stderr.write(`[greedysearch] Simple research mode: single-pass for "${H(t,80)}"
412
- `);let E=ot({totalActions:6,totalRounds:1,totalFetches:1,silent:process.env.GREEDY_RESEARCH_QUIET==="1"});E.startRound(1);let e=[],b=[],D=rg(t),v=[];E.startAction("search",`${D.length} angles in parallel`);let P=await Promise.allSettled(D.map((N)=>hg(N,{locale:n,short:!0})));for(let N=0;N<D.length;N++){let o=D[N],rt=P[N];if(E.endAction(),rt.status==="fulfilled"){let Z=rt.value;v.push({angle:o,result:Z});let A=Tt(Z,o);e=wg(e,A)}else process.stderr.write(`[greedysearch] Simple search angle "${o}" failed: ${rt.reason.message}
413
- `)}if(process.stderr.write(`PROGRESS:research:simple:fetching
414
- `),e.length>0)try{E.startFetch(`top ${Math.min(f,e.length)} sources`),b=await On(e,Math.min(f,e.length),8000,Math.min(3,f)),E.endFetch(!0),e=Gt(e,b)}catch(N){E.endFetch(!1),process.stderr.write(`[greedysearch] Source fetching failed: ${N.message}
415
- `)}b=Eg(b,e),process.stderr.write(`PROGRESS:research:simple:evidence
416
- `);let R=[];try{let N=await Zf({query:t,questions:w,fetchedSources:b,extractedSourceKeys:$});R=N.evidence||[];for(let o of N.evidence){let rt=Array.isArray(o.answers)?o.answers:[];for(let A of rt){let $t=A?.id||A?.question;if($t){let q=w.find((xt)=>xt.id===$t);if(q){if(q.status="closed",q.closedRound=1,A.evidence)q.evidence=Vf([...q.evidence||[],A.evidence],4)}}}let Z=Array.isArray(o.newQuestions)?o.newQuestions:[];for(let A of Z){let $t=H(String(A),320);if($t&&!w.some((q)=>q.question===$t))w.push({id:`Q${w.length+1}`,question:$t,status:"open",reason:"Discovered gap/follow-up",createdRound:1,evidence:[],sourceIds:[]})}}}catch(N){process.stderr.write(`[greedysearch] Evidence extraction failed: ${N.message}
417
- `)}process.stderr.write(`PROGRESS:research:simple:synthesizing
418
- `);let y={answer:"",agreement:{level:"mixed",summary:"Single-pass synthesis."},differences:[],caveats:[],claims:[],recommendedSources:e.slice(0,4).map((N)=>N.id),synthesized:!1};if(R.length>0)try{E.startAction("synth-evidence","from evidence");let N=await X(An(t,e,w,R),{timeoutMs:120000});E.endAction(),y={...y,...ft(N?.answer||"")||{}},y.synthesized=Array.isArray(y.claims)&&y.claims.length>0}catch(N){process.stderr.write(`[greedysearch] Evidence synthesis failed: ${N.message}
419
- `)}if(!y.synthesized&&e.length>0)try{E.startAction("synth-final","fallback report");let N=await X(Tn(t,[{round:1,learnings:[],gaps:[],actions:[]}],e,w,R),{timeoutMs:120000});E.endAction(),y={...y,...ft(N?.answer||"")||{}},y.synthesized=Array.isArray(y.claims)&&y.claims.length>0}catch(N){process.stderr.write(`[greedysearch] Final synthesis failed: ${N.message}
420
- `)}process.stderr.write(`PROGRESS:research:simple:audit
421
- `);let j=kn(y.answer||"",e),I=await Hn(e,j);Yn(w,y,j);let F=Vf(y.caveats||[]),T=Bt({sources:e,fetchedSources:b,synthesis:y,citationAudit:j,gaps:F,questions:w,rounds:[{round:1,actions:[],learnings:[],gaps:F}],qualityScore:y.synthesized?8:5,qualityThreshold:p,maxSources:f}),k=new Date().toISOString(),G=Date.now()-r,ht={startedAt:g,finishedAt:k,durationMs:G,rounds:1,terminationReason:"simple_single_pass"},d=null,wt;if(a){process.stderr.write(`PROGRESS:research:simple:bundle
422
- `);try{d=await In({query:t,rounds:[{round:1,actions:[],learnings:[],gaps:F,evidence:R}],sources:e,fetchedSources:b,evidenceItems:R,synthesis:y,citationAudit:j,citationUrls:I,floor:T,manifest:{...ht,engines:kt,synthesizer:"gemini",actionsRun:1,searches:1,fetches:b.length,sourcesFetched:b.filter((N)=>N?.contentChars>100).length,engineFailures:[],floorMet:T.floorMet},allGaps:F,questions:w,outDir:l}),wt=d.sourceFiles,delete d.sourceFiles}catch(N){process.stderr.write(`[greedysearch] Research bundle write failed: ${N.message}
423
- `),d={error:N.message||String(N)},wt=await mt(b)}}else wt=await mt(b);return process.stderr.write(`PROGRESS:research:done
424
- `),E.endRound(),E.finish(),{query:t,_research:{mode:"simple",breadth:1,iterations:1,maxSources:f,rounds:[{round:1,actions:[],learnings:[],gaps:F,evidence:R}],learnings:[],gaps:F,evidence:R,questions:w,questionProgress:Dg(w),qualityHistory:[y.synthesized?8:5],terminationReason:"simple_single_pass",qualityThreshold:p,floor:T,bundle:d,manifest:ht},_citationAudit:j,_citationUrls:I,_sources:e,_fetchedSources:wt,_synthesis:y,_confidence:{sourcesCount:e.length,fetchedSourceSuccessRate:b.length>0?b.filter((N)=>N.contentChars>100).length/b.length:0,agreementLevel:y.agreement?.level||"mixed",floorMet:T.floorMet}}}var Rg=Pg(new URL(".",import.meta.url)).replace(/^\/([A-Z]:)/,"$1"),yg=Y(Rg,"..","..","bin","search.mjs"),Og=Y(process.cwd(),".pi","greedysearch-research"),Ng=28000;function ig(t){return String(t||"research").toLowerCase().replaceAll(/[^a-z0-9]+/g,"-").replaceAll(/^-|-$/g,"").slice(0,60)||"research"}function lt(t,n=1/0){let f=new Set,p=[];for(let a of t||[]){let l=H(String(a||""),1000);if(!l||f.has(l))continue;if(f.add(l),p.push(l),p.length>=n)break}return p}async function Tg(...t){let{fetchMultipleSources:n}=await Promise.resolve().then(() => (Xt(),Nn));return n(...t)}async function Vn(...t){let{writeSourcesToFiles:n}=await Promise.resolve().then(() => (gn(),gf));return n(...t)}function Ag({breadth:t=3,iterations:n=2,maxSources:f}){let p=Gn(t,1,5,3),a=Gn(n,1,3,2),l=Gn(f??Math.max(5,p*a*2),3,12,8);return{breadth:p,iterations:a,maxSources:l}}function Gn(t,n,f,p){let a=Number.parseInt(String(t??""),10);if(!Number.isFinite(a))return p;return Math.min(f,Math.max(n,a))}function kg(t,n,f,{expand:p=!0,includeOriginal:a=!0,exclude:l=[]}={}){let g=Array.isArray(t?.queries)?t.queries:[],r=[],w=new Set([...l].map(($)=>c($).toLowerCase()));for(let $ of g){let _=typeof $==="string"?$:$?.query,h=typeof $==="string"?"":$?.researchGoal||"";Mn(r,_,h,{exclude:w})}if(a)Mn(r,n,"Original user query",{prepend:!0,exclude:w});if(p){let $=[{query:`${n} official docs GitHub`,researchGoal:"Find primary project docs, repository details, and maintainer claims."},{query:`${n} benchmarks limitations compatibility`,researchGoal:"Validate performance claims and uncover unsupported APIs or caveats."},{query:`${n} alternatives comparison production use cases`,researchGoal:"Compare against conventional headless browsers and identify when to choose it."},{query:`${n} anti bot detection Cloudflare screenshots visual rendering`,researchGoal:"Check automation risks, rendering gaps, screenshots, and bot-detection behavior."}];for(let _ of $){if(r.length>=f)break;Mn(r,_.query,_.researchGoal,{exclude:w})}}return r.slice(0,f)}function Mn(t,n,f="",{prepend:p=!1,exclude:a=new Set}={}){if(!n||typeof n!=="string")return;let l=c(n);if(!l||a.has(l.toLowerCase())||t.some((r)=>r.query.toLowerCase()===l.toLowerCase()))return;let g={query:l,researchGoal:H(f,320)};if(p)t.unshift(g);else t.push(g)}function c(t){return Lg(Hg(String(t)))}function Hg(t){let n="",f=0;while(f<t.length){let p=t.indexOf("[",f);if(p===-1){n+=t.slice(f);break}let a=t.indexOf("]",p+1);if(a===-1||t[a+1]!=="("||a===p+1){n+=t.slice(f,p+1),f=p+1;continue}let l=t.indexOf(")",a+2);if(l===-1){n+=t.slice(f,p+1),f=p+1;continue}let g=t.slice(a+2,l).trimStart();if(!g.startsWith("http://")&&!g.startsWith("https://")){n+=t.slice(f,p+1),f=p+1;continue}n+=t.slice(f,p),n+=t.slice(p+1,a),f=l+1}return n}function Lg(t){let n="",f=!1;for(let p of t)if(p===" "||p==="\t"||p===`
425
- `||p==="\r"){if(!f)n+=" ";f=!0}else n+=p,f=!1;return n.trim()}function Jn(t){return new Set(String(t).toLowerCase().normalize("NFD").replaceAll(/[\u0300-\u036f]/g,"").split(/[^\w]+/).filter((n)=>n.length>1))}function Kn(t,n){let f=Jn(t),p=Jn(n),a=new Set([...f,...p]).size;if(a===0)return 1;let l=0;for(let g of f)if(p.has(g))l++;return l/a}function zf(t,n,{threshold:f=0.75,roundIndex:p=0,originalQuery:a=null}={}){let l=c(t).toLowerCase();if(n.has(l))return!0;if(a&&p>0&&l===c(a).toLowerCase())return!0;for(let g of n)if(Kn(l,g)>=f)return!0;return!1}function Yg(t,n,f,p){let a=n.map((l)=>({queries:l.queries?.map((g)=>g.query||"")||[],learnings:l.learnings||[],gaps:l.gaps||[]}));return["You are evaluating the quality of an iterative research run.","Assess coverage across: official sources, limitations/risks, benchmarks/performance, production usage, and counter-evidence.","Score each dimension 0-10. Overall score 0-10.","Identify remaining knowledge gaps.","Propose targeted next actions (search queries or direct URL fetches) that would most improve the research.","Decide whether to continue or stop.","terminationReason must be one of: quality_threshold | max_rounds | no_novel_actions | insufficient_evidence.","",`Original research question: ${t}`,`Rounds completed: ${JSON.stringify(a,null,2)}`,`Accumulated learnings: ${JSON.stringify(f.slice(0,12),null,2)}`,`Known gaps: ${JSON.stringify(p.slice(0,8),null,2)}`,"","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:","BEGIN_JSON",JSON.stringify({score:7.5,coverage:{officialSources:8,limitations:5,benchmarks:7,productionUseCases:6,counterEvidence:4},knowledgeGaps:["specific gap or missing evidence"],shouldContinue:!0,terminationReason:"quality_threshold",nextActions:[{type:"search",query:"targeted search query"},{type:"fetchUrl",url:"https://example.com/primary-doc"}]},null,2),"END_JSON"].join(`
426
- `)}function Ig(t,n,f,p,a){let l=[],g=[{template:(r)=>`${r} official documentation`,label:"official docs"},{template:(r)=>`${r} GitHub issues discussions`,label:"community signals"},{template:(r)=>`${r} benchmarks performance comparison`,label:"benchmarks"},{template:(r)=>`${r} limitations risks caveats`,label:"limitations"},{template:(r)=>`${r} production deployment experience`,label:"production usage"},{template:(r)=>`${n} ${r} counter evidence`,label:"counter-evidence"}];for(let r=0;r<t.length&&l.length<p;r++){let w=t[r],$=g[r%g.length],_=$.template(w);if(!zf(_,f,{roundIndex:a}))l.push({query:_,researchGoal:`Gap-driven: ${w} (${$.label})`})}return l}async function Fg(t,n,f,p,a){try{let l=await X(Yg(t,n,f,p),{timeoutMs:120000}),g=Ht(l,{}),r=typeof g.score==="number"?Math.min(10,Math.max(0,g.score)):a.length>0?a[a.length-1]:5,w=Array.isArray(g.knowledgeGaps)?g.knowledgeGaps.map((E)=>String(E)).filter(Boolean).slice(0,6):[],$=Array.isArray(g.nextActions)?g.nextActions.slice(0,5):[],_=typeof g.shouldContinue==="boolean"?g.shouldContinue:r<8,h=g.terminationReason||null;return{score:r,coverage:g.coverage||{},knowledgeGaps:w,shouldContinue:_,nextActions:$,terminationReason:h||(r>=8.5?"quality_threshold":null),evaluationError:""}}catch(l){return process.stderr.write(`[greedysearch] Quality evaluation failed: ${l.message}
427
- `),{score:a.length>0?a[a.length-1]:5,coverage:{},knowledgeGaps:[],shouldContinue:!0,nextActions:[],terminationReason:null,evaluationError:l.message}}}function Gg(t){let n={};for(let f of Object.keys(t||{}).filter((p)=>!p.startsWith("_"))){let p=t?.[f];if(!p)continue;n[f]=p.error?{status:"error",error:String(p.error)}:{status:"ok",answer:H(p.answer||"",1400),sources:Array.isArray(p.sources)?p.sources.slice(0,5).map((a)=>({title:H(a.title||"",160),url:a.url||""})):[]}}return n}function Mg(t,n,f=[],p=[],a=[]){let l=p.length>0?`
428
- Known knowledge gaps to target:
429
- ${p.map((r)=>`- ${r}`).join(`
430
- `)}`:"",g=a.length>0?`
431
- Already fetched URLs (do not re-fetch):
432
- ${a.map((r)=>`- ${r}`).join(`
433
- `)}`:"";return["You are planning web research actions for a multi-engine search agent.","You can plan two types of actions:",' - "search": run a multi-engine SERP search query',' - "fetchUrl": directly fetch a specific URL (docs page, GitHub repo, specification, etc.)','Prefer "fetchUrl" when a specific primary source URL is known or obvious.','Use "search" for broad discovery or when specific URLs are unknown.',`Return at most ${n} actions.`,"Avoid near-duplicate search queries and already-fetched URLs.","",`User topic: ${t}`,f.length?`
434
- Prior learnings to build on:
435
- ${f.map((r)=>`- ${r}`).join(`
436
- `)}`:"",l,g,"","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:","BEGIN_JSON",JSON.stringify({actions:[{type:"search",query:"specific search query",researchGoal:"what this action should clarify"},{type:"fetchUrl",url:"https://example.com/docs/relevant-page",researchGoal:"extract specific information from this page"}]},null,2),"END_JSON"].join(`
437
- `)}function Sf(t){if(!t||typeof t!=="object")return null;let n=t.type,f=H(t.researchGoal||"",320);if(n==="search"){if(t.query==null)return null;let p=c(t.query);return p?{type:"search",query:p,researchGoal:f}:null}if(n==="fetchUrl"){if(t.url==null)return null;let p=m(t.url);return p?{type:"fetchUrl",url:p,researchGoal:f}:null}return null}async function Wg(t,{locale:n=null,short:f=!0,usedQueries:p,usedUrls:a,maxChars:l=8000}={}){if(t.type==="search"){let g=c(t.query).toLowerCase();p.add(g);try{let r=await Ug(t.query,{locale:n,short:f}),w=Tt(r,t.query);return{ok:!0,action:t,result:r,sources:w}}catch(r){return{ok:!1,action:t,error:r.message,sources:[]}}}if(t.type==="fetchUrl"){let g=m(t.url);if(a.has(g))return{ok:!1,action:t,error:`URL already fetched: ${g}`,sources:[]};try{let r=await mg(g,l);a.add(g);let w=Vg(g),$={id:"",canonicalUrl:r.finalUrl||g,displayUrl:r.url||g,domain:w,title:r.title||g,engines:["fetch"],engineCount:1,perEngine:{},sourceType:tn(w,r.title||"",r.finalUrl||g),isOfficial:!1,smartScore:0,fetch:{attempted:!0,ok:!r.error&&(r.contentChars||0)>100,status:r.status||null,finalUrl:r.finalUrl||g,content:r.content||"",contentChars:r.contentChars||0,snippet:r.snippet||"",error:r.error||""}};return{ok:!0,action:t,result:null,sources:[$],fetchResult:{id:$.id,url:g,finalUrl:r.finalUrl||g,title:r.title||"",content:r.content||"",contentChars:r.contentChars||0,snippet:r.snippet||"",status:r.status||null,error:r.error||"",source:r.source||"http",duration:r.duration||0}}}catch(r){return{ok:!1,action:t,error:r.message,sources:[]}}}return{ok:!1,action:t,error:`Unknown action type: ${t.type}`,sources:[]}}async function mg(t,n){let{fetchSourceContent:f}=await Promise.resolve().then(() => (Xt(),Nn));return await f(t,n)}function Vg(t){try{return new URL(t).hostname.toLowerCase().replace(/^www\./,"")}catch{return""}}async function Jg(t,n){let f=[],{parseGitHubUrl:p}=await Promise.resolve().then(() => (Uf(),Bf));for(let a of t){if(a.type!=="fetchUrl"){f.push(a);continue}let l=p(a.url);if(!l||l.type!=="root"){f.push(a);continue}let{owner:g,repo:r}=l,w=`https://github.com/${g}/${r}`;if(n.has(w))continue;let $=[w],_=[`${w}/blob/main/CONTRIBUTING.md`,`${w}/blob/master/CONTRIBUTING.md`,`${w}/blob/main/CHANGELOG.md`,`${w}/blob/master/CHANGELOG.md`,`${w}/blob/main/docs/README.md`];for(let h of _){if($.length>=3)break;if(!n.has(h))$.push(h)}for(let h of $)f.push({type:"fetchUrl",url:h,researchGoal:a.researchGoal||`Fetch GitHub content for ${g}/${r}`})}return f}function Zg(t,n){let f=ft(t?.answer||"")||{},p=Array.isArray(f?.actions)?f.actions:[],a=[];for(let l of p){let g=Sf(l);if(g&&a.length<n)a.push(g)}return a}function Kg(t){return(t||[]).map((n)=>({type:"search",query:typeof n==="string"?n:n.query,researchGoal:typeof n==="string"?"":n.researchGoal||""})).filter((n)=>n.query)}function yt(t){return m(t?.finalUrl||t?.canonicalUrl||t?.url||"")||t?.id||""}function cf(t,n){let f=Array.isArray(t?.extractions)?t.extractions:[],p=new Map,a=new Map;for(let l of n||[]){if(l?.id)a.set(String(l.id),l);let g=yt(l);if(g)p.set(g,l)}return f.map((l)=>{let g=a.get(String(l?.sourceId||""))||p.get(m(l?.url||"")||""),r=String(l?.sourceId||g?.id||""),w=m(l?.url||g?.finalUrl||g?.url||""),$=Array.isArray(l?.answers)?l.answers.map((_)=>({id:String(_?.id||""),evidence:H(_?.evidence||"",500),sourceIds:[r].filter(Boolean)})).filter((_)=>_.id):[];return{sourceId:r,url:w,title:g?.title||l?.title||"",rational:H(l?.rational||"",700),evidence:H(l?.evidence||"",1600),summary:H(l?.summary||"",700),answers:$,newQuestions:lt(l?.newQuestions||[],6)}}).filter((l)=>l.sourceId||l.url||l.summary||l.evidence)}function Xg(t,n,f,p=new Set){let a=(n||[]).filter((g)=>g.status!=="closed").slice(0,12).map((g)=>({id:g.id,question:g.question})),l=(f||[]).filter((g)=>g?.content||g?.snippet).filter((g)=>!p.has(yt(g))).slice(0,6).map((g,r)=>({id:g.id||`F${r+1}`,title:g.title||"",url:g.finalUrl||g.url||g.canonicalUrl||"",content:H(g.content||g.snippet||"",5000)}));return["You are doing goal-based evidence extraction for an iterative research run.","For each source, extract only information that helps answer the open questions.","Use original wording/details where useful. Do not invent answers; leave questions open if evidence is insufficient.","If a source answers one or more tracked questions, identify those question IDs explicitly.","Also propose genuinely new sub-questions discovered from the evidence.","",`Original research question: ${t}`,`Open question ledger: ${JSON.stringify(a,null,2)}`,`Fetched sources: ${JSON.stringify(l,null,2)}`,"","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:","BEGIN_JSON",JSON.stringify({extractions:[{sourceId:"S1",url:"https://example.com/source",rational:"why this source matters for the goal",evidence:"specific quoted/paraphrased evidence with numbers, dates, caveats",summary:"concise contribution to the research question",answers:[{id:"Q1",evidence:"brief evidence that closes the question"}],newQuestions:["new sub-question raised by this source"]}]},null,2),"END_JSON"].join(`
438
- `)}async function Zf({query:t,questions:n,fetchedSources:f,extractedSourceKeys:p}){let a=(f||[]).filter((l)=>(l?.content||l?.snippet)&&!p.has(yt(l)));if(a.length===0)return{evidence:[],error:""};try{let l=await X(Xg(t,n,a,p),{timeoutMs:120000}),g=Ht(l,{extractions:[]}),r=cf(g,a);for(let w of a){let $=yt(w);if($)p.add($)}return{evidence:r,error:""}}catch(l){return{evidence:[],error:l.message||String(l)}}}function og(t,n,f,p,a,l,g=[]){let r=(n||[]).filter((R)=>R.status!=="closed").slice(0,12).map((R)=>({id:R.id,question:R.question})),w=(a||[]).filter((R)=>R?.content||R?.snippet),$=(l||[]).filter((R)=>R?.content||R?.snippet),_=({extractionCount:R,extractionLimit:y,learningCount:j,learningLimit:I})=>{let F=w.slice(0,R).map((k,G)=>({id:k.id||`F${G+1}`,title:k.title||"",url:k.finalUrl||k.url||k.canonicalUrl||"",content:H(k.content||k.snippet||"",y)})),T=$.slice(0,j).map((k,G)=>({id:`F${G+1}`,title:k.title||"",url:k.finalUrl||k.url||"",snippet:H(k.content||k.snippet||"",I)}));return["You are doing two combined research tasks for one round of an iterative research run. Perform BOTH tasks and return a single combined JSON object.","","TASK A — Goal-based evidence extraction:","For each source under 'Sources for evidence extraction', extract only information that helps answer the open questions.","Use original wording/details where useful. Do not invent answers; leave questions open if evidence is insufficient.","If a source answers one or more tracked questions, identify those question IDs explicitly.","Also propose genuinely new sub-questions discovered from the evidence.","","TASK B — Compact research-state learning extraction:","Using the round queries, question ledger, extracted source evidence, engine summaries, and fetched source snippets below, create dense, non-overlapping learnings with exact names, numbers, dates, limitations, and caveats where available.","Also propose follow-up search queries that would most improve confidence or fill gaps.","",`Original research question: ${t}`,`Open question ledger: ${JSON.stringify(r)}`,`Round queries: ${JSON.stringify(f)}`,`Question ledger: ${JSON.stringify(n)}`,`Extracted source evidence so far: ${JSON.stringify(g.slice(-12))}`,`Engine summaries: ${JSON.stringify(p)}`,`Sources for evidence extraction (Task A): ${JSON.stringify(F)}`,`Fetched source snippets (Task B context): ${JSON.stringify(T)}`,"","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers, combining both tasks into one object:","BEGIN_JSON",JSON.stringify({extractions:[{sourceId:"S1",url:"https://example.com/source",rational:"why this source matters for the goal",evidence:"specific quoted/paraphrased evidence with numbers, dates, caveats",summary:"concise contribution to the research question",answers:[{id:"Q1",evidence:"brief evidence that closes the question"}],newQuestions:["new sub-question raised by this source"]}],learnings:["concise, information-dense learning"],answeredQuestions:[{id:"Q1",evidence:"brief evidence that closes this question",sourceIds:["S1"]}],newQuestions:["new sub-question discovered from the evidence"],followUpQueries:["specific next search query"],gaps:["important uncertainty or missing evidence"]},null,2),"END_JSON"].join(`
439
- `)},h=Math.min(4,w.length),E=3000,e=Math.min(6,$.length),b=2000,D=_({extractionCount:h,extractionLimit:E,learningCount:e,learningLimit:b}),v=!1,P=600;while(D.length>Ng){if(E>P||b>P)E=Math.max(P,Math.floor(E/2)),b=Math.max(P,Math.floor(b/2));else if(e>1)e-=1;else if(h>1)h-=1;else if(E>1||b>1)E=Math.max(1,Math.floor(E/2)),b=Math.max(1,Math.floor(b/2));else throw Error(`[greedysearch] evidence/learning prompt exceeds Gemini input cap after source trimming: ${D.length} chars`);v=!0,D=_({extractionCount:h,extractionLimit:E,learningCount:e,learningLimit:b})}if(v)console.error(`[greedysearch] evidence/learning prompt trimmed to fit Gemini input cap: ${D.length} chars`);return D}async function Bg({query:t,questions:n,fetchedSources:f,extractedSourceKeys:p,roundQueries:a,searchSummaries:l,evidenceItems:g=[]}){let r=(f||[]).filter((E)=>(E?.content||E?.snippet)&&!p.has(yt(E))),w=[],$="",_={learnings:[],followUpQueries:[],gaps:[]},h="";try{let E=await X(og(t,n,a,l,r,f,g),{timeoutMs:180000}),e=Ht(E,{});w=cf(e,r);for(let b of r){let D=yt(b);if(D)p.add(D)}_={..._,...e}}catch(E){let e=E.message||String(E);$=e,h=e}return{evidence:w,evidenceError:$,learningPayload:_,learningError:h}}function Tn(t,n,f,p=[],a=[]){let l=n.flatMap((w)=>w.learnings||[]),g=n.flatMap((w)=>w.gaps||[]),r=f.slice(0,12).map((w)=>({id:w.id,title:w.title,domain:w.domain,url:w.canonicalUrl,type:w.sourceType,engines:w.engines,fetch:w.fetch?.attempted?{ok:w.fetch.ok,snippet:H(w.fetch.snippet||"",1200),publishedTime:w.fetch.publishedTime||""}:void 0}));return["You are writing the final research report for an iterative deep-research run.","Produce a thorough markdown report organized into clear sections.","","Use the learnings and source registry below. Every substantive claim MUST be backed by an [S1] citation.",'Where engines disagree, surface the conflicting claims explicitly in the "differences" array.','Include a "Key Claims" structure that maps each distinct claim to its supporting source IDs.',"","Report structure:","1. ## Summary — A 2-4 sentence executive summary of findings","2. ## Key Findings — The main findings, organized by theme or question, each with inline citations","3. ## Areas of Disagreement — Where engines or sources conflict (if any)","4. ## Limitations & Caveats — Important qualifiers, gaps, or uncertainties","",`Original research question: ${t}`,`Learnings: ${JSON.stringify(l,null,2)}`,`Known gaps/caveats: ${JSON.stringify(g,null,2)}`,`Question ledger: ${JSON.stringify(p,null,2)}`,`Goal-based extracted evidence: ${JSON.stringify(a.slice(-20),null,2)}`,`Source registry: ${JSON.stringify(r,null,2)}`,"","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:","BEGIN_JSON",JSON.stringify({answer:"markdown report with sections and inline [S1] citations",agreement:{level:"high|medium|low|mixed|conflicting",summary:"one-sentence confidence summary"},differences:["notable disagreement or conflict between sources"],caveats:["important caveat or qualification"],claims:[{claim:"specific factual statement from the research",support:"strong|moderate|weak|conflicting",sourceIds:["S1","S2"]}],recommendedSources:["S1","S2"]},null,2),"END_JSON"].join(`
440
- `)}function An(t,n=[],f=[],p=[]){let a=n.slice(0,12).map((w)=>({id:w.id,title:w.title,domain:w.domain,url:w.canonicalUrl,type:w.sourceType,engines:w.engines})),l=p.slice(-20),g=new Set;for(let w of l)for(let $ of w.answers||[])if($?.id)g.add($.id);let r=(f||[]).filter((w)=>w.status!=="closed").map((w)=>({id:w.id,question:w.question}));return["You are writing the final research report from goal-based extracted evidence.","Per-round learnings were not produced, but the per-source evidence extraction step succeeded.","Synthesize a thorough markdown report using ONLY the evidence below. Every substantive claim MUST be backed by an [S1] citation.","","Report structure:","1. ## Summary — A 2-4 sentence executive summary of findings","2. ## Key Findings — The main findings, organized by theme or question, each with inline citations","3. ## Limitations & Caveats — Important qualifiers, gaps, or uncertainties","",`Original research question: ${t}`,`Per-source extracted evidence: ${JSON.stringify(l,null,2)}`,`Source registry: ${JSON.stringify(a,null,2)}`,`Questions already answered by the evidence: ${JSON.stringify(Array.from(g))}`,`Questions still open after this evidence: ${JSON.stringify(r)}`,"","Respond ONLY with JSON wrapped in BEGIN_JSON / END_JSON markers:","BEGIN_JSON",JSON.stringify({answer:"markdown report with sections and inline [S1] citations",agreement:{level:"high|medium|low|mixed|conflicting",summary:"one-sentence confidence summary"},differences:["notable disagreement or conflict between sources"],caveats:["important caveat or qualification"],claims:[{claim:"specific factual statement supported by the evidence",support:"strong|moderate|weak|conflicting",sourceIds:["S1","S2"]}],recommendedSources:["S1","S2"]},null,2),"END_JSON"].join(`
441
- `)}async function Ug(t,{locale:n=null,short:f=!0}={}){let p=[yg,"all","--inline","--stdin","--fast"];if(!f)p.push("--full");if(n)p.push("--locale",n);return new Promise((a,l)=>{let g=jg(jt(),p,{stdio:["pipe","pipe","pipe"],env:{...process.env,GREEDY_SEARCH_RESEARCH_CHILD:"1"}});g.stdin.write(t),g.stdin.end();let r="",w="",$="";g.stdout.on("data",(h)=>r+=h),g.stderr.on("data",(h)=>{w+=h,$+=h.toString();let E=$.split(`
442
- `);$=E.pop()||"";for(let e of E)if(Sg(e))process.stderr.write(`${e}
443
- `)});let _=setTimeout(()=>{g.kill(),l(Error(`research child search timed out for: ${t}`))},140000);g.on("close",(h)=>{if(clearTimeout(_),h!==0){l(Error(w.trim()||`search child exited with code ${h}`));return}try{a(JSON.parse(r.trim()))}catch{l(Error(`Invalid JSON from research child: ${r.slice(0,200)}`))}})})}function Qg(t){let n=new Map;for(let f of t.flat()){let p=m(f.canonicalUrl||f.url);if(!p)continue;let a=n.get(p);if(!a){n.set(p,{...f,canonicalUrl:p});continue}a.engines=[...new Set([...a.engines||[],...f.engines||[]])],a.engineCount=a.engines.length,a.smartScore=Math.max(a.smartScore||0,f.smartScore||0)}return Array.from(n.values()).sort((f,p)=>{let a=et(p)-et(f);if(a!==0)return a;return(f.domain||"").localeCompare(p.domain||"")}).slice(0,12).map((f,p)=>({...f,id:`S${p+1}`}))}var xg=Pt.join("|"),zg=new RegExp(`^\\[(${xg})\\]`);function Sg(t){return/^PROGRESS:/.test(t)||/^\[greedysearch\]/.test(t)||zg.test(t)||/^GreedySearch Chrome/.test(t)||/^Launching GreedySearch Chrome/.test(t)||/^Headless mode/.test(t)||/^Ready\.?$/.test(t)}function Ht(t,n={}){return ft(t?.answer||"")||n}function kn(t,n){if(!t||!Array.isArray(n))return{cited:[],missing:[],unfetched:[],ok:!0};let f=/\b[SF](\d+)\b/g,p=new Set,a;while((a=f.exec(t))!==null)p.add(`S${a[1]}`),p.add(`F${a[1]}`);let l=new Map;for(let $ of n){let _=$?.id;if(_)l.set(_,$)}let g=Array.from(p),r=[],w=[];for(let $ of g){let _=l.get($);if(!_){let h=$.match(/^(S|F)(\d+)$/);if(h){let E=parseInt(h[2],10)-1;if(E>=0&&E<n.length){let e=n[E];if(e){if(!(e.fetch?.ok||e.content&&e.content.length>100||e.contentChars&&e.contentChars>100))w.push($);continue}}}r.push($)}else if(!(_.fetch?.ok||_.content&&_.content.length>100||_.contentChars&&_.contentChars>100))w.push($)}return{cited:g,missing:r,unfetched:w,ok:r.length===0}}async function cg(t,{timeoutMs:n=6000,concurrency:f=4}={}){let p=Math.max(1,Math.floor(f||1)),a=(t||[]).filter((E)=>E?.id&&(E?.canonicalUrl||E?.finalUrl||E?.url));if(a.length===0)return{reachable:[],dead:[],skipped:[],ok:!0};let l=[],g=[],r=[],w=Array(a.length),$=0;async function _(){while(!0){let E=$++;if(E>=a.length)return;let e=a[E];try{let b=e.fetch?.finalUrl||e.canonicalUrl||e.finalUrl||e.url;if(!b){w[E]={id:e.id,url:"",status:"skipped"};continue}try{let D=new URL(b);if(D.protocol!=="http:"&&D.protocol!=="https:"){w[E]={id:e.id,url:b,status:"skipped"};continue}}catch{w[E]={id:e.id,url:b,status:"skipped"};continue}try{let D=new AbortController,v=setTimeout(()=>D.abort(),n);try{let P=await fetch(b,{method:"HEAD",redirect:"follow",signal:D.signal,headers:{"User-Agent":"Mozilla/5.0 (compatible; GreedySearch/2.0; +https://github.com/apmantza/greedysearch-dm)"}});clearTimeout(v);let R=P.status>=200&&P.status<400,y=[401,403,405,429].includes(P.status),j="dead";if(R)j="reachable";else if(y)j="skipped";w[E]={id:e.id,url:b,status:j,httpStatus:P.status,reason:y?"bot-protected-or-head-disallowed":void 0}}catch(P){clearTimeout(v),w[E]={id:e.id,url:b,status:"dead",error:P.name==="AbortError"?"timeout":P.message}}}catch(D){w[E]={id:e.id,url:b,status:"dead",error:D.message}}}catch(b){w[E]={id:"?",url:"",status:"dead",error:b?.message||"unknown"}}}}let h=Math.min(a.length,p);await Promise.all(Array.from({length:h},()=>_()));for(let E of w)if(E.status==="reachable")l.push(E);else if(E.status==="dead")g.push(E);else r.push(E);return{reachable:l,dead:g,skipped:r,ok:g.length===0}}async function Hn(t,n=null){process.stderr.write(`PROGRESS:research:check-urls
444
- `);try{let f=new Set(n?.cited||[]),p=f.size?(t||[]).filter((l)=>f.has(l?.id)):t,a=await cg(p,{timeoutMs:6000,concurrency:4});if(!a.ok)process.stderr.write(`[greedysearch] ${a.dead.length} dead citation URL(s) detected
445
- `);return a}catch(f){return process.stderr.write(`[greedysearch] URL reachability check failed: ${f.message}
446
- `),null}}function Bt({sources:t=[],fetchedSources:n=[],synthesis:f={},citationAudit:p=null,gaps:a=[],questions:l=[],rounds:g=[],qualityScore:r=0,qualityThreshold:w=8.5,maxSources:$=8,requireCitations:_=!0,requireQuestions:h=!0}={}){let E=n.filter((T)=>T?.fetch?.ok||(T?.contentChars||0)>100||String(T?.content||"").length>100),e=t.filter((T)=>["official-docs","repo","maintainer-blog","academic"].includes(String(T?.sourceType||""))),b=Array.isArray(f?.claims)?f.claims:[],D=p?p.cited?.length||0:0,v=Zn(l),P=(l||[]).filter((T)=>!T.createdRound||T.reason==="Original research question"),R=Zn(P),y=(g||[]).length,j=Math.min(4,Math.max(2,Number($)||8)),I=y<=1?Math.min(2,j):j,F={roundsRun:g.length>=1,fetchedSources:E.length>=I,primarySources:e.length>=1,qualityScore:r>=Math.min(w,8)||_&&b.length>0&&D>0,claimsExtracted:!_||b.length>0,citationsPresent:!_||D>0,citationsValid:!_||p?.ok===!0,unfetchedCitations:!_||(p?.unfetched||[]).length===0,requiredQuestionsClosed:!h||R.open===0};return{floorMet:Object.values(F).every(Boolean),checks:F,metrics:{fetchedOk:E.length,primarySources:e.length,claims:b.length,cited:D,gaps:a.length,openQuestions:v.open,closedQuestions:v.closed,totalQuestions:v.total,openRequiredQuestions:R.open,closedRequiredQuestions:R.closed,totalRequiredQuestions:R.total,qualityScore:r,minFetched:I}}}function Qf(t,n){let f=new Map;for(let p of n||[]){let a=m(p?.canonicalUrl||p?.finalUrl||p?.url);if(a&&p?.id)f.set(a,p.id)}return(t||[]).map((p,a)=>{let l=m(p?.finalUrl||p?.canonicalUrl||p?.url);return{...p,id:p?.id||f.get(l)||`F${a+1}`}})}function Ln(t){return[{id:"Q1",question:H(c(t),320),status:"open",reason:"Original research question",evidence:[],sourceIds:[]}]}function dg(t){let n=0;for(let f of t||[]){let p=Number.parseInt(String(f.id||"").replace(/^Q/i,""),10);if(Number.isFinite(p))n=Math.max(n,p)}return`Q${n+1}`}function df(t,n){let f=c(n).toLowerCase();return(t||[]).find((p)=>p.question?.toLowerCase()===f||Kn(p.question||"",f)>=0.82)}function Wn(t,n,{reason:f="",round:p=null}={}){let a=H(c(n),320);if(!a)return null;let l=df(t,a);if(l)return l;let g={id:dg(t),question:a,status:"open",reason:H(f,240),createdRound:p,evidence:[],sourceIds:[]};return t.push(g),g}function Qt(t,n,{evidence:f="",sourceIds:p=[],round:a=null}={}){let l=t.find((g)=>g.id===n)||df(t,n);if(!l)return null;if(l.status="closed",l.closedRound=l.closedRound||a,f)l.evidence=lt([...l.evidence||[],f],4);if(Array.isArray(p))l.sourceIds=lt([...l.sourceIds||[],...p],8);return l}function Zn(t){let n=t.length,f=t.filter((p)=>p.status==="closed").length;return{total:n,closed:f,open:Math.max(0,n-f)}}function Ut(t,{roundNumber:n,actions:f=[],learningPayload:p={}}={}){for(let w of f){let $=w?.action||w,_=$?.researchGoal&&$.researchGoal!=="Original user query"?$.researchGoal:$?.query||$?.url||"";if(_)Wn(t,_,{reason:"Planned research action",round:n})}let a=5,l=t.filter((w)=>w.status==="open"&&w.reason==="Discovered gap/follow-up");if(l.length>a){let w=l.sort(($,_)=>($.createdRound||0)-(_.createdRound||0)).slice(0,l.length-a);for(let $ of w)$.status="resolved",$.closedRound=n,$.evidence=lt([...$.evidence||[],"Auto-resolved to cap open-question ledger"],4)}let g=Array.isArray(p.answeredQuestions)?p.answeredQuestions:[];for(let w of g){if(typeof w==="string"){Qt(t,w,{round:n});continue}let $=w?.id||w?.question;if(!$&&w?.question){let _=Wn(t,w.question,{reason:"Answered during learning extraction",round:n});if(_)Qt(t,_.id,{round:n});continue}Qt(t,$,{evidence:w?.evidence||w?.answer||"",sourceIds:Array.isArray(w?.sourceIds)?w.sourceIds:[],round:n})}let r=Array.isArray(p.newQuestions)?p.newQuestions:[];for(let w of r)Wn(t,w,{reason:"Discovered gap/follow-up",round:n});return t}function qg(t,n){if(!Array.isArray(t)||t.length===0)return[];let f=["arxiv.org","semanticscholar.org","doi.org"],p=new Set,a=[];for(let l of t){let g=l?.canonicalUrl||l?.finalUrl||l?.url||"";if(!g)continue;let r="";try{r=new URL(g).hostname.toLowerCase().replace(/^www\./,"")}catch{continue}if(!f.some(($)=>r===$||r.endsWith(`.${$}`)))continue;if(n.has(g)||p.has(g))continue;p.add(g);let w=g.includes("/pdf/")?g.replace(/\/pdf\//,"/html/").replace(/\.pdf$/i,""):g;a.push({url:w,label:l?.title||l?.id||r})}return a.slice(0,2)}function Yn(t,n,f){if(!n?.answer||f?.ok!==!0)return t;let p=Array.isArray(n.claims)?n.claims:[],a=Array.isArray(f.cited)?f.cited:[];if(p.length===0||a.length===0)return t;for(let l of t){if(l.status==="closed")continue;let g=null,r=0;for(let w of p){let $=Kn(l.question||"",w.claim||"");if($>r)r=$,g=w}if(l.id==="Q1"||r>=0.18)Qt(t,l.id,{evidence:g?.claim||"Answered in final cited synthesis",sourceIds:Array.isArray(g?.sourceIds)?g.sourceIds:a.slice(0,4)})}return t}function sg(t){if(!t.length)return"No tracked questions.";return t.map((n)=>{let f=n.sourceIds?.length?` (${n.sourceIds.join(", ")})`:"";return`- [${n.status==="closed"?"x":" "}] ${n.id}: ${n.question}${f}`}).join(`
447
- `)}function mn(t,n="None recorded."){let f=lt(t);return f.length?f.map((p)=>`- ${p}`).join(`
448
- `):n}function ug(t,{query:n,rounds:f,sources:p,fetchedSources:a,citationAudit:l,citationUrls:g,floor:r,manifest:w}){let $=(a||[]).filter((D)=>D?.contentChars>100||D?.fetch?.ok),_=(p||[]).filter((D)=>["official-docs","repo","maintainer-blog","academic"].includes(String(D?.sourceType||""))),h=new Set(l?.cited||[]),E=(p||[]).filter((D)=>h.has(D?.id)),e=[`# Provenance: ${n}`,"",`- **Date:** ${w?.startedAt||new Date().toISOString()}`,`- **Duration:** ${w?.durationMs?`${(w.durationMs/1000).toFixed(1)}s`:"unknown"}`,`- **Mode:** ${w?.terminationReason==="simple_single_pass"?"simple (single-pass)":"iterative"}`,`- **Rounds:** ${w?.rounds||f?.length||1}`,"","## Sources","",`- **Consulted:** ${p?.length||0}`,`- **Fetched successfully:** ${$.length}`,`- **Primary sources:** ${_.length}`,`- **Cited in report:** ${E.length}`,""];if(E.length>0){e.push("### Cited sources","");for(let D of E){let v=D.canonicalUrl||D.finalUrl||D.url||"",P=D.fetch?.ok?"✓":"✗";e.push(`- **${D.id}:** [${D.title||v}](${v}) (${D.sourceType||"unknown"}, fetched: ${P})`)}e.push("")}if(g&&(g.reachable.length>0||g.dead.length>0)){if(e.push("## URL reachability",""),g.dead.length>0){e.push(""),e.push("**Dead links:**");for(let D of g.dead)e.push(`- ${D.id}: ${D.url} (${D.httpStatus||D.error||"unknown"})`)}if(g.reachable.length>0)e.push(""),e.push(`**Reachable:** ${g.reachable.length}/${g.reachable.length+g.dead.length}`);e.push("")}let b=!l?"NOT CHECKED":l.ok&&(g?.ok??!0)?"PASS":l.ok===!1?"FAIL (missing citations)":"FAIL (dead links)";if(e.push("## Verification","",`- **Citations:** ${l?.ok?"PASS":`FAIL — missing: ${(l?.missing||[]).join(", ")}`}`,`- **URL reachability:** ${g?g.ok?"PASS":`FAIL — ${g.dead.length} dead`:"SKIPPED"}`,`- **Floor:** ${r?.floorMet?"PASS":"PARTIAL"}`,`- **Overall:** ${b}`,""),r?.checks){e.push("## Floor checks","");for(let[D,v]of Object.entries(r.checks))e.push(`- [${v?"x":" "}] ${D}`);e.push("")}J(Y(t,"provenance.md"),e.join(`
449
- `),"utf8")}async function In({query:t,rounds:n,sources:f,fetchedSources:p,evidenceItems:a=[],synthesis:l,citationAudit:g,floor:r,manifest:w,allGaps:$=[],questions:_=[],citationUrls:h=null,outDir:E=null}){let e=new Date().toISOString().replaceAll(/[:.]/g,"-").slice(0,19),b=E||Y(Og,`${e}_${ig(t)}`),D=Y(b,"reports"),v=Y(b,"sources"),P=Y(b,"data");Fn(D,{recursive:!0}),Fn(v,{recursive:!0}),Fn(P,{recursive:!0});let R=await Vn(p,v),y=lt([...$,...n.flatMap((j)=>j.gaps||[])]);J(Y(b,"STATUS.md"),[r.floorMet?"STATUS: DONE":"STATUS: PARTIAL","",`Query: ${t}`,`Stop reason: ${w.terminationReason||"max_rounds"}`,"","## Deterministic floor checks",...Object.entries(r.checks).map(([j,I])=>`- [${I?"x":" "}] ${j}`),"","## Questions",sg(_),"","## Open gaps",mn(y),""].join(`
450
- `),"utf8"),J(Y(b,"OUTLINE.md"),["# Research bundle outline","","- `reports/SUMMARY.md` — final cited report","- `reports/CLAIMS.md` — extracted claims with support/source IDs","- `reports/EVIDENCE.md` — goal-based source evidence","- `reports/GAPS.md` — remaining caveats and uncertainties","- `provenance.md` — human-readable run metadata and verification","- `sources/` — fetched source markdown files","- `data/manifest.json` — machine-readable run metadata","- `data/rounds.json` — per-round actions/learnings/gaps","- `data/sources.json` — ranked source registry","- `data/questions.json` — open/closed question ledger",""].join(`
451
- `),"utf8"),J(Y(D,"SUMMARY.md"),String(l.answer||""),"utf8"),J(Y(D,"CLAIMS.md"),["# Key claims","",...Array.isArray(l.claims)&&l.claims.length?l.claims.map((j)=>{let I=Array.isArray(j.sourceIds)?j.sourceIds.join(", "):"";return`- ${j.claim||""} (${j.support||"support unknown"}${I?`; ${I}`:""})`}):["No structured claims were extracted."],""].join(`
452
- `),"utf8"),J(Y(D,"EVIDENCE.md"),["# Extracted evidence","",...a.length?a.map((j)=>[`## ${j.sourceId||j.url||"Source"}`,j.url?`<${j.url}>`:"",j.rational?`**Rational:** ${j.rational}`:"",j.evidence?`**Evidence:** ${j.evidence}`:"",j.summary?`**Summary:** ${j.summary}`:"",""].filter(Boolean).join(`
453
- `)):["No goal-based evidence was extracted."],""].join(`
454
- `),"utf8"),J(Y(D,"GAPS.md"),["# Gaps and caveats","","## Caveats",mn(l.caveats||[]),"","## Research gaps",mn(y),""].join(`
455
- `),"utf8"),J(Y(P,"manifest.json"),JSON.stringify({...w,floor:r,citationAudit:g},null,2),"utf8"),J(Y(P,"rounds.json"),JSON.stringify(n,null,2),"utf8"),J(Y(P,"sources.json"),JSON.stringify(f,null,2),"utf8"),J(Y(P,"questions.json"),JSON.stringify(_,null,2),"utf8"),J(Y(P,"evidence.json"),JSON.stringify(a,null,2),"utf8"),J(Y(v,"index.md"),["# Source index","",...R.map((j)=>{let I=j.title||j.url,F=j.finalUrl||j.url,T=j.contentPath?` — ${j.contentPath}`:"";return`- ${j.id||"?"}: [${I}](${F})${T}`}),""].join(`
456
- `),"utf8");try{ug(b,{query:t,rounds:n,sources:f,fetchedSources:p,citationAudit:g,citationUrls:h,floor:r,manifest:w})}catch(j){process.stderr.write(`[greedysearch] Provenance sidecar write failed (non-critical): ${j.message}
457
- `)}return{dir:b,statusPath:Y(b,"STATUS.md"),summaryPath:Y(D,"SUMMARY.md"),manifestPath:Y(P,"manifest.json"),provenancePath:Y(b,"provenance.md"),sourceCount:R.length,sourceFiles:R}}async function F0({query:t,breadth:n,iterations:f,maxSources:p,locale:a=null,short:l=!1,qualityThreshold:g=8.5,writeBundle:r=process.env.GREEDY_RESEARCH_BUNDLE!=="0",researchOutDir:w=null}={}){let $=Ag({breadth:n,iterations:f,maxSources:p}),_=n!==void 0&&n!==null,h=f!==void 0&&f!==null;if(!_&&!h)try{let O=await pf(t);if(process.stderr.write(`[greedysearch] Complexity: ${O.complexity} (${O.reasoning})
458
- `),O.complexity==="simple")return process.stderr.write(`[greedysearch] Simple query detected — using fast single-pass path
459
- `),Jf({query:t,locale:a,maxSources:Math.min(p??5,5),qualityThreshold:g,writeBundle:r,researchOutDir:w});if(!_)$.breadth=O.suggestedBreadth;if(!h)$.iterations=O.suggestedIterations}catch(O){process.stderr.write(`[greedysearch] Scale classification failed, using defaults: ${O.message}
460
- `)}let e=[],b=[],D=[],v=Ln(t),P=null,R=[],y=[],j=[],I=new Set,F=new Set,T=new Set,k=[],G="max_rounds",ht=new Date().toISOString(),d=Date.now(),wt=0,N=0,o=0,rt=[],Z=ot({totalActions:$.iterations*$.breadth,totalRounds:$.iterations,totalFetches:$.iterations,silent:process.env.GREEDY_RESEARCH_QUIET==="1"});Z.startRound(1),process.stderr.write(`[greedysearch] Research mode: breadth ${$.breadth}, iterations ${$.iterations}, qualityThreshold ${g}, engines ${kt.join(",")}, synthesizer gemini
461
- `);for(let O=0;O<$.iterations;O++){let L=O+1,s=Math.max(1,Math.ceil($.breadth/2**O));if(process.stderr.write(`PROGRESS:research:round-${L}:planning
462
- `),!P)try{let C=await X(Mg(t,s,b,D,[...T]),{timeoutMs:120000}),i=Zg(C,s);if(O===0)i.unshift({type:"search",query:t,researchGoal:"Original user query"});i=await Jg(i,T),P=i}catch(C){process.stderr.write(`[greedysearch] Action planning failed, using fallback queries: ${C.message}
463
- `);let i=kg(null,t,s,{includeOriginal:O===0,exclude:F});P=Kg(i)}let Q=(P||[]).filter((C)=>{if(C.type==="search"){let i=!zf(C.query,F,{roundIndex:O,originalQuery:t});if(!i)process.stderr.write(`[greedysearch] Novelty gate rejected search: ${C.query}
464
- `);return i}if(C.type==="fetchUrl"){let i=!T.has(C.url);if(!i)process.stderr.write(`[greedysearch] Novelty gate rejected fetch: ${C.url}
465
- `);return i}return!1}).slice(0,s),Bn=qg(R,T);if(!Q.some((C)=>C.type==="fetchUrl")&&Bn.length>0){let C=Bn[0];Q.push({type:"fetchUrl",url:C.url,researchGoal:`Direct fetch of known academic source: ${C.label||C.url}`}),process.stderr.write(`[greedysearch] Forced fetchUrl for academic source: ${C.url}
466
- `)}let Un=Array(Q.length),sf=Math.min(3,Q.length),uf=0;async function tp(){while(!0){let C=uf++;if(C>=Q.length)return;let i=Q[C];process.stderr.write(`PROGRESS:research:round-${L}:action-${C+1}/${Q.length}
467
- `),process.stderr.write(`[greedysearch] Action ${C+1}/${Q.length} [${i.type}]: ${(i.query||i.url).slice(0,80)}
468
- `),Z.startAction(i.type,(i.query||i.url||"").slice(0,60));let W=await Wg(i,{locale:a,short:l,usedQueries:F,usedUrls:T,maxChars:8000});Z.endAction(),Un[C]=W}}await Promise.all(Array.from({length:sf},()=>tp()));let Dt=[];for(let C=0;C<Q.length;C++){let i=Q[C],W=Un[C];if(Dt.push(W),wt++,i.type==="search")N++;if(i.type==="fetchUrl")o++,Z.endFetch(W.ok);if(!W.ok)rt.push({round:L,type:i.type,target:i.query||i.url,error:W.error}),process.stderr.write(`[greedysearch] Action failed: ${W.error}
469
- `)}let Qn=Dt.filter((C)=>C.action.type==="search"),xn=Dt.filter((C)=>C.action.type==="fetchUrl");Ut(v,{roundNumber:L,actions:Dt}),R=Qg([R,Qn.flatMap((C)=>C.sources||[]),xn.flatMap((C)=>C.sources||[])]);for(let C of xn)if(C.fetchResult)y.push(C.fetchResult);y=xf(y);let zt=Math.max(0,$.maxSources-y.filter((C)=>C?.content||C?.contentChars>100).length);if(zt>0&&R.length>0){process.stderr.write(`PROGRESS:research:round-${L}:fetching
470
- `);let C=new Set,i=(tt)=>{try{return m(tt||"")}catch{return""}};for(let tt of y)for(let It of[tt?.url,tt?.finalUrl,tt?.canonicalUrl]){let vt=i(It);if(vt)C.add(vt)}let W=R.filter((tt)=>{let It=[tt?.canonicalUrl,tt?.finalUrl,tt?.url].map((vt)=>i(vt)).filter(Boolean);return It.length>0&&It.every((vt)=>!C.has(vt))}),fp=await Tg(W,Math.min(zt,W.length),8000,Math.min(3,zt||1));y=xf([...y,...fp]),R=Gt(R,y)}y=Qf(y,R);let np=Dt.map((C)=>({query:C.action.query||C.action.url||"",researchGoal:C.action.researchGoal||""}));process.stderr.write(`PROGRESS:research:round-${L}:evidence
471
- `),process.stderr.write(`PROGRESS:research:round-${L}:learning
472
- `);let Yt=await Bg({query:t,questions:v,fetchedSources:y,extractedSourceKeys:I,roundQueries:np,searchSummaries:Qn.map((C)=>({query:C.action.query,researchGoal:C.action.researchGoal,error:C.error||"",engines:Gg(C.result)})),evidenceItems:j}),bt={evidence:Yt.evidence,error:Yt.evidenceError};if(bt.error)process.stderr.write(`[greedysearch] Evidence extraction failed: ${bt.error}
473
- `);j=[...j,...bt.evidence];for(let C of bt.evidence)Ut(v,{roundNumber:L,learningPayload:{answeredQuestions:C.answers||[],newQuestions:C.newQuestions||[]}});let{learningPayload:Ct,learningError:St}=Yt;if(St)process.stderr.write(`[greedysearch] Learning extraction failed: ${St}
474
- `);let zn=Array.isArray(Ct.learnings)?Ct.learnings.map((C)=>String(C)).filter(Boolean).slice(0,8):[],ct=Array.isArray(Ct.gaps)?Ct.gaps.map((C)=>String(C)).filter(Boolean).slice(0,6):[];if(b=lt([...b,...zn]),D=lt([...D,...ct]),Ut(v,{roundNumber:L,actions:[],learningPayload:Ct,gaps:ct}),e.push({round:L,actions:Dt.map((C)=>({type:C.action.type,query:C.action.query||"",url:C.action.url||"",researchGoal:C.action.researchGoal||"",error:C.error||"",sourceCount:C.sources?.length||0})),learnings:zn,gaps:ct,evidence:bt.evidence,evidenceError:bt.error,learningError:St}),process.stderr.write(`PROGRESS:research:round-${L}:evaluating
475
- `),Z.endRound(),L<$.iterations)Z.startRound(L+1);let V=L===$.iterations?{score:k.length>0?k[k.length-1]:5,coverage:{},knowledgeGaps:[],shouldContinue:!1,nextActions:[],terminationReason:null,evaluationError:""}:await Fg(t,e,b,D,k);k.push(V.score),D=lt([...D,...V.knowledgeGaps||[]]),Ut(v,{roundNumber:L,gaps:V.knowledgeGaps||[]});let Sn=Bt({sources:R,fetchedSources:y,gaps:D,questions:v,rounds:e,qualityScore:V.score,qualityThreshold:g,maxSources:$.maxSources,requireCitations:!1,requireQuestions:!1});if(process.stderr.write(`[greedysearch] Quality score round ${L}: ${V.score.toFixed(1)} (shouldContinue: ${V.shouldContinue}, floor: ${Sn.floorMet})
476
- `),V.score>=g&&Sn.floorMet&&(!V.shouldContinue||V.terminationReason==="quality_threshold")){G=V.terminationReason||"quality_threshold",process.stderr.write(`[greedysearch] Research floor reached (score: ${V.score.toFixed(1)}). Terminating early.
477
- `);break}let _t=Math.max(1,Math.ceil(s/2)),u=(Ct.followUpQueries||[]).map((C)=>({type:"search",query:c(String(C)),researchGoal:"Follow-up from learning extraction"})).filter((C)=>C.query&&C.query.toLowerCase()!==t.toLowerCase()).slice(0,_t);if(u.length<_t&&V.nextActions.length>0){let C=V.nextActions.map((W)=>Sf(W)).filter(Boolean);u=[...u,...C].slice(0,_t)}if(u.length<_t&&D.length>0){let C=Ig(D,t,F,_t-u.length,O+1),i=C.map((W)=>({type:"search",query:W.query,researchGoal:W.researchGoal}));if(u=[...u,...i].slice(0,_t),C.length>0)process.stderr.write(`[greedysearch] Generated ${C.length} gap-driven fallback actions.
478
- `)}P=u.length>=_t?u:null}process.stderr.write(`PROGRESS:research:final-report
479
- `);let A={answer:b.length?b.map((O)=>`- ${O}`).join(`
480
- `):"Research completed, but no structured learnings were extracted.",agreement:{level:"mixed",summary:"Research synthesis fallback."},differences:[],caveats:[],claims:[],recommendedSources:R.slice(0,4).map((O)=>O.id),synthesized:!1};try{let O=await X(Tn(t,e,R,v,j),{timeoutMs:180000}),L=Ht(O,{}),s=Array.isArray(L?.claims)&&L.claims.length>0;A={...A,...L,rawAnswer:O.answer||"",geminiSources:O.sources||[],synthesized:s}}catch(O){process.stderr.write(`[greedysearch] Final report failed: ${O.message}
481
- `),A.error=O.message}if(!(A.synthesized===!0&&Array.isArray(A.claims)&&A.claims.length>0)&&j.length>0){process.stderr.write(`[greedysearch] Falling back to evidence-based synthesis (no per-round learnings).
482
- `);try{let O=An(t,R,v,j),L=await X(O,{timeoutMs:180000}),s=Ht(L,{});A={...A,...s,rawAnswer:L.answer||A.answer||"",geminiSources:L.sources||A.geminiSources||[],synthesized:!0,synthesisMode:"evidence_fallback"}}catch(O){process.stderr.write(`[greedysearch] Evidence-based synthesis failed: ${O.message}
483
- `),A.evidenceFallbackError=O.message}}let q=new Date().toISOString(),xt=Date.now()-d,qf=k.at(-1)||0;y=Qf(y,R),process.stderr.write(`PROGRESS:research:audit-citations
484
- `);let Ot=kn(A.answer||"",R),Xn=await Hn(R,Ot);Yn(v,A,Ot);let Et=Bt({sources:R,fetchedSources:y,synthesis:A,citationAudit:Ot,gaps:D,questions:v,rounds:e,qualityScore:qf,qualityThreshold:g,maxSources:$.maxSources});if(Et.floorMet&&G==="max_rounds")G="done_floor_met";else if(!Et.floorMet&&G==="quality_threshold")G="max_rounds_floor_unmet";let on={startedAt:ht,finishedAt:q,durationMs:xt,engines:kt,synthesizer:"gemini",rounds:e.length,actionsRun:wt,searches:N,fetches:o,sourcesFetched:y.filter((O)=>O?.contentChars>100).length,engineFailures:rt,terminationReason:G,floorMet:Et.floorMet},Nt=null,Lt;if(r){process.stderr.write(`PROGRESS:research:bundle
485
- `);try{Nt=await In({query:t,rounds:e,sources:R,fetchedSources:y,evidenceItems:j,synthesis:A,citationAudit:Ot,citationUrls:Xn,floor:Et,manifest:on,allGaps:D,questions:v,outDir:w}),Lt=Nt.sourceFiles,delete Nt.sourceFiles}catch(O){Nt={error:O.message||String(O)},Lt=await Vn(y)}}else Lt=await Vn(y);return process.stderr.write(`PROGRESS:research:done
486
- `),Z.finish(),{query:t,_research:{mode:"iterative",breadth:$.breadth,iterations:$.iterations,maxSources:$.maxSources,rounds:e,learnings:b,gaps:D,evidence:j,questions:v,questionProgress:Zn(v),qualityHistory:k,terminationReason:G,qualityThreshold:g,floor:Et,bundle:Nt,manifest:on},_citationAudit:Ot,_citationUrls:Xn,_sources:R,_fetchedSources:Lt,_synthesis:A,_confidence:{sourcesCount:R.length,fetchedSourceSuccessRate:y.length>0?Number((y.filter((O)=>O.contentChars>100).length/y.length).toFixed(2)):0,agreementLevel:A.agreement?.level||"mixed",floorMet:Et.floorMet}}}function xf(t){let n=new Map;for(let g of t){let r=g?.id||m(g?.finalUrl||g?.url||"");if(!r)continue;let w=n.get(r);if(!w||(g.contentChars||0)>(w.contentChars||0))n.set(r,g)}let f=new Map;function p(g){let r=f.get(g);if(!r){let w=String(g.content||g.snippet||"");r={length:w.length,tokens:Jn(w.slice(0,4000))},f.set(g,r)}return r}function a(g,r){let w=0,$=g.size<=r.size?g:r,_=g.size<=r.size?r:g;for(let E of $)if(_.has(E))w++;let h=g.size+r.size-w;if(h===0)return 1;return w/h}let l=[];for(let g of n.values()){let r=p(g),w=l.findIndex(($)=>{let _=f.get($);if(r.length<400||_.length<400)return!1;return a(r.tokens,_.tokens)>=0.9});if(w===-1){l.push(g);continue}if((g.contentChars||0)>(l[w].contentChars||0))l[w]=g}return l}export{In as writeResearchBundle,ug as writeProvenanceSidecar,Sf as validateAction,Ut as updateQuestionLedger,Jn as tokenSet,F0 as runResearchMode,Hn as runCitationUrlCheck,Yn as reconcileQuestionsFromSynthesis,Kg as queriesToActions,Zg as parseActionPlan,kg as normalizeResearchQueries,Kn as jaccardSimilarity,zf as isDuplicateQuery,Zf as extractEvidenceFromSources,Bg as extractEvidenceAndLearnings,Ln as createQuestionLedger,Bt as computeResearchFloor,Ag as clampResearchOptions,cg as checkCitationUrls,An as buildSynthesisFromEvidencePrompt,Tn as buildFinalReportPrompt,Ig as buildFallbackQueriesFromGaps,og as buildEvidenceAndLearningPrompt,kn as auditCitations};
2063
+ } catch (error) {
2064
+ process.stderr.write(`[greedysearch] Final report failed: ${error.message}
2065
+ `);
2066
+ synthesis.error = error.message;
2067
+ }
2068
+ const hasStructuredSynthesis = synthesis.synthesized === true && Array.isArray(synthesis.claims) && synthesis.claims.length > 0;
2069
+ if (!hasStructuredSynthesis && evidenceItems.length > 0) {
2070
+ process.stderr.write(`[greedysearch] Falling back to evidence-based synthesis (no per-round learnings).
2071
+ `);
2072
+ try {
2073
+ const evidencePrompt = buildSynthesisFromEvidencePrompt(query, combinedSources, questions, evidenceItems);
2074
+ const rawEvidenceReport = await runGeminiPrompt(evidencePrompt, {
2075
+ timeoutMs: 180000
2076
+ });
2077
+ const parsedEvidence = parseGeminiJson(rawEvidenceReport, {});
2078
+ synthesis = {
2079
+ ...synthesis,
2080
+ ...parsedEvidence,
2081
+ rawAnswer: rawEvidenceReport.answer || synthesis.answer || "",
2082
+ geminiSources: rawEvidenceReport.sources || synthesis.geminiSources || [],
2083
+ synthesized: true,
2084
+ synthesisMode: "evidence_fallback"
2085
+ };
2086
+ } catch (error) {
2087
+ process.stderr.write(`[greedysearch] Evidence-based synthesis failed: ${error.message}
2088
+ `);
2089
+ synthesis.evidenceFallbackError = error.message;
2090
+ }
2091
+ }
2092
+ const finishedAt = new Date().toISOString();
2093
+ const durationMs = Date.now() - startMs;
2094
+ const qualityScore = qualityHistory.at(-1) || 0;
2095
+ fetchedSources = annotateFetchedSourcesWithIds(fetchedSources, combinedSources);
2096
+ process.stderr.write(`PROGRESS:research:audit-citations
2097
+ `);
2098
+ const citationAudit = auditCitations(synthesis.answer || "", combinedSources);
2099
+ const citationUrls = await runCitationUrlCheck(combinedSources, citationAudit);
2100
+ reconcileQuestionsFromSynthesis(questions, synthesis, citationAudit);
2101
+ const floor = computeResearchFloor({
2102
+ sources: combinedSources,
2103
+ fetchedSources,
2104
+ synthesis,
2105
+ citationAudit,
2106
+ gaps: allGaps,
2107
+ questions,
2108
+ rounds,
2109
+ qualityScore,
2110
+ qualityThreshold,
2111
+ maxSources: options.maxSources
2112
+ });
2113
+ if (floor.floorMet && terminationReason === "max_rounds") {
2114
+ terminationReason = "done_floor_met";
2115
+ } else if (!floor.floorMet && terminationReason === "quality_threshold") {
2116
+ terminationReason = "max_rounds_floor_unmet";
2117
+ }
2118
+ const manifest = {
2119
+ startedAt,
2120
+ finishedAt,
2121
+ durationMs,
2122
+ engines: RESEARCH_ENGINES,
2123
+ synthesizer: "gemini",
2124
+ rounds: rounds.length,
2125
+ actionsRun: totalActionsRun,
2126
+ searches: totalSearches,
2127
+ fetches: totalFetches,
2128
+ sourcesFetched: fetchedSources.filter((s) => s?.contentChars > 100).length,
2129
+ engineFailures,
2130
+ terminationReason,
2131
+ floorMet: floor.floorMet
2132
+ };
2133
+ let bundle = null;
2134
+ let fetchedFiles;
2135
+ if (writeBundle) {
2136
+ process.stderr.write(`PROGRESS:research:bundle
2137
+ `);
2138
+ try {
2139
+ bundle = await writeResearchBundle({
2140
+ query,
2141
+ rounds,
2142
+ sources: combinedSources,
2143
+ fetchedSources,
2144
+ evidenceItems,
2145
+ synthesis,
2146
+ citationAudit,
2147
+ citationUrls,
2148
+ floor,
2149
+ manifest,
2150
+ allGaps,
2151
+ questions,
2152
+ outDir: researchOutDir
2153
+ });
2154
+ fetchedFiles = bundle.sourceFiles;
2155
+ delete bundle.sourceFiles;
2156
+ } catch (error) {
2157
+ bundle = { error: error.message || String(error) };
2158
+ fetchedFiles = await writeResearchSourcesToFiles(fetchedSources);
2159
+ }
2160
+ } else {
2161
+ fetchedFiles = await writeResearchSourcesToFiles(fetchedSources);
2162
+ }
2163
+ process.stderr.write(`PROGRESS:research:done
2164
+ `);
2165
+ progressTracker.finish();
2166
+ return {
2167
+ query,
2168
+ _research: {
2169
+ mode: "iterative",
2170
+ breadth: options.breadth,
2171
+ iterations: options.iterations,
2172
+ maxSources: options.maxSources,
2173
+ rounds,
2174
+ learnings: allLearnings,
2175
+ gaps: allGaps,
2176
+ evidence: evidenceItems,
2177
+ questions,
2178
+ questionProgress: questionProgress(questions),
2179
+ qualityHistory,
2180
+ terminationReason,
2181
+ qualityThreshold,
2182
+ floor,
2183
+ bundle,
2184
+ manifest
2185
+ },
2186
+ _citationAudit: citationAudit,
2187
+ _citationUrls: citationUrls,
2188
+ _sources: combinedSources,
2189
+ _fetchedSources: fetchedFiles,
2190
+ _synthesis: synthesis,
2191
+ _confidence: {
2192
+ sourcesCount: combinedSources.length,
2193
+ fetchedSourceSuccessRate: fetchedSources.length > 0 ? Number((fetchedSources.filter((source) => source.contentChars > 100).length / fetchedSources.length).toFixed(2)) : 0,
2194
+ agreementLevel: synthesis.agreement?.level || "mixed",
2195
+ floorMet: floor.floorMet
2196
+ }
2197
+ };
2198
+ }
2199
+ function dedupeFetchedSources(sources) {
2200
+ const byUrl = new Map;
2201
+ for (const source of sources) {
2202
+ const key = source?.id || normalizeUrl(source?.finalUrl || source?.url || "");
2203
+ if (!key)
2204
+ continue;
2205
+ const existing = byUrl.get(key);
2206
+ if (!existing || (source.contentChars || 0) > (existing.contentChars || 0)) {
2207
+ byUrl.set(key, source);
2208
+ }
2209
+ }
2210
+ const tokenInfo = new Map;
2211
+ function getTokenInfo(source) {
2212
+ let info = tokenInfo.get(source);
2213
+ if (!info) {
2214
+ const content = String(source.content || source.snippet || "");
2215
+ info = {
2216
+ length: content.length,
2217
+ tokens: tokenSet(content.slice(0, 4000))
2218
+ };
2219
+ tokenInfo.set(source, info);
2220
+ }
2221
+ return info;
2222
+ }
2223
+ function jaccardSetSimilarity(a, b) {
2224
+ let intersection = 0;
2225
+ const smaller = a.size <= b.size ? a : b;
2226
+ const larger = a.size <= b.size ? b : a;
2227
+ for (const t of smaller) {
2228
+ if (larger.has(t))
2229
+ intersection++;
2230
+ }
2231
+ const union = a.size + b.size - intersection;
2232
+ if (union === 0)
2233
+ return 1;
2234
+ return intersection / union;
2235
+ }
2236
+ const out = [];
2237
+ for (const source of byUrl.values()) {
2238
+ const sourceInfo = getTokenInfo(source);
2239
+ const duplicateIndex = out.findIndex((existing) => {
2240
+ const existingInfo = tokenInfo.get(existing);
2241
+ if (sourceInfo.length < 400 || existingInfo.length < 400) {
2242
+ return false;
2243
+ }
2244
+ return jaccardSetSimilarity(sourceInfo.tokens, existingInfo.tokens) >= 0.9;
2245
+ });
2246
+ if (duplicateIndex === -1) {
2247
+ out.push(source);
2248
+ continue;
2249
+ }
2250
+ if ((source.contentChars || 0) > (out[duplicateIndex].contentChars || 0)) {
2251
+ out[duplicateIndex] = source;
2252
+ }
2253
+ }
2254
+ return out;
2255
+ }