@oh-my-pi/pi-coding-agent 17.2.3 → 17.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-c5hpqt9r.md → CHANGELOG-q2w43aw3.md} +92 -0
  3. package/dist/cli.js +3979 -3548
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/advisor/advise-tool.d.ts +6 -0
  6. package/dist/types/advisor/runtime.d.ts +4 -4
  7. package/dist/types/capability/mcp.d.ts +9 -0
  8. package/dist/types/cli/args.d.ts +4 -1
  9. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  10. package/dist/types/cli/command-help.d.ts +99 -0
  11. package/dist/types/cli/help-extra.d.ts +2 -0
  12. package/dist/types/cli/models-cli.d.ts +1 -1
  13. package/dist/types/cli/thinking-levels.d.ts +5 -0
  14. package/dist/types/cli/update-cli.d.ts +13 -0
  15. package/dist/types/commands/browser-relay.d.ts +38 -0
  16. package/dist/types/commands/commit.d.ts +3 -0
  17. package/dist/types/commands/complete.d.ts +1 -1
  18. package/dist/types/commands/completions.d.ts +6 -0
  19. package/dist/types/commands/grep.d.ts +3 -0
  20. package/dist/types/commands/join.d.ts +4 -0
  21. package/dist/types/commands/launch-help.d.ts +171 -0
  22. package/dist/types/commands/launch.d.ts +5 -1
  23. package/dist/types/commands/models.d.ts +3 -0
  24. package/dist/types/commands/say.d.ts +10 -0
  25. package/dist/types/commands/worktree.d.ts +3 -0
  26. package/dist/types/config/keybindings.d.ts +6 -1
  27. package/dist/types/config/model-discovery.d.ts +2 -2
  28. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  29. package/dist/types/config/models-config-schema.d.ts +2 -0
  30. package/dist/types/config/models-config.d.ts +1 -0
  31. package/dist/types/config/service-tier.d.ts +2 -0
  32. package/dist/types/config/settings-schema.d.ts +70 -22
  33. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  34. package/dist/types/cursor.d.ts +7 -4
  35. package/dist/types/discovery/helpers.d.ts +6 -0
  36. package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
  37. package/dist/types/edit/index.d.ts +3 -3
  38. package/dist/types/edit/modes/replace.d.ts +22 -15
  39. package/dist/types/edit/renderer.d.ts +1 -1
  40. package/dist/types/eval/py/kernel.d.ts +3 -1
  41. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  42. package/dist/types/extensibility/extensions/loader.d.ts +6 -2
  43. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  44. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  45. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  46. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
  47. package/dist/types/launch/broker.d.ts +1 -1
  48. package/dist/types/launch/client.d.ts +6 -3
  49. package/dist/types/launch/ensure.d.ts +8 -0
  50. package/dist/types/launch/paths.d.ts +3 -2
  51. package/dist/types/launch/protocol.d.ts +2 -2
  52. package/dist/types/lsp/client.d.ts +2 -0
  53. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  54. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  55. package/dist/types/lsp/mux/server.d.ts +18 -0
  56. package/dist/types/lsp/types.d.ts +22 -2
  57. package/dist/types/main.d.ts +2 -0
  58. package/dist/types/mcp/request-id.d.ts +19 -0
  59. package/dist/types/mcp/types.d.ts +14 -0
  60. package/dist/types/memory-backend/index.d.ts +1 -0
  61. package/dist/types/memory-backend/messages.d.ts +15 -0
  62. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  63. package/dist/types/mnemopi/state.d.ts +0 -23
  64. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  65. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  66. package/dist/types/modes/components/model-browser.d.ts +7 -5
  67. package/dist/types/modes/components/model-picker.d.ts +9 -3
  68. package/dist/types/modes/components/read-tool-group.d.ts +11 -0
  69. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  70. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  71. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  72. package/dist/types/modes/interactive-mode.d.ts +1 -0
  73. package/dist/types/modes/types.d.ts +2 -1
  74. package/dist/types/modes/workflow.d.ts +2 -1
  75. package/dist/types/registry/agent-lifecycle.d.ts +14 -5
  76. package/dist/types/registry/agent-registry.d.ts +4 -3
  77. package/dist/types/sdk.d.ts +4 -2
  78. package/dist/types/secrets/index.d.ts +7 -9
  79. package/dist/types/session/agent-session-types.d.ts +3 -1
  80. package/dist/types/session/agent-session.d.ts +6 -1
  81. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  82. package/dist/types/session/model-controls.d.ts +0 -1
  83. package/dist/types/session/session-advisors.d.ts +8 -1
  84. package/dist/types/session/session-context.d.ts +2 -1
  85. package/dist/types/session/session-maintenance.d.ts +2 -0
  86. package/dist/types/session/session-manager.d.ts +16 -5
  87. package/dist/types/session/session-stats.d.ts +10 -0
  88. package/dist/types/session/session-storage.d.ts +15 -3
  89. package/dist/types/system-prompt.d.ts +6 -1
  90. package/dist/types/task/index.d.ts +2 -1
  91. package/dist/types/task/spawn-policy.d.ts +5 -0
  92. package/dist/types/thinking.d.ts +1 -7
  93. package/dist/types/tools/ast-grep.d.ts +1 -1
  94. package/dist/types/tools/browser/registry.d.ts +2 -1
  95. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  96. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  97. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  98. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  99. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  100. package/dist/types/tools/browser/render.d.ts +1 -0
  101. package/dist/types/tools/browser.d.ts +3 -0
  102. package/dist/types/tools/computer/exposure.d.ts +9 -7
  103. package/dist/types/tools/computer/protocol.d.ts +70 -17
  104. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  105. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  106. package/dist/types/tools/computer/worker.d.ts +42 -7
  107. package/dist/types/tools/computer-renderer.d.ts +7 -5
  108. package/dist/types/tools/computer.d.ts +25 -44
  109. package/dist/types/tools/context.d.ts +3 -0
  110. package/dist/types/tools/default-renderer.d.ts +3 -0
  111. package/dist/types/tools/glob.d.ts +1 -1
  112. package/dist/types/tools/grep.d.ts +1 -1
  113. package/dist/types/tools/render-utils.d.ts +2 -0
  114. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  115. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  116. package/dist/types/tools/xdev.d.ts +8 -0
  117. package/dist/types/utils/clipboard.d.ts +2 -2
  118. package/dist/types/web/kagi.d.ts +1 -0
  119. package/dist/types/web/search/index.d.ts +1 -1
  120. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  121. package/dist/types/web/search/providers/base.d.ts +2 -0
  122. package/dist/types/web/search/providers/brave.d.ts +1 -0
  123. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  124. package/dist/types/web/search/providers/codex.d.ts +1 -0
  125. package/dist/types/web/search/providers/exa.d.ts +1 -0
  126. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  127. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  128. package/dist/types/web/search/providers/jina.d.ts +1 -0
  129. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  130. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  131. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  132. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  133. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  134. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  135. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  136. package/dist/types/web/search/providers/utils.d.ts +6 -9
  137. package/dist/types/web/search/providers/zai.d.ts +1 -0
  138. package/dist/types/web/search/types.d.ts +4 -0
  139. package/package.json +12 -12
  140. package/scripts/legacy-pi-virtual-module.ts +12 -2
  141. package/src/advisor/advise-tool.ts +18 -0
  142. package/src/advisor/runtime.ts +6 -6
  143. package/src/capability/mcp.ts +7 -0
  144. package/src/cli/args.ts +7 -87
  145. package/src/cli/browser-relay-cli.ts +119 -0
  146. package/src/cli/command-help.ts +103 -0
  147. package/src/cli/completion-gen.ts +5 -5
  148. package/src/cli/flag-tables.ts +9 -0
  149. package/src/cli/help-extra.ts +89 -0
  150. package/src/cli/models-cli.ts +13 -16
  151. package/src/cli/setup-cli.ts +13 -21
  152. package/src/cli/thinking-levels.ts +7 -0
  153. package/src/cli/update-cli.ts +26 -7
  154. package/src/cli-commands.ts +170 -33
  155. package/src/cli.ts +17 -5
  156. package/src/collab/host.ts +1 -1
  157. package/src/commands/acp.ts +3 -1
  158. package/src/commands/agents.ts +3 -2
  159. package/src/commands/auth-broker.ts +3 -2
  160. package/src/commands/auth-gateway.ts +3 -2
  161. package/src/commands/bench.ts +2 -3
  162. package/src/commands/browser-relay.ts +53 -0
  163. package/src/commands/cleanse.ts +2 -2
  164. package/src/commands/commit.ts +3 -2
  165. package/src/commands/complete.ts +2 -1
  166. package/src/commands/completions.ts +3 -2
  167. package/src/commands/config.ts +3 -2
  168. package/src/commands/dry-balance.ts +2 -2
  169. package/src/commands/gallery.ts +3 -2
  170. package/src/commands/gc.ts +3 -2
  171. package/src/commands/grep.ts +3 -2
  172. package/src/commands/grievances.ts +3 -2
  173. package/src/commands/install.ts +2 -2
  174. package/src/commands/join.ts +3 -2
  175. package/src/commands/launch-help.ts +116 -0
  176. package/src/commands/launch.ts +7 -191
  177. package/src/commands/models.ts +3 -2
  178. package/src/commands/plugin.ts +3 -2
  179. package/src/commands/read.ts +3 -2
  180. package/src/commands/say.ts +3 -2
  181. package/src/commands/setup.ts +3 -2
  182. package/src/commands/shell.ts +3 -2
  183. package/src/commands/ssh.ts +3 -2
  184. package/src/commands/stats.ts +3 -2
  185. package/src/commands/tiny-models.ts +2 -2
  186. package/src/commands/token.ts +2 -2
  187. package/src/commands/ttsr.ts +2 -2
  188. package/src/commands/update.ts +3 -2
  189. package/src/commands/usage.ts +3 -2
  190. package/src/commands/web-search.ts +3 -2
  191. package/src/commands/worktree.ts +3 -2
  192. package/src/config/keybindings.ts +19 -9
  193. package/src/config/mcp-schema.json +5 -0
  194. package/src/config/model-discovery.ts +30 -13
  195. package/src/config/model-registry.ts +16 -6
  196. package/src/config/models-config-schema-bundle.ts +9 -0
  197. package/src/config/resolve-config-value.ts +2 -1
  198. package/src/config/service-tier.ts +5 -0
  199. package/src/config/settings-schema.ts +73 -21
  200. package/src/cursor-bridge-tools.ts +1 -1
  201. package/src/cursor.ts +10 -14
  202. package/src/discovery/builtin.ts +10 -0
  203. package/src/discovery/helpers.ts +10 -0
  204. package/src/discovery/mcp-json.ts +11 -1
  205. package/src/discovery/omp-extension-roots.ts +79 -20
  206. package/src/discovery/omp-plugins.ts +10 -1
  207. package/src/edit/diff.ts +17 -8
  208. package/src/edit/index.ts +20 -7
  209. package/src/edit/modes/replace.ts +93 -40
  210. package/src/edit/renderer.ts +1 -1
  211. package/src/edit/streaming.ts +14 -17
  212. package/src/eval/py/kernel.ts +2 -1
  213. package/src/eval/py/spawn-options.ts +24 -29
  214. package/src/export/html/template.js +8 -26
  215. package/src/extensibility/extensions/loader.ts +66 -28
  216. package/src/extensibility/extensions/wrapper.ts +1 -0
  217. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  218. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  219. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  220. package/src/extensibility/plugins/marketplace/registry.ts +4 -8
  221. package/src/extensibility/skills.ts +11 -0
  222. package/src/extensibility/utils.ts +10 -2
  223. package/src/launch/broker.ts +1 -1
  224. package/src/launch/client.ts +32 -13
  225. package/src/launch/ensure.ts +73 -0
  226. package/src/launch/paths.ts +3 -6
  227. package/src/launch/protocol.ts +2 -2
  228. package/src/lsp/client.ts +23 -2
  229. package/src/lsp/index.ts +8 -0
  230. package/src/lsp/mux/daemon.ts +348 -0
  231. package/src/lsp/mux/protocol.ts +96 -0
  232. package/src/lsp/mux/server.ts +797 -0
  233. package/src/lsp/types.ts +28 -2
  234. package/src/main.ts +31 -24
  235. package/src/mcp/config.ts +1 -0
  236. package/src/mcp/oauth-flow.ts +11 -1
  237. package/src/mcp/request-id.ts +24 -0
  238. package/src/mcp/transports/http.ts +4 -2
  239. package/src/mcp/transports/sse.ts +4 -2
  240. package/src/mcp/transports/stdio.ts +4 -2
  241. package/src/mcp/types.ts +15 -0
  242. package/src/memory-backend/index.ts +1 -0
  243. package/src/memory-backend/messages.ts +19 -0
  244. package/src/mnemopi/embed-client.ts +49 -2
  245. package/src/mnemopi/state.ts +57 -23
  246. package/src/modes/components/agent-hub.ts +1 -1
  247. package/src/modes/components/assistant-message.ts +11 -0
  248. package/src/modes/components/chat-transcript-builder.ts +4 -0
  249. package/src/modes/components/custom-editor.ts +10 -1
  250. package/src/modes/components/model-browser.ts +25 -17
  251. package/src/modes/components/model-picker.ts +10 -6
  252. package/src/modes/components/read-tool-group.ts +36 -0
  253. package/src/modes/components/status-line/component.ts +1 -1
  254. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  255. package/src/modes/components/tool-execution.ts +85 -20
  256. package/src/modes/components/tree-selector.ts +46 -48
  257. package/src/modes/controllers/command-controller.ts +2 -2
  258. package/src/modes/controllers/event-controller.ts +199 -10
  259. package/src/modes/controllers/input-controller.ts +39 -1
  260. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  261. package/src/modes/controllers/selector-controller.ts +47 -6
  262. package/src/modes/interactive-mode.ts +2 -0
  263. package/src/modes/types.ts +6 -1
  264. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  265. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  266. package/src/modes/utils/ui-helpers.ts +6 -10
  267. package/src/modes/workflow.ts +8 -2
  268. package/src/prompts/agents/init.md +1 -1
  269. package/src/prompts/system/plan-mode-active.md +2 -2
  270. package/src/prompts/system/system-prompt.md +26 -52
  271. package/src/prompts/system/workflow-notice.md +3 -3
  272. package/src/prompts/tools/ast-grep.md +1 -1
  273. package/src/prompts/tools/bash.md +1 -1
  274. package/src/prompts/tools/browser.md +1 -0
  275. package/src/prompts/tools/computer.md +21 -21
  276. package/src/prompts/tools/glob.md +1 -1
  277. package/src/prompts/tools/grep.md +1 -1
  278. package/src/prompts/tools/replace.md +5 -4
  279. package/src/prompts/tools/task.md +4 -4
  280. package/src/registry/agent-lifecycle.ts +36 -11
  281. package/src/registry/agent-registry.ts +12 -5
  282. package/src/sdk.ts +102 -35
  283. package/src/secrets/index.ts +16 -18
  284. package/src/session/agent-session-types.ts +3 -1
  285. package/src/session/agent-session.ts +23 -2
  286. package/src/session/agent-storage.ts +6 -4
  287. package/src/session/history-storage.ts +4 -2
  288. package/src/session/indexed-session-storage.ts +9 -0
  289. package/src/session/messages.ts +31 -7
  290. package/src/session/model-controls.ts +0 -1
  291. package/src/session/prewalk.ts +24 -2
  292. package/src/session/session-advisors.ts +21 -3
  293. package/src/session/session-context.ts +16 -12
  294. package/src/session/session-dump-format.ts +1 -1
  295. package/src/session/session-maintenance.ts +41 -4
  296. package/src/session/session-manager.ts +181 -94
  297. package/src/session/session-paths.ts +118 -55
  298. package/src/session/session-stats.ts +48 -3
  299. package/src/session/session-storage.ts +30 -22
  300. package/src/session/session-tools.ts +25 -23
  301. package/src/session/stream-guards.ts +1 -1
  302. package/src/slash-commands/builtin-registry.ts +29 -26
  303. package/src/system-prompt.ts +14 -4
  304. package/src/task/executor.ts +8 -1
  305. package/src/task/index.ts +23 -8
  306. package/src/task/spawn-policy.ts +14 -0
  307. package/src/thinking.ts +2 -8
  308. package/src/tiny/text.ts +12 -0
  309. package/src/tools/ast-grep.ts +10 -4
  310. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  311. package/src/tools/browser/cmux/rpc.ts +2 -8
  312. package/src/tools/browser/registry.ts +53 -2
  313. package/src/tools/browser/relay/bridge.ts +945 -0
  314. package/src/tools/browser/relay/daemon.ts +117 -0
  315. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  316. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  317. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  318. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  319. package/src/tools/browser/relay/kind.ts +41 -0
  320. package/src/tools/browser/relay/protocol.ts +54 -0
  321. package/src/tools/browser/relay/server.ts +141 -0
  322. package/src/tools/browser/render.ts +4 -1
  323. package/src/tools/browser/shared-daemon.ts +7 -58
  324. package/src/tools/browser/tab-supervisor.ts +5 -4
  325. package/src/tools/browser/tab-worker.ts +33 -12
  326. package/src/tools/browser.ts +26 -1
  327. package/src/tools/computer/exposure.ts +11 -35
  328. package/src/tools/computer/protocol.ts +51 -10
  329. package/src/tools/computer/supervisor.ts +227 -124
  330. package/src/tools/computer/worker-entry.ts +11 -6
  331. package/src/tools/computer/worker.ts +695 -81
  332. package/src/tools/computer-renderer.ts +104 -65
  333. package/src/tools/computer.ts +134 -428
  334. package/src/tools/context.ts +3 -0
  335. package/src/tools/default-renderer.ts +19 -4
  336. package/src/tools/glob.ts +9 -2
  337. package/src/tools/grep.ts +12 -6
  338. package/src/tools/index.ts +10 -6
  339. package/src/tools/inspect-image.ts +32 -6
  340. package/src/tools/render-utils.ts +2 -0
  341. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  342. package/src/tools/tool-timeouts.ts +1 -0
  343. package/src/tools/xdev.ts +29 -2
  344. package/src/utils/clipboard.ts +40 -14
  345. package/src/utils/command-args.ts +26 -28
  346. package/src/utils/tool-choice.ts +0 -13
  347. package/src/web/kagi.ts +2 -1
  348. package/src/web/search/index.ts +18 -2
  349. package/src/web/search/providers/anthropic.ts +7 -2
  350. package/src/web/search/providers/base.ts +2 -0
  351. package/src/web/search/providers/brave.ts +3 -1
  352. package/src/web/search/providers/browser-page.ts +6 -4
  353. package/src/web/search/providers/codex.ts +4 -1
  354. package/src/web/search/providers/duckduckgo.ts +2 -1
  355. package/src/web/search/providers/ecosia.ts +2 -1
  356. package/src/web/search/providers/exa.ts +4 -2
  357. package/src/web/search/providers/firecrawl.ts +3 -1
  358. package/src/web/search/providers/gemini.ts +8 -2
  359. package/src/web/search/providers/google.ts +2 -1
  360. package/src/web/search/providers/jina.ts +12 -2
  361. package/src/web/search/providers/kagi.ts +3 -0
  362. package/src/web/search/providers/kimi.ts +5 -1
  363. package/src/web/search/providers/mojeek.ts +2 -1
  364. package/src/web/search/providers/parallel.ts +5 -1
  365. package/src/web/search/providers/perplexity.ts +6 -3
  366. package/src/web/search/providers/public.ts +1 -1
  367. package/src/web/search/providers/searxng.ts +11 -5
  368. package/src/web/search/providers/startpage.ts +10 -4
  369. package/src/web/search/providers/synthetic.ts +11 -5
  370. package/src/web/search/providers/tavily.ts +3 -1
  371. package/src/web/search/providers/tinyfish.ts +3 -1
  372. package/src/web/search/providers/utils.ts +12 -10
  373. package/src/web/search/providers/xai.ts +2 -2
  374. package/src/web/search/providers/zai.ts +18 -3
  375. package/src/web/search/types.ts +6 -0
