@herbertgao/pi-extensions 2026.9.20 → 2026.10.1

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 (357) hide show
  1. package/README.md +18 -13
  2. package/THIRD_PARTY_NOTICES.md +31 -5
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +1 -1
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +1 -1
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  11. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  12. package/node_modules/@dietrichgebert/ponytail/README.es.md +87 -204
  13. package/node_modules/@dietrichgebert/ponytail/README.ja.md +212 -0
  14. package/node_modules/@dietrichgebert/ponytail/README.ko.md +92 -226
  15. package/node_modules/@dietrichgebert/ponytail/README.md +58 -244
  16. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +212 -0
  17. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  18. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +63 -16
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  24. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  25. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  26. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  27. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  28. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  29. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +7 -2
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +1 -1
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +16 -14
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +3 -3
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +12 -7
  34. package/node_modules/@herbertgao/pi-bark/README.md +39 -3
  35. package/node_modules/@herbertgao/pi-bark/package.json +1 -1
  36. package/node_modules/@herbertgao/pi-bark/src/index.ts +275 -15
  37. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +50 -50
  38. package/node_modules/@herbertgao/pi-cc-extensions/README.md +7 -7
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +25 -2
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +24 -1
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -4
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +47 -5
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/mcp-chip.ts +136 -0
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +56 -11
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +36 -4
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +11 -2
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +303 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +1 -1
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +36 -0
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +2 -2
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +88 -34
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +6 -5
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -1
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +31 -6
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +3 -3
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +3 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +173 -36
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +1 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/package.json +6 -4
  61. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  62. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  63. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +3 -2
  64. package/node_modules/@herbertgao/sol-pi/CONTRIBUTING.md +22 -0
  65. package/node_modules/@herbertgao/sol-pi/README.md +36 -17
  66. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +10 -6
  67. package/node_modules/@herbertgao/sol-pi/agents-install.md +15 -12
  68. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +24 -8
  69. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +2 -2
  70. package/node_modules/@herbertgao/sol-pi/docs/star-history.md +18 -0
  71. package/node_modules/@herbertgao/sol-pi/package.json +4 -2
  72. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +1 -1
  73. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +1 -1
  74. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +23 -4
  75. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +32 -1
  76. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +3 -2
  77. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +1 -1
  78. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +27 -7
  79. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +137 -41
  80. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +1 -0
  81. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +2 -1
  82. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/projection.ts +58 -0
  83. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +28 -6
  84. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +2 -1
  85. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +15 -2
  86. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  87. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  88. package/node_modules/@pi-plugins/fast-mode/README.md +7 -1
  89. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  90. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +7 -1
  91. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  92. package/node_modules/@pi-plugins/fast-mode/package.json +5 -5
  93. package/node_modules/pi-antigravity/README.md +3 -0
  94. package/node_modules/pi-antigravity/package.json +8 -8
  95. package/node_modules/pi-antigravity/src/auth/oauth.ts +6 -3
  96. package/node_modules/pi-antigravity/src/index.ts +114 -110
  97. package/node_modules/pi-antigravity/src/stream/stream.ts +1 -1
  98. package/node_modules/pi-antigravity/src/utils/util.ts +18 -1
  99. package/node_modules/pi-multi-account/CHANGELOG.md +20 -0
  100. package/node_modules/pi-multi-account/README.md +46 -8
  101. package/node_modules/pi-multi-account/config-file-transaction.ts +42 -0
  102. package/node_modules/pi-multi-account/index.ts +255 -269
  103. package/node_modules/pi-multi-account/model-pins.ts +36 -0
  104. package/node_modules/pi-multi-account/package.json +9 -7
  105. package/node_modules/pi-multi-account/slot-proxy-auth.ts +12 -2
  106. package/node_modules/pi-multi-account/slot-proxy.ts +27 -5
  107. package/node_modules/pi-typesafe/README.md +1 -1
  108. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  109. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  110. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  111. package/node_modules/pi-typesafe/dist/client.js +22 -7
  112. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  113. package/node_modules/pi-typesafe/dist/index.js +1 -1
  114. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  115. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  116. package/node_modules/pi-typesafe/package.json +1 -1
  117. package/node_modules/pi-web-access/CHANGELOG.md +65 -0
  118. package/node_modules/pi-web-access/README.md +63 -14
  119. package/node_modules/pi-web-access/brave-rate-limit.ts +176 -0
  120. package/node_modules/pi-web-access/brave.ts +28 -11
  121. package/node_modules/pi-web-access/curator-page.ts +5 -1
  122. package/node_modules/pi-web-access/curator-server.ts +2 -0
  123. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  124. package/node_modules/pi-web-access/degoog.ts +323 -0
  125. package/node_modules/pi-web-access/dist/index.js +3647 -2740
  126. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  127. package/node_modules/pi-web-access/exa.ts +5 -1
  128. package/node_modules/pi-web-access/extract.ts +41 -3
  129. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  130. package/node_modules/pi-web-access/gemini-search.ts +36 -16
  131. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  132. package/node_modules/pi-web-access/index.ts +164 -1207
  133. package/node_modules/pi-web-access/keenable.ts +221 -0
  134. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  135. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  136. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  137. package/node_modules/pi-web-access/openai-search.ts +58 -11
  138. package/node_modules/pi-web-access/package.json +23 -5
  139. package/node_modules/pi-web-access/perplexity.ts +64 -56
  140. package/node_modules/pi-web-access/summary-review.ts +8 -2
  141. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  142. package/node_modules/pi-web-access/video-extract.ts +1 -1
  143. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  144. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  145. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  146. package/node_modules/{@herbertgao/resume-from → resume-from}/README.md +16 -12
  147. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/adapter.d.ts +0 -5
  148. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/adapter.js +2 -3
  149. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/contract.d.ts +2 -2
  150. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/reader.d.ts +3 -0
  151. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/reader.js +27 -15
  152. package/node_modules/{@herbertgao/resume-from/dist/adapters/codex → resume-from/dist/adapters/claude-code}/redaction.js +16 -3
  153. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/writer.d.ts +2 -4
  154. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/writer.js +1 -1
  155. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/contract.d.ts +2 -4
  156. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/index.js +1 -2
  157. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/read.d.ts +16 -0
  158. package/node_modules/resume-from/dist/adapters/codex/read.js +723 -0
  159. package/node_modules/{@herbertgao/resume-from/dist/adapters/pi → resume-from/dist/adapters/codex}/redaction.js +16 -3
  160. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/rollout.d.ts +44 -4
  161. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/rollout.js +88 -4
  162. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/write.d.ts +2 -2
  163. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/write.js +2 -2
  164. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/contract.d.ts +5 -1
  165. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/adapter.js +10 -6
  166. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/contract.d.ts +2 -2
  167. package/node_modules/{@herbertgao/resume-from/dist/adapters/claude-code → resume-from/dist/adapters/pi}/redaction.js +16 -3
  168. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/serialize.d.ts +2 -4
  169. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/serialize.js +2 -2
  170. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/render.d.ts +1 -1
  171. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/render.js +4 -4
  172. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/runner.js +2 -2
  173. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/contract.d.ts +2 -2
  174. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/command.js +31 -8
  175. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/wiring.d.ts +1 -1
  176. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/wiring.js +1 -2
  177. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/confirmation.d.ts +4 -1
  178. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/confirmation.js +2 -2
  179. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/contract.d.ts +8 -6
  180. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/contract.d.ts +10 -5
  181. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/finder.d.ts +2 -1
  182. package/node_modules/resume-from/dist/import/discovery/finder.js +269 -0
  183. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/contract.d.ts +3 -3
  184. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/lander.js +3 -3
  185. package/node_modules/resume-from/dist/import/pipeline.d.ts +28 -0
  186. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/pipeline.js +31 -11
  187. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/builder.d.ts +2 -2
  188. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/builder.js +9 -5
  189. package/node_modules/resume-from/dist/import/preview/warnings.d.ts +14 -0
  190. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/warnings.js +26 -20
  191. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/wiring.js +12 -2
  192. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/contract.d.ts +9 -3
  193. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/git.d.ts +4 -4
  194. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/git.js +7 -4
  195. package/node_modules/resume-from/dist/platform/repo/reader.d.ts +3 -0
  196. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/reader.js +60 -20
  197. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/session/contract.d.ts +8 -1
  198. package/node_modules/{@herbertgao/resume-from → resume-from}/package.json +19 -32
  199. package/node_modules/{@herbertgao/resume-from → resume-from}/shims/pi/extensions/resume-from.js +2 -2
  200. package/package.json +21 -21
  201. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  202. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  203. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  204. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  205. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  206. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  207. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  208. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  209. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  210. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  211. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  212. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  213. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  214. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  215. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.js +0 -270
  216. package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.js +0 -129
  217. package/node_modules/@herbertgao/resume-from/dist/import/pipeline.d.ts +0 -17
  218. package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.d.ts +0 -11
  219. package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.d.ts +0 -8
  220. /package/node_modules/{@herbertgao/resume-from → resume-from}/LICENSE +0 -0
  221. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/contract.js +0 -0
  222. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/entries.d.ts +0 -0
  223. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/entries.js +0 -0
  224. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/index.d.ts +0 -0
  225. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/index.js +0 -0
  226. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/layout.d.ts +0 -0
  227. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/layout.js +0 -0
  228. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/readback.d.ts +0 -0
  229. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/readback.js +0 -0
  230. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/redaction.d.ts +0 -0
  231. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/validation.d.ts +0 -0
  232. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/validation.js +0 -0
  233. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/contract.js +0 -0
  234. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/index.d.ts +0 -0
  235. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/readback.d.ts +0 -0
  236. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/readback.js +0 -0
  237. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/redaction.d.ts +0 -0
  238. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/validation.d.ts +0 -0
  239. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/validation.js +0 -0
  240. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/contract.js +0 -0
  241. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/adapter.d.ts +0 -0
  242. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/contract.js +0 -0
  243. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/format.d.ts +0 -0
  244. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/format.js +0 -0
  245. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/index.d.ts +0 -0
  246. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/index.js +0 -0
  247. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/parse.d.ts +0 -0
  248. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/parse.js +0 -0
  249. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/redaction.d.ts +0 -0
  250. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/validate.d.ts +0 -0
  251. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/validate.js +0 -0
  252. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/bin.d.ts +0 -0
  253. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/bin.js +0 -0
  254. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/contract.d.ts +0 -0
  255. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/contract.js +0 -0
  256. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/agents.d.ts +0 -0
  257. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/agents.js +0 -0
  258. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/args.d.ts +0 -0
  259. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/args.js +0 -0
  260. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/contract.d.ts +0 -0
  261. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/contract.js +0 -0
  262. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/index.d.ts +0 -0
  263. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/index.js +0 -0
  264. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/presentation.d.ts +0 -0
  265. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/presentation.js +0 -0
  266. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/runner.d.ts +0 -0
  267. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/contract.js +0 -0
  268. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/entry.d.ts +0 -0
  269. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/entry.js +0 -0
  270. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/index.d.ts +0 -0
  271. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/index.js +0 -0
  272. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/command.d.ts +0 -0
  273. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/contract.d.ts +0 -0
  274. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/contract.js +0 -0
  275. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/index.d.ts +0 -0
  276. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/index.js +0 -0
  277. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/picker.d.ts +0 -0
  278. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/picker.js +0 -0
  279. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/presentation.d.ts +0 -0
  280. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/presentation.js +0 -0
  281. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/register.d.ts +0 -0
  282. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/register.js +0 -0
  283. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/ui.d.ts +0 -0
  284. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/ui.js +0 -0
  285. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/profile.d.ts +0 -0
  286. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/profile.js +0 -0
  287. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/registry.d.ts +0 -0
  288. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/registry.js +0 -0
  289. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/contract.js +0 -0
  290. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/contract.js +0 -0
  291. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/errors.d.ts +0 -0
  292. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/errors.js +0 -0
  293. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/homes.d.ts +0 -0
  294. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/homes.js +0 -0
  295. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/index.d.ts +0 -0
  296. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/index.js +0 -0
  297. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/ordering.d.ts +0 -0
  298. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/ordering.js +0 -0
  299. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/errors.d.ts +0 -0
  300. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/errors.js +0 -0
  301. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/index.d.ts +0 -0
  302. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/index.js +0 -0
  303. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/contract.js +0 -0
  304. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/errors.d.ts +0 -0
  305. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/errors.js +0 -0
  306. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/handover.d.ts +0 -0
  307. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/handover.js +0 -0
  308. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/index.d.ts +0 -0
  309. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/index.js +0 -0
  310. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/lander.d.ts +0 -0
  311. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/marker.d.ts +0 -0
  312. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/marker.js +0 -0
  313. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/contract.d.ts +0 -0
  314. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/contract.js +0 -0
  315. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/format.d.ts +0 -0
  316. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/format.js +0 -0
  317. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/index.d.ts +0 -0
  318. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/index.js +0 -0
  319. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/contract.d.ts +0 -0
  320. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/contract.js +0 -0
  321. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/index.d.ts +0 -0
  322. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/index.js +0 -0
  323. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/rules.d.ts +0 -0
  324. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/rules.js +0 -0
  325. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/wiring.d.ts +0 -0
  326. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/index.d.ts +0 -0
  327. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/index.js +0 -0
  328. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/contract.d.ts +0 -0
  329. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/contract.js +0 -0
  330. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/defaults.d.ts +0 -0
  331. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/defaults.js +0 -0
  332. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/errors.d.ts +0 -0
  333. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/errors.js +0 -0
  334. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/index.d.ts +0 -0
  335. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/index.js +0 -0
  336. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/loader.d.ts +0 -0
  337. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/loader.js +0 -0
  338. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/paths.d.ts +0 -0
  339. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/paths.js +0 -0
  340. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/validate.d.ts +0 -0
  341. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/validate.js +0 -0
  342. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/contract.js +0 -0
  343. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/index.d.ts +0 -0
  344. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/index.js +0 -0
  345. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/contract.d.ts +0 -0
  346. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/contract.js +0 -0
  347. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/file-committer.d.ts +0 -0
  348. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/file-committer.js +0 -0
  349. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/index.d.ts +0 -0
  350. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/index.js +0 -0
  351. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/contract.d.ts +0 -0
  352. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/contract.js +0 -0
  353. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/estimator.d.ts +0 -0
  354. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/estimator.js +0 -0
  355. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/index.d.ts +0 -0
  356. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/index.js +0 -0
  357. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/session/contract.js +0 -0
