@wenathlan/devthink 2.0.12 → 2.0.14

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 (444) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/search.ts ADDED
@@ -0,0 +1,1760 @@
1
+ /**
2
+ * @file search.ts
3
+ * @module provider/search
4
+ * @description
5
+ * Third-person observer: the library watches how Antigravity Manager and
6
+ * Gemini CLI perform grounding via googleSearch + urlContext and reproduces
7
+ * the contract without relying on localhost v1 proxy. The observer calls
8
+ * cloudcode-pa.googleapis.com directly, cascades endpoints on 403/404/5xx,
9
+ * injects native Gemini tools, and parses groundingMetadata rigorously.
10
+ *
11
+ * Layout: library-first, root-first — file lives beside its siblings
12
+ * Runtime: only node:* builtins + global fetch (Node >=18). Zero external deps.
13
+ * Date governance: 25/08/2026 — Canto do Buriti, PI, BR — version awareness 2.0.0
14
+ *
15
+ * Contract:
16
+ * - Endpoint: https://cloudcode-pa.googleapis.com/v1internal:generateContent (primary)
17
+ * cascade: daily-cloudcode-pa.googleapis.com, sandbox fallback
18
+ * - No localhost v1 base-url proxy, direct cloudcode-pa (security + user choice)
19
+ * - Native tools: googleSearch + urlContext — both injected when enabled
20
+ * - Model: SEARCH_MODEL gemini-3-flash with preview fallback chain
21
+ * - Thinking: thinkingLevel → thinkingBudget mapping (8192/16384/32768) clamped 32k
22
+ * - Grounding parse: groundingMetadata.webSearchQueries, groundingChunks[].web.uri/title,
23
+ * groundingSupports, searchEntryPoint.renderedContent
24
+ * - Proxy support: env HTTPS_PROXY/HTTP_PROXY/NO_PROXY + options.proxyUrl
25
+ * manual CONNECT tunneling via node:net + node:tls — no external deps
26
+ * - Bypass metadata: mimics antigravity/{ver} {os}/{arch} UA, strips x-goog-user-project,
27
+ * jitter 0-80ms, FNV-1a deterministic session_id note (not needed here but coherent)
28
+ *
29
+ * EN: Antigravity bypass relies on mimicking antigravity/{ver} {os}/{arch} UA
30
+ * and bypassing x-goog-user-project stripping, endpoint cascade 403/404/5xx.
31
+ * PT-BR: Bypass Antigravity depende de imitar UA antigravity/{ver} {os}/{arch}
32
+ * e remover x-goog-user-project, cascata de endpoints em 403/404/5xx.
33
+ *
34
+ * Production-ready: JSDoc, exhaustive error handling, timeout, abort, proxy,
35
+ * endpoint cascade, thinking budget, grounding parsing, no placeholder.
36
+ *
37
+ * @author devthink
38
+ * @license MIT
39
+ * @version 2.0.0
40
+ */
41
+
42
+ import * as os from "node:os";
43
+ import { platform as osPlatform, arch as osArch, release as osRelease } from "node:os";
44
+ import * as net from "node:net";
45
+ import * as tls from "node:tls";
46
+ import * as http from "node:http";
47
+ import * as https from "node:https";
48
+ import { createHash } from "node:crypto";
49
+ import { join as pathJoin } from "node:path";
50
+ import { mkdirSync, writeFileSync } from "node:fs";
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Frozen-owner imports — identical values owned by constants.ts / core.ts /
54
+ // models.ts, re-exported below so the module surface stays unchanged.
55
+ // ---------------------------------------------------------------------------
56
+
57
+ import {
58
+ ANTIGRAVITY_USER_AGENT_FALLBACK,
59
+ ANTIGRAVITY_VERSION_FALLBACK,
60
+ DEFAULT_TIMEOUT_MS,
61
+ GENERATE_CONTENT_ENDPOINT,
62
+ JITTER_MAX_MS,
63
+ JITTER_MIN_MS,
64
+ PLUGIN_VERSION,
65
+ STREAM_GENERATE_ENDPOINT,
66
+ FETCH_TIMEOUT_MS,
67
+ } from "./constants.js";
68
+ import { fnv1a32, getJitter, isRetryableStatus } from "./core.js";
69
+ import {
70
+ buildAntagravityHeaders,
71
+ getDynamicUserAgent,
72
+ getXGoogApiClient,
73
+ normalizeArch,
74
+ normalizePlatform,
75
+ } from "./fingerprint.js";
76
+ // v2.1.15 Phase A: model-identification families (search/default model ids,
77
+ // thinking tables, image-gen helpers) are imported from models.ts — THE
78
+ // single owner — instead of the former constants.js re-exports.
79
+ import {
80
+ CODE_ASSIST_ENDPOINTS,
81
+ SEARCH_MODEL,
82
+ SEARCH_MODEL_FALLBACKS,
83
+ SEARCH_MODEL_PREVIEW,
84
+ THINKING_BUDGET_MAP,
85
+ THINKING_BUDGET_MAX_CLAUDE,
86
+ THINKING_BUDGET_MAX_GEMINI,
87
+ THINKING_LEVELS,
88
+ buildImageGenConfig,
89
+ isImageGenModel,
90
+ } from "./models.js";
91
+ import type { ThinkingLevel } from "./models.js";
92
+ // v2.1.15 Phase A: parseGrounding moved from core.ts — web-citation
93
+ // extraction is search-grounding domain logic.
94
+ function parseGrounding(meta: any): { uri: string; title?: string }[] {
95
+ const chunks = meta?.groundingChunks || meta?.grounding_chunks || [];
96
+ return chunks.map((c: any) => (c.web ? { uri: c.web.uri, title: c.web.title } : null)).filter(Boolean);
97
+ }
98
+
99
+ export {
100
+ ANTIGRAVITY_USER_AGENT_FALLBACK,
101
+ ANTIGRAVITY_VERSION_FALLBACK,
102
+ CODE_ASSIST_ENDPOINTS,
103
+ DEFAULT_TIMEOUT_MS,
104
+ GENERATE_CONTENT_ENDPOINT,
105
+ JITTER_MAX_MS,
106
+ JITTER_MIN_MS,
107
+ SEARCH_MODEL,
108
+ SEARCH_MODEL_FALLBACKS,
109
+ SEARCH_MODEL_PREVIEW,
110
+ STREAM_GENERATE_ENDPOINT,
111
+ THINKING_BUDGET_MAP,
112
+ THINKING_BUDGET_MAX_CLAUDE,
113
+ THINKING_BUDGET_MAX_GEMINI,
114
+ THINKING_LEVELS,
115
+ buildImageGenConfig,
116
+ getDynamicUserAgent,
117
+ getJitter,
118
+ isImageGenModel,
119
+ };
120
+ export type { ThinkingLevel };
121
+ export { parseGrounding };
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Constants — observer notes prod is authoritative
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /** Endpoint cascade — observer watches traffic: PROD first, then DAILY, SANDBOX */
128
+ export const DEFAULT_ENDPOINT_ORDER: readonly string[] = [
129
+ CODE_ASSIST_ENDPOINTS.PROD,
130
+ CODE_ASSIST_ENDPOINTS.DAILY,
131
+ CODE_ASSIST_ENDPOINTS.SANDBOX,
132
+ ] as const;
133
+
134
+ /** Timeouts — observer notes 10s per control call, 60s for content */
135
+ export const SEARCH_TIMEOUT_MS = 60_000 as const;
136
+ // local variant: diverges from models (models.FETCH_TIMEOUT_MS is the 10s control-plane timeout; this file pins it to the 60s content timeout)
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Types — grounding contract from Gemini API
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * Web chunk returned by groundingMetadata.
144
+ * Observer documents: each chunk points to a source URL used for grounding.
145
+ */
146
+ export interface WebGroundingChunk {
147
+ /** source uri, e.g. https://developer.mozilla.org/... */
148
+ uri: string;
149
+ /** title returned by search */
150
+ title: string;
151
+ /** derived domain — convenience */
152
+ domain?: string | undefined;
153
+ }
154
+
155
+ /** Raw web object inside groundingChunks */
156
+ export interface GroundingChunkWeb {
157
+ uri?: string;
158
+ title?: string;
159
+ domain?: string;
160
+ }
161
+
162
+ export interface GroundingChunk {
163
+ web?: GroundingChunkWeb;
164
+ /** alternative shapes: retrievedContext etc. — kept for resilience */
165
+ retrievedContext?: { uri?: string; title?: string; text?: string };
166
+ [k: string]: unknown;
167
+ }
168
+
169
+ export interface GroundingSupportSegment {
170
+ startIndex?: number;
171
+ endIndex?: number;
172
+ text?: string;
173
+ }
174
+
175
+ export interface GroundingSupport {
176
+ segment?: GroundingSupportSegment;
177
+ groundingChunkIndices?: number[];
178
+ confidenceScores?: number[];
179
+ }
180
+
181
+ export interface SearchEntryPoint {
182
+ renderedContent?: string;
183
+ [k: string]: unknown;
184
+ }
185
+
186
+ export interface GroundingMetadata {
187
+ webSearchQueries?: string[];
188
+ groundingChunks?: GroundingChunk[];
189
+ groundingSupports?: GroundingSupport[];
190
+ searchEntryPoint?: SearchEntryPoint;
191
+ retrievalQueries?: string[];
192
+ [k: string]: unknown;
193
+ }
194
+
195
+ /** Request shape */
196
+ export interface ExecuteSearchInput {
197
+ /** user query to ground with googleSearch/urlContext */
198
+ query: string;
199
+ /** optional system instruction — helps guide grounded answer */
200
+ systemPrompt?: string | undefined;
201
+ /** optional additional context appended to user query for better grounding */
202
+ additionalContext?: string | undefined;
203
+ }
204
+
205
+ export interface ExecuteSearchAuth {
206
+ /** OAuth access_token Bearer — required */
207
+ accessToken: string;
208
+ /** project id resolved via loadCodeAssist/onboardUser — optional but improves success */
209
+ projectId?: string;
210
+ /** optional custom user agent — fallback uses dynamic UA */
211
+ userAgent?: string;
212
+ /** optional antigravity version for UA — defaults to 1.19.2 */
213
+ version?: string;
214
+ /** optional client metadata override */
215
+ clientMetadata?: string;
216
+ }
217
+
218
+ export interface ExecuteSearchOptions {
219
+ /** model — defaults to SEARCH_MODEL gemini-3-flash */
220
+ model?: string;
221
+ /** thinking level mapping to budget */
222
+ thinkingLevel?: ThinkingLevel;
223
+ /** direct budget override — clamped 0-32768 */
224
+ thinkingBudget?: number;
225
+ /** include groundingMetadata in result — default true */
226
+ includeGrounding?: boolean;
227
+ /** inject url_context companion tool — default true per spec */
228
+ useUrlContext?: boolean;
229
+ /** inject google_search tool — default true */
230
+ useGoogleSearch?: boolean;
231
+ /** proxy url e.g. http://127.0.0.1:8080 — null disables env proxy */
232
+ proxyUrl?: string | null;
233
+ /** timeout per fetch — default 60s */
234
+ timeoutMs?: number;
235
+ /** custom endpoint list for cascade — default PROD/DAILY/SANDBOX */
236
+ endpoints?: string[];
237
+ /** include rendered search entry point? */
238
+ includeSearchEntryPoint?: boolean;
239
+ /** temperature for grounded answer — default 0.2 */
240
+ temperature?: number;
241
+ /** max output tokens — default 8192 */
242
+ maxOutputTokens?: number;
243
+ /** debug logging */
244
+ debug?: boolean;
245
+ /** jitter between endpoint retries — default true */
246
+ jitter?: boolean;
247
+ /** max retries across endpoints — internal */
248
+ maxRetries?: number;
249
+ }
250
+
251
+ /** Final grounded result */
252
+ export interface SearchResult {
253
+ /** synthesized answer text */
254
+ text: string;
255
+ /** original query */
256
+ query: string;
257
+ /** model used */
258
+ model: string;
259
+ /** true if grounding sources returned */
260
+ grounded: boolean;
261
+ /** queries Gemini issued to Google Search */
262
+ webSearchQueries: string[];
263
+ /** parsed web sources — uri/title */
264
+ groundingChunks: WebGroundingChunk[];
265
+ /** raw supports — segment attribution */
266
+ groundingSupports?: GroundingSupport[] | undefined;
267
+ /** rendered search entry point HTML if present */
268
+ searchEntryPoint?: SearchEntryPoint | undefined;
269
+ /** raw groundingMetadata as returned */
270
+ groundingMetadata?: GroundingMetadata | undefined;
271
+ /** raw candidate response for debugging */
272
+ raw?: unknown | undefined;
273
+ /** endpoint that succeeded */
274
+ endpointUsed?: string | undefined;
275
+ /** thinking budget used */
276
+ thinkingBudgetUsed?: number | undefined;
277
+ /** was fallback project used? */
278
+ isFallbackProject?: boolean;
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Error — production-ready with status context
283
+ // ---------------------------------------------------------------------------
284
+
285
+ export class SearchError extends Error {
286
+ public readonly status?: number | undefined;
287
+ public readonly endpoint?: string | undefined;
288
+ public readonly retryable: boolean;
289
+ public readonly code?: string | undefined;
290
+
291
+ constructor(
292
+ message: string,
293
+ opts: { status?: number; endpoint?: string; retryable?: boolean; code?: string; cause?: unknown } = {},
294
+ ) {
295
+ super(message);
296
+ this.name = "SearchError";
297
+ this.status = opts.status;
298
+ this.endpoint = opts.endpoint;
299
+ this.retryable = opts.retryable ?? false;
300
+ this.code = opts.code;
301
+ if (opts.cause) (this as any).cause = opts.cause;
302
+ }
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Observer helpers — third-person, coherent with fingerprint.ts / project.ts
307
+ // ---------------------------------------------------------------------------
308
+
309
+ // v2.1.16: the local normalizePlatform/normalizeArch copies were deleted —
310
+ // byte-identical (every branch) to the fingerprint.ts exports, which are now
311
+ // imported above (search is L4, fingerprint is L2 — downward import, allowed).
312
+
313
+ // getDynamicUserAgent is imported from constants.js (identical signature and
314
+ // outputs; the owner's trim placement differs but the fallback constant carries
315
+ // no whitespace, so results match for every input) and re-exported above.
316
+
317
+ // v2.1.16: the stale hardcoded pluginVersion literal (2.1.14) was replaced
318
+ // by the constants.js PLUGIN_VERSION import — the version-envelope owner — so
319
+ // the Client-Metadata header stops drifting behind the release version. The
320
+ // ANTIGRAVITY ideType/ideVersion/osVersion/arch fields stay local (the
321
+ // fingerprint.ts owner builder derives them differently).
322
+ function buildClientMetadata(version?: string): string {
323
+ const ver = version ?? ANTIGRAVITY_VERSION_FALLBACK;
324
+ const plat = normalizePlatform(osPlatform()).toUpperCase();
325
+ const archUpper = normalizeArch(osArch()).toUpperCase();
326
+ const parts = [
327
+ `ideType=ANTIGRAVITY`,
328
+ `platform=${plat}`,
329
+ `ideVersion=${ver}`,
330
+ `pluginVersion=${PLUGIN_VERSION}`,
331
+ `osVersion=${osRelease()}`,
332
+ `arch=${archUpper}`,
333
+ ];
334
+ return parts.join(",");
335
+ }
336
+
337
+ // getXGoogApiClient is imported from constants.js (byte-identical body:
338
+ // `antigravity/{ver} gl-node/{node} gax/4.9.0 grpc/1.14.0`).
339
+
340
+ // local variant: diverges from models (models.buildAntagravityHeaders takes an optional UA, omits X-Goog-Api-Client/Client-Metadata and strips FORBIDDEN_HEADERS; this variant requires the UA, emits both bypass headers and strips only x-goog-user-project)
341
+
342
+ /**
343
+ * Builds Antigravity header map for cloudcode-pa.
344
+ * Observer notes: AM only sends User-Agent on content requests, not X-Goog-Api-Client
345
+ * for onboardUser/loadCodeAssist, but for generateContent it sends both.
346
+ * Also strips x-goog-user-project to avoid 403 IAM (requirement).
347
+ *
348
+ * @param accessToken - bearer
349
+ * @param userAgent - UA string
350
+ * @param extra - extra headers
351
+ */
352
+
353
+ /**
354
+ * FNV-1a 32-bit — deterministic hashing for logs, not secrets.
355
+ */
356
+ // local variant: diverges from core (uses plain multiplication `(hash * 0x01000193) >>> 0` instead of Math.imul; floats lose low bits so results differ from core/models for most inputs)
357
+
358
+ function hashToken(token: string): string {
359
+ try {
360
+ return createHash("sha256").update(token).digest("hex").slice(0, 16);
361
+ } catch {
362
+ return fnv1a32(token).toString(16);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Jitter 0-80ms anti-rate-limit — observed in AM.
368
+ * Identical to constants.getJitter — imported from the frozen owner and
369
+ * re-exported above.
370
+ */
371
+
372
+ function sleep(ms: number): Promise<void> {
373
+ return new Promise((r) => setTimeout(r, ms));
374
+ }
375
+
376
+ /**
377
+ * Checks if HTTP status is retryable for endpoint cascade.
378
+ * Observer: 403 project IAM mismatch, 404 rollout, 5xx instability → try next endpoint.
379
+ */
380
+ // local variant: diverges from models (models.isRetryableStatus also treats 408 as retryable; this cascade variant does not)
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Proxy support — only node:* builtins + fetch
384
+ // ---------------------------------------------------------------------------
385
+
386
+ export interface ProxyResolution {
387
+ /** final proxy url to use, null if disabled */
388
+ proxyUrl: string | null;
389
+ /** reason / source for debugging */
390
+ source: "option" | "env_https_proxy" | "env_http_proxy" | "env_all_proxy" | "disabled" | "none" | "no_proxy_bypass";
391
+ }
392
+
393
+ /**
394
+ * Parses NO_PROXY env — comma separated list of host suffixes, IPs, CIDRs (simplified).
395
+ */
396
+ export function isNoProxyMatch(targetHost: string, noProxyRaw: string | undefined): boolean {
397
+ if (!noProxyRaw) return false;
398
+ const list = noProxyRaw
399
+ .split(",")
400
+ .map((s) => s.trim().toLowerCase())
401
+ .filter(Boolean);
402
+ if (list.length === 0) return false;
403
+ const host = targetHost.toLowerCase();
404
+ for (const pattern of list) {
405
+ if (pattern === "*" || pattern === "*.*") return true;
406
+ if (pattern === host) return true;
407
+ if (pattern.startsWith(".")) {
408
+ // .example.com matches foo.example.com
409
+ if (host.endsWith(pattern)) return true;
410
+ }
411
+ if (host.endsWith(`.${pattern}`)) return true;
412
+ if (pattern.startsWith("*.")) {
413
+ const suffix = pattern.slice(2);
414
+ if (host === suffix || host.endsWith(`.${suffix}`)) return true;
415
+ }
416
+ }
417
+ return false;
418
+ }
419
+
420
+ /**
421
+ * Resolves proxy URL respecting options.proxyUrl, env vars, and NO_PROXY.
422
+ * Observer scans env case-insensitively as per curl conventions.
423
+ *
424
+ * @param optionsProxy - explicit option, string or null (null disables)
425
+ * @param targetUrl - target to check NO_PROXY against
426
+ */
427
+ export function resolveProxyUrl(optionsProxy: string | null | undefined, targetUrl?: string): ProxyResolution {
428
+ // explicit null disables proxy even if env present
429
+ if (optionsProxy === null) {
430
+ return { proxyUrl: null, source: "disabled" };
431
+ }
432
+ if (typeof optionsProxy === "string" && optionsProxy.trim().length > 0) {
433
+ return { proxyUrl: optionsProxy.trim(), source: "option" };
434
+ }
435
+
436
+ // env var resolution — order HTTPS_PROXY > HTTP_PROXY > ALL_PROXY
437
+ const envHttps = process.env.HTTPS_PROXY || process.env.https_proxy;
438
+ const envHttp = process.env.HTTP_PROXY || process.env.http_proxy;
439
+ const envAll = process.env.ALL_PROXY || process.env.all_proxy;
440
+ const noProxyRaw = process.env.NO_PROXY || process.env.no_proxy;
441
+
442
+ if (targetUrl) {
443
+ try {
444
+ const u = new URL(targetUrl);
445
+ if (isNoProxyMatch(u.hostname, noProxyRaw)) {
446
+ return { proxyUrl: null, source: "no_proxy_bypass" };
447
+ }
448
+ } catch {
449
+ // ignore malformed target
450
+ }
451
+ }
452
+
453
+ if (envHttps && envHttps.trim()) {
454
+ return { proxyUrl: envHttps.trim(), source: "env_https_proxy" };
455
+ }
456
+ if (envHttp && envHttp.trim()) {
457
+ return { proxyUrl: envHttp.trim(), source: "env_http_proxy" };
458
+ }
459
+ if (envAll && envAll.trim()) {
460
+ return { proxyUrl: envAll.trim(), source: "env_all_proxy" };
461
+ }
462
+ return { proxyUrl: null, source: "none" };
463
+ }
464
+
465
+ /**
466
+ * Builds Proxy-Authorization header if proxy URL contains credentials.
467
+ */
468
+ function getProxyAuthHeader(proxyUrl: URL): string | null {
469
+ if (proxyUrl.username) {
470
+ try {
471
+ const user = decodeURIComponent(proxyUrl.username);
472
+ const pass = decodeURIComponent(proxyUrl.password);
473
+ const token = Buffer.from(`${user}:${pass}`).toString("base64");
474
+ return `Basic ${token}`;
475
+ } catch {
476
+ return null;
477
+ }
478
+ }
479
+ return null;
480
+ }
481
+
482
+ /**
483
+ * Decodes chunked transfer-encoding.
484
+ * Observer implements minimal but spec-compliant decoder for proxy HTTPS path.
485
+ */
486
+ function decodeChunked(body: string): string {
487
+ let out = "";
488
+ let i = 0;
489
+ const len = body.length;
490
+ while (i < len) {
491
+ const crlf = body.indexOf("\r\n", i);
492
+ if (crlf === -1) break;
493
+ const line = body.slice(i, crlf).trim();
494
+ if (!line) {
495
+ i = crlf + 2;
496
+ continue;
497
+ }
498
+ // chunk size may have extension ; — ignore
499
+ const semi = line.indexOf(";");
500
+ const sizeHex = semi === -1 ? line : line.slice(0, semi);
501
+ const size = parseInt(sizeHex, 16);
502
+ if (Number.isNaN(size)) break;
503
+ if (size === 0) break; // last chunk
504
+ i = crlf + 2;
505
+ if (i + size > len) {
506
+ // incomplete — take what we have
507
+ out += body.slice(i);
508
+ break;
509
+ }
510
+ out += body.slice(i, i + size);
511
+ i += size;
512
+ // skip trailing CRLF after chunk
513
+ if (body.slice(i, i + 2) === "\r\n") i += 2;
514
+ }
515
+ return out;
516
+ }
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // Low-level proxy fetch implementations — only node:net/tls/http/https
520
+ // ---------------------------------------------------------------------------
521
+
522
+ interface RawProxyResult {
523
+ status: number;
524
+ statusText: string;
525
+ headers: Record<string, string>;
526
+ bodyText: string;
527
+ }
528
+
529
+ interface ProxyFetchResponse {
530
+ ok: boolean;
531
+ status: number;
532
+ statusText: string;
533
+ headers: { get: (name: string) => string | null; [k: string]: any };
534
+ text: () => Promise<string>;
535
+ json: () => Promise<any>;
536
+ arrayBuffer?: () => Promise<ArrayBuffer>;
537
+ }
538
+
539
+ /**
540
+ * Creates a fetch-like Response wrapper from raw proxy result.
541
+ */
542
+ function wrapRawResult(raw: RawProxyResult): ProxyFetchResponse {
543
+ const lowerHeaders: Record<string, string> = {};
544
+ for (const [k, v] of Object.entries(raw.headers)) lowerHeaders[k.toLowerCase()] = v;
545
+
546
+ return {
547
+ ok: raw.status >= 200 && raw.status < 300,
548
+ status: raw.status,
549
+ statusText: raw.statusText,
550
+ headers: {
551
+ get: (name: string) => {
552
+ const v = lowerHeaders[name.toLowerCase()];
553
+ return v ?? null;
554
+ },
555
+ ...raw.headers,
556
+ },
557
+ text: async () => raw.bodyText,
558
+ json: async () => {
559
+ try {
560
+ return JSON.parse(raw.bodyText);
561
+ } catch (e: any) {
562
+ throw new SearchError(
563
+ `proxy response not JSON: ${e?.message ?? String(e)} — body: ${raw.bodyText.slice(0, 500)}`,
564
+ {
565
+ status: raw.status,
566
+ },
567
+ );
568
+ }
569
+ },
570
+ };
571
+ }
572
+
573
+ /**
574
+ * HTTP target via HTTP proxy — absolute URL in request line.
575
+ * Observer uses node:http.request which already handles chunked.
576
+ *
577
+ * @param targetUrl - full target url
578
+ * @param proxyUrl - proxy url object
579
+ * @param init - fetch init
580
+ * @param timeoutMs - timeout
581
+ * @param signal - abort signal
582
+ */
583
+ function httpRequestViaHttpProxy(
584
+ targetUrl: URL,
585
+ proxyUrl: URL,
586
+ init: RequestInit,
587
+ timeoutMs: number,
588
+ signal?: AbortSignal,
589
+ ): Promise<RawProxyResult> {
590
+ return new Promise<RawProxyResult>((resolve, reject) => {
591
+ const method = init.method ?? "POST";
592
+ const bodyStr = typeof init.body === "string" ? init.body : init.body ? (init.body as any).toString() : undefined;
593
+
594
+ const proxyAuth = getProxyAuthHeader(proxyUrl);
595
+ const headers: Record<string, string> = { ...(init.headers as Record<string, string> | undefined) };
596
+
597
+ // Ensure proxy-required headers
598
+ if (!headers["Host"]) headers["Host"] = targetUrl.host;
599
+ if (proxyAuth) headers["Proxy-Authorization"] = proxyAuth;
600
+ if (!headers["Connection"]) headers["Connection"] = "close";
601
+
602
+ const reqOpts: http.RequestOptions = {
603
+ host: proxyUrl.hostname,
604
+ port: proxyUrl.port ? parseInt(proxyUrl.port, 10) : 80,
605
+ method,
606
+ path: targetUrl.toString(), // absolute URI for HTTP proxy
607
+ headers,
608
+ };
609
+
610
+ let finished = false;
611
+ const cleanup = () => {
612
+ finished = true;
613
+ };
614
+
615
+ const req = http.request(reqOpts, (res) => {
616
+ const chunks: Buffer[] = [];
617
+ res.on("data", (c: Buffer) => chunks.push(c));
618
+ res.on("end", () => {
619
+ if (finished) return;
620
+ cleanup();
621
+ const bodyBuf = Buffer.concat(chunks);
622
+ const bodyText = bodyBuf.toString("utf8");
623
+ const hdrs: Record<string, string> = {};
624
+ for (const [k, v] of Object.entries(res.headers)) {
625
+ if (Array.isArray(v)) hdrs[k.toLowerCase()] = v.join(", ");
626
+ else if (v) hdrs[k.toLowerCase()] = String(v);
627
+ }
628
+ resolve({
629
+ status: res.statusCode ?? 0,
630
+ statusText: res.statusMessage ?? "",
631
+ headers: hdrs,
632
+ bodyText,
633
+ });
634
+ });
635
+ });
636
+
637
+ req.on("error", (err) => {
638
+ if (finished) return;
639
+ cleanup();
640
+ reject(err);
641
+ });
642
+
643
+ if (timeoutMs) {
644
+ req.setTimeout(timeoutMs, () => {
645
+ req.destroy(new Error(`proxy http request timeout after ${timeoutMs}ms to ${targetUrl.hostname}`));
646
+ });
647
+ }
648
+
649
+ if (signal) {
650
+ if (signal.aborted) {
651
+ req.destroy(new Error("aborted"));
652
+ return;
653
+ }
654
+ const onAbort = () => {
655
+ req.destroy(new Error("aborted by signal"));
656
+ };
657
+ signal.addEventListener("abort", onAbort, { once: true });
658
+ req.on("close", () => signal.removeEventListener("abort", onAbort));
659
+ }
660
+
661
+ if (bodyStr) req.write(bodyStr);
662
+ req.end();
663
+ });
664
+ }
665
+
666
+ /**
667
+ * HTTPS target via HTTP proxy using CONNECT tunneling + TLS.
668
+ * Only node:net + node:tls builtins — observer builds tunnel manually.
669
+ *
670
+ * Flow:
671
+ * 1. TCP connect to proxy
672
+ * 2. Send CONNECT host:443
673
+ * 3. Wait for 200
674
+ * 4. TLS over socket
675
+ * 5. Send HTTPS request manually
676
+ *
677
+ * @param targetUrl - https url
678
+ * @param proxyUrl - http proxy url
679
+ * @param init - fetch init
680
+ * @param timeoutMs - timeout
681
+ * @param signal - abort signal
682
+ */
683
+ function httpsRequestViaHttpProxyConnect(
684
+ targetUrl: URL,
685
+ proxyUrl: URL,
686
+ init: RequestInit,
687
+ timeoutMs: number,
688
+ signal?: AbortSignal,
689
+ ): Promise<RawProxyResult> {
690
+ return new Promise<RawProxyResult>((resolve, reject) => {
691
+ const targetHost = targetUrl.hostname;
692
+ const targetPort = targetUrl.port ? parseInt(targetUrl.port, 10) : 443;
693
+ const proxyHost = proxyUrl.hostname;
694
+ const proxyPort = proxyUrl.port ? parseInt(proxyUrl.port, 10) : 8080;
695
+
696
+ let settled = false;
697
+ const doReject = (err: any) => {
698
+ if (settled) return;
699
+ settled = true;
700
+ reject(err);
701
+ };
702
+ const doResolve = (v: RawProxyResult) => {
703
+ if (settled) return;
704
+ settled = true;
705
+ resolve(v);
706
+ };
707
+
708
+ const socket = net.createConnection({ host: proxyHost, port: proxyPort });
709
+
710
+ if (signal?.aborted) {
711
+ socket.destroy(new Error("aborted before connect"));
712
+ return;
713
+ }
714
+
715
+ const abortHandler = () => {
716
+ socket.destroy(new Error("aborted by signal"));
717
+ };
718
+ signal?.addEventListener("abort", abortHandler, { once: true });
719
+
720
+ let timeoutId: NodeJS.Timeout | null = null;
721
+ if (timeoutMs) {
722
+ timeoutId = setTimeout(() => {
723
+ socket.destroy(new Error(`proxy CONNECT timeout ${timeoutMs}ms`));
724
+ }, timeoutMs);
725
+ }
726
+
727
+ socket.on("error", (err) => {
728
+ if (timeoutId) clearTimeout(timeoutId);
729
+ signal?.removeEventListener("abort", abortHandler);
730
+ doReject(err);
731
+ });
732
+
733
+ socket.on("connect", () => {
734
+ const proxyAuth = getProxyAuthHeader(proxyUrl);
735
+ const authLine = proxyAuth ? `Proxy-Authorization: ${proxyAuth}\r\n` : "";
736
+ const connectReq =
737
+ `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\n` +
738
+ `Host: ${targetHost}:${targetPort}\r\n` +
739
+ `${authLine}` +
740
+ `Proxy-Connection: Keep-Alive\r\n` +
741
+ `Connection: Keep-Alive\r\n\r\n`;
742
+ socket.write(connectReq);
743
+ });
744
+
745
+ let connectBuf = "";
746
+ const onConnectData = (chunk: Buffer) => {
747
+ connectBuf += chunk.toString("utf8");
748
+ if (!connectBuf.includes("\r\n\r\n")) return; // wait full header
749
+
750
+ socket.off("data", onConnectData);
751
+
752
+ const headerEnd = connectBuf.indexOf("\r\n\r\n");
753
+ const headerPart = connectBuf.slice(0, headerEnd);
754
+ const lines = headerPart.split("\r\n");
755
+ const statusLine = lines[0] ?? "";
756
+ const m = statusLine.match(/HTTP\/\d\.\d\s+(\d+)/);
757
+ const status = m ? parseInt(m[1]!, 10) : 0;
758
+
759
+ if (status !== 200) {
760
+ if (timeoutId) clearTimeout(timeoutId);
761
+ signal?.removeEventListener("abort", abortHandler);
762
+ socket.destroy();
763
+ doReject(new SearchError(`proxy CONNECT failed: ${statusLine}`, { status, retryable: status >= 500 }));
764
+ return;
765
+ }
766
+
767
+ // Tunnel established — leftover after \r\n\r\n may be start of TLS handshake? Actually not, it should be empty.
768
+ // Now TLS over socket
769
+ const tlsSocket = tls.connect({
770
+ socket: socket as any,
771
+ servername: targetHost,
772
+ ALPNProtocols: ["http/1.1"],
773
+ } as any);
774
+
775
+ tlsSocket.on("error", (err) => {
776
+ if (timeoutId) clearTimeout(timeoutId);
777
+ signal?.removeEventListener("abort", abortHandler);
778
+ doReject(err);
779
+ });
780
+
781
+ if (timeoutMs) {
782
+ tlsSocket.setTimeout(timeoutMs, () => {
783
+ tlsSocket.destroy(new Error(`TLS request timeout ${timeoutMs}ms`));
784
+ });
785
+ }
786
+
787
+ tlsSocket.on("secureConnect", () => {
788
+ const method = init.method ?? "POST";
789
+ const path = `${targetUrl.pathname}${targetUrl.search}`;
790
+ const initHeaders = (init.headers as Record<string, string>) ?? {};
791
+ const headers: Record<string, string> = { ...initHeaders };
792
+
793
+ if (!headers["Host"]) headers["Host"] = targetUrl.host;
794
+ if (!headers["Connection"]) headers["Connection"] = "close";
795
+
796
+ const bodyStr =
797
+ typeof init.body === "string"
798
+ ? init.body
799
+ : init.body
800
+ ? Buffer.isBuffer(init.body)
801
+ ? (init.body as Buffer).toString("utf8")
802
+ : String(init.body)
803
+ : "";
804
+
805
+ if (bodyStr && !headers["Content-Length"]) {
806
+ headers["Content-Length"] = Buffer.byteLength(bodyStr).toString();
807
+ }
808
+
809
+ let reqStr = `${method} ${path} HTTP/1.1\r\n`;
810
+ for (const [k, v] of Object.entries(headers)) {
811
+ reqStr += `${k}: ${v}\r\n`;
812
+ }
813
+ reqStr += `\r\n`;
814
+ if (bodyStr) reqStr += bodyStr;
815
+
816
+ tlsSocket.write(reqStr);
817
+ });
818
+
819
+ let respBuf = Buffer.alloc(0);
820
+ tlsSocket.on("data", (d: Buffer) => {
821
+ respBuf = Buffer.concat([respBuf, d]);
822
+ });
823
+
824
+ tlsSocket.on("end", () => {
825
+ if (timeoutId) clearTimeout(timeoutId);
826
+ signal?.removeEventListener("abort", abortHandler);
827
+ try {
828
+ const respStr = respBuf.toString("utf8");
829
+ const hdrEnd = respStr.indexOf("\r\n\r\n");
830
+ if (hdrEnd === -1) {
831
+ doReject(new SearchError(`invalid HTTPS response via proxy tunnel from ${targetHost}`));
832
+ return;
833
+ }
834
+ const hdrPart = respStr.slice(0, hdrEnd);
835
+ let bodyPart = respStr.slice(hdrEnd + 4);
836
+ const hdrLines = hdrPart.split("\r\n");
837
+ const statusLine = hdrLines[0] ?? "";
838
+ const sm = statusLine.match(/HTTP\/\d\.\d\s+(\d+)\s*(.*)/);
839
+ const statusCode = sm ? parseInt(sm[1]!, 10) : 0;
840
+ const statusText = sm ? (sm[2] ?? "") : "";
841
+
842
+ const hdrs: Record<string, string> = {};
843
+ for (let i = 1; i < hdrLines.length; i++) {
844
+ const line = hdrLines[i]!;
845
+ const idx = line.indexOf(":");
846
+ if (idx === -1) continue;
847
+ const k = line.slice(0, idx).trim().toLowerCase();
848
+ const v = line.slice(idx + 1).trim();
849
+ hdrs[k] = v;
850
+ }
851
+
852
+ // Handle chunked
853
+ if (hdrs["transfer-encoding"]?.toLowerCase() === "chunked") {
854
+ bodyPart = decodeChunked(bodyPart);
855
+ }
856
+
857
+ doResolve({
858
+ status: statusCode,
859
+ statusText,
860
+ headers: hdrs,
861
+ bodyText: bodyPart,
862
+ });
863
+ } catch (e: any) {
864
+ doReject(new SearchError(`failed parsing HTTPS proxy response: ${e?.message ?? String(e)}`, { cause: e }));
865
+ } finally {
866
+ try {
867
+ tlsSocket.destroy();
868
+ } catch {
869
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
870
+ }
871
+ }
872
+ });
873
+ };
874
+
875
+ socket.on("data", onConnectData);
876
+ });
877
+ }
878
+
879
+ /**
880
+ * Generic proxy-aware fetch — observer tries native fetch if no proxy,
881
+ * otherwise manual CONNECT for https or absolute-URL for http.
882
+ *
883
+ * @param url - target
884
+ * @param init - fetch init
885
+ * @param proxyUrlStr - resolved proxy url string or null
886
+ * @param timeoutMs - timeout
887
+ */
888
+ async function fetchWithProxy(
889
+ url: string,
890
+ init: RequestInit,
891
+ proxyUrlStr: string | null,
892
+ timeoutMs: number,
893
+ ): Promise<{
894
+ ok: boolean;
895
+ status: number;
896
+ statusText?: string;
897
+ headers: any;
898
+ text: () => Promise<string>;
899
+ json: () => Promise<any>;
900
+ }> {
901
+ if (!proxyUrlStr) {
902
+ // native path — with timeout + signal merge
903
+ const controller = new AbortController();
904
+ const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms ${url}`)), timeoutMs);
905
+
906
+ // merge external signal if present
907
+ const externalSignal = init.signal as AbortSignal | undefined;
908
+ let onExternalAbort: (() => void) | null = null;
909
+ if (externalSignal) {
910
+ if (externalSignal.aborted) {
911
+ clearTimeout(timer);
912
+ controller.abort(externalSignal.reason);
913
+ } else {
914
+ onExternalAbort = () => controller.abort(externalSignal.reason);
915
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
916
+ }
917
+ }
918
+
919
+ try {
920
+ const res = await fetch(url, { ...init, signal: controller.signal });
921
+ return res as any;
922
+ } finally {
923
+ clearTimeout(timer);
924
+ if (onExternalAbort && externalSignal) {
925
+ externalSignal.removeEventListener("abort", onExternalAbort);
926
+ }
927
+ }
928
+ }
929
+
930
+ // proxy path
931
+ let proxyUrl: URL;
932
+ let targetUrl: URL;
933
+ try {
934
+ proxyUrl = new URL(proxyUrlStr);
935
+ } catch {
936
+ throw new SearchError(`invalid proxyUrl: ${proxyUrlStr}`);
937
+ }
938
+ try {
939
+ targetUrl = new URL(url);
940
+ } catch {
941
+ throw new SearchError(`invalid target url: ${url}`);
942
+ }
943
+
944
+ const method = init.method ?? "POST";
945
+
946
+ const proxyResult =
947
+ targetUrl.protocol === "https:"
948
+ ? await httpsRequestViaHttpProxyConnect(
949
+ targetUrl,
950
+ proxyUrl,
951
+ { ...init, method },
952
+ timeoutMs,
953
+ init.signal as AbortSignal | undefined,
954
+ )
955
+ : await httpRequestViaHttpProxy(
956
+ targetUrl,
957
+ proxyUrl,
958
+ { ...init, method },
959
+ timeoutMs,
960
+ init.signal as AbortSignal | undefined,
961
+ );
962
+
963
+ return wrapRawResult(proxyResult) as any;
964
+ }
965
+
966
+ // ---------------------------------------------------------------------------
967
+ // Fetch with timeout helper — mirrors project.ts fetchWithTimeout
968
+ // ---------------------------------------------------------------------------
969
+
970
+ /**
971
+ * Fetch with timeout using AbortController.
972
+ * Observer pattern: never leaks abort timer.
973
+ *
974
+ * @param url - url
975
+ * @param init - init
976
+ * @param timeoutMs - timeout
977
+ * @param proxyUrl - optional proxy
978
+ */
979
+ // local variant: diverges from models (models.fetchWithTimeout takes `init.timeoutMs` and aborts a plain fetch; this variant takes an explicit timeoutMs plus proxyUrl and routes through fetchWithProxy)
980
+ export async function fetchWithProxyTimeout(
981
+ url: string,
982
+ init: RequestInit,
983
+ timeoutMs: number = FETCH_TIMEOUT_MS,
984
+ proxyUrl: string | null = null,
985
+ ): Promise<any> {
986
+ return fetchWithProxy(url, init, proxyUrl, timeoutMs);
987
+ }
988
+
989
+ // ---------------------------------------------------------------------------
990
+ // Thinking budget resolver — per RESEARCH.md 100+ terms
991
+ // ---------------------------------------------------------------------------
992
+
993
+ /**
994
+ * Resolves thinking budget from level or direct number.
995
+ * Observer clamps to Gemini max 32768.
996
+ *
997
+ * @param level - thinkingLevel minimal/low/medium/high
998
+ * @param directBudget - optional direct thinkingBudget
999
+ * @returns clamped budget number
1000
+ */
1001
+ export function resolveThinkingBudget(level?: ThinkingLevel, directBudget?: number): number {
1002
+ if (typeof directBudget === "number" && Number.isFinite(directBudget)) {
1003
+ const clamped = Math.max(0, Math.min(directBudget, THINKING_BUDGET_MAX_GEMINI));
1004
+ return Math.floor(clamped);
1005
+ }
1006
+ if (level && (THINKING_LEVELS as readonly string[]).includes(level)) {
1007
+ return THINKING_BUDGET_MAP[level] ?? THINKING_BUDGET_MAP.medium;
1008
+ }
1009
+ // default medium per governance
1010
+ return THINKING_BUDGET_MAP.medium;
1011
+ }
1012
+
1013
+ // ---------------------------------------------------------------------------
1014
+ // Model helpers — 2026 awareness
1015
+ // ---------------------------------------------------------------------------
1016
+
1017
+ /**
1018
+ * Normalizes model name — strips models/ prefix for body, keeps original for logging.
1019
+ * Observer notes cloudcode-pa accepts both gemini-3-flash and models/gemini-3-flash.
1020
+ *
1021
+ * @param raw - model id
1022
+ */
1023
+ export function normalizeModelName(raw: string): { bare: string; withPrefix: string; original: string } {
1024
+ const original = String(raw ?? "").trim();
1025
+ const bare = original.replace(/^models\//i, "").trim();
1026
+ const withPrefix = `models/${bare}`;
1027
+ return { bare, withPrefix, original };
1028
+ }
1029
+
1030
+ /**
1031
+ * Builds request body for cloudcode-pa v1internal:generateContent.
1032
+ * Observer has seen two shapes in the wild; prefers wrapped shape when projectId present.
1033
+ *
1034
+ * Shape A (Antigravity internal with project):
1035
+ * { model, project, request: { contents, tools, generationConfig, systemInstruction } }
1036
+ *
1037
+ * Shape B (flat Gemini SA):
1038
+ * { model, contents, tools, generationConfig, systemInstruction }
1039
+ *
1040
+ * @param model - bare model
1041
+ * @param projectId - optional project
1042
+ * @param contents - Gemini contents array
1043
+ * @param tools - googleSearch + urlContext tools
1044
+ * @param generationConfig - thinking + temp
1045
+ * @param systemInstruction - optional system instruction
1046
+ */
1047
+ function buildGenerateContentBody(opts: {
1048
+ model: string;
1049
+ projectId?: string | undefined;
1050
+ contents: any[];
1051
+ tools: any[];
1052
+ generationConfig: any;
1053
+ systemInstruction?: any | undefined;
1054
+ }): any {
1055
+ const { model, projectId, contents, tools, generationConfig, systemInstruction } = opts;
1056
+
1057
+ if (projectId) {
1058
+ // wrapped — observed in AM content traffic
1059
+ return {
1060
+ model,
1061
+ project: projectId,
1062
+ request: {
1063
+ contents,
1064
+ tools,
1065
+ generationConfig,
1066
+ ...(systemInstruction ? { systemInstruction } : {}),
1067
+ },
1068
+ };
1069
+ }
1070
+ // flat
1071
+ return {
1072
+ model,
1073
+ contents,
1074
+ tools,
1075
+ generationConfig,
1076
+ ...(systemInstruction ? { systemInstruction } : {}),
1077
+ };
1078
+ }
1079
+
1080
+ /**
1081
+ * Builds alternative body with models/ prefix if primary fails with 404/400.
1082
+ * Fallback chain: bare → withPrefix → wrapped with other model variant.
1083
+ */
1084
+ function buildAlternativeBodies(
1085
+ primaryBody: any,
1086
+ normalized: ReturnType<typeof normalizeModelName>,
1087
+ projectId?: string,
1088
+ ): any[] {
1089
+ const bodies: any[] = [];
1090
+ const { bare, withPrefix } = normalized;
1091
+
1092
+ // 1. try bare flat if primary was wrapped
1093
+ if (primaryBody.project) {
1094
+ bodies.push({
1095
+ model: bare,
1096
+ contents: primaryBody.request.contents,
1097
+ tools: primaryBody.request.tools,
1098
+ generationConfig: primaryBody.request.generationConfig,
1099
+ ...(primaryBody.request.systemInstruction ? { systemInstruction: primaryBody.request.systemInstruction } : {}),
1100
+ });
1101
+ }
1102
+ // 2. try withPrefix
1103
+ bodies.push({
1104
+ ...primaryBody,
1105
+ model: withPrefix,
1106
+ });
1107
+ if (primaryBody.request) {
1108
+ bodies.push({
1109
+ model: withPrefix,
1110
+ project: projectId,
1111
+ request: { ...primaryBody.request },
1112
+ });
1113
+ bodies.push({
1114
+ model: withPrefix,
1115
+ contents: primaryBody.request.contents,
1116
+ tools: primaryBody.request.tools,
1117
+ generationConfig: primaryBody.request.generationConfig,
1118
+ ...(primaryBody.request.systemInstruction ? { systemInstruction: primaryBody.request.systemInstruction } : {}),
1119
+ });
1120
+ } else {
1121
+ bodies.push({
1122
+ model: withPrefix,
1123
+ contents: primaryBody.contents,
1124
+ tools: primaryBody.tools,
1125
+ generationConfig: primaryBody.generationConfig,
1126
+ ...(primaryBody.systemInstruction ? { systemInstruction: primaryBody.systemInstruction } : {}),
1127
+ });
1128
+ }
1129
+ // 3. try fallback models from SEARCH_MODEL_FALLBACKS
1130
+ for (const fallbackModel of SEARCH_MODEL_FALLBACKS) {
1131
+ if (fallbackModel === bare || fallbackModel === withPrefix) continue;
1132
+ bodies.push({
1133
+ model: fallbackModel,
1134
+ project: projectId,
1135
+ request: primaryBody.request ?? {
1136
+ contents: primaryBody.contents,
1137
+ tools: primaryBody.tools,
1138
+ generationConfig: primaryBody.generationConfig,
1139
+ },
1140
+ });
1141
+ if (bodies.length > 8) break;
1142
+ }
1143
+ return bodies;
1144
+ }
1145
+
1146
+ // ---------------------------------------------------------------------------
1147
+ // Grounding parse — rigorous, handles heterogenous server shapes
1148
+ // ---------------------------------------------------------------------------
1149
+
1150
+ /**
1151
+ * Extracts text from Gemini candidate content parts.
1152
+ * Observer notes parts may be [{text}, {thought}, {inlineData}] — only text counted.
1153
+ *
1154
+ * @param candidate - candidate object
1155
+ */
1156
+ function extractTextFromCandidate(candidate: any): string {
1157
+ if (!candidate) return "";
1158
+ // direct text field (some internal shapes)
1159
+ if (typeof candidate.text === "string") return candidate.text;
1160
+ if (candidate.content?.parts && Array.isArray(candidate.content.parts)) {
1161
+ const texts: string[] = [];
1162
+ for (const part of candidate.content.parts) {
1163
+ if (part && typeof part.text === "string" && part.text.length > 0) {
1164
+ // thought parts are flagged think? but per spec includeThoughts=false we get only answer
1165
+ if (part.thought === true) continue;
1166
+ texts.push(part.text);
1167
+ }
1168
+ }
1169
+ if (texts.length > 0) return texts.join("\n");
1170
+ }
1171
+ // alternative: candidates[0].content.parts aggregation handled outside
1172
+ return "";
1173
+ }
1174
+
1175
+ /**
1176
+ * Parses groundingMetadata from various response shapes.
1177
+ * Observer is resilient: checks candidate.groundingMetadata, response.groundingMetadata,
1178
+ * and alternative field grounding_metadata.
1179
+ *
1180
+ * @param response - full JSON response
1181
+ * @param candidate - first candidate
1182
+ */
1183
+ function parseGroundingMetadataFromResponse(response: any, candidate: any): GroundingMetadata | undefined {
1184
+ if (candidate?.groundingMetadata) return candidate.groundingMetadata as GroundingMetadata;
1185
+ if (candidate?.grounding_metadata) return candidate.grounding_metadata as GroundingMetadata;
1186
+ if (response?.groundingMetadata) return response.groundingMetadata as GroundingMetadata;
1187
+ if (response?.grounding_metadata) return response.grounding_metadata as GroundingMetadata;
1188
+ // sometimes in candidates[0].citationMetadata.grounding? not
1189
+ if (response?.candidates && Array.isArray(response.candidates) && response.candidates[0]?.groundingMetadata) {
1190
+ return response.candidates[0].groundingMetadata as GroundingMetadata;
1191
+ }
1192
+ return undefined;
1193
+ }
1194
+
1195
+ /**
1196
+ * Normalizes groundingChunks into WebGroundingChunk[] with uri/title/domain.
1197
+ * Observer filters out chunks without uri, dedupes by uri preservation order.
1198
+ *
1199
+ * @param meta - groundingMetadata
1200
+ */
1201
+ function normalizeGroundingChunks(meta: GroundingMetadata | undefined): WebGroundingChunk[] {
1202
+ if (!meta?.groundingChunks || !Array.isArray(meta.groundingChunks)) return [];
1203
+ const seen = new Set<string>();
1204
+ const out: WebGroundingChunk[] = [];
1205
+ for (const ch of meta.groundingChunks) {
1206
+ const web = (ch as any).web ?? (ch as any).retrievedContext ?? {};
1207
+ const rawUri = web.uri ?? (ch as any).uri ?? "";
1208
+ const rawTitle = web.title ?? (ch as any).title ?? "";
1209
+ if (!rawUri || typeof rawUri !== "string") continue;
1210
+ // normalize uri
1211
+ let uri: string;
1212
+ try {
1213
+ // basic validation — must be http(s)
1214
+ const u = new URL(rawUri);
1215
+ if (!u.protocol.startsWith("http")) continue;
1216
+ uri = u.toString();
1217
+ } catch {
1218
+ continue;
1219
+ }
1220
+ if (seen.has(uri)) continue;
1221
+ seen.add(uri);
1222
+ let domain: string | undefined;
1223
+ try {
1224
+ domain = new URL(uri).hostname;
1225
+ } catch {
1226
+ domain = undefined;
1227
+ }
1228
+ out.push({
1229
+ uri,
1230
+ title: String(rawTitle || domain || uri).slice(0, 500),
1231
+ domain,
1232
+ });
1233
+ }
1234
+ return out;
1235
+ }
1236
+
1237
+ /**
1238
+ * Extracts webSearchQueries from groundingMetadata.
1239
+ */
1240
+ function extractWebSearchQueries(meta: GroundingMetadata | undefined): string[] {
1241
+ if (!meta) return [];
1242
+ const queries = meta.webSearchQueries ?? meta.retrievalQueries ?? [];
1243
+ if (!Array.isArray(queries)) return [];
1244
+ return queries
1245
+ .filter((q) => typeof q === "string" && (q as string).trim().length > 0)
1246
+ .map((q) => String(q).trim())
1247
+ .slice(0, 20); // cap 20 queries per spec sanity
1248
+ }
1249
+
1250
+ // ---------------------------------------------------------------------------
1251
+ // Core — executeSearch
1252
+ // ---------------------------------------------------------------------------
1253
+
1254
+ /**
1255
+ * Executes grounded search using Gemini native googleSearch + urlContext tools.
1256
+ * Third-person observer pattern: imitates Antigravity's google_search tool wrapper
1257
+ * but calls cloudcode-pa.googleapis.com directly, no localhost proxy.
1258
+ *
1259
+ * The observer:
1260
+ * 1. Validates query + accessToken.
1261
+ * 2. Resolves proxy (env + option) with NO_PROXY bypass.
1262
+ * 3. Resolves thinking budget from level or direct number, clamped 32k.
1263
+ * 4. Builds contents + tools [{googleSearch:{}},{urlContext:{}}] + generationConfig.
1264
+ * 5. Builds Antigravity headers (Bearer, UA dynamic, Client-Metadata, Api-Client)
1265
+ * stripping x-goog-user-project per bypass spec.
1266
+ * 6. Cascades endpoints PROD → DAILY → SANDBOX on 403/404/5xx with jitter 0-80ms.
1267
+ * 7. Parses groundingMetadata.groundingChunks[].web.uri/title and webSearchQueries.
1268
+ *
1269
+ * @param input - query + optional systemPrompt
1270
+ * @param auth - OAuth access token + projectId + UA override
1271
+ * @param opts - model, thinking, proxy, timeout, endpoints, etc.
1272
+ * @returns grounded SearchResult with parsed chunks
1273
+ *
1274
+ * @throws SearchError on validation, timeout, 401, exhausted cascade, or invalid JSON.
1275
+ *
1276
+ * @example
1277
+ * ```ts
1278
+ * const result = await executeSearch(
1279
+ * { query: "latest Gemini 3 flash grounding changes 2026" },
1280
+ * { accessToken: "...", projectId: "rising-fact-p41fc" },
1281
+ * { thinkingLevel: "medium", useUrlContext: true }
1282
+ * );
1283
+ * console.log(result.text);
1284
+ * console.log(result.groundingChunks.map(c=>c.uri));
1285
+ * ```
1286
+ */
1287
+ export async function executeSearch(
1288
+ input: ExecuteSearchInput,
1289
+ auth: ExecuteSearchAuth,
1290
+ opts: ExecuteSearchOptions = {},
1291
+ ): Promise<SearchResult> {
1292
+ // -----------------------------------------------------------------------
1293
+ // Validation — production ready, no placeholder
1294
+ // -----------------------------------------------------------------------
1295
+ if (!input || typeof input.query !== "string" || input.query.trim().length === 0) {
1296
+ throw new SearchError("query must be a non-empty string", { code: "INVALID_QUERY" });
1297
+ }
1298
+ const query = input.query.trim();
1299
+ if (query.length > 20000) {
1300
+ throw new SearchError(`query too long (${query.length} > 20000 chars)`, { code: "QUERY_TOO_LONG" });
1301
+ }
1302
+ if (!auth || typeof auth.accessToken !== "string" || auth.accessToken.trim().length === 0) {
1303
+ throw new SearchError("accessToken required — missing auth.accessToken Bearer", { code: "MISSING_TOKEN" });
1304
+ }
1305
+ const accessToken = auth.accessToken.trim();
1306
+ // basic Bearer format check — JWT-ish or ya29.
1307
+ if (accessToken.length < 20) {
1308
+ throw new SearchError("accessToken appears too short to be valid", { code: "INVALID_TOKEN" });
1309
+ }
1310
+
1311
+ // -----------------------------------------------------------------------
1312
+ // Resolve model — SEARCH_MODEL gemini-3-flash per spec
1313
+ // -----------------------------------------------------------------------
1314
+ const rawModel = (opts.model ?? SEARCH_MODEL).trim() || SEARCH_MODEL;
1315
+ const normalizedModel = normalizeModelName(rawModel);
1316
+ const modelBare = normalizedModel.bare || SEARCH_MODEL;
1317
+
1318
+ // -----------------------------------------------------------------------
1319
+ // Thinking budget — level → budget map, clamped
1320
+ // -----------------------------------------------------------------------
1321
+ const thinkingBudget = resolveThinkingBudget(opts.thinkingLevel, opts.thinkingBudget);
1322
+
1323
+ // -----------------------------------------------------------------------
1324
+ // Tools — native googleSearch + urlContext per requirement
1325
+ // -----------------------------------------------------------------------
1326
+ const useGoogleSearch = opts.useGoogleSearch !== false; // default true
1327
+ const useUrlContext = opts.useUrlContext !== false; // default true
1328
+ const tools: any[] = [];
1329
+ if (useGoogleSearch) tools.push({ googleSearch: {} });
1330
+ if (useUrlContext) tools.push({ urlContext: {} });
1331
+ // fallback — at least one grounding tool required
1332
+ if (tools.length === 0) tools.push({ googleSearch: {} });
1333
+
1334
+ // -----------------------------------------------------------------------
1335
+ // Contents + system instruction
1336
+ // -----------------------------------------------------------------------
1337
+ const contents: any[] = [
1338
+ {
1339
+ role: "user",
1340
+ parts: [
1341
+ {
1342
+ text: input.additionalContext ? `${query}\n\nContext: ${input.additionalContext}` : query,
1343
+ },
1344
+ ],
1345
+ },
1346
+ ];
1347
+
1348
+ const systemInstruction = input.systemPrompt
1349
+ ? { parts: [{ text: String(input.systemPrompt).slice(0, 8000) }] }
1350
+ : undefined;
1351
+
1352
+ const generationConfig: any = {
1353
+ // Thinking — Gemini 3 supports thinkingConfig
1354
+ thinkingConfig: {
1355
+ thinkingBudget,
1356
+ includeThoughts: false,
1357
+ },
1358
+ temperature: typeof opts.temperature === "number" ? opts.temperature : 0.2,
1359
+ maxOutputTokens: typeof opts.maxOutputTokens === "number" ? opts.maxOutputTokens : 8192,
1360
+ };
1361
+
1362
+ const primaryBody = buildGenerateContentBody({
1363
+ model: modelBare,
1364
+ projectId: auth.projectId,
1365
+ contents,
1366
+ tools,
1367
+ generationConfig,
1368
+ systemInstruction,
1369
+ });
1370
+
1371
+ // -----------------------------------------------------------------------
1372
+ // Proxy resolution — supports HTTP_PROXY/HTTPS_PROXY/NO_PROXY
1373
+ // -----------------------------------------------------------------------
1374
+ // Use first endpoint as NO_PROXY check target — if endpoint in NO_PROXY list, bypass
1375
+ const firstEndpointForProxyCheck = (opts.endpoints?.[0] ?? DEFAULT_ENDPOINT_ORDER[0]) + GENERATE_CONTENT_ENDPOINT;
1376
+ const proxyResolution = resolveProxyUrl(opts.proxyUrl, firstEndpointForProxyCheck);
1377
+ const proxyUrlStr = proxyResolution.proxyUrl;
1378
+
1379
+ // -----------------------------------------------------------------------
1380
+ // UA + headers — observer builds dynamic UA antigravity/{ver} {os}/{arch}
1381
+ // -----------------------------------------------------------------------
1382
+ const version = auth.version ?? ANTIGRAVITY_VERSION_FALLBACK;
1383
+ const userAgent = auth.userAgent ?? getDynamicUserAgent(version);
1384
+ const extraHeaders: Record<string, string> = {};
1385
+ // allow projectId as header? No — strip per bypass. Project sent in body only.
1386
+
1387
+ const baseHeaders = buildAntagravityHeaders(accessToken, { userAgent, extra: extraHeaders });
1388
+
1389
+ // -----------------------------------------------------------------------
1390
+ // Endpoint cascade
1391
+ // -----------------------------------------------------------------------
1392
+ const endpoints = opts.endpoints && opts.endpoints.length > 0 ? opts.endpoints : [...DEFAULT_ENDPOINT_ORDER];
1393
+ const timeoutMs = typeof opts.timeoutMs === "number" && opts.timeoutMs > 0 ? opts.timeoutMs : SEARCH_TIMEOUT_MS;
1394
+ const jitterEnabled = opts.jitter !== false;
1395
+
1396
+ let lastError: SearchError | Error | null = null;
1397
+ let attemptedBodiesTried = 0;
1398
+
1399
+ // For debugging
1400
+ if (opts.debug) {
1401
+ // eslint-disable-next-line no-console
1402
+ console.debug(
1403
+ `[search] observer start queryLen=${query.length} model=${modelBare} budget=${thinkingBudget} proxy=${proxyResolution.source} tokenHash=${hashToken(accessToken)} endpoints=${endpoints.join(",")}`,
1404
+ );
1405
+ }
1406
+
1407
+ // Try each endpoint, with body fallback strategy
1408
+ for (let epIdx = 0; epIdx < endpoints.length; epIdx++) {
1409
+ const base = endpoints[epIdx]!.replace(/\/$/, "");
1410
+ const url = `${base}${GENERATE_CONTENT_ENDPOINT}`;
1411
+
1412
+ // Try primary body + alternative bodies if 400/404
1413
+ const bodiesToTry = [primaryBody, ...buildAlternativeBodies(primaryBody, normalizedModel, auth.projectId)];
1414
+ // deduplicate by JSON stringify quickly via model name (small optimization)
1415
+ const seenModels = new Set<string>();
1416
+ const dedupedBodies: any[] = [];
1417
+ for (const b of bodiesToTry) {
1418
+ const key = `${b.model}|${b.project ?? ""}|${b.request ? "wrapped" : "flat"}`;
1419
+ if (seenModels.has(key)) continue;
1420
+ seenModels.add(key);
1421
+ dedupedBodies.push(b);
1422
+ }
1423
+
1424
+ for (let bodyIdx = 0; bodyIdx < dedupedBodies.length; bodyIdx++) {
1425
+ const body = dedupedBodies[bodyIdx];
1426
+ attemptedBodiesTried++;
1427
+
1428
+ // To avoid infinite loops, limit total attempts
1429
+ if (opts.maxRetries && attemptedBodiesTried > (opts.maxRetries ?? 8) * endpoints.length) break;
1430
+
1431
+ const fetchHeaders = { ...baseHeaders };
1432
+
1433
+ const fetchInit: RequestInit = {
1434
+ method: "POST",
1435
+ headers: fetchHeaders as any,
1436
+ body: JSON.stringify(body),
1437
+ };
1438
+
1439
+ if (opts.debug) {
1440
+ // eslint-disable-next-line no-console
1441
+ console.debug(
1442
+ `[search] attempt ep=${base} bodyModel=${body.model} wrapped=${!!body.request} attempt=${attemptedBodiesTried}`,
1443
+ );
1444
+ }
1445
+
1446
+ let res: any;
1447
+ try {
1448
+ res = await fetchWithProxyTimeout(url, fetchInit, timeoutMs, proxyUrlStr);
1449
+ } catch (networkErr: any) {
1450
+ // network failure — retryable
1451
+ const err = new SearchError(`network failure to ${base}: ${networkErr?.message ?? String(networkErr)}`, {
1452
+ endpoint: base,
1453
+ retryable: true,
1454
+ code: "NETWORK_ERROR",
1455
+ cause: networkErr,
1456
+ });
1457
+ lastError = err;
1458
+ if (opts.debug) console.debug(`[search] network error ${err.message}`);
1459
+ // jitter before next endpoint/body
1460
+ if (jitterEnabled) await sleep(getJitter());
1461
+ continue; // next body / endpoint
1462
+ }
1463
+
1464
+ // HTTP status handling
1465
+ const status: number = res.status ?? 0;
1466
+ if (!res.ok) {
1467
+ let bodyText = "";
1468
+ try {
1469
+ bodyText = await res.text();
1470
+ } catch {
1471
+ bodyText = "";
1472
+ }
1473
+
1474
+ // Parse error for log trimming
1475
+ const truncated = bodyText.slice(0, 2000);
1476
+
1477
+ const retryable = isRetryableStatus(status);
1478
+ const code =
1479
+ status === 401
1480
+ ? "UNAUTHORIZED"
1481
+ : status === 403
1482
+ ? "FORBIDDEN"
1483
+ : status === 429
1484
+ ? "RATE_LIMITED"
1485
+ : status >= 500
1486
+ ? "SERVER_ERROR"
1487
+ : "HTTP_ERROR";
1488
+
1489
+ const err = new SearchError(`search failed ${status} at ${base}: ${truncated}`, {
1490
+ status,
1491
+ endpoint: base,
1492
+ retryable,
1493
+ code,
1494
+ });
1495
+
1496
+ lastError = err;
1497
+
1498
+ if (opts.debug) {
1499
+ // eslint-disable-next-line no-console
1500
+ console.debug(`[search] http ${status} retryable=${retryable} body=${truncated.slice(0, 500)}`);
1501
+ }
1502
+
1503
+ // 401 Unauthorized — no point retrying other endpoints with same token
1504
+ if (status === 401) {
1505
+ throw err;
1506
+ }
1507
+
1508
+ // 403/404/429/5xx — try next endpoint (cascade) or next body variant if 400
1509
+ if (status === 400 && bodyIdx === 0) {
1510
+ // bad request maybe due to model naming — try next body variant without switching endpoint
1511
+ if (jitterEnabled) await sleep(getJitter());
1512
+ continue;
1513
+ }
1514
+
1515
+ if (retryable) {
1516
+ if (jitterEnabled) await sleep(getJitter());
1517
+ // break inner body loop to try next endpoint
1518
+ break;
1519
+ }
1520
+
1521
+ // non-retryable 400 etc. — if first body, try alternative bodies
1522
+ if (status === 400 && bodyIdx < dedupedBodies.length - 1) {
1523
+ if (jitterEnabled) await sleep(getJitter());
1524
+ continue;
1525
+ }
1526
+
1527
+ // otherwise keep as lastError and try next endpoint
1528
+ if (jitterEnabled) await sleep(getJitter());
1529
+ break;
1530
+ }
1531
+
1532
+ // -----------------------------------------------------------------
1533
+ // Success path — parse JSON
1534
+ // -----------------------------------------------------------------
1535
+ let json: any;
1536
+ try {
1537
+ json = await res.json();
1538
+ } catch (parseErr: any) {
1539
+ let txt = "";
1540
+ try {
1541
+ txt = await res.text();
1542
+ } catch {
1543
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1544
+ }
1545
+ throw new SearchError(
1546
+ `failed parsing search JSON from ${base}: ${parseErr?.message ?? String(parseErr)} — text: ${txt.slice(0, 1000)}`,
1547
+ {
1548
+ status,
1549
+ endpoint: base,
1550
+ code: "INVALID_JSON",
1551
+ cause: parseErr,
1552
+ },
1553
+ );
1554
+ }
1555
+
1556
+ // Response shape may be array? Some internal endpoints wrap in []?
1557
+ // Normalize: if json is array, take first
1558
+ const responseObj = Array.isArray(json) ? json[0] : json;
1559
+
1560
+ // Candidates extraction — heterogenous shapes
1561
+ const candidates = responseObj?.candidates ?? responseObj?.response?.candidates ?? [];
1562
+ const firstCandidate = Array.isArray(candidates) && candidates.length > 0 ? candidates[0] : responseObj;
1563
+
1564
+ const text =
1565
+ extractTextFromCandidate(firstCandidate) ||
1566
+ (typeof responseObj?.text === "string" ? responseObj.text : "") ||
1567
+ (typeof responseObj?.output === "string" ? responseObj.output : "");
1568
+
1569
+ if (!text || text.trim().length === 0) {
1570
+ // Sometimes grounding only returns metadata without text when query is just URLs? Treat as empty but still return grounding
1571
+ if (opts.debug)
1572
+ console.debug(`[search] empty text but status ok, json keys=${Object.keys(responseObj).join(",")}`);
1573
+ }
1574
+
1575
+ const groundingMeta = parseGroundingMetadataFromResponse(responseObj, firstCandidate);
1576
+ const groundingChunks = normalizeGroundingChunks(groundingMeta);
1577
+ const webSearchQueries = extractWebSearchQueries(groundingMeta);
1578
+
1579
+ const result: SearchResult = {
1580
+ text: text ?? "",
1581
+ query,
1582
+ model: body.model ?? modelBare,
1583
+ grounded: groundingChunks.length > 0 || webSearchQueries.length > 0,
1584
+ webSearchQueries,
1585
+ groundingChunks,
1586
+ groundingSupports: groundingMeta?.groundingSupports,
1587
+ searchEntryPoint: groundingMeta?.searchEntryPoint,
1588
+ groundingMetadata: opts.includeGrounding !== false ? groundingMeta : undefined,
1589
+ raw: responseObj,
1590
+ endpointUsed: base,
1591
+ thinkingBudgetUsed: thinkingBudget,
1592
+ isFallbackProject: auth.projectId === "rising-fact-p41fc" || body.project === "rising-fact-p41fc",
1593
+ };
1594
+
1595
+ if (opts.debug) {
1596
+ // eslint-disable-next-line no-console
1597
+ console.debug(
1598
+ `[search] success endpoint=${base} grounded=${result.grounded} chunks=${result.groundingChunks.length} queries=${result.webSearchQueries.length} textLen=${result.text.length}`,
1599
+ );
1600
+ }
1601
+
1602
+ return result;
1603
+ } // end bodies loop
1604
+
1605
+ // after body loop for this endpoint, if we exhausted bodies and have retryable error, continue to next endpoint
1606
+ // jitter already applied
1607
+ } // end endpoints loop
1608
+
1609
+ // -----------------------------------------------------------------------
1610
+ // All endpoints failed — throw last error with context
1611
+ // -----------------------------------------------------------------------
1612
+ if (lastError) {
1613
+ if (lastError instanceof SearchError) throw lastError;
1614
+ throw new SearchError(
1615
+ `all search endpoints failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
1616
+ {
1617
+ code: "ALL_ENDPOINTS_FAILED",
1618
+ cause: lastError,
1619
+ },
1620
+ );
1621
+ }
1622
+ throw new SearchError(
1623
+ `all search endpoints failed without specific error — attempted ${endpoints.length} endpoints, ${attemptedBodiesTried} bodies`,
1624
+ {
1625
+ code: "ALL_ENDPOINTS_FAILED",
1626
+ },
1627
+ );
1628
+ }
1629
+
1630
+ // ---------------------------------------------------------------------------
1631
+ // Convenience wrappers — library-first root-first exports
1632
+ // ---------------------------------------------------------------------------
1633
+
1634
+ /**
1635
+ * Convenience: execute search with project fallback rising-fact-p41fc.
1636
+ * Observer notes this fallback works for Antigravity but 403 for Gemini CLI models.
1637
+ *
1638
+ * @param query - search query
1639
+ * @param accessToken - bearer token
1640
+ * @param opts - optional search options + projectId override
1641
+ */
1642
+ export async function searchWithFallback(
1643
+ query: string,
1644
+ accessToken: string,
1645
+ opts: ExecuteSearchOptions & { projectId?: string; systemPrompt?: string } = {},
1646
+ ): Promise<SearchResult> {
1647
+ const projectId = opts.projectId ?? "rising-fact-p41fc";
1648
+ return executeSearch({ query, systemPrompt: opts.systemPrompt }, { accessToken, projectId }, opts);
1649
+ }
1650
+
1651
+ /**
1652
+ * Extracts only grounding URIs from result — helper for TUI.
1653
+ *
1654
+ * @param result - SearchResult
1655
+ * @returns uri strings
1656
+ */
1657
+ export function extractGroundingUris(result: SearchResult): string[] {
1658
+ return result.groundingChunks.map((c) => c.uri).filter(Boolean);
1659
+ }
1660
+
1661
+ /**
1662
+ * Extracts grounding titles for UI display.
1663
+ */
1664
+ export function extractGroundingTitles(result: SearchResult): Array<{ uri: string; title: string }> {
1665
+ return result.groundingChunks.map((c) => ({ uri: c.uri, title: c.title }));
1666
+ }
1667
+
1668
+ // ---------------------------------------------------------------------------
1669
+ // Plugin helpers - search gating, grounding formatting, image generation
1670
+ // (ported from search(3).ts lineage)
1671
+ // ---------------------------------------------------------------------------
1672
+
1673
+ /**
1674
+ * Decides whether native googleSearch grounding should be enabled for a model
1675
+ * given plugin config (`google_search_enabled`: true | "auto" | falsy).
1676
+ */
1677
+ export const shouldEnableSearch = (model: string, cfg: any): boolean => {
1678
+ if (cfg?.google_search_enabled === true) return true;
1679
+ if (cfg?.google_search_enabled === "auto") {
1680
+ const l = model.toLowerCase();
1681
+ return l.includes("gemini") && !l.includes("image");
1682
+ }
1683
+ return false;
1684
+ };
1685
+
1686
+ /** Builds the native grounding tool list: googleSearch + urlContext. */
1687
+ export const buildSearchTools = (): any[] => [{ googleSearch: {} }, { urlContext: {} }];
1688
+
1689
+ /** Normalizes groundingMetadata chunks into {uri,title} pairs.
1690
+ * Identical to core.parseGrounding — imported from the frozen owner and
1691
+ * re-exported above. */
1692
+
1693
+ /** Formats grounding sources as markdown citation lines. */
1694
+ export const extractCitations = (grounding: any[]): string =>
1695
+ grounding.map((g) => `[${g.title || "source"}](${g.uri})`).join("\n");
1696
+
1697
+ /**
1698
+ * Saves a base64-encoded PNG under ~/.config/opencode/generated-images (or a
1699
+ * custom dir) using an timestamped random filename; returns the written path.
1700
+ */
1701
+ export const saveGeneratedImage = async (b64: string, dir?: string): Promise<string> => {
1702
+ const base = dir || pathJoin(os.homedir(), ".config", "opencode", "generated-images");
1703
+ try {
1704
+ mkdirSync(base, { recursive: true });
1705
+ } catch {
1706
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1707
+ }
1708
+ const name = `img-${Date.now()}-${Math.random().toString(16).slice(2)}.png`;
1709
+ const filePath = pathJoin(base, name);
1710
+ const buf = Buffer.from(b64, "base64");
1711
+ writeFileSync(filePath, buf);
1712
+ return filePath;
1713
+ };
1714
+
1715
+ /** Renders grounding sources as a markdown bullet list. */
1716
+ export const groundingToMarkdown = (grounding: any[]): string =>
1717
+ grounding.map((g) => `- [${g.title || g.uri}](${g.uri})`).join("\n");
1718
+
1719
+ // ---------------------------------------------------------------------------
1720
+ // Barrel default export — library-first
1721
+ // ---------------------------------------------------------------------------
1722
+
1723
+ export const Search = {
1724
+ SEARCH_MODEL,
1725
+ SEARCH_MODEL_PREVIEW,
1726
+ SEARCH_MODEL_FALLBACKS,
1727
+ CODE_ASSIST_ENDPOINTS,
1728
+ DEFAULT_ENDPOINT_ORDER,
1729
+ GENERATE_CONTENT_ENDPOINT,
1730
+ THINKING_BUDGET_MAP,
1731
+ THINKING_LEVELS,
1732
+ THINKING_BUDGET_MAX_GEMINI,
1733
+ ANTIGRAVITY_VERSION_FALLBACK,
1734
+ ANTIGRAVITY_USER_AGENT_FALLBACK,
1735
+ getDynamicUserAgent,
1736
+ buildAntagravityHeaders,
1737
+ fetchWithProxyTimeout,
1738
+ resolveProxyUrl,
1739
+ isNoProxyMatch,
1740
+ resolveThinkingBudget,
1741
+ normalizeModelName,
1742
+ executeSearch,
1743
+ searchWithFallback,
1744
+ extractGroundingUris,
1745
+ extractGroundingTitles,
1746
+ shouldEnableSearch,
1747
+ buildSearchTools,
1748
+ parseGrounding,
1749
+ extractCitations,
1750
+ isImageGenModel,
1751
+ buildImageGenConfig,
1752
+ saveGeneratedImage,
1753
+ groundingToMarkdown,
1754
+ SearchError,
1755
+ isRetryableStatus,
1756
+ getJitter,
1757
+ fnv1a32,
1758
+ };
1759
+
1760
+ export default Search;