@@ -26,6 +26,7 @@ interface BrowserFallbackOptions {
26
26
  export interface BrowserFetchOptions {
27
27
  fetch?: FetchImpl;
28
28
  signal: AbortSignal;
29
+ timeoutMs?: number;
29
30
  randomizeHeaders?: boolean;
30
31
  referer?: string;
31
32
  init?: Omit<RequestInit, "headers" | "signal">;
@@ -50,6 +51,7 @@ async function browseHtmlPage(
50
51
  url: string,
51
52
  options: BrowserFallbackOptions,
52
53
  signal: AbortSignal,
54
+ timeoutMs = SEARCH_HARD_TIMEOUT_MS,
53
55
  ): Promise<LoadedHtmlPage> {
54
56
  const { homeUrl, ready } = options;
55
57
  const attempts = Math.max(1, options.attempts ?? 1);
@@ -76,14 +78,14 @@ async function browseHtmlPage(
76
78
  await applyStealthPatches(handle.browser, activePage, handle.stealth);
77
79
  if (homeUrl) {
78
80
  await untilAborted(signal, () =>
79
- activePage.goto(homeUrl, { waitUntil: "domcontentloaded", timeout: SEARCH_HARD_TIMEOUT_MS }),
81
+ activePage.goto(homeUrl, { waitUntil: "domcontentloaded", timeout: timeoutMs }),
80
82
  );
81
83
  }
82
84
  for (let attempt = 0; attempt < attempts; attempt++) {
83
85
  if (attempt > 0 && options.retryDelayMs) await Bun.sleep(options.retryDelayMs);
84
86
 
85
87
  const response = await untilAborted(signal, () =>
86
- activePage.goto(url, { waitUntil: "domcontentloaded", timeout: SEARCH_HARD_TIMEOUT_MS }),
88
+ activePage.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs }),
87
89
  );
88
90
  if (options.afterNavigation) await options.afterNavigation(activePage, signal);
89
91
  if (ready) {
@@ -113,11 +115,11 @@ export async function browserFetch(url: string, options: BrowserFetchOptions): P
113
115
  page = await fetchHtmlPage(url, options, fetchImpl);
114
116
  } catch (error) {
115
117
  if (options.fetch || !options.browser) throw error;
116
- return browseHtmlPage(url, options.browser, options.signal);
118
+ return browseHtmlPage(url, options.browser, options.signal, options.timeoutMs);
117
119
  }
118
120
 
119
121
  if (!options.browser || options.fetch) return page;
120
122
  const isSuccessful = page.status >= 200 && page.status < 300;
121
123
  if (isSuccessful && !options.browser.shouldFallback(page)) return page;
122
- return browseHtmlPage(url, options.browser, options.signal);
124
+ return browseHtmlPage(url, options.browser, options.signal, options.timeoutMs);
123
125
  }
@@ -145,6 +145,7 @@ function shouldRetryWithNextDefaultModel(error: unknown): boolean {
145
145
 
146
146
  export interface CodexSearchParams {
147
147
  signal?: AbortSignal;
148
+ timeoutMs?: number;
148
149
  fetch?: FetchImpl;
149
150
  query: string;
150
151
  system_prompt?: string;
@@ -442,6 +443,7 @@ async function callCodexSearch(
442
443
  query: string,
443
444
  options: {
444
445
  signal?: AbortSignal;
446
+ timeoutMs?: number;
445
447
  systemPrompt?: string;
446
448
  searchContextSize?: "low" | "medium" | "high";
447
449
  model: CodexModelCandidate;
@@ -496,7 +498,7 @@ async function callCodexSearch(
496
498
  method: "POST",
497
499
  headers,
498
500
  body: JSON.stringify(body),
499
- signal: withHardTimeout(options.signal),
501
+ signal: withHardTimeout(options.signal, options.timeoutMs),
500
502
  });
501
503
 
502
504
  if (!response.ok) {
@@ -645,6 +647,7 @@ async function runCodexSearchCandidates(options: {
645
647
  try {
646
648
  return await callCodexSearch(options.auth, options.query, {
647
649
  signal: options.params.signal,
650
+ timeoutMs: options.params.timeoutMs,
648
651
  systemPrompt: options.params.systemPrompt,
649
652
  searchContextSize: "high",
650
653
  model: candidate,
@@ -305,6 +305,7 @@ async function callDuckDuckGoHtml(params: SearchParams, form: URLSearchParams, s
305
305
  const page = await browserFetch(DUCKDUCKGO_HTML_URL, {
306
306
  fetch: params.fetch ?? fetch,
307
307
  signal,
308
+ timeoutMs: params.timeoutMs,
308
309
  referer: "https://html.duckduckgo.com/",
309
310
  init: {
310
311
  method: "POST",
@@ -334,7 +335,7 @@ async function callDuckDuckGoHtml(params: SearchParams, form: URLSearchParams, s
334
335
  /** Execute a DuckDuckGo web search via the no-JS HTML frontend. */
335
336
  export async function searchDuckDuckGo(params: SearchParams): Promise<SearchResponse> {
336
337
  const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
337
- const signal = withHardTimeout(params.signal);
338
+ const signal = withHardTimeout(params.signal, params.timeoutMs);
338
339
  const sources: SearchSource[] = [];
339
340
  const seen = new Set<string>();
340
341
  let form: URLSearchParams | undefined = createDuckDuckGoForm(params);
@@ -99,7 +99,7 @@ function isBlockedPage(page: LoadedHtmlPage): boolean {
99
99
  }
100
100
 
101
101
  async function callEcosiaHtml(params: SearchParams): Promise<string> {
102
- const signal = withHardTimeout(params.signal);
102
+ const signal = withHardTimeout(params.signal, params.timeoutMs);
103
103
  const url = new URL(ECOSIA_SEARCH_URL);
104
104
  // Ecosia serves Google-backed results, so classic operators pass through
105
105
  // inline; canonicalize aliases (domain: -> site:, since: -> after:) and
@@ -111,6 +111,7 @@ async function callEcosiaHtml(params: SearchParams): Promise<string> {
111
111
  page = await browserFetch(url.href, {
112
112
  fetch: params.fetch,
113
113
  signal,
114
+ timeoutMs: params.timeoutMs,
114
115
  referer: ECOSIA_HOME_URL,
115
116
  browser: {
116
117
  homeUrl: ECOSIA_HOME_URL,
@@ -115,6 +115,7 @@ export interface ExaSearchParams {
115
115
  start_published_date?: string;
116
116
  end_published_date?: string;
117
117
  signal?: AbortSignal;
118
+ timeoutMs?: number;
118
119
  fetch?: FetchImpl;
119
120
  /**
120
121
  * Credential source. Resolved before falling back to `EXA_API_KEY` so
@@ -315,7 +316,7 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
315
316
  "x-api-key": apiKey,
316
317
  },
317
318
  body: JSON.stringify(body),
318
- signal: withHardTimeout(params.signal),
319
+ signal: withHardTimeout(params.signal, params.timeoutMs),
319
320
  });
320
321
 
321
322
  if (!response.ok) {
@@ -360,7 +361,7 @@ async function callExaMcpSearch(params: ExaSearchParams): Promise<ExaSearchRespo
360
361
  arguments: buildExaMcpArgs(params),
361
362
  },
362
363
  }),
363
- signal: withHardTimeout(params.signal),
364
+ signal: withHardTimeout(params.signal, params.timeoutMs),
364
365
  });
365
366
  if (!response.ok) {
366
367
  throw new Error(`MCP request failed: ${response.status} ${response.statusText}`);
@@ -478,6 +479,7 @@ export class ExaProvider extends SearchProvider {
478
479
  ...directiveParams(parsed),
479
480
  num_results: params.numSearchResults ?? params.limit,
480
481
  signal: params.signal,
482
+ timeoutMs: params.timeoutMs,
481
483
  authStorage: params.authStorage,
482
484
  sessionId: params.sessionId,
483
485
  fetch: params.fetch,
@@ -38,6 +38,7 @@ export interface FirecrawlSearchParams {
38
38
  /** Explicit `tbs` (custom date range); takes precedence over `recency`. */
39
39
  tbs?: string;
40
40
  signal?: AbortSignal;
41
+ timeoutMs?: number;
41
42
  fetch?: FetchImpl;
42
43
  }
43
44
 
@@ -91,7 +92,7 @@ async function callFirecrawlSearch(
91
92
  method: "POST",
92
93
  headers,
93
94
  body: JSON.stringify(buildRequestBody(params)),
94
- signal: withHardTimeout(params.signal),
95
+ signal: withHardTimeout(params.signal, params.timeoutMs),
95
96
  });
96
97
 
97
98
  if (!response.ok) {
@@ -145,6 +146,7 @@ export async function searchFirecrawl(params: SearchParams): Promise<SearchRespo
145
146
  recency: params.recency,
146
147
  tbs,
147
148
  signal: params.signal,
149
+ timeoutMs: params.timeoutMs,
148
150
  fetch: params.fetch,
149
151
  };
150
152
  const keyResolver = params.authStorage.resolver("firecrawl", {
@@ -61,6 +61,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
61
61
  /** Sampling temperature (0–1). Lower = more focused/factual. */
62
62
  temperature?: number;
63
63
  signal?: AbortSignal;
64
+ timeoutMs?: number;
64
65
  authStorage: AuthStorage;
65
66
  sessionId?: string;
66
67
  fetch?: FetchImpl;
@@ -309,6 +310,7 @@ async function callGeminiSearch(
309
310
  toolParams: GeminiToolParams,
310
311
  fetchImpl: FetchImpl | undefined,
311
312
  signal: AbortSignal | undefined,
313
+ timeoutMs: number | undefined,
312
314
  mode?: "auto" | "production" | "sandbox",
313
315
  ): Promise<GeminiSearchResult> {
314
316
  let endpoints: string[];
@@ -383,7 +385,7 @@ async function callGeminiSearch(
383
385
  ...headers,
384
386
  },
385
387
  body: JSON.stringify(requestBody),
386
- signal: withHardTimeout(signal),
388
+ signal: withHardTimeout(signal, timeoutMs),
387
389
  });
388
390
 
389
391
  let response: Response | undefined;
@@ -442,6 +444,7 @@ async function callGeminiDeveloperSearch(
442
444
  toolParams: GeminiToolParams,
443
445
  fetchImpl: FetchImpl | undefined,
444
446
  signal: AbortSignal | undefined,
447
+ timeoutMs: number | undefined,
445
448
  ): Promise<GeminiSearchResult> {
446
449
  const normalizedSystemPrompt = systemPrompt?.toWellFormed();
447
450
  const requestBody: Record<string, unknown> = {
@@ -480,7 +483,7 @@ async function callGeminiDeveloperSearch(
480
483
  Accept: "text/event-stream",
481
484
  },
482
485
  body: JSON.stringify(requestBody),
483
- signal: withHardTimeout(signal),
486
+ signal: withHardTimeout(signal, timeoutMs),
484
487
  fetch: fetchImpl,
485
488
  maxAttempts: MAX_RETRIES + 1,
486
489
  defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
@@ -548,6 +551,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
548
551
  },
549
552
  params.fetch,
550
553
  params.signal,
554
+ params.timeoutMs,
551
555
  params.antigravityEndpointMode,
552
556
  ),
553
557
  { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
@@ -575,6 +579,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
575
579
  },
576
580
  params.fetch,
577
581
  params.signal,
582
+ params.timeoutMs,
578
583
  );
579
584
  }
580
585
 
@@ -619,6 +624,7 @@ export class GeminiProvider extends SearchProvider {
619
624
  code_execution: params.codeExecution,
620
625
  url_context: params.urlContext,
621
626
  signal: params.signal,
627
+ timeoutMs: params.timeoutMs,
622
628
  authStorage: params.authStorage,
623
629
  sessionId: params.sessionId,
624
630
  fetch: params.fetch,
@@ -117,13 +117,14 @@ function blockReason(page: LoadedHtmlPage): "javascript" | "traffic" | undefined
117
117
  }
118
118
 
119
119
  async function callGoogleHtml(params: SearchParams, numResults: number): Promise<string> {
120
- const signal = withHardTimeout(params.signal);
120
+ const signal = withHardTimeout(params.signal, params.timeoutMs);
121
121
  const url = buildSearchUrl(params, numResults);
122
122
  let page: LoadedHtmlPage;
123
123
  try {
124
124
  page = await browserFetch(url, {
125
125
  fetch: params.fetch,
126
126
  signal,
127
+ timeoutMs: params.timeoutMs,
127
128
  referer: GOOGLE_HOME_URL,
128
129
  browser: {
129
130
  homeUrl: GOOGLE_HOME_URL,
@@ -22,6 +22,7 @@ export interface JinaSearchParams {
22
22
  /** Single bare host for Jina's `X-Site` in-site search header. */
23
23
  site?: string;
24
24
  signal?: AbortSignal;
25
+ timeoutMs?: number;
25
26
  fetch?: FetchImpl;
26
27
  }
27
28
 
@@ -45,6 +46,7 @@ async function callJinaSearch(
45
46
  site?: string,
46
47
  signal?: AbortSignal,
47
48
  fetchImpl: FetchImpl = fetch,
49
+ timeoutMs?: number,
48
50
  ): Promise<JinaSearchResponse> {
49
51
  const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}`;
50
52
  const headers: Record<string, string> = {
@@ -54,7 +56,7 @@ async function callJinaSearch(
54
56
  if (site) headers["X-Site"] = site;
55
57
  const response = await fetchImpl(requestUrl, {
56
58
  headers,
57
- signal: withHardTimeout(signal),
59
+ signal: withHardTimeout(signal, timeoutMs),
58
60
  });
59
61
 
60
62
  if (!response.ok) {
@@ -75,7 +77,14 @@ export async function searchJina(params: JinaSearchParams): Promise<SearchRespon
75
77
  throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
76
78
  }
77
79
 
78
- const response = await callJinaSearch(apiKey, params.query, params.site, params.signal, params.fetch);
80
+ const response = await callJinaSearch(
81
+ apiKey,
82
+ params.query,
83
+ params.site,
84
+ params.signal,
85
+ params.fetch,
86
+ params.timeoutMs,
87
+ );
79
88
  const sources: SearchSource[] = [];
80
89
 
81
90
  for (const result of response) {
@@ -128,6 +137,7 @@ export class JinaProvider extends SearchProvider {
128
137
  num_results: params.numSearchResults ?? params.limit,
129
138
  site,
130
139
  signal: params.signal,
140
+ timeoutMs: params.timeoutMs,
131
141
  fetch: params.fetch,
132
142
  });
133
143
  }
@@ -26,6 +26,7 @@ export async function searchKagi(params: {
26
26
  recency?: SearchParams["recency"];
27
27
  parsedQuery?: StructuredQuery;
28
28
  signal?: AbortSignal;
29
+ timeoutMs?: number;
29
30
  authStorage: AuthStorage;
30
31
  sessionId?: string;
31
32
  fetch?: FetchImpl;
@@ -45,6 +46,7 @@ export async function searchKagi(params: {
45
46
  recency: params.recency,
46
47
  sessionId: params.sessionId,
47
48
  signal: params.signal,
49
+ timeoutMs: params.timeoutMs,
48
50
  fetch: params.fetch,
49
51
  },
50
52
  params.authStorage,
@@ -87,6 +89,7 @@ export class KagiProvider extends SearchProvider {
87
89
  num_results: params.numSearchResults ?? params.limit,
88
90
  recency: params.recency,
89
91
  signal: params.signal,
92
+ timeoutMs: params.timeoutMs,
90
93
  authStorage: params.authStorage,
91
94
  sessionId: params.sessionId,
92
95
  fetch: fetchImpl,
@@ -42,6 +42,7 @@ export interface KimiSearchParams {
42
42
  num_results?: number;
43
43
  include_content?: boolean;
44
44
  signal?: AbortSignal;
45
+ timeoutMs?: number;
45
46
  authStorage: AuthStorage;
46
47
  sessionId?: string;
47
48
  fetch?: FetchImpl;
@@ -105,6 +106,7 @@ async function callKimiSearch(
105
106
  limit: number;
106
107
  includeContent: boolean;
107
108
  signal?: AbortSignal;
109
+ timeoutMs?: number;
108
110
  fetch?: FetchImpl;
109
111
  },
110
112
  ): Promise<{ response: KimiSearchResponse; requestId?: string }> {
@@ -122,7 +124,7 @@ async function callKimiSearch(
122
124
  enable_page_crawling: params.includeContent,
123
125
  timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
124
126
  }),
125
- signal: withHardTimeout(params.signal),
127
+ signal: withHardTimeout(params.signal, params.timeoutMs),
126
128
  });
127
129
 
128
130
  if (!response.ok) {
@@ -161,6 +163,7 @@ export async function searchKimi(params: KimiSearchParams): Promise<SearchRespon
161
163
  limit,
162
164
  includeContent: params.include_content ?? false,
163
165
  signal: params.signal,
166
+ timeoutMs: params.timeoutMs,
164
167
  fetch: params.fetch,
165
168
  }),
166
169
  { signal: params.signal },
@@ -209,6 +212,7 @@ export class KimiProvider extends SearchProvider {
209
212
  parsedQuery: params.parsedQuery,
210
213
  num_results: params.numSearchResults ?? params.limit,
211
214
  signal: params.signal,
215
+ timeoutMs: params.timeoutMs,
212
216
  authStorage: params.authStorage,
213
217
  sessionId: params.sessionId,
214
218
  fetch: fetchImpl,
@@ -137,13 +137,14 @@ function isRobotPage(page: LoadedHtmlPage): boolean {
137
137
  }
138
138
 
139
139
  async function callMojeekHtml(params: SearchParams, numResults: number): Promise<string> {
140
- const signal = withHardTimeout(params.signal);
140
+ const signal = withHardTimeout(params.signal, params.timeoutMs);
141
141
  const url = buildSearchUrl(params, numResults);
142
142
  let page: LoadedHtmlPage;
143
143
  try {
144
144
  page = await browserFetch(url, {
145
145
  fetch: params.fetch,
146
146
  signal,
147
+ timeoutMs: params.timeoutMs,
147
148
  randomizeHeaders: false,
148
149
  referer: MOJEEK_HOME_URL,
149
150
  browser: {
@@ -59,6 +59,7 @@ async function searchWithAuthStorage(
59
59
  queries: string[],
60
60
  params: {
61
61
  signal?: AbortSignal;
62
+ timeoutMs?: number;
62
63
  fetch?: FetchImpl;
63
64
  },
64
65
  authStorage: AuthStorage,
@@ -97,7 +98,7 @@ async function searchWithAuthStorage(
97
98
  },
98
99
  ...(sourcePolicy && { source_policy: sourcePolicy }),
99
100
  }),
100
- signal: withHardTimeout(params.signal),
101
+ signal: withHardTimeout(params.signal, params.timeoutMs),
101
102
  });
102
103
 
103
104
  if (!response.ok) {
@@ -116,6 +117,7 @@ export async function searchParallel(
116
117
  query: string;
117
118
  num_results?: number;
118
119
  signal?: AbortSignal;
120
+ timeoutMs?: number;
119
121
  fetch?: FetchImpl;
120
122
  parsedQuery?: StructuredQuery;
121
123
  },
@@ -134,6 +136,7 @@ export async function searchParallel(
134
136
  [query],
135
137
  {
136
138
  signal: params.signal,
139
+ timeoutMs: params.timeoutMs,
137
140
  fetch: params.fetch,
138
141
  },
139
142
  authStorage,
@@ -172,6 +175,7 @@ export class ParallelProvider extends SearchProvider {
172
175
  query: params.query,
173
176
  num_results: params.numSearchResults ?? params.limit,
174
177
  signal: params.signal,
178
+ timeoutMs: params.timeoutMs,
175
179
  fetch: params.fetch,
176
180
  parsedQuery: params.parsedQuery,
177
181
  },
@@ -322,6 +322,7 @@ function sourcesFromTextPayload(text: string | undefined): SearchSource[] {
322
322
  }
323
323
  export interface PerplexitySearchParams {
324
324
  signal?: AbortSignal;
325
+ timeoutMs?: number;
325
326
  query: string;
326
327
  system_prompt?: string;
327
328
  /** Pre-parsed view of `query` from the search pipeline; parsed locally when absent. */
@@ -498,10 +499,11 @@ async function callPerplexityApi(
498
499
  request: PerplexityRequest,
499
500
  fetchImpl: FetchImpl | undefined,
500
501
  signal?: AbortSignal,
502
+ timeoutMs?: number,
501
503
  ): Promise<SearchResponse> {
502
504
  const metadata: PerplexityApiStreamMetadata = {};
503
505
  const context = buildPerplexityContext(request);
504
- const requestSignal = withHardTimeout(signal);
506
+ const requestSignal = withHardTimeout(signal, timeoutMs);
505
507
  const onSseEvent = (event: { data: string }): void => {
506
508
  collectPerplexityMetadata(metadata, event.data);
507
509
  };
@@ -690,7 +692,7 @@ async function callPerplexityAsk(
690
692
  query_str: effectiveQuery,
691
693
  params: requestParams,
692
694
  }),
693
- signal: withHardTimeout(params.signal),
695
+ signal: withHardTimeout(params.signal, params.timeoutMs),
694
696
  };
695
697
 
696
698
  // The consumer ask endpoint intermittently drops the socket before sending an
@@ -902,7 +904,7 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
902
904
  for (const auth of authMethods) {
903
905
  if (auth.type === "api_key") {
904
906
  try {
905
- const result = await callPerplexityApi(auth, request, params.fetch, params.signal);
907
+ const result = await callPerplexityApi(auth, request, params.fetch, params.signal, params.timeoutMs);
906
908
  result.authMode = "api_key";
907
909
  return applySourceLimit(result, params.num_results);
908
910
  } catch (error) {
@@ -978,6 +980,7 @@ export class PerplexityProvider extends SearchProvider {
978
980
  search(params: SearchParams): Promise<SearchResponse> {
979
981
  return searchPerplexity({
980
982
  signal: params.signal,
983
+ timeoutMs: params.timeoutMs,
981
984
  query: params.query,
982
985
  parsedQuery: params.parsedQuery,
983
986
  temperature: params.temperature,
@@ -130,7 +130,7 @@ export async function searchPublicWeb(
130
130
  // hard deadline; the straggler controller lets the aggregate cancel
131
131
  // still-running engines once it decides to return.
132
132
  const straggler = new AbortController();
133
- const signal = AbortSignal.any([withHardTimeout(params.signal), straggler.signal]);
133
+ const signal = AbortSignal.any([withHardTimeout(params.signal, params.timeoutMs), straggler.signal]);
134
134
 
135
135
  const responses: (SearchResponse | undefined)[] = new Array(engineIds.length);
136
136
  const failures: { provider: { id: SearchProviderId; label: string }; error: unknown }[] = [];
@@ -197,11 +197,12 @@ async function fetchEngineNameMap(
197
197
  auth: SearXNGAuth | null,
198
198
  fetchImpl: FetchImpl | undefined,
199
199
  signal: AbortSignal | undefined,
200
+ timeoutMs?: number,
200
201
  ): Promise<Map<string, string> | null> {
201
202
  try {
202
203
  const response = await (fetchImpl ?? fetch)(`${base}/config`, {
203
204
  headers: buildHeaders(auth),
204
- signal: withHardTimeout(signal),
205
+ signal: withHardTimeout(signal, timeoutMs),
205
206
  });
206
207
  if (!response.ok) return null;
207
208
  const config = (await response.json()) as SearXNGConfig;
@@ -224,11 +225,12 @@ function getEngineNameMap(
224
225
  auth: SearXNGAuth | null,
225
226
  fetchImpl: FetchImpl | undefined,
226
227
  signal: AbortSignal | undefined,
228
+ timeoutMs?: number,
227
229
  ): Promise<Map<string, string> | null> {
228
230
  const base = endpoint.replace(/\/+$/, "");
229
231
  let cached = engineNameMapCache.get(base);
230
232
  if (!cached) {
231
- cached = fetchEngineNameMap(base, auth, fetchImpl, signal).then(map => {
233
+ cached = fetchEngineNameMap(base, auth, fetchImpl, signal, timeoutMs).then(map => {
232
234
  if (!map) engineNameMapCache.delete(base);
233
235
  return map;
234
236
  });
@@ -247,13 +249,14 @@ async function resolveEngineNames(
247
249
  auth: SearXNGAuth | null,
248
250
  fetchImpl: FetchImpl | undefined,
249
251
  signal: AbortSignal | undefined,
252
+ timeoutMs?: number,
250
253
  ): Promise<string | undefined> {
251
254
  const entries = raw
252
255
  .split(",")
253
256
  .map(entry => entry.trim())
254
257
  .filter(Boolean);
255
258
  if (!entries.length) return undefined;
256
- const map = await getEngineNameMap(endpoint, auth, fetchImpl, signal);
259
+ const map = await getEngineNameMap(endpoint, auth, fetchImpl, signal, timeoutMs);
257
260
  if (!map) return entries.join(",");
258
261
  return entries.map(entry => map.get(entry.toLowerCase()) ?? entry).join(",");
259
262
  }
@@ -324,6 +327,7 @@ async function callSearXNGSearch(
324
327
  engines?: string;
325
328
  language?: string;
326
329
  signal?: AbortSignal;
330
+ timeoutMs?: number;
327
331
  fetch?: FetchImpl;
328
332
  },
329
333
  auth: SearXNGAuth | null,
@@ -332,7 +336,7 @@ async function callSearXNGSearch(
332
336
 
333
337
  const response = await (params.fetch ?? fetch)(url, {
334
338
  headers,
335
- signal: withHardTimeout(params.signal),
339
+ signal: withHardTimeout(params.signal, params.timeoutMs),
336
340
  });
337
341
 
338
342
  if (!response.ok) {
@@ -352,6 +356,7 @@ export async function searchSearXNG(params: {
352
356
  num_results?: number;
353
357
  recency?: "day" | "week" | "month" | "year";
354
358
  signal?: AbortSignal;
359
+ timeoutMs?: number;
355
360
  fetch?: FetchImpl;
356
361
  }): Promise<SearchResponse> {
357
362
  const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
@@ -386,7 +391,7 @@ export async function searchSearXNG(params: {
386
391
  if (parsed.lang) language = parsed.lang;
387
392
 
388
393
  const engines = configuredEngines
389
- ? await resolveEngineNames(configuredEngines, endpoint, auth, params.fetch, params.signal)
394
+ ? await resolveEngineNames(configuredEngines, endpoint, auth, params.fetch, params.signal, params.timeoutMs)
390
395
  : undefined;
391
396
 
392
397
  const response = await callSearXNGSearch(
@@ -455,6 +460,7 @@ export class SearXNGProvider extends SearchProvider {
455
460
  num_results: params.numSearchResults ?? params.limit,
456
461
  recency: params.recency,
457
462
  signal: params.signal,
463
+ timeoutMs: params.timeoutMs,
458
464
  fetch: params.fetch,
459
465
  });
460
466
  }
@@ -121,10 +121,14 @@ function parseHtmlResults(html: string): ParsedResult[] {
121
121
  * any failure (network, non-OK status, challenge shell, markup drift) yields
122
122
  * `undefined` and the caller falls back to a direct GET.
123
123
  */
124
- async function fetchFormInputs(fetchImpl: FetchImpl, signal: AbortSignal): Promise<Record<string, string> | undefined> {
124
+ async function fetchFormInputs(
125
+ fetchImpl: FetchImpl,
126
+ signal: AbortSignal,
127
+ timeoutMs?: number,
128
+ ): Promise<Record<string, string> | undefined> {
125
129
  let page: LoadedHtmlPage;
126
130
  try {
127
- page = await browserFetch(STARTPAGE_HOME_URL, { fetch: fetchImpl, signal });
131
+ page = await browserFetch(STARTPAGE_HOME_URL, { fetch: fetchImpl, signal, timeoutMs });
128
132
  } catch (error) {
129
133
  if (signal.aborted) throw error;
130
134
  return undefined;
@@ -135,7 +139,7 @@ async function fetchFormInputs(fetchImpl: FetchImpl, signal: AbortSignal): Promi
135
139
 
136
140
  async function callStartpageHtml(params: SearchParams): Promise<string> {
137
141
  const fetchImpl = params.fetch ?? fetch;
138
- const signal = withHardTimeout(params.signal);
142
+ const signal = withHardTimeout(params.signal, params.timeoutMs);
139
143
  const withDate = params.recency ? RECENCY_TO_STARTPAGE_WITH_DATE[params.recency] : undefined;
140
144
  // Startpage proxies Google, so the operator set works inline; rebuild via
141
145
  // the shared scraper formatter to canonicalize aliases (domain: → site:,
@@ -143,7 +147,7 @@ async function callStartpageHtml(params: SearchParams): Promise<string> {
143
147
  // queries pass through byte-identical.
144
148
  const query = formatScraperQuery(params.query, params.parsedQuery);
145
149
 
146
- const formInputs = await fetchFormInputs(fetchImpl, signal);
150
+ const formInputs = await fetchFormInputs(fetchImpl, signal, params.timeoutMs);
147
151
  let page: LoadedHtmlPage;
148
152
  if (formInputs) {
149
153
  const form = new URLSearchParams(formInputs);
@@ -152,6 +156,7 @@ async function callStartpageHtml(params: SearchParams): Promise<string> {
152
156
  page = await browserFetch(STARTPAGE_SEARCH_URL, {
153
157
  fetch: fetchImpl,
154
158
  signal,
159
+ timeoutMs: params.timeoutMs,
155
160
  referer: STARTPAGE_HOME_URL,
156
161
  init: { method: "POST", body: form.toString() },
157
162
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -163,6 +168,7 @@ async function callStartpageHtml(params: SearchParams): Promise<string> {
163
168
  page = await browserFetch(url.href, {
164
169
  fetch: fetchImpl,
165
170
  signal,
171
+ timeoutMs: params.timeoutMs,
166
172
  referer: STARTPAGE_HOME_URL,
167
173
  });
168
174
  }
@@ -43,6 +43,7 @@ async function callSyntheticSearch(
43
43
  query: string,
44
44
  signal?: AbortSignal,
45
45
  fetchImpl: FetchImpl = fetch,
46
+ timeoutMs?: number,
46
47
  ): Promise<SyntheticSearchResponse> {
47
48
  const response = await fetchImpl(SYNTHETIC_SEARCH_URL, {
48
49
  method: "POST",
@@ -51,7 +52,7 @@ async function callSyntheticSearch(
51
52
  Authorization: `Bearer ${apiKey}`,
52
53
  },
53
54
  body: JSON.stringify({ query }),
54
- signal: withHardTimeout(signal),
55
+ signal: withHardTimeout(signal, timeoutMs),
55
56
  });
56
57
 
57
58
  if (!response.ok) {
@@ -80,10 +81,15 @@ export async function searchSynthetic(params: SearchParamsWithFetch): Promise<Se
80
81
  : params.query;
81
82
 
82
83
  const fetchImpl = params.fetch;
83
- const data = await withAuth(keyOrResolver, key => callSyntheticSearch(key, query, params.signal, fetchImpl), {
84
- signal: params.signal,
85
- missingKeyMessage: "Synthetic credentials not found. Set SYNTHETIC_API_KEY or login with 'omp /login synthetic'.",
86
- });
84
+ const data = await withAuth(
85
+ keyOrResolver,
86
+ key => callSyntheticSearch(key, query, params.signal, fetchImpl, params.timeoutMs),
87
+ {
88
+ signal: params.signal,
89
+ missingKeyMessage:
90
+ "Synthetic credentials not found. Set SYNTHETIC_API_KEY or login with 'omp /login synthetic'.",
91
+ },
92
+ );
87
93
  const sources: SearchSource[] = [];
88
94
 
89
95
  for (const result of data.results ?? []) {
@@ -30,6 +30,7 @@ export interface TavilySearchParams {
30
30
  /** `before:` upper bound, ISO `YYYY-MM-DD`, mapped to `end_date`. */
31
31
  end_date?: string;
32
32
  signal?: AbortSignal;
33
+ timeoutMs?: number;
33
34
  fetch?: FetchImpl;
34
35
  }
35
36
 
@@ -120,7 +121,7 @@ async function callTavilySearch(apiKey: string, params: TavilySearchParams): Pro
120
121
  Authorization: `Bearer ${apiKey}`,
121
122
  },
122
123
  body: JSON.stringify(buildRequestBody(params)),
123
- signal: withHardTimeout(params.signal),
124
+ signal: withHardTimeout(params.signal, params.timeoutMs),
124
125
  });
125
126
 
126
127
  if (!response.ok) {
@@ -189,6 +190,7 @@ export async function searchTavily(params: SearchParams): Promise<SearchResponse
189
190
  num_results: params.numSearchResults ?? params.limit,
190
191
  recency: params.recency,
191
192
  signal: params.signal,
193
+ timeoutMs: params.timeoutMs,
192
194
  fetch: params.fetch,
193
195
  };
194
196
  if (parsed.hasDirectives) {