@@ -1,30 +1,22 @@
1
1
  import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Box, Text, truncateToWidth, type KeyId } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
- import pLimit from "p-limit";
5
- import type { ImageContent, TextContent } from "@earendil-works/pi-ai/compat";
6
- import type { ExtractedContent, ExtractOptions } from "./extract.ts";
4
+ import type { ExtractedContent } from "./extract.ts";
7
5
  import { normalizeFetchContentParams } from "./fetch-params.ts";
8
- import { resolveAuthFetchProfile, type AuthFetchProfile } from "./auth-fetch.ts";
9
- import { findContent, type FindMode } from "./content-find.ts";
10
6
  import { answerFromPage } from "./page-query.ts";
11
7
  import { rewriteSearchQuery } from "./query-rewrite.ts";
12
8
  import { clearCloneCache } from "./github-extract.ts";
13
- import { ALL_SEARCH_PROVIDERS, assertSearchProviderSelectionAllowed, getAllowedSearchProviders, getConfiguredSearchRouting, normalizeSearchProviderSelection, providerLabel, RESOLVED_SEARCH_PROVIDERS, search, type AttributedSearchResponse, type ProviderAvailability, type SearchProvider, type SearchProviderSelection, type ResolvedSearchProvider } from "./gemini-search.ts";
9
+ import { ALL_SEARCH_PROVIDERS, getAllowedSearchProviders, getConfiguredSearchRouting, providerLabel, RESOLVED_SEARCH_PROVIDERS, search, type AttributedSearchResponse, type ProviderAvailability, type SearchProvider, type SearchProviderSelection, type ResolvedSearchProvider } from "./gemini-search.ts";
14
10
  export type { ProviderAvailability } from "./gemini-search.ts";
15
- import type { SearchResult } from "./perplexity.ts";
16
- import { formatSeconds, getWebSearchConfigDir, getWebSearchConfigPath, resolveCuratorNetworkConfig, runWithProxy } from "./utils.ts";
11
+ import { formatSeconds, getWebSearchConfigDir, resolveCuratorNetworkConfig, runWithProxy } from "./utils.ts";
17
12
  import {
18
13
  clearResults,
19
14
  deleteResult,
20
15
  generateId,
21
16
  getAllResults,
22
- getResult,
23
17
  restoreFromSession,
24
18
  storeFetchedContentResult,
25
- storeResult,
26
19
  type QueryResultData,
27
- type StoredSearchData,
28
20
  } from "./storage.ts";
29
21
  import { activityMonitor, type ActivityEntry } from "./activity.ts";
30
22
  import { startCuratorServer, type CuratorSearchEntry, type CuratorServerHandle, type IndexedCuratorSearchEntry } from "./curator-server.ts";
@@ -47,9 +39,10 @@ import { isGeminiApiAvailable } from "./gemini-api.ts";
47
39
  import { getActiveGoogleEmail, getGeminiWebAvailabilityDiagnostic, getGeminiWebAvailabilityDiagnosticDetails, isGeminiWebAvailable } from "./gemini-web.ts";
48
40
  import { isBrowserCookieAccessAllowed } from "./gemini-web-config.ts";
49
41
  import { isBraveAvailable } from "./brave.ts";
50
- import { isCurrentModelHostedSearchEligible, isOpenAISearchAvailable } from "./openai-search.ts";
42
+ import { isCurrentModelHostedSearchEligible, isOpenAISearchAvailable, isOpenAISubscriptionModelSelected } from "./openai-search.ts";
51
43
  import { isParallelAvailable } from "./parallel.ts";
52
44
  import { isParallelMcpAvailable } from "./parallel-mcp.ts";
45
+ import { parseStringifiedArrays } from "./tool-arguments.ts";
53
46
  import { isTinyFishAvailable } from "./tinyfish.ts";
54
47
  import { isSearch1APIAvailable } from "./search1api.ts";
55
48
  import { isSearchinfinityAvailable } from "./searchinfinity.ts";
@@ -63,6 +56,7 @@ import { isKagiAvailable } from "./kagi.ts";
63
56
  import { isBochaAvailable } from "./bocha.ts";
64
57
  import { isOllamaAvailable } from "./ollama.ts";
65
58
  import { isSearXNGAvailable } from "./searxng.ts";
59
+ import { isDegoogAvailable } from "./degoog.ts";
66
60
  import { isDuckDuckGoAvailable } from "./duckduckgo.ts";
67
61
  import { isAnySearchAvailable } from "./anysearch.ts";
68
62
  import { isXaiSearchAvailable } from "./xai-search.ts";
@@ -73,6 +67,7 @@ import { isSerpBaseAvailable } from "./serpbase.ts";
73
67
  import { isSerpApiAvailable } from "./serpapi.ts";
74
68
  import { isSerperAvailable } from "./serper.ts";
75
69
  import { isSerplyAvailable } from "./serply.ts";
70
+ import { isKeenableAvailable } from "./keenable.ts";
76
71
  import { isBaizhiAvailable } from "./baizhi.ts";
77
72
  import { isZaiAvailable } from "./zai.ts";
78
73
  import { isValyuAvailable } from "./valyu.ts";
@@ -80,15 +75,38 @@ import { isXcrawlAvailable } from "./xcrawl.ts";
80
75
  import { buildSearchErrorPlan, type SearchErrorDetails, type SearchErrorPlan } from "./render-search-error.ts";
81
76
  import { findModelWithProviderRouting, isModelInScope, splitThinkingSuffix } from "./summary-model-scope.ts";
82
77
  import { registerCuratorRunLifecycle, resolveWebSearchWorkflow, type WebSearchWorkflow } from "./curator-run.ts";
83
- import {
84
- buildResearchArtifact,
85
- withClaimAssessment,
86
- storeResearchArtifact,
87
- getResearchArtifact,
88
- type RecencyFilter,
89
- type ResearchArtifact,
90
- } from "./source-check.ts";
91
78
  import { registerWebToolActivation } from "./tool-activation.ts";
79
+ import {
80
+ createWebToolCore,
81
+ DEFAULT_TOOL_NAMES,
82
+ expandQueryString,
83
+ fetchAllContent,
84
+ fetchUncoveredContent,
85
+ getMaxInlineContentChars,
86
+ isToolEnabled,
87
+ loadConfig,
88
+ markToolError,
89
+ normalizeProviderInput,
90
+ normalizeQueryList,
91
+ normalizeRecencyFilter,
92
+ parseConfigRoot,
93
+ registeredFetchToolNames,
94
+ resolveFetchModeConfig,
95
+ resolveRequestedProvider,
96
+ runSearchQueries,
97
+ storedContentSourceNames,
98
+ stripThumbnails,
99
+ toCuratorProvider,
100
+ WEB_SEARCH_CONFIG_PATH,
101
+ withRegisteredFetchOptions,
102
+ type FetchedContentData,
103
+ type FetchMode,
104
+ type SearchReturnOptions,
105
+ type SummaryWorkflow,
106
+ type ToolNames,
107
+ type WebSearchCallOptions,
108
+ type WebSearchConfig,
109
+ } from "./web-tool-core.ts";
92
110
 
93
111
  // Match pi-ai's StringEnum without loading its compat barrel during registration.
94
112
  function StringEnum<T extends string[]>(values: T, options?: { description?: string; default?: T[number] }) {
@@ -102,33 +120,33 @@ function StringEnum<T extends string[]>(values: T, options?: { description?: str
102
120
 
103
121
  type ExtensionTheme = ExtensionContext["ui"]["theme"];
104
122
 
105
- const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
106
-
107
- let extractModulePromise: Promise<typeof import("./extract.ts")> | undefined;
108
- async function fetchAllContent(
109
- urls: string[],
110
- signal?: AbortSignal,
111
- options?: ExtractOptions,
112
- ): Promise<ExtractedContent[]> {
113
- const extractModule = await (extractModulePromise ??= import("./extract.ts"));
114
- return extractModule.fetchAllContent(urls, signal, options);
115
- }
116
-
117
- function withRegisteredFetchOptions(
118
- options: ExtractOptions | undefined,
119
- toolNames: ExtractOptions["toolNames"],
120
- proxy?: string,
121
- ): ExtractOptions {
122
- return {
123
- ...(options ?? {}),
124
- toolNames,
125
- ...(proxy !== undefined ? { proxy } : {}),
126
- };
127
- }
128
-
129
- function isAbortError(err: unknown): boolean {
130
- return (err instanceof Error ? err.message : String(err)).toLowerCase().includes("abort");
131
- }
123
+ // structuredContent shapes that codemode scripts receive (web-tool-contract.ts),
124
+ // on success and on isError results.
125
+ const nullableString = (description: string) => Type.Union([Type.String(), Type.Null()], { description });
126
+ const webSearchOutputSchema = Type.Object({
127
+ responseId: Type.String({ description: "Id of the stored search results" }),
128
+ fetchId: nullableString("Stored page content id when includeContent fetched or started fetching content, else null"),
129
+ queries: Type.Array(Type.Object({
130
+ query: Type.String(),
131
+ answer: Type.String({ description: "Provider answer text, empty when the provider returned only results" }),
132
+ error: nullableString("Why this query failed, null on success"),
133
+ provider: Type.Optional(Type.String()),
134
+ providers: Type.Optional(Type.Array(Type.String())),
135
+ results: Type.Array(Type.Object({ title: Type.String(), url: Type.String(), snippet: Type.String() })),
136
+ })),
137
+ });
138
+ const fetchContentOutputSchema = Type.Object({
139
+ responseId: nullableString("Id of the stored content, null when nothing was stored"),
140
+ urls: Type.Array(Type.Object({
141
+ url: Type.String(),
142
+ title: Type.String(),
143
+ content: Type.String({ description: "Full extracted content, not limited by the inline character cap" }),
144
+ error: nullableString("Why this URL failed, null on success"),
145
+ mimeType: Type.Optional(Type.String()),
146
+ status: Type.Optional(Type.Number()),
147
+ duration: Type.Optional(Type.Number()),
148
+ })),
149
+ });
132
150
 
133
151
  /** Shared collapsed/expanded renderer for an error/cancel plan produced by
134
152
  * buildSearchErrorPlan(). Used by every tool renderResult's error branch so
@@ -148,59 +166,8 @@ function renderSearchErrorPlan(plan: SearchErrorPlan, expanded: boolean, theme:
148
166
  return box;
149
167
  }
150
168
 
151
- interface WebSearchConfig {
152
- anysearchApiKey?: unknown;
153
- xcrawlApiKey?: unknown;
154
- brightdataApiKey?: unknown;
155
- brightdataSerpZone?: unknown;
156
- kagiApiKey?: unknown;
157
- ollamaApiKey?: unknown;
158
- serpbaseApiKey?: unknown;
159
- serpapiApiKey?: unknown;
160
- serperApiKey?: unknown;
161
- serplyApiKey?: unknown;
162
- youApiKey?: unknown;
163
- baizhiApiKey?: unknown;
164
- zaiApiKey?: unknown;
165
- tinyfishApiKey?: unknown;
166
- valyuApiKey?: unknown;
167
- xaiApiKey?: unknown;
168
- provider?: unknown;
169
- searchProvider?: unknown;
170
- workflow?: string;
171
- curatorTimeoutSeconds?: unknown;
172
- autoOpenBrowser?: unknown;
173
- curatorRemote?: unknown;
174
- summaryModel?: string;
175
- summaryGenerationDeadlineMs?: unknown;
176
- maxInlineContentChars?: unknown;
177
- fetch?: {
178
- defaultMode?: unknown;
179
- allowedModes?: unknown;
180
- };
181
- webSearch?: {
182
- enabled?: boolean;
183
- allowedProviders?: unknown;
184
- };
185
- tools?: Partial<Record<keyof ToolNames, { enabled?: boolean }>>;
186
- toolActivation?: unknown;
187
- commands?: Partial<Record<"websearch" | "curator" | "search" | "google-account", { enabled?: boolean }>>;
188
- toolNames?: Partial<ToolNames>;
189
- shortcuts?: {
190
- curate?: KeyId;
191
- activity?: KeyId;
192
- };
193
- ssrf?: {
194
- /** CIDR ranges exempted from the SSRF guard (e.g. fake-IP proxy ranges). */
195
- allowRanges?: string[];
196
- /** Skip local hostname DNS preflight when an HTTP(S)_PROXY env var applies. */
197
- trustEnvProxy?: boolean;
198
- };
199
- }
200
-
201
169
  type CuratorWorkflow = "summary-review";
202
170
  export type CuratorProvider = SearchProvider;
203
- type SummaryWorkflow = "summary-review" | "auto-summary";
204
171
 
205
172
  interface CuratorBootstrap {
206
173
  availableProviders: ProviderAvailability;
@@ -208,25 +175,6 @@ interface CuratorBootstrap {
208
175
  timeoutSeconds: number;
209
176
  }
210
177
 
211
- function parseConfigRoot(raw: string): Record<string, unknown> {
212
- let parsed: unknown;
213
- try {
214
- parsed = JSON.parse(raw);
215
- } catch (err) {
216
- const message = err instanceof Error ? err.message : String(err);
217
- throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
218
- }
219
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
220
- throw new Error(`Invalid config in ${WEB_SEARCH_CONFIG_PATH}: expected a JSON object`);
221
- }
222
- return parsed as Record<string, unknown>;
223
- }
224
-
225
- function loadConfig(): WebSearchConfig {
226
- if (!existsSync(WEB_SEARCH_CONFIG_PATH)) return {};
227
- return parseConfigRoot(readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8")) as WebSearchConfig;
228
- }
229
-
230
178
  function saveConfig(updates: Partial<WebSearchConfig>): void {
231
179
  let config: Record<string, unknown> = {};
232
180
  if (existsSync(WEB_SEARCH_CONFIG_PATH)) {
@@ -239,57 +187,18 @@ function saveConfig(updates: Partial<WebSearchConfig>): void {
239
187
  writeFileSync(WEB_SEARCH_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
240
188
  }
241
189
 
242
- type ToolNames = {
243
- webSearch: string;
244
- sourceCheck: string;
245
- fetchContent: string;
246
- getSearchContent: string;
247
- };
248
-
249
- const DEFAULT_TOOL_NAMES: ToolNames = {
250
- webSearch: "web_search",
251
- sourceCheck: "source_check",
252
- fetchContent: "fetch_content",
253
- getSearchContent: "get_search_content",
254
- };
255
190
  const TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
256
191
  const DEFAULT_SHORTCUTS = { curate: "ctrl+shift+s", activity: "ctrl+shift+w" } satisfies Record<string, KeyId>;
257
192
  const DEFAULT_CURATOR_TIMEOUT_SECONDS = 20;
258
193
  const DEFAULT_REMOTE_CURATOR_TIMEOUT_SECONDS = 60;
259
194
  const MAX_CURATOR_TIMEOUT_SECONDS = 600;
260
195
  const MAX_SUMMARY_GENERATION_DEADLINE_MS = 600_000;
261
- const SEARCH_QUERY_CONCURRENCY = 3;
262
- const FETCH_MODES = ["readable", "raw", "answer"] as const;
263
- type FetchMode = typeof FETCH_MODES[number];
264
196
  const FETCH_MODE_DESCRIPTIONS: Record<FetchMode, string> = {
265
197
  readable: "extract readable content as markdown",
266
198
  raw: "return the exact textual body using direct HTTP only",
267
199
  answer: "answer a prompt using only fetched content",
268
200
  };
269
201
 
270
- function resolveFetchModeConfig(config: WebSearchConfig): { defaultMode: FetchMode; allowedModes: FetchMode[] } {
271
- const configuredModes = config.fetch?.allowedModes ?? FETCH_MODES;
272
- if (!Array.isArray(configuredModes) || configuredModes.length === 0 || configuredModes.some(mode => !FETCH_MODES.includes(mode as FetchMode))) {
273
- throw new Error(`fetch.allowedModes in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array containing only "readable", "raw", or "answer"`);
274
- }
275
- const allowedModes = configuredModes as FetchMode[];
276
- const duplicateMode = allowedModes.find((mode, index) => allowedModes.indexOf(mode) !== index);
277
- if (duplicateMode) {
278
- throw new Error(`fetch.allowedModes in ${WEB_SEARCH_CONFIG_PATH} must not contain duplicates: "${duplicateMode}"`);
279
- }
280
- const defaultMode = config.fetch?.defaultMode ?? "readable";
281
- if (!allowedModes.includes(defaultMode as FetchMode)) {
282
- throw new Error(`fetch.defaultMode in ${WEB_SEARCH_CONFIG_PATH} must be one of fetch.allowedModes`);
283
- }
284
- return { defaultMode: defaultMode as FetchMode, allowedModes };
285
- }
286
-
287
- // Limit each batch independently so separate Pi tool calls can still run in parallel.
288
- function runSearchQueries<T>(queries: string[], run: (query: string, index: number) => Promise<T>): Promise<T[]> {
289
- const limit = pLimit(SEARCH_QUERY_CONCURRENCY);
290
- return Promise.all(queries.map((query, index) => limit(() => run(query, index))));
291
- }
292
-
293
202
  // Keep primary query indexes stable for curator slots, then interleave additional
294
203
  // provider entries deterministically instead of assigning by completion order.
295
204
  function curatorResultIndex(queryIndex: number, entryIndex: number, queryCount: number): number {
@@ -307,23 +216,10 @@ function searchProviderSchema(description: string, allowedProviders: readonly Re
307
216
  ], { description });
308
217
  }
309
218
 
310
- function isToolEnabled(config: WebSearchConfig, key: keyof ToolNames): boolean {
311
- const override = config.tools?.[key]?.enabled;
312
- if (typeof override === "boolean") return override;
313
- return key !== "webSearch" && key !== "sourceCheck" || config.webSearch?.enabled !== false;
314
- }
315
-
316
219
  function isCommandEnabled(config: WebSearchConfig, name: "websearch" | "curator" | "search" | "google-account"): boolean {
317
220
  return config.commands?.[name]?.enabled !== false;
318
221
  }
319
222
 
320
- function joinToolNames(names: string[]): string {
321
- if (names.length === 0) return "stored content";
322
- if (names.length === 1) return names[0];
323
- if (names.length === 2) return `${names[0]} or ${names[1]}`;
324
- return `${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`;
325
- }
326
-
327
223
  function resolveToolNames(config: WebSearchConfig): ToolNames {
328
224
  if (config.toolNames !== undefined && (!config.toolNames || typeof config.toolNames !== "object" || Array.isArray(config.toolNames))) {
329
225
  throw new Error(`toolNames in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
@@ -362,40 +258,12 @@ function loadConfigForExtensionInit(): WebSearchConfig {
362
258
  }
363
259
  }
364
260
 
365
- function normalizeProviderInput(value: unknown, label = "provider"): SearchProviderSelection | undefined {
366
- if (value === undefined) return undefined;
367
- return normalizeSearchProviderSelection(value, label);
368
- }
369
-
370
- function resolveRequestedProvider(requested: unknown): SearchProviderSelection {
371
- const normalizedRequested = normalizeProviderInput(requested);
372
- if (normalizedRequested && normalizedRequested !== "auto") {
373
- assertSearchProviderSelectionAllowed(normalizedRequested, "Requested provider");
374
- return normalizedRequested;
375
- }
376
- const config = loadConfig();
377
- const provider = normalizeProviderInput(config.searchProvider ?? config.provider, `provider in ${WEB_SEARCH_CONFIG_PATH}`) ?? "auto";
378
- assertSearchProviderSelectionAllowed(provider, `configured provider in ${WEB_SEARCH_CONFIG_PATH}`);
379
- return provider;
380
- }
381
-
382
- function toCuratorProvider(provider: SearchProviderSelection): SearchProvider | undefined {
383
- if (Array.isArray(provider)) return "all";
384
- return provider === "auto" ? undefined : provider;
385
- }
386
-
387
261
  function resolveCuratorSearchProvider(requested: unknown, current: SearchProviderSelection): SearchProviderSelection {
388
262
  const normalized = normalizeProviderInput(requested);
389
263
  if (!normalized || normalized === "auto") return current;
390
264
  return normalized === "all" && Array.isArray(current) ? current : normalized;
391
265
  }
392
266
 
393
- function normalizeRecencyFilter(value: unknown): RecencyFilter | undefined {
394
- return value === "day" || value === "week" || value === "month" || value === "year"
395
- ? value
396
- : undefined;
397
- }
398
-
399
267
  function normalizeCuratorTimeoutSeconds(value: unknown): number | undefined {
400
268
  if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
401
269
  const normalized = Math.floor(value);
@@ -403,43 +271,6 @@ function normalizeCuratorTimeoutSeconds(value: unknown): number | undefined {
403
271
  return Math.min(normalized, MAX_CURATOR_TIMEOUT_SECONDS);
404
272
  }
405
273
 
406
- function normalizeQueryList(queryList: unknown[]): string[] {
407
- const normalized: string[] = [];
408
- for (const query of queryList) {
409
- if (typeof query !== "string") continue;
410
- const trimmed = query.trim();
411
- if (trimmed.length > 0) normalized.push(trimmed);
412
- }
413
- return normalized;
414
- }
415
-
416
- // Some local models serialize a multi-query list into the single-string `query`
417
- // field as a JSON array (query: "[\"a\", \"b\"]") instead of using the
418
- // `queries` parameter. Forwarding that raw string verbatim makes every backend
419
- // search for the literal array text and return zero results, with no signal to
420
- // the model that its argument shape was wrong. Expand a string that parses as a
421
- // JSON array of strings so each element is searched independently.
422
- function expandQueryString(query: unknown): string[] {
423
- if (typeof query !== "string") return [];
424
- const trimmed = query.trim();
425
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
426
- try {
427
- const parsed: unknown = JSON.parse(trimmed);
428
- // Only expand an unambiguously string-only array. Mixed or non-string
429
- // arrays are kept as the literal query so we never silently drop
430
- // members or collapse them into an empty search.
431
- if (Array.isArray(parsed) && parsed.every((entry): entry is string => typeof entry === "string")) {
432
- return parsed
433
- .map((entry) => entry.trim())
434
- .filter((entry) => entry.length > 0);
435
- }
436
- } catch {
437
- // Not JSON — treat as a literal query string.
438
- }
439
- }
440
- return [query];
441
- }
442
-
443
274
  function getCuratorTimeoutSeconds(): number {
444
275
  const source = loadConfig();
445
276
  const explicit = normalizeCuratorTimeoutSeconds(source.curatorTimeoutSeconds);
@@ -456,6 +287,13 @@ export function getSummaryGenerationDeadlineMs(): number {
456
287
  return Math.min(value, MAX_SUMMARY_GENERATION_DEADLINE_MS);
457
288
  }
458
289
 
290
+ export function getSummaryInstructions(): string | undefined {
291
+ const value = loadConfig().summaryInstructions;
292
+ if (typeof value !== "string") return undefined;
293
+ const trimmed = value.trim();
294
+ return trimmed.length > 0 ? trimmed : undefined;
295
+ }
296
+
459
297
  function shouldAutoOpenCuratorBrowser(config: WebSearchConfig): boolean {
460
298
  if (config.autoOpenBrowser === false) return false;
461
299
  if (resolveCuratorNetworkConfig().enabled && config.autoOpenBrowser !== true) return false;
@@ -484,6 +322,7 @@ async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderA
484
322
  bocha: allowedProviders.has("bocha") && isBochaAvailable(),
485
323
  ollama: allowedProviders.has("ollama") && isOllamaAvailable(),
486
324
  searxng: allowedProviders.has("searxng") && isSearXNGAvailable(),
325
+ degoog: allowedProviders.has("degoog") && isDegoogAvailable(),
487
326
  duckduckgo: allowedProviders.has("duckduckgo") && isDuckDuckGoAvailable(),
488
327
  perplexity: allowedProviders.has("perplexity") && isPerplexityAvailable(),
489
328
  exa: allowedProviders.has("exa") && isExaAvailable(),
@@ -501,6 +340,7 @@ async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderA
501
340
  baizhi: allowedProviders.has("baizhi") && isBaizhiAvailable(),
502
341
  zai: allowedProviders.has("zai") && isZaiAvailable(),
503
342
  valyu: allowedProviders.has("valyu") && isValyuAvailable(),
343
+ keenable: allowedProviders.has("keenable") && isKeenableAvailable(),
504
344
  };
505
345
  return {
506
346
  all: ALL_SEARCH_PROVIDERS.some(provider => provider === "gemini" ? geminiApiAvail : providers[provider]),
@@ -516,10 +356,6 @@ async function getOptionalGeminiWebAvailability() {
516
356
  }
517
357
  }
518
358
 
519
- function shouldUseOpenAICodexDefault(ctx?: Pick<ExtensionContext, "model">): boolean {
520
- return ctx?.model?.provider === "openai-codex";
521
- }
522
-
523
359
  function shouldPreferOpenAI(options: Pick<PendingCurate, "numResults" | "recencyFilter"> | undefined, preferOpenAICodexDefault: boolean): boolean {
524
360
  if (options?.recencyFilter) return false;
525
361
  if (typeof options?.numResults === "number" && Number.isFinite(options.numResults) && Math.floor(options.numResults) !== 5) {
@@ -546,10 +382,10 @@ async function loadCuratorBootstrap(
546
382
  export function resolveCuratorDefaultProvider(
547
383
  provider: SearchProviderSelection,
548
384
  available: ProviderAvailability,
549
- ctx?: Pick<ExtensionContext, "model">,
385
+ ctx?: Parameters<typeof isOpenAISubscriptionModelSelected>[0],
550
386
  options?: Pick<PendingCurate, "numResults" | "recencyFilter">,
551
387
  ): SearchProvider {
552
- return resolveProvider(provider, available, options, shouldUseOpenAICodexDefault(ctx), ctx);
388
+ return resolveProvider(provider, available, options, isOpenAISubscriptionModelSelected(ctx), ctx);
553
389
  }
554
390
 
555
391
  function firstAvailableProvider(available: ProviderAvailability, preferOpenAI: boolean, fallback: ResolvedSearchProvider): ResolvedSearchProvider | "auto" {
@@ -614,6 +450,7 @@ interface PendingCurate {
614
450
  numResults?: number;
615
451
  recencyFilter?: "day" | "week" | "month" | "year";
616
452
  domainFilter?: string[];
453
+ category?: string;
617
454
  availableProviders: ProviderAvailability;
618
455
  defaultProvider: SearchProvider;
619
456
  searchProvider: SearchProviderSelection;
@@ -631,194 +468,6 @@ interface PendingCurate {
631
468
  browserOpenError?: string;
632
469
  }
633
470
 
634
-
635
- const DEFAULT_MAX_INLINE_CONTENT_CHARS = 30_000;
636
- const MIN_INLINE_CONTENT_CHARS = 1_000;
637
- const MAX_INLINE_CONTENT_CHARS = 200_000;
638
-
639
- function getMaxInlineContentChars(config = loadConfig()): number {
640
- const value = config.maxInlineContentChars;
641
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < MIN_INLINE_CONTENT_CHARS) {
642
- return DEFAULT_MAX_INLINE_CONTENT_CHARS;
643
- }
644
- return Math.min(value, MAX_INLINE_CONTENT_CHARS);
645
- }
646
-
647
- function stripThumbnails(results: ExtractedContent[]): ExtractedContent[] {
648
- return results.map(({ thumbnail, frames, ...rest }) => rest);
649
- }
650
-
651
- function storeFetchResult(pi: { appendEntry(type: string, data: unknown): void }, responseId: string, data: StoredSearchData & { type: "fetch"; urls: ExtractedContent[] }, authProfile?: AuthFetchProfile): boolean {
652
- if (authProfile?.cache === "off") return false;
653
- pi.appendEntry("web-search-results", storeFetchedContentResult(responseId, data));
654
- return true;
655
- }
656
-
657
- function initialContentSlice(content: string, maxChars: number): {
658
- text: string;
659
- endOffset: number;
660
- totalBytes: number;
661
- totalLines: number;
662
- shownBytes: number;
663
- shownLines: number;
664
- } {
665
- let endOffset = Math.min(content.length, maxChars);
666
- if (endOffset < content.length) {
667
- const lineBreak = content.lastIndexOf("\n", endOffset);
668
- if (lineBreak >= Math.floor(maxChars * 0.8)) endOffset = lineBreak + 1;
669
- }
670
- const text = content.slice(0, endOffset);
671
- return {
672
- text,
673
- endOffset,
674
- totalBytes: Buffer.byteLength(content),
675
- totalLines: content.length === 0 ? 0 : content.split("\n").length,
676
- shownBytes: Buffer.byteLength(text),
677
- shownLines: text.length === 0 ? 0 : text.split("\n").length,
678
- };
679
- }
680
-
681
- function normalizeFindQueries(value: string | string[]): string[] {
682
- const queries = (Array.isArray(value) ? value : [value]).map(query => query.trim()).filter(Boolean);
683
- if (queries.length === 0) throw new Error("findText must contain at least one non-empty string");
684
- return queries;
685
- }
686
-
687
- interface GetSearchContentParams {
688
- responseId: string;
689
- query?: string;
690
- queryIndex?: number;
691
- url?: string;
692
- urlIndex?: number;
693
- offset?: number;
694
- limit?: number;
695
- findText?: string | string[];
696
- findMode?: FindMode;
697
- }
698
-
699
- type RawGetSearchContentParams = Omit<GetSearchContentParams, "findMode"> & { findMode?: unknown };
700
-
701
- function normalizeFindMode(value: unknown): FindMode | undefined {
702
- if (value === undefined) return undefined;
703
- if (value === "exact" || value === "case-insensitive" || value === "fuzzy") return value;
704
- throw new Error('findMode must be "exact", "case-insensitive", or "fuzzy"');
705
- }
706
-
707
- function normalizeGetSearchContentParams(params: RawGetSearchContentParams): GetSearchContentParams {
708
- const normalized: GetSearchContentParams = { ...params, findMode: normalizeFindMode(params.findMode) };
709
-
710
- if (normalized.query?.trim() === "") delete normalized.query;
711
- if (normalized.url?.trim() === "") delete normalized.url;
712
-
713
- if (normalized.findText !== undefined) {
714
- delete normalized.offset;
715
- delete normalized.limit;
716
- }
717
-
718
- return normalized;
719
- }
720
-
721
- function formatInputValue(value: unknown): string {
722
- if (typeof value === "string") return JSON.stringify(value);
723
- if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
724
- try {
725
- const serialized = JSON.stringify(value);
726
- return serialized === undefined ? String(value) : serialized;
727
- } catch {
728
- return String(value);
729
- }
730
- }
731
-
732
- function formatSearchSummary(results: SearchResult[], answer: string): string {
733
- if (results.length === 0) {
734
- return answer ? `${answer}\n\n---\n\n**Sources:**\nNo sources returned.` : "No results found.";
735
- }
736
- let output = answer ? `${answer}\n\n---\n\n**Sources:**\n` : "";
737
- output += results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}`).join("\n\n");
738
- return output;
739
- }
740
-
741
- function formatSourceCheckResult(artifact: ResearchArtifact, getSearchContentTool: string | null = DEFAULT_TOOL_NAMES.getSearchContent): string {
742
- const assessment = artifact.claims?.[0];
743
- const lines = [`# Source check: ${artifact.query}`, ""];
744
- if (assessment) {
745
- lines.push(`**Status:** ${assessment.status} (confidence ${assessment.confidence.toFixed(2)})`);
746
- lines.push(`**Rationale:** ${assessment.rationale}`);
747
- if (assessment.supporting_passages.length > 0) lines.push(`**Supporting passages:** ${assessment.supporting_passages.join(", ")}`);
748
- if (assessment.contradicting_passages.length > 0) lines.push(`**Contradicting passages:** ${assessment.contradicting_passages.join(", ")}`);
749
- lines.push("");
750
- }
751
- if (artifact.sources.length > 0) {
752
- lines.push("## Sources");
753
- for (const source of artifact.sources) lines.push(`${source.rank}. [${source.quality}] ${source.title}\n ${source.url}`);
754
- lines.push("");
755
- }
756
- if (artifact.errors?.length) lines.push(`Search errors: ${artifact.errors.map((entry) => `${entry.query}: ${entry.error}`).join("; ")}`);
757
- lines.push(getSearchContentTool
758
- ? `Artifact responseId: ${artifact.id} (retrievable via ${getSearchContentTool}).`
759
- : `Artifact responseId: ${artifact.id}. Content retrieval is not registered.`);
760
- return lines.join("\n");
761
- }
762
-
763
- function duplicateQuerySet(results: QueryResultData[]): Set<string> {
764
- const counts = new Map<string, number>();
765
- for (const result of results) {
766
- counts.set(result.query, (counts.get(result.query) ?? 0) + 1);
767
- }
768
- const duplicates = new Set<string>();
769
- for (const [query, count] of counts) {
770
- if (count > 1) duplicates.add(query);
771
- }
772
- return duplicates;
773
- }
774
-
775
- function formatQueryHeader(query: string, provider: string | undefined, duplicateQueries: Set<string>): string {
776
- const suffix = duplicateQueries.has(query) && provider ? ` (${provider})` : "";
777
- return `## Query: "${query}"${suffix}\n\n`;
778
- }
779
-
780
- function hasFullInlineCoverage(urls: string[], inlineContent: ExtractedContent[] | undefined): boolean {
781
- if (!inlineContent || inlineContent.length === 0) return false;
782
- const coveredUrls = new Set(inlineContent.map(c => c.url));
783
- return urls.every(url => coveredUrls.has(url));
784
- }
785
-
786
- function formatFullResults(queryData: QueryResultData): string {
787
- let output = `## Results for: "${queryData.query}"\n\n`;
788
- const providers = queryData.providers ?? (queryData.provider ? [queryData.provider] : []);
789
- if (providers.length > 0) output += `**Provider${providers.length === 1 ? "" : "s"}:** ${providers.join(", ")}\n\n`;
790
- if (queryData.answer) {
791
- output += `${queryData.answer}\n\n---\n\n`;
792
- }
793
- for (const r of queryData.results) {
794
- output += `### ${r.title}\n${r.url}${r.snippet ? `\n\n${r.snippet}` : ""}\n\n`;
795
- }
796
- return output;
797
- }
798
-
799
- function boundSearchPresentation(
800
- text: string,
801
- guidance: string,
802
- truncationGuidance: string,
803
- maxChars: number,
804
- ): { text: string; truncated: boolean; originalChars: number; returnedChars: number; omittedChars: number } {
805
- const fullText = `${text}${guidance}`;
806
- if (fullText.length <= maxChars) {
807
- return { text: fullText, truncated: false, originalChars: text.length, returnedChars: text.length, omittedChars: 0 };
808
- }
809
- const marker = `\n\n---\n[Output truncated.]${truncationGuidance}`;
810
- // Reserve marker space so the complete model-visible response never exceeds the configured ceiling.
811
- const prefixLength = maxChars - marker.length;
812
- const bounded = `${text.slice(0, prefixLength)}${marker}`;
813
- return {
814
- text: bounded,
815
- truncated: true,
816
- originalChars: text.length,
817
- returnedChars: prefixLength,
818
- omittedChars: text.length - prefixLength,
819
- };
820
- }
821
-
822
471
  function abortPendingFetches(): void {
823
472
  for (const controller of pendingFetches.values()) {
824
473
  controller.abort();
@@ -1094,17 +743,21 @@ export default function (pi: ExtensionAPI) {
1094
743
  const sourceCheckEnabled = isToolEnabled(initConfig, "sourceCheck");
1095
744
  const fetchContentEnabled = isToolEnabled(initConfig, "fetchContent");
1096
745
  const getSearchContentEnabled = isToolEnabled(initConfig, "getSearchContent");
746
+ const coreSettings = {
747
+ toolNames,
748
+ enabledTools: {
749
+ webSearch: webSearchEnabled,
750
+ sourceCheck: sourceCheckEnabled,
751
+ fetchContent: fetchContentEnabled,
752
+ getSearchContent: getSearchContentEnabled,
753
+ },
754
+ maxInlineContentChars: getMaxInlineContentChars(initConfig),
755
+ fetchModes: fetchModeConfig,
756
+ };
1097
757
  // Names as registered this session, so fetch failure guidance never points
1098
758
  // at tools that are disabled or were renamed after init.
1099
- const registeredToolNames = {
1100
- ...(webSearchEnabled ? { webSearch: toolNames.webSearch } : {}),
1101
- ...(fetchContentEnabled ? { fetchContent: toolNames.fetchContent } : {}),
1102
- };
1103
- const storedContentSources = joinToolNames([
1104
- ...(webSearchEnabled ? [toolNames.webSearch] : []),
1105
- ...(sourceCheckEnabled ? [toolNames.sourceCheck] : []),
1106
- ...(fetchContentEnabled ? [toolNames.fetchContent] : []),
1107
- ]);
759
+ const registeredToolNames = registeredFetchToolNames(coreSettings);
760
+ const storedContentSources = storedContentSourceNames(coreSettings);
1108
761
  const searchQueryDescription = webSearchEnabled
1109
762
  ? `Get content for this query (${toolNames.webSearch})`
1110
763
  : "Get content for a stored search query";
@@ -1117,13 +770,13 @@ export default function (pi: ExtensionAPI) {
1117
770
  const curateKey = initConfig.shortcuts?.curate || DEFAULT_SHORTCUTS.curate;
1118
771
  const activityKey = initConfig.shortcuts?.activity || DEFAULT_SHORTCUTS.activity;
1119
772
 
1120
- function startBackgroundFetch(urls: string[], proxy?: string): string | null {
773
+ function startBackgroundFetch(urls: string[], proxy?: string, provided?: ExtractedContent[]): string | null {
1121
774
  if (urls.length === 0) return null;
1122
775
  const fetchId = generateId();
1123
776
  const controller = new AbortController();
1124
777
  pendingFetches.set(fetchId, controller);
1125
778
  Promise.resolve()
1126
- .then(() => runWithProxy(proxy, () => fetchAllContent(urls, controller.signal, withRegisteredFetchOptions(undefined, registeredToolNames, proxy))))
779
+ .then(() => fetchUncoveredContent(urls, provided, missing => runWithProxy(proxy, () => fetchAllContent(missing, controller.signal, withRegisteredFetchOptions(undefined, registeredToolNames, proxy)))))
1127
780
  .then((fetched) => {
1128
781
  if (!sessionActive || !pendingFetches.has(fetchId)) return;
1129
782
  const data = {
@@ -1131,7 +784,7 @@ export default function (pi: ExtensionAPI) {
1131
784
  type: "fetch",
1132
785
  timestamp: Date.now(),
1133
786
  urls: stripThumbnails(fetched),
1134
- } satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
787
+ } satisfies FetchedContentData;
1135
788
  pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
1136
789
  const ok = fetched.filter(f => !f.error).length;
1137
790
  const availability = ok === fetched.length
@@ -1167,29 +820,18 @@ export default function (pi: ExtensionAPI) {
1167
820
  return fetchId;
1168
821
  }
1169
822
 
1170
- function storeAndPublishSearch(results: QueryResultData[]): string {
1171
- const id = generateId();
1172
- const data: StoredSearchData = {
1173
- id, type: "search", timestamp: Date.now(), queries: results,
1174
- };
1175
- storeResult(id, data);
1176
- pi.appendEntry("web-search-results", data);
1177
- return id;
1178
- }
1179
-
1180
- interface SearchReturnOptions {
1181
- queryList: string[];
1182
- results: QueryResultData[];
1183
- urls: string[];
1184
- includeContent: boolean;
1185
- inlineContent?: ExtractedContent[];
1186
- curated?: boolean;
1187
- curatedFrom?: number;
1188
- workflow?: SummaryWorkflow;
1189
- approvedSummary?: string;
1190
- summaryMeta?: SummaryMeta;
1191
- proxy?: string;
1192
- }
823
+ const core = createWebToolCore({
824
+ settings: coreSettings,
825
+ storeFetchedContent(id, data) {
826
+ pi.appendEntry("web-search-results", storeFetchedContentResult(id, data));
827
+ },
828
+ publishResult(data) {
829
+ pi.appendEntry("web-search-results", data);
830
+ },
831
+ startBackgroundFetch,
832
+ answerFromPage: (request, ctx, signal) => answerFromPage(request, ctx!, signal),
833
+ });
834
+ const buildSearchReturn = core.buildSearchReturn;
1193
835
 
1194
836
  function normalizeSummaryMeta(meta: SummaryMeta | undefined, summaryText: string): SummaryMeta {
1195
837
  const normalizedText = summaryText.trim();
@@ -1279,6 +921,7 @@ export default function (pi: ExtensionAPI) {
1279
921
  feedback,
1280
922
  undefined,
1281
923
  getSummaryGenerationDeadlineMs(),
924
+ getSummaryInstructions(),
1282
925
  );
1283
926
  } catch (err) {
1284
927
  const isEmptyResponse = err instanceof Error && err.message.includes("Summary model returned empty response");
@@ -1391,127 +1034,6 @@ export default function (pi: ExtensionAPI) {
1391
1034
  };
1392
1035
  }
1393
1036
 
1394
- function buildSearchReturn(opts: SearchReturnOptions): AgentToolResult<Record<string, unknown>> {
1395
- const sc = opts.results.filter(r => !r.error).length;
1396
- const tr = opts.results.reduce((sum, r) => sum + r.results.length, 0);
1397
-
1398
- const hasApprovedSummary = typeof opts.approvedSummary === "string" && opts.approvedSummary.trim().length > 0;
1399
- const maxInlineContentChars = getMaxInlineContentChars(initConfig);
1400
- let output = "";
1401
- if (hasApprovedSummary) {
1402
- output = opts.approvedSummary!.trim();
1403
- } else {
1404
- if (opts.curated) {
1405
- output += "[These results were manually curated by the user in the browser. Use them as-is — do not re-search or discard.]\n\n";
1406
- }
1407
- const providerNames = opts.results.map(result => {
1408
- const providers = result.providers ?? (result.provider ? [result.provider] : []);
1409
- return providers.join(", ") || "unknown";
1410
- });
1411
- output += opts.results.length === 1
1412
- ? `**Provider:** ${providerNames[0]}\n\n`
1413
- : `**Providers used:** ${providerNames.map((name, index) => `Query ${index + 1}: ${name}`).join("; ")}\n\n`;
1414
- const duplicateQueries = opts.curated ? duplicateQuerySet(opts.results) : new Set<string>();
1415
- for (const { query, answer, results, error, provider } of opts.results) {
1416
- if (opts.queryList.length > 1) {
1417
- output += opts.curated
1418
- ? formatQueryHeader(query, provider, duplicateQueries)
1419
- : `## Query: "${query}"\n\n`;
1420
- }
1421
- if (error) output += `Error: ${error}\n\n`;
1422
- else output += formatSearchSummary(results, answer) + "\n\n";
1423
- }
1424
- }
1425
-
1426
- const hasInlineReady = hasFullInlineCoverage(opts.urls, opts.inlineContent);
1427
- let fetchId: string | null = null;
1428
- if (hasInlineReady && opts.inlineContent) {
1429
- fetchId = generateId();
1430
- const data = {
1431
- id: fetchId,
1432
- type: "fetch",
1433
- timestamp: Date.now(),
1434
- urls: opts.inlineContent,
1435
- } satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
1436
- pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
1437
- } else if (opts.includeContent) {
1438
- fetchId = startBackgroundFetch(opts.urls, opts.proxy);
1439
- }
1440
-
1441
- const searchId = storeAndPublishSearch(opts.results);
1442
- const isBackgroundFetch = fetchId !== null && !hasInlineReady;
1443
- const unboundedPresentation = output.trim();
1444
- const buildGuidance = (forTruncation: boolean): string => {
1445
- let value = "";
1446
- if (hasInlineReady && opts.inlineContent && fetchId) {
1447
- value += `\n---\nFull content for ${opts.inlineContent.length} sources is ready as responseId "${fetchId}". `;
1448
- value += getSearchContentEnabled
1449
- ? `Use ${toolNames.getSearchContent}({ responseId: "${fetchId}", urlIndex: 0, offset: 0, limit: ${maxInlineContentChars} }) to retrieve the first bounded page.`
1450
- : forTruncation ? `Enable ${toolNames.getSearchContent} to retrieve the full stored content.` : "";
1451
- } else if (isBackgroundFetch && fetchId) {
1452
- value += `\n---\nContent fetching in background as responseId "${fetchId}". Will notify when ready.`;
1453
- }
1454
- if (getSearchContentEnabled || forTruncation) {
1455
- value += `\n---\nFull search results are stored as responseId "${searchId}". `;
1456
- value += getSearchContentEnabled
1457
- ? `Use ${toolNames.getSearchContent}({ responseId: "${searchId}", queryIndex: 0, offset: 0, limit: ${maxInlineContentChars} }) to retrieve the first bounded page${opts.results.length > 1 ? `; repeat with queryIndex 1 through ${opts.results.length - 1}` : ""}.`
1458
- : `Enable ${toolNames.getSearchContent} to retrieve the full stored results.`;
1459
- }
1460
- return value;
1461
- };
1462
- const presentation = hasApprovedSummary
1463
- ? { text: unboundedPresentation, truncated: false, originalChars: unboundedPresentation.length, returnedChars: unboundedPresentation.length, omittedChars: 0 }
1464
- : boundSearchPresentation(unboundedPresentation, buildGuidance(false), buildGuidance(true), maxInlineContentChars);
1465
-
1466
- return {
1467
- content: [{ type: "text", text: presentation.text }],
1468
- details: {
1469
- queries: opts.queryList,
1470
- queryCount: opts.queryList.length,
1471
- successfulQueries: sc,
1472
- totalResults: tr,
1473
- includeContent: opts.includeContent,
1474
- fetchId,
1475
- fetchUrls: isBackgroundFetch ? opts.urls : undefined,
1476
- searchId,
1477
- queryProviders: opts.results.map(result => ({
1478
- query: result.query,
1479
- providers: result.providers ?? (result.provider ? [result.provider] : []),
1480
- })),
1481
- truncated: presentation.truncated,
1482
- originalChars: presentation.originalChars,
1483
- returnedChars: presentation.returnedChars,
1484
- omittedChars: presentation.omittedChars,
1485
- ...(opts.curated ? {
1486
- curated: true,
1487
- curatedFrom: opts.curatedFrom,
1488
- curatedQueries: opts.results.map(r => ({
1489
- query: r.query,
1490
- provider: r.provider || null,
1491
- answer: r.answer || null,
1492
- sources: r.results.map(s => ({ title: s.title, url: s.url })),
1493
- error: r.error,
1494
- })),
1495
- } : {}),
1496
- ...((opts.workflow && hasApprovedSummary)
1497
- ? {
1498
- summary: {
1499
- text: opts.approvedSummary!.trim(),
1500
- workflow: opts.workflow,
1501
- model: opts.summaryMeta?.model ?? null,
1502
- durationMs: opts.summaryMeta?.durationMs ?? 0,
1503
- tokenEstimate: opts.summaryMeta?.tokenEstimate ?? 0,
1504
- fallbackUsed: opts.summaryMeta?.fallbackUsed === true,
1505
- fallbackReason: opts.summaryMeta?.fallbackReason,
1506
- phase: opts.summaryMeta?.phase,
1507
- edited: opts.summaryMeta?.edited === true,
1508
- },
1509
- }
1510
- : {}),
1511
- },
1512
- };
1513
- }
1514
-
1515
1037
  function filterByQueryIndices(selectedQueryIndices: number[], results: Map<number, QueryResultData>) {
1516
1038
  const filteredResults: QueryResultData[] = [];
1517
1039
  const filteredUrls: string[] = [];
@@ -1689,6 +1211,7 @@ export default function (pi: ExtensionAPI) {
1689
1211
  recencyFilter: pc.recencyFilter,
1690
1212
  domainFilter: pc.domainFilter,
1691
1213
  includeContent: pc.includeContent,
1214
+ category: pc.category,
1692
1215
  signal: addSearchSignal,
1693
1216
  extensionContext: ctx,
1694
1217
  });
@@ -1813,11 +1336,20 @@ export default function (pi: ExtensionAPI) {
1813
1336
  },
1814
1337
  });
1815
1338
 
1816
- pi.on("session_start", async (_event, ctx) => handleSessionChange(ctx));
1817
- pi.on("session_tree", async (_event, ctx) => handleSessionChange(ctx));
1339
+ // web_search calls a tool made, such as a codemode script, reach the tool_call
1340
+ // handlers before execute with parentToolCallId set; execute consumes the id.
1341
+ // Ids of calls a later handler blocked are dropped when the session changes.
1342
+ const nestedSearchCallIds = new Set<string>();
1343
+ if (webSearchEnabled) pi.on("tool_call", (event) => {
1344
+ if (event.parentToolCallId && event.toolName === toolNames.webSearch) nestedSearchCallIds.add(event.toolCallId);
1345
+ });
1346
+
1347
+ pi.on("session_start", async (_event, ctx) => { nestedSearchCallIds.clear(); handleSessionChange(ctx); });
1348
+ pi.on("session_tree", async (_event, ctx) => { nestedSearchCallIds.clear(); handleSessionChange(ctx); });
1818
1349
 
1819
1350
  pi.on("session_shutdown", () => {
1820
1351
  sessionActive = false;
1352
+ nestedSearchCallIds.clear();
1821
1353
  abortPendingFetches();
1822
1354
  closeCurator();
1823
1355
  clearCloneCache();
@@ -1832,6 +1364,7 @@ export default function (pi: ExtensionAPI) {
1832
1364
  if (webSearchEnabled) pi.registerTool({
1833
1365
  name: toolNames.webSearch,
1834
1366
  label: "Web Search",
1367
+ prepareArguments: (args) => parseStringifiedArrays(args, ["provider", "queries", "domainFilter"]) as never,
1835
1368
  description:
1836
1369
  `Search the web with ${allowedSearchProviders.map(providerLabel).join(", ")}. Provider arrays run simultaneously; ${allPolicyDescription}. The default workflow is none: it returns bounded source-linked search results or provider answers without a curator or generated summary, identifies the providers used, and stores full results for retrieval by responseId. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query. When includeContent is true, full page content is fetched in the background. Set workflow to "summary-review" to open the curator with an auto-generated summary draft or "auto-summary" to generate a summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it.`,
1837
1370
  promptSnippet:
@@ -1845,6 +1378,7 @@ export default function (pi: ExtensionAPI) {
1845
1378
  StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" }),
1846
1379
  ),
1847
1380
  domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
1381
+ category: Type.Optional(Type.String({ description: "Exa only: result category, e.g. news, research paper" })),
1848
1382
  provider: Type.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}; omit this field to use the configured provider, or use auto when none is configured`, allowedSearchProviders)),
1849
1383
  workflow: Type.Optional(
1850
1384
  StringEnum(["none", "summary-review", "auto-summary"], {
@@ -1855,15 +1389,20 @@ export default function (pi: ExtensionAPI) {
1855
1389
  description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for every outbound request in this call (search APIs and content fetches). Node fetch ignores HTTP(S)_PROXY env vars, so set this (or `proxy` in web-search.json) when direct access is blocked; empty string forces direct access.",
1856
1390
  })),
1857
1391
  }),
1392
+ outputSchema: webSearchOutputSchema,
1858
1393
 
1859
1394
  async execute(callId, params, signal, onUpdate, ctx) {
1860
- return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
1395
+ // Nested calls neither open the curator unless asked nor fetch content in the background.
1396
+ const nested = nestedSearchCallIds.delete(callId);
1397
+ return markToolError(await runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
1861
1398
  const rawQueryList: unknown[] = Array.isArray(params.queries)
1862
1399
  ? params.queries
1863
1400
  : (params.query !== undefined ? expandQueryString(params.query) : []);
1864
1401
  const queryList = normalizeQueryList(rawQueryList);
1865
1402
  const configWorkflow = loadConfigForExtensionInit().workflow;
1866
- const workflow = curatorRunState.resolve(params.workflow, configWorkflow, ctx?.hasUI !== false);
1403
+ const workflow = nested && params.workflow === undefined
1404
+ ? "none"
1405
+ : curatorRunState.resolve(params.workflow, configWorkflow, ctx?.hasUI !== false);
1867
1406
  const shouldCurate = workflow === "summary-review";
1868
1407
  const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
1869
1408
 
@@ -1929,6 +1468,7 @@ export default function (pi: ExtensionAPI) {
1929
1468
  numResults: params.numResults,
1930
1469
  recencyFilter,
1931
1470
  domainFilter: params.domainFilter,
1471
+ category: params.category,
1932
1472
  availableProviders,
1933
1473
  defaultProvider,
1934
1474
  searchProvider,
@@ -1990,6 +1530,7 @@ export default function (pi: ExtensionAPI) {
1990
1530
  recencyFilter,
1991
1531
  domainFilter: params.domainFilter,
1992
1532
  includeContent: params.includeContent,
1533
+ category: params.category,
1993
1534
  signal: searchSignal,
1994
1535
  extensionContext: ctx,
1995
1536
  });
@@ -2074,102 +1615,41 @@ export default function (pi: ExtensionAPI) {
2074
1615
  return promise;
2075
1616
  }
2076
1617
 
2077
- let completedSearches = 0;
2078
- const allUrls: string[] = [];
2079
- const allInlineContent: ExtractedContent[] = [];
2080
- const resolvedProvider = resolveRequestedProvider(params.provider);
2081
-
2082
- const queryResponses = await runSearchQueries(queryList, async (query) => {
2083
- signal?.throwIfAborted();
2084
- onUpdate?.({
2085
- content: [{ type: "text", text: `Searching "${query}" (${completedSearches}/${queryList.length} complete)...` }],
2086
- details: { phase: "search", progress: completedSearches / queryList.length, currentQuery: query },
2087
- });
2088
-
2089
- try {
2090
- const { answer, results, inlineContent, provider, providerResponses } = await search(query, {
2091
- provider: resolvedProvider,
2092
- numResults: params.numResults,
2093
- recencyFilter,
2094
- domainFilter: params.domainFilter,
2095
- includeContent: params.includeContent,
2096
- signal,
2097
- extensionContext: ctx,
2098
- });
2099
-
2100
- const providers = providerResponses?.map(response => response.provider) ?? [provider];
2101
- return { result: { query, answer, results, error: null, provider, providers } satisfies QueryResultData, inlineContent };
2102
- } catch (err) {
2103
- if (signal?.aborted || isAbortError(err)) throw err;
2104
- const message = err instanceof Error ? err.message : String(err);
2105
- const requestedProvider = toCuratorProvider(resolvedProvider);
2106
- return {
2107
- result: { query, answer: "", results: [], error: message, provider: requestedProvider } satisfies QueryResultData,
2108
- inlineContent: undefined,
2109
- };
2110
- } finally {
2111
- completedSearches++;
2112
- if (!signal?.aborted) {
2113
- onUpdate?.({
2114
- content: [{ type: "text", text: `Completed ${completedSearches}/${queryList.length} searches.` }],
2115
- details: { phase: "search", progress: completedSearches / queryList.length, currentQuery: query },
2116
- });
2117
- }
2118
- }
2119
- });
2120
- const searchResults = queryResponses.map(response => response.result);
2121
- for (const response of queryResponses) {
2122
- for (const result of response.result.results) {
2123
- if (!allUrls.includes(result.url)) allUrls.push(result.url);
2124
- }
2125
- if (response.inlineContent) allInlineContent.push(...response.inlineContent);
2126
- }
2127
-
2128
- let approvedSummary: string | undefined;
2129
- let summaryMeta: SummaryMeta | undefined;
1618
+ let summarize: WebSearchCallOptions["summarize"];
2130
1619
  if (workflow === "auto-summary") {
2131
- if (!ctx) {
2132
- return {
2133
- content: [{ type: "text", text: "Error: Auto-summary requires an active extension context." }],
2134
- details: { error: "Missing extension context" },
1620
+ summarize = async (searchResults) => {
1621
+ if (!ctx) {
1622
+ return {
1623
+ content: [{ type: "text", text: "Error: Auto-summary requires an active extension context." }],
1624
+ details: { error: "Missing extension context" },
1625
+ };
1626
+ }
1627
+ onUpdate?.({
1628
+ content: [{ type: "text", text: "Generating summary..." }],
1629
+ details: { phase: "generating-summary", progress: 1 },
1630
+ });
1631
+ const summaryContext: SummaryGenerationContext = {
1632
+ model: ctx.model,
1633
+ modelRegistry: ctx.modelRegistry,
1634
+ sessionManager: ctx.sessionManager,
1635
+ scopedModels: ctx.scopedModels,
2135
1636
  };
2136
- }
2137
- onUpdate?.({
2138
- content: [{ type: "text", text: "Generating summary..." }],
2139
- details: { phase: "generating-summary", progress: 1 },
2140
- });
2141
- const summaryContext: SummaryGenerationContext = {
2142
- model: ctx.model,
2143
- modelRegistry: ctx.modelRegistry,
2144
- sessionManager: ctx.sessionManager,
2145
- scopedModels: ctx.scopedModels,
1637
+ const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
1638
+ const generated = await generateSummaryDraft(
1639
+ searchResults,
1640
+ summaryContext,
1641
+ signal,
1642
+ summaryModelChoices.defaultSummaryModel ?? undefined,
1643
+ undefined,
1644
+ undefined,
1645
+ getSummaryGenerationDeadlineMs(),
1646
+ getSummaryInstructions(),
1647
+ );
1648
+ return { approvedSummary: generated.summary, summaryMeta: generated.meta };
2146
1649
  };
2147
- const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
2148
- const generated = await generateSummaryDraft(
2149
- searchResults,
2150
- summaryContext,
2151
- signal,
2152
- summaryModelChoices.defaultSummaryModel ?? undefined,
2153
- undefined,
2154
- undefined,
2155
- getSummaryGenerationDeadlineMs(),
2156
- );
2157
- approvedSummary = generated.summary;
2158
- summaryMeta = generated.meta;
2159
1650
  }
2160
-
2161
- return buildSearchReturn({
2162
- queryList,
2163
- results: searchResults,
2164
- urls: allUrls,
2165
- includeContent: params.includeContent ?? false,
2166
- inlineContent: allInlineContent.length > 0 ? allInlineContent : undefined,
2167
- workflow: workflow === "auto-summary" ? "auto-summary" : undefined,
2168
- approvedSummary,
2169
- summaryMeta,
2170
- proxy: typeof params.proxy === "string" ? params.proxy : undefined,
2171
- });
2172
- });
1651
+ return core.webSearch(params, signal, { extensionContext: ctx, onUpdate, summarize, awaitContent: nested });
1652
+ }));
2173
1653
  },
2174
1654
 
2175
1655
  renderCall(args, theme) {
@@ -2444,84 +1924,7 @@ export default function (pi: ExtensionAPI) {
2444
1924
  })),
2445
1925
  }),
2446
1926
  async execute(_callId, params, signal, _onUpdate, ctx) {
2447
- return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
2448
- const claim = typeof params.claim === "string" ? params.claim.trim() : "";
2449
- if (!claim) {
2450
- return { content: [{ type: "text", text: "Error: 'claim' is required." }], details: { error: "Missing claim" } };
2451
- }
2452
-
2453
- const requestedQueries = Array.isArray(params.queries)
2454
- ? params.queries.filter((query): query is string => typeof query === "string").map((query) => query.trim()).filter(Boolean)
2455
- : [];
2456
- const queries = (requestedQueries.length > 0 ? requestedQueries : [claim]).slice(0, 8);
2457
- const numResults = typeof params.numResults === "number" && Number.isFinite(params.numResults)
2458
- ? Math.min(20, Math.max(1, Math.floor(params.numResults)))
2459
- : 5;
2460
- const domainFilter = Array.isArray(params.domainFilter)
2461
- ? params.domainFilter.filter((domain): domain is string => typeof domain === "string")
2462
- : undefined;
2463
- const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
2464
- const resultsByUrl = new Map<string, SearchResult>();
2465
- const summaries: string[] = [];
2466
- const errors: Array<{ query: string; error: string }> = [];
2467
- let provider: string | undefined;
2468
-
2469
- for (const query of queries) {
2470
- if (signal?.aborted) break;
2471
- try {
2472
- const response = await search(query, {
2473
- provider: resolveRequestedProvider(params.provider),
2474
- numResults,
2475
- recencyFilter,
2476
- domainFilter,
2477
- signal,
2478
- extensionContext: ctx,
2479
- });
2480
- if (signal?.aborted) break;
2481
- provider ??= response.provider;
2482
- if (response.answer) summaries.push(`${query}: ${response.answer}`);
2483
- for (const result of response.results) {
2484
- if (!resultsByUrl.has(result.url)) resultsByUrl.set(result.url, result);
2485
- }
2486
- } catch (err) {
2487
- if (signal?.aborted || isAbortError(err)) break;
2488
- errors.push({ query, error: err instanceof Error ? err.message : String(err) });
2489
- }
2490
- }
2491
-
2492
- const results = [...resultsByUrl.values()].slice(0, 20).map((result, index) => ({ ...result, rank: index + 1 }));
2493
- let fetched: ExtractedContent[] = [];
2494
- if (params.fetchContent && results.length > 0) {
2495
- const urls = results.slice(0, 5).map((result) => result.url);
2496
- try {
2497
- fetched = await fetchAllContent(urls, signal, withRegisteredFetchOptions(undefined, registeredToolNames, typeof params.proxy === "string" ? params.proxy : undefined));
2498
- } catch (err) {
2499
- if (signal?.aborted || isAbortError(err)) throw err;
2500
- fetched = urls.map((url) => ({ url, title: "", content: "", error: err instanceof Error ? err.message : String(err) }));
2501
- }
2502
- }
2503
- const artifact = withClaimAssessment(buildResearchArtifact({
2504
- query: claim,
2505
- provider,
2506
- summary: summaries.length > 0 ? summaries.join("\n\n") : undefined,
2507
- results,
2508
- fetched,
2509
- recency: recencyFilter,
2510
- domainFilter,
2511
- }), [claim]);
2512
- if (errors.length > 0) artifact.errors = errors;
2513
- storeResearchArtifact(artifact);
2514
- pi.appendEntry("web-search-results", {
2515
- id: artifact.id,
2516
- type: "research",
2517
- timestamp: artifact.timestamp,
2518
- artifact,
2519
- });
2520
- return {
2521
- content: [{ type: "text", text: formatSourceCheckResult(artifact, getSearchContentEnabled ? toolNames.getSearchContent : null) }],
2522
- details: { responseId: artifact.id, artifact, sourceCount: artifact.sources.length, passageCount: artifact.passages.length },
2523
- };
2524
- });
1927
+ return markToolError(await core.sourceCheck(params, signal, { extensionContext: ctx }));
2525
1928
  },
2526
1929
  });
2527
1930
 
@@ -2568,177 +1971,10 @@ export default function (pi: ExtensionAPI) {
2568
1971
  description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for this fetch. Needed when the target is unreachable directly; localhost and NO_PROXY hosts always bypass the proxy. Empty string forces direct access.",
2569
1972
  })),
2570
1973
  }),
1974
+ outputSchema: fetchContentOutputSchema,
2571
1975
 
2572
1976
  async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<Record<string, unknown>>> {
2573
- let normalized: ReturnType<typeof normalizeFetchContentParams>;
2574
- try {
2575
- normalized = normalizeFetchContentParams(params);
2576
- } catch (err) {
2577
- const error = err instanceof Error ? err.message : String(err);
2578
- return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
2579
- }
2580
- const { urlList, options } = normalized;
2581
- const mode = options.mode ?? fetchModeConfig.defaultMode;
2582
- if (!fetchModeConfig.allowedModes.includes(mode)) {
2583
- const error = `Fetch mode "${mode}" is disabled by fetch.allowedModes.`;
2584
- return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
2585
- }
2586
- return runWithProxy(options.proxy, async () => {
2587
- if (mode === "answer" && !options.prompt) {
2588
- return { content: [{ type: "text", text: "Error: mode answer requires prompt." }], details: { error: "mode answer requires prompt" } };
2589
- }
2590
- if (mode === "raw" && (options.forceClone === true || options.timestamp || options.frames || options.prompt || options.model || options.answerModel)) {
2591
- return { content: [{ type: "text", text: "Error: mode raw cannot be combined with forceClone, prompt, timestamp, frames, model, or answerModel." }], details: { error: "Incompatible raw mode options" } };
2592
- }
2593
- if (mode !== "answer" && options.answerModel) {
2594
- return { content: [{ type: "text", text: "Error: answerModel requires mode answer." }], details: { error: "answerModel requires mode answer" } };
2595
- }
2596
- if (mode === "answer" && options.model) {
2597
- return { content: [{ type: "text", text: "Error: use answerModel, not model, with mode answer." }], details: { error: "model is incompatible with mode answer" } };
2598
- }
2599
- if (mode === "answer" && options.auth !== undefined) {
2600
- return { content: [{ type: "text", text: "Error: auth cannot be combined with mode answer." }], details: { error: "auth cannot be combined with mode answer" } };
2601
- }
2602
- let authFetchProfile: AuthFetchProfile | undefined;
2603
- if (options.auth !== undefined) {
2604
- try {
2605
- authFetchProfile = resolveAuthFetchProfile(options.auth);
2606
- } catch (err) {
2607
- const error = err instanceof Error ? err.message : String(err);
2608
- return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
2609
- }
2610
- }
2611
- if (urlList.length === 0) {
2612
- return {
2613
- content: [{ type: "text", text: "Error: No URL provided." }],
2614
- details: { error: "No URL provided" },
2615
- };
2616
- }
2617
-
2618
- onUpdate?.({
2619
- content: [{ type: "text", text: `Fetching ${urlList.length} URL(s)...` }],
2620
- details: { phase: "fetch", progress: 0 },
2621
- });
2622
-
2623
- const { answerModel: _answerModel, auth: _auth, ...extractionOptions } = { ...options, mode };
2624
- const { prompt: _prompt, ...answerExtractionOptions } = extractionOptions;
2625
- const fetchOptions = {
2626
- ...(mode === "answer" ? answerExtractionOptions : extractionOptions),
2627
- ...(authFetchProfile ? { authFetchProfile } : {}),
2628
- };
2629
- const fetchResults = await fetchAllContent(urlList, signal, withRegisteredFetchOptions(fetchOptions, registeredToolNames, options.proxy));
2630
- const presentedResults = mode === "answer"
2631
- ? await Promise.all(fetchResults.map(async result => {
2632
- if (result.error) return result;
2633
- if (result.thumbnail || result.mimeType?.startsWith("image/")) {
2634
- return { ...result, error: "Page answer requires textual fetched content" };
2635
- }
2636
- try {
2637
- const answer = await answerFromPage({
2638
- question: options.prompt!,
2639
- pageText: result.content,
2640
- sourceUrl: result.url,
2641
- ...(options.answerModel ? { model: options.answerModel } : {}),
2642
- }, ctx, signal);
2643
- return { ...result, content: answer.text };
2644
- } catch (err) {
2645
- return { ...result, error: `Page answer failed: ${err instanceof Error ? err.message : String(err)}` };
2646
- }
2647
- }))
2648
- : fetchResults;
2649
- const successful = presentedResults.filter((r) => !r.error).length;
2650
- const totalChars = presentedResults.reduce((sum, r) => sum + r.content.length, 0);
2651
-
2652
- const responseId = generateId();
2653
- const data = {
2654
- id: responseId,
2655
- type: "fetch",
2656
- timestamp: Date.now(),
2657
- urls: stripThumbnails(fetchResults),
2658
- } satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
2659
- const storedContent = storeFetchResult(pi, responseId, data, authFetchProfile);
2660
-
2661
- if (urlList.length === 1) {
2662
- const result = presentedResults[0];
2663
- if (result.error) {
2664
- return {
2665
- content: [{ type: "text", text: `Error: ${result.error}` }],
2666
- details: { urls: urlList, urlCount: 1, successful: 0, error: result.error, ...(storedContent ? { responseId } : {}), prompt: params.prompt, timestamp: params.timestamp, frames: params.frames },
2667
- };
2668
- }
2669
-
2670
- const fullLength = result.content.length;
2671
- const slice = initialContentSlice(result.content, getMaxInlineContentChars());
2672
- const truncated = slice.endOffset < fullLength;
2673
- let output = slice.text;
2674
-
2675
- if (truncated) {
2676
- output += `\n\n---\nShowing ${slice.endOffset} of ${fullLength} chars, ${slice.shownBytes} of ${slice.totalBytes} bytes, and ${slice.shownLines} of ${slice.totalLines} lines. `;
2677
- output += storedContent
2678
- ? getSearchContentEnabled
2679
- ? `Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0, offset: ${slice.endOffset} }) for the next slice.`
2680
- : "Content retrieval is not registered."
2681
- : "Authenticated fetch cache is off; repeat the fetch to read more.";
2682
- }
2683
-
2684
- const content: Array<TextContent | ImageContent> = [];
2685
- if (result.frames?.length) {
2686
- for (const frame of result.frames) {
2687
- content.push({ type: "image", data: frame.data, mimeType: frame.mimeType });
2688
- content.push({ type: "text", text: `Frame at ${frame.timestamp}` });
2689
- }
2690
- } else if (result.thumbnail) {
2691
- content.push({ type: "image", data: result.thumbnail.data, mimeType: result.thumbnail.mimeType });
2692
- }
2693
- content.push({ type: "text", text: output });
2694
-
2695
- const imageCount = (result.frames?.length ?? 0) + (result.thumbnail ? 1 : 0);
2696
- return {
2697
- content,
2698
- details: {
2699
- urls: urlList,
2700
- urlCount: 1,
2701
- successful: 1,
2702
- totalChars: fullLength,
2703
- title: result.title,
2704
- ...(storedContent ? { responseId } : {}),
2705
- truncated,
2706
- hasImage: imageCount > 0,
2707
- imageCount,
2708
- prompt: params.prompt,
2709
- timestamp: params.timestamp,
2710
- frames: params.frames,
2711
- duration: result.duration,
2712
- mode,
2713
- mimeType: result.mimeType,
2714
- status: result.status,
2715
- totalBytes: slice.totalBytes,
2716
- totalLines: slice.totalLines,
2717
- shownBytes: slice.shownBytes,
2718
- shownLines: slice.shownLines,
2719
- },
2720
- };
2721
- }
2722
-
2723
- let output = "## Fetched URLs\n\n";
2724
- for (const { url, title, content, error } of presentedResults) {
2725
- if (error) {
2726
- output += `- ${url}: Error - ${error}\n`;
2727
- } else {
2728
- output += `- ${title || url} (${content.length} chars)\n`;
2729
- }
2730
- }
2731
- output += storedContent
2732
- ? getSearchContentEnabled
2733
- ? `\n---\nUse ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 }) to retrieve bounded content slices.`
2734
- : "\n---\nContent retrieval is not registered."
2735
- : "\n---\nAuthenticated fetch cache is off; repeat the fetch to read content.";
2736
-
2737
- return {
2738
- content: [{ type: "text", text: output }],
2739
- details: { urls: urlList, urlCount: urlList.length, successful, totalChars, ...(storedContent ? { responseId } : {}) },
2740
- };
2741
- });
1977
+ return markToolError(await core.fetchContent(params, signal, { extensionContext: ctx, onUpdate }));
2742
1978
  },
2743
1979
 
2744
1980
  renderCall(args, theme) {
@@ -2902,286 +2138,7 @@ export default function (pi: ExtensionAPI) {
2902
2138
  }),
2903
2139
 
2904
2140
  async execute(_toolCallId, rawParams): Promise<AgentToolResult<Record<string, unknown>>> {
2905
- const params = normalizeGetSearchContentParams(rawParams);
2906
- if (params.findMode !== undefined && params.findText === undefined) {
2907
- return {
2908
- content: [{ type: "text", text: `findMode ${formatInputValue(params.findMode)} requires findText; provide findText or omit findMode.` }],
2909
- details: { error: "findMode requires findText" },
2910
- };
2911
- }
2912
- const data = getResult(params.responseId);
2913
- if (!data) {
2914
- return {
2915
- content: [{ type: "text", text: `Error: No stored results for responseId ${formatInputValue(params.responseId)}. Use a responseId returned by ${storedContentSources}.` }],
2916
- details: { error: "Not found", responseId: params.responseId },
2917
- };
2918
- }
2919
-
2920
- if (data.type === "research") {
2921
- const artifact = getResearchArtifact(params.responseId);
2922
- if (!artifact) {
2923
- return {
2924
- content: [{ type: "text", text: `Error: stored research artifact for responseId ${formatInputValue(params.responseId)} was not found. Use a responseId returned by ${storedContentSources}.` }],
2925
- details: { error: "Artifact not found", responseId: params.responseId },
2926
- };
2927
- }
2928
- const serialized = JSON.stringify(artifact, null, 2);
2929
- if (params.findText !== undefined) {
2930
- try {
2931
- const found = findContent(serialized, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
2932
- const { text, ...findDetails } = found;
2933
- return {
2934
- content: [{ type: "text", text }],
2935
- details: { responseId: artifact.id, type: "research", contentLength: serialized.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
2936
- };
2937
- } catch (err) {
2938
- const error = err instanceof Error ? err.message : String(err);
2939
- return {
2940
- content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in research artifact for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
2941
- details: { error, responseId: params.responseId, type: "research" },
2942
- };
2943
- }
2944
- }
2945
- const offset = params.offset ?? 0;
2946
- const limit = params.limit ?? maxInlineContentChars;
2947
- if (!Number.isInteger(offset) || offset < 0) {
2948
- return {
2949
- content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for responseId ${formatInputValue(params.responseId)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
2950
- details: { error: "Invalid offset", offset },
2951
- };
2952
- }
2953
- if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
2954
- return {
2955
- content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for responseId ${formatInputValue(params.responseId)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
2956
- details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
2957
- };
2958
- }
2959
- if (offset > serialized.length) {
2960
- return {
2961
- content: [{ type: "text", text: `Offset ${offset} is out of range for responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${serialized.length}. Use an offset within that range.` }],
2962
- details: { error: "Offset out of range", offset, contentLength: serialized.length },
2963
- };
2964
- }
2965
- const endOffset = Math.min(offset + limit, serialized.length);
2966
- const artifactSlice = serialized.slice(offset, endOffset);
2967
- const hasMore = endOffset < serialized.length;
2968
- return {
2969
- content: [{ type: "text", text: artifactSlice }],
2970
- details: { responseId: artifact.id, type: "research", contentLength: serialized.length, offset, limit, returnedChars: artifactSlice.length, nextOffset: hasMore ? endOffset : null, truncated: hasMore },
2971
- };
2972
- }
2973
-
2974
- if (data.type === "search" && data.queries) {
2975
- let queryData: QueryResultData | undefined;
2976
-
2977
- if (params.query !== undefined) {
2978
- queryData = data.queries.find((q) => q.query === params.query);
2979
- if (!queryData) {
2980
- const available = data.queries.map((q) => `"${q.query}"`).join(", ");
2981
- return {
2982
- content: [{ type: "text", text: `Query ${formatInputValue(params.query)} was not found for responseId ${formatInputValue(params.responseId)}. Received query=${formatInputValue(params.query)}. Available queries: ${available || "none"}. Use one of the available queries or queryIndex.` }],
2983
- details: { error: "Query not found" },
2984
- };
2985
- }
2986
- } else if (params.queryIndex !== undefined) {
2987
- queryData = data.queries[params.queryIndex];
2988
- if (!queryData) {
2989
- const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
2990
- return {
2991
- content: [{ type: "text", text: `Query index ${formatInputValue(params.queryIndex)} is out of range for responseId ${formatInputValue(params.responseId)}. Received queryIndex=${formatInputValue(params.queryIndex)}; valid indexes are 0-${data.queries.length - 1}. Available queries: ${available || "none"}. Use one of the available indexes.` }],
2992
- details: { error: "Index out of range" },
2993
- };
2994
- }
2995
- } else {
2996
- const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
2997
- return {
2998
- content: [{ type: "text", text: `Specify query or queryIndex for responseId ${formatInputValue(params.responseId)}. Available queries: ${available || "none"}.` }],
2999
- details: { error: "No query specified" },
3000
- };
3001
- }
3002
-
3003
- if (queryData.error) {
3004
- return {
3005
- content: [{ type: "text", text: `Error retrieving query ${formatInputValue(queryData.query)} from responseId ${formatInputValue(params.responseId)}: ${queryData.error}. Check the stored search result and retry with another query or queryIndex if needed.` }],
3006
- details: { error: queryData.error, query: queryData.query },
3007
- };
3008
- }
3009
-
3010
- const fullResults = formatFullResults(queryData);
3011
- if (params.findText !== undefined) {
3012
- try {
3013
- const found = findContent(fullResults, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
3014
- const { text, ...findDetails } = found;
3015
- return {
3016
- content: [{ type: "text", text }],
3017
- details: { responseId: params.responseId, query: queryData.query, resultCount: queryData.results.length, contentLength: fullResults.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
3018
- };
3019
- } catch (err) {
3020
- const error = err instanceof Error ? err.message : String(err);
3021
- return {
3022
- content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in query ${formatInputValue(queryData.query)} for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
3023
- details: { error, query: queryData.query },
3024
- };
3025
- }
3026
- }
3027
-
3028
- const offset = params.offset ?? 0;
3029
- const limit = params.limit ?? maxInlineContentChars;
3030
- if (!Number.isInteger(offset) || offset < 0) {
3031
- return {
3032
- content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for query ${formatInputValue(queryData.query)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
3033
- details: { error: "Invalid offset", offset },
3034
- };
3035
- }
3036
- if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
3037
- return {
3038
- content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for query ${formatInputValue(queryData.query)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
3039
- details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
3040
- };
3041
- }
3042
- if (offset > fullResults.length) {
3043
- return {
3044
- content: [{ type: "text", text: `Offset ${offset} is out of range for query ${formatInputValue(queryData.query)} in responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${fullResults.length}. Use an offset within that range.` }],
3045
- details: { error: "Offset out of range", offset, contentLength: fullResults.length },
3046
- };
3047
- }
3048
- const queryIndex = data.queries.indexOf(queryData);
3049
- let returnedChars = Math.min(limit, fullResults.length - offset);
3050
- let endOffset = offset + returnedChars;
3051
- let continuation = "";
3052
- while (endOffset < fullResults.length) {
3053
- continuation = `\n\n---\nShowing chars ${offset}-${endOffset} of ${fullResults.length}. Use ${toolNames.getSearchContent}({ responseId: "${params.responseId}", queryIndex: ${queryIndex}, offset: ${endOffset}, limit: ${limit} }) for the next slice.`;
3054
- const overflow = returnedChars + continuation.length - maxInlineContentChars;
3055
- if (overflow <= 0) break;
3056
- returnedChars -= overflow;
3057
- endOffset = offset + returnedChars;
3058
- }
3059
- const resultSlice = fullResults.slice(offset, endOffset);
3060
- const hasMore = endOffset < fullResults.length;
3061
- const text = `${resultSlice}${hasMore ? continuation : ""}`;
3062
- return {
3063
- content: [{ type: "text", text }],
3064
- details: {
3065
- responseId: params.responseId,
3066
- query: queryData.query,
3067
- resultCount: queryData.results.length,
3068
- contentLength: fullResults.length,
3069
- offset,
3070
- limit,
3071
- returnedChars,
3072
- nextOffset: hasMore ? endOffset : null,
3073
- truncated: hasMore,
3074
- },
3075
- };
3076
- }
3077
-
3078
- if (data.type === "fetch" && data.urls) {
3079
- let urlData: ExtractedContent | undefined;
3080
- let selectedUrlIndex = -1;
3081
-
3082
- if (params.url !== undefined) {
3083
- selectedUrlIndex = data.urls.findIndex((u) => u.url === params.url);
3084
- urlData = data.urls[selectedUrlIndex];
3085
- if (!urlData) {
3086
- const available = data.urls.map((u) => u.url).join("\n ");
3087
- return {
3088
- content: [{ type: "text", text: `URL ${formatInputValue(params.url)} was not found for responseId ${formatInputValue(params.responseId)}. Received url=${formatInputValue(params.url)}. Available URLs:\n ${available || " none"}\nUse one of the available URLs or urlIndex.` }],
3089
- details: { error: "URL not found" },
3090
- };
3091
- }
3092
- } else if (params.urlIndex !== undefined) {
3093
- selectedUrlIndex = params.urlIndex;
3094
- urlData = data.urls[selectedUrlIndex];
3095
- if (!urlData) {
3096
- const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
3097
- return {
3098
- content: [{ type: "text", text: `URL index ${formatInputValue(params.urlIndex)} is out of range for responseId ${formatInputValue(params.responseId)}. Received urlIndex=${formatInputValue(params.urlIndex)}; valid indexes are 0-${data.urls.length - 1}. Available URLs:\n ${available || " none"}\nUse one of the available indexes.` }],
3099
- details: { error: "Index out of range" },
3100
- };
3101
- }
3102
- } else {
3103
- const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
3104
- return {
3105
- content: [{ type: "text", text: `Specify url or urlIndex for responseId ${formatInputValue(params.responseId)}. Available URLs:\n ${available || " none"}` }],
3106
- details: { error: "No URL specified" },
3107
- };
3108
- }
3109
-
3110
- if (urlData.error) {
3111
- return {
3112
- content: [{ type: "text", text: `Error retrieving URL ${formatInputValue(urlData.url)} from responseId ${formatInputValue(params.responseId)}: ${urlData.error}. Check the stored fetch result and retry with another URL or urlIndex if needed.` }],
3113
- details: { error: urlData.error, url: urlData.url },
3114
- };
3115
- }
3116
-
3117
- if (params.findText !== undefined) {
3118
- try {
3119
- const found = findContent(urlData.content, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
3120
- const { text, ...findDetails } = found;
3121
- return {
3122
- content: [{ type: "text", text: `# ${urlData.title || urlData.url}\n\n${text}` }],
3123
- details: { url: urlData.url, title: urlData.title, contentLength: urlData.content.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
3124
- };
3125
- } catch (err) {
3126
- const error = err instanceof Error ? err.message : String(err);
3127
- return {
3128
- content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in URL ${formatInputValue(urlData.url)} for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
3129
- details: { error, url: urlData.url },
3130
- };
3131
- }
3132
- }
3133
-
3134
- const offset = params.offset ?? 0;
3135
- const limit = params.limit ?? maxInlineContentChars;
3136
- if (!Number.isInteger(offset) || offset < 0) {
3137
- return {
3138
- content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for URL ${formatInputValue(urlData.url)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
3139
- details: { error: "Invalid offset", offset },
3140
- };
3141
- }
3142
- if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
3143
- return {
3144
- content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for URL ${formatInputValue(urlData.url)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
3145
- details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
3146
- };
3147
- }
3148
- if (offset > urlData.content.length) {
3149
- return {
3150
- content: [{ type: "text", text: `Offset ${offset} is out of range for URL ${formatInputValue(urlData.url)} in responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${urlData.content.length}. Use an offset within that range.` }],
3151
- details: { error: "Offset out of range", offset, contentLength: urlData.content.length },
3152
- };
3153
- }
3154
-
3155
- const endOffset = Math.min(offset + limit, urlData.content.length);
3156
- const contentSlice = urlData.content.slice(offset, endOffset);
3157
- const hasMore = endOffset < urlData.content.length;
3158
- let text = `# ${urlData.title || urlData.url}\n\n${contentSlice}`;
3159
- if (hasMore || offset > 0) {
3160
- text += `\n\n---\nShowing chars ${offset}-${endOffset} of ${urlData.content.length}.`;
3161
- if (hasMore) {
3162
- text += ` Use ${toolNames.getSearchContent}({ responseId: "${params.responseId}", urlIndex: ${selectedUrlIndex}, offset: ${endOffset}, limit: ${limit} }) for the next slice.`;
3163
- }
3164
- }
3165
-
3166
- return {
3167
- content: [{ type: "text", text }],
3168
- details: {
3169
- url: urlData.url,
3170
- title: urlData.title,
3171
- contentLength: urlData.content.length,
3172
- offset,
3173
- limit,
3174
- returnedChars: contentSlice.length,
3175
- nextOffset: hasMore ? endOffset : null,
3176
- truncated: hasMore,
3177
- },
3178
- };
3179
- }
3180
-
3181
- return {
3182
- content: [{ type: "text", text: `Invalid stored data for responseId ${formatInputValue(params.responseId)}: received type ${formatInputValue(data.type)}. Use a responseId returned by ${storedContentSources}.` }],
3183
- details: { error: "Invalid data" },
3184
- };
2141
+ return markToolError(await core.getSearchContent(rawParams));
3185
2142
  },
3186
2143
 
3187
2144
  renderCall(args, theme) {