@asterxsk/kiln 0.1.0 → 0.3.0

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 (491) hide show
  1. package/README.md +155 -170
  2. package/agent/AGENTS.md +67 -67
  3. package/agent/README.md +5 -5
  4. package/agent/extensions/ask-user/index.ts +418 -418
  5. package/agent/extensions/ask-user/install.ps1 +4 -0
  6. package/agent/extensions/ask-user/install.sh +3 -0
  7. package/agent/extensions/ask-user/package-lock.json +769 -769
  8. package/agent/extensions/ask-user/package.json +19 -19
  9. package/agent/extensions/ask-user/prompt.ts +45 -45
  10. package/agent/extensions/ask-user/tsconfig.json +7 -7
  11. package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
  12. package/agent/extensions/background-terminals/index.ts +627 -627
  13. package/agent/extensions/background-terminals/install.ps1 +4 -0
  14. package/agent/extensions/background-terminals/install.sh +3 -0
  15. package/agent/extensions/background-terminals/manager.test.ts +735 -735
  16. package/agent/extensions/background-terminals/output.test.ts +109 -109
  17. package/agent/extensions/background-terminals/package-lock.json +769 -769
  18. package/agent/extensions/background-terminals/package.json +17 -17
  19. package/agent/extensions/background-terminals/prompt.test.ts +125 -125
  20. package/agent/extensions/background-terminals/ps.test.ts +82 -82
  21. package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
  22. package/agent/extensions/background-terminals/src/domain.ts +87 -87
  23. package/agent/extensions/background-terminals/src/manager.ts +907 -907
  24. package/agent/extensions/background-terminals/src/output.ts +84 -84
  25. package/agent/extensions/background-terminals/src/prompt.ts +142 -142
  26. package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
  27. package/agent/extensions/background-terminals/src/runtime.ts +36 -36
  28. package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
  29. package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
  30. package/agent/extensions/background-terminals/tsconfig.json +7 -7
  31. package/agent/extensions/destructive/install.ps1 +4 -0
  32. package/agent/extensions/destructive/install.sh +3 -0
  33. package/agent/extensions/file-search/index.spec.ts +443 -443
  34. package/agent/extensions/file-search/index.ts +459 -459
  35. package/agent/extensions/file-search/install.ps1 +4 -0
  36. package/agent/extensions/file-search/install.sh +3 -0
  37. package/agent/extensions/file-search/package-lock.json +2253 -2253
  38. package/agent/extensions/file-search/package.json +23 -23
  39. package/agent/extensions/file-search/src/args.ts +122 -122
  40. package/agent/extensions/file-search/src/binaries.ts +422 -422
  41. package/agent/extensions/file-search/src/output.ts +126 -126
  42. package/agent/extensions/file-search/src/process.ts +146 -146
  43. package/agent/extensions/file-search/src/prompt.ts +52 -52
  44. package/agent/extensions/file-search/tsconfig.json +7 -7
  45. package/agent/extensions/goal/install.ps1 +4 -0
  46. package/agent/extensions/goal/install.sh +3 -0
  47. package/agent/extensions/herdr-agent-state.ts +257 -0
  48. package/agent/extensions/kiln-update/README.md +25 -0
  49. package/agent/extensions/kiln-update/index.ts +99 -0
  50. package/agent/extensions/kiln-update/install.ps1 +27 -0
  51. package/agent/extensions/kiln-update/install.sh +24 -0
  52. package/agent/extensions/modelconf/PLAN.md +915 -915
  53. package/agent/extensions/modelconf/index.ts +296 -296
  54. package/agent/extensions/modelconf/install.ps1 +4 -0
  55. package/agent/extensions/modelconf/install.sh +3 -0
  56. package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
  57. package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
  58. package/agent/extensions/pi-web-access/LICENSE +21 -21
  59. package/agent/extensions/pi-web-access/README.md +470 -470
  60. package/agent/extensions/pi-web-access/SECURITY.md +5 -5
  61. package/agent/extensions/pi-web-access/activity.ts +101 -101
  62. package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
  63. package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
  64. package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
  65. package/agent/extensions/pi-web-access/content-find.ts +139 -139
  66. package/agent/extensions/pi-web-access/credential-source.ts +191 -191
  67. package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
  68. package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
  69. package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
  70. package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
  71. package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
  72. package/agent/extensions/pi-web-access/exa.ts +526 -526
  73. package/agent/extensions/pi-web-access/extract.ts +1196 -1196
  74. package/agent/extensions/pi-web-access/feature-config.ts +29 -29
  75. package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
  76. package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
  77. package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
  78. package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
  79. package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
  80. package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
  81. package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
  82. package/agent/extensions/pi-web-access/github-api.ts +197 -197
  83. package/agent/extensions/pi-web-access/github-extract.ts +746 -746
  84. package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
  85. package/agent/extensions/pi-web-access/index.ts +1737 -1737
  86. package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
  87. package/agent/extensions/pi-web-access/package.json +64 -64
  88. package/agent/extensions/pi-web-access/page-query.ts +96 -96
  89. package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
  90. package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
  91. package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
  92. package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
  93. package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
  94. package/agent/extensions/pi-web-access/source-check.ts +282 -282
  95. package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
  96. package/agent/extensions/pi-web-access/storage.ts +521 -521
  97. package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
  98. package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
  99. package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
  100. package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
  101. package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
  102. package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
  103. package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
  104. package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
  105. package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
  106. package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
  107. package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
  108. package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
  109. package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
  110. package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
  111. package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
  112. package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
  113. package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
  114. package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
  115. package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
  116. package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
  117. package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
  118. package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
  119. package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
  120. package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
  121. package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
  122. package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
  123. package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
  124. package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
  125. package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
  126. package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
  127. package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
  128. package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
  129. package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
  130. package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
  131. package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
  132. package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
  133. package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
  134. package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
  135. package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
  136. package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
  137. package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
  138. package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
  139. package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
  140. package/agent/extensions/pi-web-access/tsconfig.json +11 -11
  141. package/agent/extensions/pi-web-access/utils.ts +451 -451
  142. package/agent/extensions/pi-web-access/video-extract.ts +392 -392
  143. package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
  144. package/agent/extensions/shared/activity-status.ts +31 -31
  145. package/agent/extensions/shared/child-session.test.ts +270 -270
  146. package/agent/extensions/shared/child-session.ts +148 -148
  147. package/agent/extensions/shared/context-utilization.test.ts +48 -48
  148. package/agent/extensions/shared/context-utilization.ts +47 -47
  149. package/agent/extensions/shared/dashboard-state.ts +99 -99
  150. package/agent/extensions/shared/install.ps1 +4 -0
  151. package/agent/extensions/shared/install.sh +3 -0
  152. package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
  153. package/agent/extensions/shared/tool-call-timeout.ts +104 -104
  154. package/agent/extensions/skillsconf/install.ps1 +4 -0
  155. package/agent/extensions/skillsconf/install.sh +3 -0
  156. package/agent/extensions/statusline/index.ts +282 -0
  157. package/agent/extensions/statusline/install.ps1 +27 -0
  158. package/agent/extensions/statusline/install.sh +24 -0
  159. package/agent/extensions/subagents/by-the-way.test.ts +29 -29
  160. package/agent/extensions/subagents/claude.test.ts +119 -119
  161. package/agent/extensions/subagents/codex.test.ts +102 -102
  162. package/agent/extensions/subagents/context-usage.test.ts +107 -107
  163. package/agent/extensions/subagents/docs/design-plan.md +568 -568
  164. package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
  165. package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
  166. package/agent/extensions/subagents/index.ts +779 -779
  167. package/agent/extensions/subagents/install.ps1 +4 -0
  168. package/agent/extensions/subagents/install.sh +3 -0
  169. package/agent/extensions/subagents/manager.test.ts +276 -276
  170. package/agent/extensions/subagents/package-lock.json +2244 -2244
  171. package/agent/extensions/subagents/package.json +19 -19
  172. package/agent/extensions/subagents/result-delivery.test.ts +27 -27
  173. package/agent/extensions/subagents/src/backend.ts +73 -73
  174. package/agent/extensions/subagents/src/backends/claude.ts +701 -701
  175. package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
  176. package/agent/extensions/subagents/src/backends/pi.ts +575 -575
  177. package/agent/extensions/subagents/src/backends/stub.ts +300 -300
  178. package/agent/extensions/subagents/src/by-the-way.ts +21 -21
  179. package/agent/extensions/subagents/src/domain.ts +253 -253
  180. package/agent/extensions/subagents/src/format.ts +74 -74
  181. package/agent/extensions/subagents/src/manager.ts +736 -736
  182. package/agent/extensions/subagents/src/prompt.ts +92 -92
  183. package/agent/extensions/subagents/src/result-delivery.ts +20 -20
  184. package/agent/extensions/subagents/src/runtime.ts +53 -53
  185. package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
  186. package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
  187. package/agent/extensions/subagents/takeover.test.ts +29 -29
  188. package/agent/extensions/subagents/tsconfig.json +7 -7
  189. package/agent/extensions/taste/install.ps1 +4 -0
  190. package/agent/extensions/taste/install.sh +3 -0
  191. package/agent/extensions/todo/AGENTS.md +38 -38
  192. package/agent/extensions/todo/LICENSE +21 -21
  193. package/agent/extensions/todo/config.ts +55 -55
  194. package/agent/extensions/todo/index.ts +151 -151
  195. package/agent/extensions/todo/install.ps1 +4 -0
  196. package/agent/extensions/todo/install.sh +3 -0
  197. package/agent/extensions/todo/locales/de.json +17 -17
  198. package/agent/extensions/todo/locales/en.json +15 -15
  199. package/agent/extensions/todo/locales/es.json +17 -17
  200. package/agent/extensions/todo/locales/fr.json +17 -17
  201. package/agent/extensions/todo/locales/pt-BR.json +17 -17
  202. package/agent/extensions/todo/locales/pt.json +17 -17
  203. package/agent/extensions/todo/locales/ru.json +17 -17
  204. package/agent/extensions/todo/locales/uk.json +17 -17
  205. package/agent/extensions/todo/locales/zh.json +17 -17
  206. package/agent/extensions/todo/package-lock.json +3358 -3358
  207. package/agent/extensions/todo/package.json +67 -67
  208. package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
  209. package/agent/extensions/todo/state/invariants.ts +20 -20
  210. package/agent/extensions/todo/state/replay.ts +38 -38
  211. package/agent/extensions/todo/state/selectors.ts +107 -107
  212. package/agent/extensions/todo/state/state-reducer.ts +326 -326
  213. package/agent/extensions/todo/state/state.ts +18 -18
  214. package/agent/extensions/todo/state/store.ts +82 -82
  215. package/agent/extensions/todo/state/task-graph.ts +57 -57
  216. package/agent/extensions/todo/todo-overlay.ts +200 -200
  217. package/agent/extensions/todo/todo.ts +155 -155
  218. package/agent/extensions/todo/tool/response-envelope.ts +109 -109
  219. package/agent/extensions/todo/tool/types.ts +206 -206
  220. package/agent/extensions/todo/view/format.ts +177 -177
  221. package/agent/extensions/trim-context/install.ps1 +4 -0
  222. package/agent/extensions/trim-context/install.sh +3 -0
  223. package/agent/keybindings.json +7 -7
  224. package/agent/version.txt +1 -0
  225. package/agent.bak.20260905-190020/AGENTS.md +67 -0
  226. package/agent.bak.20260905-190020/README.md +5 -0
  227. package/agent.bak.20260905-190020/extensions/ask-user/index.ts +418 -0
  228. package/agent.bak.20260905-190020/extensions/ask-user/package-lock.json +769 -0
  229. package/agent.bak.20260905-190020/extensions/ask-user/package.json +19 -0
  230. package/agent.bak.20260905-190020/extensions/ask-user/prompt.ts +45 -0
  231. package/agent.bak.20260905-190020/extensions/ask-user/tsconfig.json +7 -0
  232. package/agent.bak.20260905-190020/extensions/background-terminals/docs/implementation-guide.md +942 -0
  233. package/agent.bak.20260905-190020/extensions/background-terminals/index.ts +629 -0
  234. package/agent.bak.20260905-190020/extensions/background-terminals/install.ps1 +23 -0
  235. package/agent.bak.20260905-190020/extensions/background-terminals/install.sh +21 -0
  236. package/agent.bak.20260905-190020/extensions/background-terminals/package-lock.json +769 -0
  237. package/agent.bak.20260905-190020/extensions/background-terminals/package.json +17 -0
  238. package/agent.bak.20260905-190020/extensions/background-terminals/src/domain.ts +87 -0
  239. package/agent.bak.20260905-190020/extensions/background-terminals/src/manager.ts +907 -0
  240. package/agent.bak.20260905-190020/extensions/background-terminals/src/output.ts +84 -0
  241. package/agent.bak.20260905-190020/extensions/background-terminals/src/prompt.ts +142 -0
  242. package/agent.bak.20260905-190020/extensions/background-terminals/src/result-delivery.ts +27 -0
  243. package/agent.bak.20260905-190020/extensions/background-terminals/src/runtime.ts +36 -0
  244. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/output-view.ts +79 -0
  245. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/ps.ts +621 -0
  246. package/agent.bak.20260905-190020/extensions/background-terminals/src/widget-order.ts +121 -0
  247. package/agent.bak.20260905-190020/extensions/background-terminals/test/manager.test.ts +735 -0
  248. package/agent.bak.20260905-190020/extensions/background-terminals/test/output.test.ts +109 -0
  249. package/agent.bak.20260905-190020/extensions/background-terminals/test/prompt.test.ts +125 -0
  250. package/agent.bak.20260905-190020/extensions/background-terminals/test/ps.test.ts +82 -0
  251. package/agent.bak.20260905-190020/extensions/background-terminals/test/result-delivery.test.ts +44 -0
  252. package/agent.bak.20260905-190020/extensions/background-terminals/tsconfig.json +7 -0
  253. package/agent.bak.20260905-190020/extensions/destructive/README.md +34 -0
  254. package/agent.bak.20260905-190020/extensions/destructive/index.ts +124 -0
  255. package/agent.bak.20260905-190020/extensions/destructive/install.ps1 +23 -0
  256. package/agent.bak.20260905-190020/extensions/destructive/install.sh +21 -0
  257. package/agent.bak.20260905-190020/extensions/file-search/index.ts +459 -0
  258. package/agent.bak.20260905-190020/extensions/file-search/install.ps1 +23 -0
  259. package/agent.bak.20260905-190020/extensions/file-search/install.sh +21 -0
  260. package/agent.bak.20260905-190020/extensions/file-search/package-lock.json +2253 -0
  261. package/agent.bak.20260905-190020/extensions/file-search/package.json +23 -0
  262. package/agent.bak.20260905-190020/extensions/file-search/src/args.ts +122 -0
  263. package/agent.bak.20260905-190020/extensions/file-search/src/binaries.ts +422 -0
  264. package/agent.bak.20260905-190020/extensions/file-search/src/output.ts +126 -0
  265. package/agent.bak.20260905-190020/extensions/file-search/src/process.ts +146 -0
  266. package/agent.bak.20260905-190020/extensions/file-search/src/prompt.ts +52 -0
  267. package/agent.bak.20260905-190020/extensions/file-search/test/index.spec.ts +443 -0
  268. package/agent.bak.20260905-190020/extensions/file-search/tsconfig.json +7 -0
  269. package/agent.bak.20260905-190020/extensions/goal/README.md +50 -0
  270. package/agent.bak.20260905-190020/extensions/goal/index.ts +155 -0
  271. package/agent.bak.20260905-190020/extensions/goal/install.ps1 +23 -0
  272. package/agent.bak.20260905-190020/extensions/goal/install.sh +21 -0
  273. package/agent.bak.20260905-190020/extensions/kiln-update/README.md +25 -0
  274. package/agent.bak.20260905-190020/extensions/kiln-update/index.ts +99 -0
  275. package/agent.bak.20260905-190020/extensions/kiln-update/install.ps1 +23 -0
  276. package/agent.bak.20260905-190020/extensions/kiln-update/install.sh +21 -0
  277. package/agent.bak.20260905-190020/extensions/modelconf/PLAN.md +915 -0
  278. package/agent.bak.20260905-190020/extensions/modelconf/README.md +66 -0
  279. package/agent.bak.20260905-190020/extensions/modelconf/index.ts +296 -0
  280. package/agent.bak.20260905-190020/extensions/modelconf/install.ps1 +23 -0
  281. package/agent.bak.20260905-190020/extensions/modelconf/install.sh +21 -0
  282. package/agent.bak.20260905-190020/extensions/modelconf/package-lock.json +1809 -0
  283. package/agent.bak.20260905-190020/extensions/modelconf/package.json +13 -0
  284. package/agent.bak.20260905-190020/extensions/modelconf/src/fuzzy.ts +26 -0
  285. package/agent.bak.20260905-190020/extensions/modelconf/src/glob.ts +13 -0
  286. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.test.ts +46 -0
  287. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.ts +164 -0
  288. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  289. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  290. package/agent.bak.20260905-190020/extensions/modelconf/tsconfig.json +15 -0
  291. package/agent.bak.20260905-190020/extensions/pi-web-access/CHANGELOG.md +690 -0
  292. package/agent.bak.20260905-190020/extensions/pi-web-access/LICENSE +21 -0
  293. package/agent.bak.20260905-190020/extensions/pi-web-access/README.md +470 -0
  294. package/agent.bak.20260905-190020/extensions/pi-web-access/SECURITY.md +5 -0
  295. package/agent.bak.20260905-190020/extensions/pi-web-access/activity.ts +101 -0
  296. package/agent.bak.20260905-190020/extensions/pi-web-access/auth-fetch.ts +148 -0
  297. package/agent.bak.20260905-190020/extensions/pi-web-access/banner.png +0 -0
  298. package/agent.bak.20260905-190020/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  299. package/agent.bak.20260905-190020/extensions/pi-web-access/chrome-cookies.ts +669 -0
  300. package/agent.bak.20260905-190020/extensions/pi-web-access/content-find.ts +139 -0
  301. package/agent.bak.20260905-190020/extensions/pi-web-access/credential-source.ts +191 -0
  302. package/agent.bak.20260905-190020/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  303. package/agent.bak.20260905-190020/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  304. package/agent.bak.20260905-190020/extensions/pi-web-access/declared-web-links.ts +173 -0
  305. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  306. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  307. package/agent.bak.20260905-190020/extensions/pi-web-access/exa.ts +526 -0
  308. package/agent.bak.20260905-190020/extensions/pi-web-access/extract.ts +1196 -0
  309. package/agent.bak.20260905-190020/extensions/pi-web-access/feature-config.ts +29 -0
  310. package/agent.bak.20260905-190020/extensions/pi-web-access/fetch-params.ts +111 -0
  311. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-adc.ts +298 -0
  312. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-api.ts +353 -0
  313. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  314. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-search.ts +21 -0
  315. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-url-context.ts +128 -0
  316. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web-config.ts +101 -0
  317. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web.ts +487 -0
  318. package/agent.bak.20260905-190020/extensions/pi-web-access/github-api.ts +197 -0
  319. package/agent.bak.20260905-190020/extensions/pi-web-access/github-extract.ts +746 -0
  320. package/agent.bak.20260905-190020/extensions/pi-web-access/github-issue-pr.ts +700 -0
  321. package/agent.bak.20260905-190020/extensions/pi-web-access/index.ts +1737 -0
  322. package/agent.bak.20260905-190020/extensions/pi-web-access/package-lock.json +5808 -0
  323. package/agent.bak.20260905-190020/extensions/pi-web-access/package.json +64 -0
  324. package/agent.bak.20260905-190020/extensions/pi-web-access/page-query.ts +96 -0
  325. package/agent.bak.20260905-190020/extensions/pi-web-access/pdf-extract.ts +409 -0
  326. package/agent.bak.20260905-190020/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  327. package/agent.bak.20260905-190020/extensions/pi-web-access/promise-try.d.ts +7 -0
  328. package/agent.bak.20260905-190020/extensions/pi-web-access/query-rewrite.ts +51 -0
  329. package/agent.bak.20260905-190020/extensions/pi-web-access/render-search-error.ts +170 -0
  330. package/agent.bak.20260905-190020/extensions/pi-web-access/rsc-extract.ts +338 -0
  331. package/agent.bak.20260905-190020/extensions/pi-web-access/search-types.ts +20 -0
  332. package/agent.bak.20260905-190020/extensions/pi-web-access/source-check.ts +282 -0
  333. package/agent.bak.20260905-190020/extensions/pi-web-access/ssrf-protection.ts +526 -0
  334. package/agent.bak.20260905-190020/extensions/pi-web-access/storage.ts +521 -0
  335. package/agent.bak.20260905-190020/extensions/pi-web-access/summary-model-scope.ts +125 -0
  336. package/agent.bak.20260905-190020/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  337. package/agent.bak.20260905-190020/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  338. package/agent.bak.20260905-190020/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  339. package/agent.bak.20260905-190020/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  340. package/agent.bak.20260905-190020/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  341. package/agent.bak.20260905-190020/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  342. package/agent.bak.20260905-190020/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  343. package/agent.bak.20260905-190020/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  344. package/agent.bak.20260905-190020/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  345. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  346. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  347. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  348. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  349. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  350. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  351. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  352. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  353. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  354. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  355. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  356. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  357. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  358. package/agent.bak.20260905-190020/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  359. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  360. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  361. package/agent.bak.20260905-190020/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  362. package/agent.bak.20260905-190020/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  363. package/agent.bak.20260905-190020/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  364. package/agent.bak.20260905-190020/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  365. package/agent.bak.20260905-190020/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  366. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  367. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  368. package/agent.bak.20260905-190020/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  369. package/agent.bak.20260905-190020/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  370. package/agent.bak.20260905-190020/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  371. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  372. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  373. package/agent.bak.20260905-190020/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  374. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  375. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  376. package/agent.bak.20260905-190020/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  377. package/agent.bak.20260905-190020/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  378. package/agent.bak.20260905-190020/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  379. package/agent.bak.20260905-190020/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  380. package/agent.bak.20260905-190020/extensions/pi-web-access/tsconfig.json +11 -0
  381. package/agent.bak.20260905-190020/extensions/pi-web-access/utils.ts +451 -0
  382. package/agent.bak.20260905-190020/extensions/pi-web-access/video-extract.ts +392 -0
  383. package/agent.bak.20260905-190020/extensions/pi-web-access/youtube-extract.ts +328 -0
  384. package/agent.bak.20260905-190020/extensions/shared/activity-status.ts +31 -0
  385. package/agent.bak.20260905-190020/extensions/shared/child-session.ts +148 -0
  386. package/agent.bak.20260905-190020/extensions/shared/context-utilization.ts +47 -0
  387. package/agent.bak.20260905-190020/extensions/shared/dashboard-state.ts +99 -0
  388. package/agent.bak.20260905-190020/extensions/shared/install.ps1 +23 -0
  389. package/agent.bak.20260905-190020/extensions/shared/install.sh +21 -0
  390. package/agent.bak.20260905-190020/extensions/shared/test/child-session.test.ts +270 -0
  391. package/agent.bak.20260905-190020/extensions/shared/test/context-utilization.test.ts +48 -0
  392. package/agent.bak.20260905-190020/extensions/shared/test/tool-call-timeout.test.ts +117 -0
  393. package/agent.bak.20260905-190020/extensions/shared/test/widget-order.test.ts +167 -0
  394. package/agent.bak.20260905-190020/extensions/shared/tool-call-timeout.ts +104 -0
  395. package/agent.bak.20260905-190020/extensions/skillsconf/README.md +74 -0
  396. package/agent.bak.20260905-190020/extensions/skillsconf/index.ts +92 -0
  397. package/agent.bak.20260905-190020/extensions/skillsconf/install.ps1 +23 -0
  398. package/agent.bak.20260905-190020/extensions/skillsconf/install.sh +21 -0
  399. package/agent.bak.20260905-190020/extensions/skillsconf/package-lock.json +1809 -0
  400. package/agent.bak.20260905-190020/extensions/skillsconf/package.json +18 -0
  401. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  402. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.ts +45 -0
  403. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.test.ts +80 -0
  404. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.ts +74 -0
  405. package/agent.bak.20260905-190020/extensions/skillsconf/src/fuzzy.ts +26 -0
  406. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.test.ts +72 -0
  407. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.ts +169 -0
  408. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  409. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  410. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/text-input.ts +91 -0
  411. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  412. package/agent.bak.20260905-190020/extensions/skillsconf/tsconfig.json +15 -0
  413. package/agent.bak.20260905-190020/extensions/statusline/install.ps1 +23 -0
  414. package/agent.bak.20260905-190020/extensions/statusline/install.sh +21 -0
  415. package/agent.bak.20260905-190020/extensions/subagents/docs/design-plan.md +568 -0
  416. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  417. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-notes.md +571 -0
  418. package/agent.bak.20260905-190020/extensions/subagents/index.ts +826 -0
  419. package/agent.bak.20260905-190020/extensions/subagents/install.ps1 +23 -0
  420. package/agent.bak.20260905-190020/extensions/subagents/install.sh +21 -0
  421. package/agent.bak.20260905-190020/extensions/subagents/package-lock.json +2244 -0
  422. package/agent.bak.20260905-190020/extensions/subagents/package.json +19 -0
  423. package/agent.bak.20260905-190020/extensions/subagents/src/backend.ts +73 -0
  424. package/agent.bak.20260905-190020/extensions/subagents/src/backends/claude.ts +701 -0
  425. package/agent.bak.20260905-190020/extensions/subagents/src/backends/codex.ts +1060 -0
  426. package/agent.bak.20260905-190020/extensions/subagents/src/backends/pi.ts +575 -0
  427. package/agent.bak.20260905-190020/extensions/subagents/src/backends/stub.ts +300 -0
  428. package/agent.bak.20260905-190020/extensions/subagents/src/by-the-way.ts +21 -0
  429. package/agent.bak.20260905-190020/extensions/subagents/src/domain.ts +253 -0
  430. package/agent.bak.20260905-190020/extensions/subagents/src/format.ts +74 -0
  431. package/agent.bak.20260905-190020/extensions/subagents/src/manager.ts +736 -0
  432. package/agent.bak.20260905-190020/extensions/subagents/src/prompt.ts +92 -0
  433. package/agent.bak.20260905-190020/extensions/subagents/src/result-delivery.ts +20 -0
  434. package/agent.bak.20260905-190020/extensions/subagents/src/runtime.ts +53 -0
  435. package/agent.bak.20260905-190020/extensions/subagents/src/ui/takeover.ts +583 -0
  436. package/agent.bak.20260905-190020/extensions/subagents/src/ui/transcript.ts +201 -0
  437. package/agent.bak.20260905-190020/extensions/subagents/src/widget-order.ts +121 -0
  438. package/agent.bak.20260905-190020/extensions/subagents/test/by-the-way.test.ts +29 -0
  439. package/agent.bak.20260905-190020/extensions/subagents/test/claude.test.ts +119 -0
  440. package/agent.bak.20260905-190020/extensions/subagents/test/codex.test.ts +102 -0
  441. package/agent.bak.20260905-190020/extensions/subagents/test/context-usage.test.ts +107 -0
  442. package/agent.bak.20260905-190020/extensions/subagents/test/manager.test.ts +276 -0
  443. package/agent.bak.20260905-190020/extensions/subagents/test/result-delivery.test.ts +27 -0
  444. package/agent.bak.20260905-190020/extensions/subagents/test/takeover.test.ts +29 -0
  445. package/agent.bak.20260905-190020/extensions/subagents/tsconfig.json +7 -0
  446. package/agent.bak.20260905-190020/extensions/taste/index.ts +443 -0
  447. package/agent.bak.20260905-190020/extensions/taste/install.ps1 +23 -0
  448. package/agent.bak.20260905-190020/extensions/taste/install.sh +21 -0
  449. package/agent.bak.20260905-190020/extensions/todo/AGENTS.md +38 -0
  450. package/agent.bak.20260905-190020/extensions/todo/LICENSE +21 -0
  451. package/agent.bak.20260905-190020/extensions/todo/config.ts +55 -0
  452. package/agent.bak.20260905-190020/extensions/todo/index.ts +151 -0
  453. package/agent.bak.20260905-190020/extensions/todo/install.ps1 +23 -0
  454. package/agent.bak.20260905-190020/extensions/todo/install.sh +21 -0
  455. package/agent.bak.20260905-190020/extensions/todo/locales/de.json +17 -0
  456. package/agent.bak.20260905-190020/extensions/todo/locales/en.json +15 -0
  457. package/agent.bak.20260905-190020/extensions/todo/locales/es.json +17 -0
  458. package/agent.bak.20260905-190020/extensions/todo/locales/fr.json +17 -0
  459. package/agent.bak.20260905-190020/extensions/todo/locales/pt-BR.json +17 -0
  460. package/agent.bak.20260905-190020/extensions/todo/locales/pt.json +17 -0
  461. package/agent.bak.20260905-190020/extensions/todo/locales/ru.json +17 -0
  462. package/agent.bak.20260905-190020/extensions/todo/locales/uk.json +17 -0
  463. package/agent.bak.20260905-190020/extensions/todo/locales/zh.json +17 -0
  464. package/agent.bak.20260905-190020/extensions/todo/package-lock.json +3358 -0
  465. package/agent.bak.20260905-190020/extensions/todo/package.json +67 -0
  466. package/agent.bak.20260905-190020/extensions/todo/state/i18n-bridge.ts +64 -0
  467. package/agent.bak.20260905-190020/extensions/todo/state/invariants.ts +20 -0
  468. package/agent.bak.20260905-190020/extensions/todo/state/replay.ts +38 -0
  469. package/agent.bak.20260905-190020/extensions/todo/state/selectors.ts +107 -0
  470. package/agent.bak.20260905-190020/extensions/todo/state/state-reducer.ts +326 -0
  471. package/agent.bak.20260905-190020/extensions/todo/state/state.ts +18 -0
  472. package/agent.bak.20260905-190020/extensions/todo/state/store.ts +82 -0
  473. package/agent.bak.20260905-190020/extensions/todo/state/task-graph.ts +57 -0
  474. package/agent.bak.20260905-190020/extensions/todo/todo-overlay.ts +204 -0
  475. package/agent.bak.20260905-190020/extensions/todo/todo.ts +155 -0
  476. package/agent.bak.20260905-190020/extensions/todo/tool/response-envelope.ts +109 -0
  477. package/agent.bak.20260905-190020/extensions/todo/tool/types.ts +206 -0
  478. package/agent.bak.20260905-190020/extensions/todo/view/format.ts +177 -0
  479. package/agent.bak.20260905-190020/extensions/todo/widget-order.ts +121 -0
  480. package/agent.bak.20260905-190020/extensions/trim-context/README.md +54 -0
  481. package/agent.bak.20260905-190020/extensions/trim-context/index.ts +487 -0
  482. package/agent.bak.20260905-190020/extensions/trim-context/install.ps1 +23 -0
  483. package/agent.bak.20260905-190020/extensions/trim-context/install.sh +21 -0
  484. package/agent.bak.20260905-190020/keybindings.json +7 -0
  485. package/bin/kiln.js +588 -358
  486. package/package.json +27 -21
  487. package/agent/install.ps1 +0 -527
  488. package/agent/install.sh +0 -511
  489. /package/{agent/extensions/status line → agent.bak.20260905-190020/extensions/ask-user}/install.ps1 +0 -0
  490. /package/{agent/extensions/status line → agent.bak.20260905-190020/extensions/ask-user}/install.sh +0 -0
  491. /package/{agent/extensions/status line → agent.bak.20260905-190020/extensions/statusline}/index.ts +0 -0
@@ -0,0 +1,1737 @@
1
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Text, truncateToWidth, type KeyId } from "@earendil-works/pi-tui";
3
+ import { Type } from "typebox";
4
+ import { StringEnum, type ImageContent, type TextContent } from "@earendil-works/pi-ai/compat";
5
+ import type { ExtractedContent, ExtractOptions } from "./extract.ts";
6
+ import { normalizeFetchContentParams } from "./fetch-params.ts";
7
+ import { resolveAuthFetchProfile, type AuthFetchProfile } from "./auth-fetch.ts";
8
+ import { findContent, type FindMode } from "./content-find.ts";
9
+ import { answerFromPage } from "./page-query.ts";
10
+ import { clearCloneCache } from "./github-extract.ts";
11
+ import { search, type ProviderAvailability } from "./gemini-search.ts";
12
+ export type { ProviderAvailability } from "./gemini-search.ts";
13
+ import type { SearchResult } from "./search-types.ts";
14
+ import { formatSeconds, getWebSearchConfigPath, installGlobalProxyFetch, runWithProxy } from "./utils.ts";
15
+ import {
16
+ clearResults,
17
+ deleteResult,
18
+ generateId,
19
+ getAllResults,
20
+ getResult,
21
+ restoreFromSession,
22
+ storeFetchedContentResult,
23
+ storeResult,
24
+ type QueryResultData,
25
+ type StoredSearchData,
26
+ } from "./storage.ts";
27
+ import { activityMonitor, type ActivityEntry } from "./activity.ts";
28
+ import { existsSync, readFileSync } from "node:fs";
29
+ import { isExaAvailable } from "./exa.ts";
30
+ import { getActiveGoogleEmail, getGeminiWebAvailabilityDiagnostic, getGeminiWebAvailabilityDiagnosticDetails, isGeminiWebAvailable } from "./gemini-web.ts";
31
+ import { isBrowserCookieAccessAllowed } from "./gemini-web-config.ts";
32
+ import { buildSearchErrorPlan, type SearchErrorDetails, type SearchErrorPlan } from "./render-search-error.ts";
33
+ import {
34
+ buildResearchArtifact,
35
+ withClaimAssessment,
36
+ storeResearchArtifact,
37
+ getResearchArtifact,
38
+ type RecencyFilter,
39
+ type ResearchArtifact,
40
+ } from "./source-check.ts";
41
+
42
+ type ExtensionTheme = ExtensionContext["ui"]["theme"];
43
+
44
+ const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
45
+
46
+ let extractModulePromise: Promise<typeof import("./extract.ts")> | undefined;
47
+ async function fetchAllContent(
48
+ urls: string[],
49
+ signal?: AbortSignal,
50
+ options?: ExtractOptions,
51
+ ): Promise<ExtractedContent[]> {
52
+ const extractModule = await (extractModulePromise ??= import("./extract.ts"));
53
+ return extractModule.fetchAllContent(urls, signal, options);
54
+ }
55
+
56
+ function withRegisteredFetchOptions(
57
+ options: ExtractOptions | undefined,
58
+ toolNames: ExtractOptions["toolNames"],
59
+ proxy?: string,
60
+ ): ExtractOptions {
61
+ return {
62
+ ...(options ?? {}),
63
+ toolNames,
64
+ ...(proxy !== undefined ? { proxy } : {}),
65
+ };
66
+ }
67
+
68
+ function isAbortError(err: unknown): boolean {
69
+ return (err instanceof Error ? err.message : String(err)).toLowerCase().includes("abort");
70
+ }
71
+
72
+ /** Shared collapsed/expanded renderer for an error/cancel plan produced by
73
+ * buildSearchErrorPlan(). Used by every tool renderResult's error branch so
74
+ * Ctrl+O (app.tools.expand) reveals diagnostics instead of a dead-end single line. */
75
+ function renderSearchErrorPlan(plan: SearchErrorPlan, expanded: boolean, theme: ExtensionTheme) {
76
+ if (expanded) {
77
+ return new Text(plan.expanded.map((l, i) => i === 0 ? theme.fg("error", l) : theme.fg("toolOutput", l)).join("\n"), 0, 0);
78
+ }
79
+ const box = new Box(1, 0, (t) => theme.bg("toolErrorBg", t));
80
+ box.addChild(new Text(theme.fg("error", plan.expanded[0]), 0, 0));
81
+ for (const line of plan.collapsed) {
82
+ box.addChild(new Text(theme.fg("dim", line), 0, 0));
83
+ }
84
+ if (plan.expandHint) {
85
+ box.addChild(new Text(theme.fg("muted", plan.expandHint), 0, 0));
86
+ }
87
+ return box;
88
+ }
89
+
90
+ interface WebSearchConfig {
91
+ anysearchApiKey?: unknown;
92
+ xcrawlApiKey?: unknown;
93
+ brightdataApiKey?: unknown;
94
+ brightdataSerpZone?: unknown;
95
+ kagiApiKey?: unknown;
96
+ ollamaApiKey?: unknown;
97
+ serpbaseApiKey?: unknown;
98
+ serperApiKey?: unknown;
99
+ tinyfishApiKey?: unknown;
100
+ valyuApiKey?: unknown;
101
+ xaiApiKey?: unknown;
102
+ maxInlineContentChars?: unknown;
103
+ webSearch?: {
104
+ enabled?: boolean;
105
+ };
106
+ tools?: Partial<Record<keyof ToolNames, { enabled?: boolean }>>;
107
+ commands?: Partial<Record<"websearch" | "search" | "google-account", { enabled?: boolean }>>;
108
+ toolNames?: Partial<ToolNames>;
109
+ shortcuts?: {
110
+ activity?: KeyId;
111
+ };
112
+ ssrf?: {
113
+ /** CIDR ranges exempted from the SSRF guard (e.g. fake-IP proxy ranges). */
114
+ allowRanges?: string[];
115
+ /** Skip local hostname DNS preflight when an HTTP(S)_PROXY env var applies. */
116
+ trustEnvProxy?: boolean;
117
+ };
118
+ }
119
+
120
+ function parseConfigRoot(raw: string): Record<string, unknown> {
121
+ let parsed: unknown;
122
+ try {
123
+ parsed = JSON.parse(raw);
124
+ } catch (err) {
125
+ const message = err instanceof Error ? err.message : String(err);
126
+ throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
127
+ }
128
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
129
+ throw new Error(`Invalid config in ${WEB_SEARCH_CONFIG_PATH}: expected a JSON object`);
130
+ }
131
+ return parsed as Record<string, unknown>;
132
+ }
133
+
134
+ function loadConfig(): WebSearchConfig {
135
+ if (!existsSync(WEB_SEARCH_CONFIG_PATH)) return {};
136
+ return parseConfigRoot(readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8")) as WebSearchConfig;
137
+ }
138
+
139
+ type ToolNames = {
140
+ webSearch: string;
141
+ sourceCheck: string;
142
+ fetchContent: string;
143
+ getSearchContent: string;
144
+ };
145
+
146
+ const DEFAULT_TOOL_NAMES: ToolNames = {
147
+ webSearch: "web_search",
148
+ sourceCheck: "source_check",
149
+ fetchContent: "fetch_content",
150
+ getSearchContent: "get_search_content",
151
+ };
152
+ const TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
153
+ const DEFAULT_SHORTCUTS = { activity: "ctrl+shift+w" } satisfies Record<string, KeyId>;
154
+
155
+ function isToolEnabled(config: WebSearchConfig, key: keyof ToolNames): boolean {
156
+ const override = config.tools?.[key]?.enabled;
157
+ if (typeof override === "boolean") return override;
158
+ return key !== "webSearch" && key !== "sourceCheck" || config.webSearch?.enabled !== false;
159
+ }
160
+
161
+ function isCommandEnabled(config: WebSearchConfig, name: "websearch" | "search" | "google-account"): boolean {
162
+ return config.commands?.[name]?.enabled !== false;
163
+ }
164
+
165
+ function joinToolNames(names: string[]): string {
166
+ if (names.length === 0) return "stored content";
167
+ if (names.length === 1) return names[0];
168
+ if (names.length === 2) return `${names[0]} or ${names[1]}`;
169
+ return `${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`;
170
+ }
171
+
172
+ function resolveToolNames(config: WebSearchConfig): ToolNames {
173
+ if (config.toolNames !== undefined && (!config.toolNames || typeof config.toolNames !== "object" || Array.isArray(config.toolNames))) {
174
+ throw new Error(`toolNames in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
175
+ }
176
+ const names = { ...DEFAULT_TOOL_NAMES };
177
+ for (const key of Object.keys(DEFAULT_TOOL_NAMES) as Array<keyof ToolNames>) {
178
+ const value = config.toolNames?.[key];
179
+ if (value === undefined) continue;
180
+ if (typeof value !== "string") throw new Error(`toolNames.${key} in ${WEB_SEARCH_CONFIG_PATH} must be a string`);
181
+ const trimmed = value.trim();
182
+ if (!TOOL_NAME_PATTERN.test(trimmed)) {
183
+ throw new Error(`toolNames.${key} in ${WEB_SEARCH_CONFIG_PATH} must start with a letter and contain only letters, numbers, underscores, or hyphens`);
184
+ }
185
+ names[key] = trimmed;
186
+ }
187
+ const registeredKeys = (Object.keys(DEFAULT_TOOL_NAMES) as Array<keyof ToolNames>)
188
+ .filter(key => isToolEnabled(config, key));
189
+ const seen = new Map<string, keyof ToolNames>();
190
+ for (const key of registeredKeys) {
191
+ const name = names[key];
192
+ const previous = seen.get(name);
193
+ if (previous) throw new Error(`toolNames.${key} duplicates toolNames.${previous} in ${WEB_SEARCH_CONFIG_PATH}`);
194
+ seen.set(name, key);
195
+ }
196
+ return names;
197
+ }
198
+
199
+ function loadConfigForExtensionInit(): WebSearchConfig {
200
+ try {
201
+ return loadConfig();
202
+ } catch (err) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ console.error(`[pi-web-access] ${message}`);
205
+ return {};
206
+ }
207
+ }
208
+
209
+ function normalizeRecencyFilter(value: unknown): RecencyFilter | undefined {
210
+ return value === "day" || value === "week" || value === "month" || value === "year"
211
+ ? value
212
+ : undefined;
213
+ }
214
+
215
+ function normalizeQueryList(queryList: unknown[]): string[] {
216
+ const normalized: string[] = [];
217
+ for (const query of queryList) {
218
+ if (typeof query !== "string") continue;
219
+ const trimmed = query.trim();
220
+ if (trimmed.length > 0) normalized.push(trimmed);
221
+ }
222
+ return normalized;
223
+ }
224
+
225
+ async function getProviderAvailability(): Promise<ProviderAvailability> {
226
+ const exa = isExaAvailable();
227
+ return { all: exa, exa };
228
+ }
229
+
230
+ const pendingFetches = new Map<string, AbortController>();
231
+ let sessionActive = false;
232
+ let widgetVisible = false;
233
+ let widgetUnsubscribe: (() => void) | null = null;
234
+
235
+
236
+ const DEFAULT_MAX_INLINE_CONTENT_CHARS = 30_000;
237
+ const MAX_INLINE_CONTENT_CHARS = 200_000;
238
+
239
+ function getMaxInlineContentChars(config = loadConfig()): number {
240
+ const value = config.maxInlineContentChars;
241
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
242
+ return DEFAULT_MAX_INLINE_CONTENT_CHARS;
243
+ }
244
+ return Math.min(value, MAX_INLINE_CONTENT_CHARS);
245
+ }
246
+
247
+ function stripThumbnails(results: ExtractedContent[]): ExtractedContent[] {
248
+ return results.map(({ thumbnail, frames, ...rest }) => rest);
249
+ }
250
+
251
+ function storeFetchResult(pi: { appendEntry(type: string, data: unknown): void }, responseId: string, data: StoredSearchData & { type: "fetch"; urls: ExtractedContent[] }, authProfile?: AuthFetchProfile): boolean {
252
+ if (authProfile?.cache === "off") return false;
253
+ pi.appendEntry("web-search-results", storeFetchedContentResult(responseId, data));
254
+ return true;
255
+ }
256
+
257
+ function initialContentSlice(content: string, maxChars: number): {
258
+ text: string;
259
+ endOffset: number;
260
+ totalBytes: number;
261
+ totalLines: number;
262
+ shownBytes: number;
263
+ shownLines: number;
264
+ } {
265
+ let endOffset = Math.min(content.length, maxChars);
266
+ if (endOffset < content.length) {
267
+ const lineBreak = content.lastIndexOf("\n", endOffset);
268
+ if (lineBreak >= Math.floor(maxChars * 0.8)) endOffset = lineBreak + 1;
269
+ }
270
+ const text = content.slice(0, endOffset);
271
+ return {
272
+ text,
273
+ endOffset,
274
+ totalBytes: Buffer.byteLength(content),
275
+ totalLines: content.length === 0 ? 0 : content.split("\n").length,
276
+ shownBytes: Buffer.byteLength(text),
277
+ shownLines: text.length === 0 ? 0 : text.split("\n").length,
278
+ };
279
+ }
280
+
281
+ function normalizeFindQueries(value: string | string[]): string[] {
282
+ const queries = (Array.isArray(value) ? value : [value]).map(query => query.trim()).filter(Boolean);
283
+ if (queries.length === 0) throw new Error("findText must contain at least one non-empty string");
284
+ return queries;
285
+ }
286
+
287
+ interface GetSearchContentParams {
288
+ responseId: string;
289
+ query?: string;
290
+ queryIndex?: number;
291
+ url?: string;
292
+ urlIndex?: number;
293
+ offset?: number;
294
+ limit?: number;
295
+ findText?: string | string[];
296
+ findMode?: FindMode;
297
+ }
298
+
299
+ type RawGetSearchContentParams = Omit<GetSearchContentParams, "findMode"> & { findMode?: unknown };
300
+
301
+ function normalizeFindMode(value: unknown): FindMode | undefined {
302
+ if (value === undefined) return undefined;
303
+ if (value === "exact" || value === "case-insensitive" || value === "fuzzy") return value;
304
+ throw new Error('findMode must be "exact", "case-insensitive", or "fuzzy"');
305
+ }
306
+
307
+ function normalizeGetSearchContentParams(params: RawGetSearchContentParams): GetSearchContentParams {
308
+ const normalized: GetSearchContentParams = { ...params, findMode: normalizeFindMode(params.findMode) };
309
+
310
+ if (normalized.query?.trim() === "") delete normalized.query;
311
+ if (normalized.url?.trim() === "") delete normalized.url;
312
+
313
+ if (normalized.findText !== undefined) {
314
+ delete normalized.offset;
315
+ delete normalized.limit;
316
+ }
317
+
318
+ return normalized;
319
+ }
320
+
321
+ function formatInputValue(value: unknown): string {
322
+ if (typeof value === "string") return JSON.stringify(value);
323
+ if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
324
+ try {
325
+ const serialized = JSON.stringify(value);
326
+ return serialized === undefined ? String(value) : serialized;
327
+ } catch {
328
+ return String(value);
329
+ }
330
+ }
331
+
332
+ function formatSearchSummary(results: SearchResult[], answer: string): string {
333
+ if (results.length === 0) {
334
+ return answer ? `${answer}\n\n---\n\n**Sources:**\nNo sources returned.` : "No results found.";
335
+ }
336
+ let output = answer ? `${answer}\n\n---\n\n**Sources:**\n` : "";
337
+ output += results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}`).join("\n\n");
338
+ return output;
339
+ }
340
+
341
+ function formatSourceCheckResult(artifact: ResearchArtifact, getSearchContentTool: string | null = DEFAULT_TOOL_NAMES.getSearchContent): string {
342
+ const assessment = artifact.claims?.[0];
343
+ const lines = [`# Source check: ${artifact.query}`, ""];
344
+ if (assessment) {
345
+ lines.push(`**Status:** ${assessment.status} (confidence ${assessment.confidence.toFixed(2)})`);
346
+ lines.push(`**Rationale:** ${assessment.rationale}`);
347
+ if (assessment.supporting_passages.length > 0) lines.push(`**Supporting passages:** ${assessment.supporting_passages.join(", ")}`);
348
+ if (assessment.contradicting_passages.length > 0) lines.push(`**Contradicting passages:** ${assessment.contradicting_passages.join(", ")}`);
349
+ lines.push("");
350
+ }
351
+ if (artifact.sources.length > 0) {
352
+ lines.push("## Sources");
353
+ for (const source of artifact.sources) lines.push(`${source.rank}. [${source.quality}] ${source.title}\n ${source.url}`);
354
+ lines.push("");
355
+ }
356
+ if (artifact.errors?.length) lines.push(`Search errors: ${artifact.errors.map((entry) => `${entry.query}: ${entry.error}`).join("; ")}`);
357
+ lines.push(getSearchContentTool
358
+ ? `Artifact responseId: ${artifact.id} (retrievable via ${getSearchContentTool}).`
359
+ : `Artifact responseId: ${artifact.id}. Content retrieval is not registered.`);
360
+ return lines.join("\n");
361
+ }
362
+
363
+ function hasFullInlineCoverage(urls: string[], inlineContent: ExtractedContent[] | undefined): boolean {
364
+ if (!inlineContent || inlineContent.length === 0) return false;
365
+ const coveredUrls = new Set(inlineContent.map(c => c.url));
366
+ return urls.every(url => coveredUrls.has(url));
367
+ }
368
+
369
+ function formatFullResults(queryData: QueryResultData): string {
370
+ let output = `## Results for: "${queryData.query}"\n\n`;
371
+ if (queryData.answer) {
372
+ output += `${queryData.answer}\n\n---\n\n`;
373
+ }
374
+ for (const r of queryData.results) {
375
+ output += `### ${r.title}\n${r.url}\n\n`;
376
+ }
377
+ return output;
378
+ }
379
+
380
+ function abortPendingFetches(): void {
381
+ for (const controller of pendingFetches.values()) {
382
+ controller.abort();
383
+ }
384
+ pendingFetches.clear();
385
+ }
386
+
387
+ function updateWidget(ctx: ExtensionContext): void {
388
+ const theme = ctx.ui.theme;
389
+ const entries = activityMonitor.getEntries();
390
+ const lines: string[] = [];
391
+
392
+ lines.push(theme.fg("accent", "─── Web Search Activity " + "─".repeat(36)));
393
+
394
+ if (entries.length === 0) {
395
+ lines.push(theme.fg("muted", " No activity yet"));
396
+ } else {
397
+ for (const e of entries) {
398
+ lines.push(" " + formatEntryLine(e, theme));
399
+ }
400
+ }
401
+
402
+ lines.push(theme.fg("accent", "─".repeat(60)));
403
+
404
+ const rateInfo = activityMonitor.getRateLimitInfo();
405
+ const resetMs = rateInfo.oldestTimestamp ? Math.max(0, rateInfo.oldestTimestamp + rateInfo.windowMs - Date.now()) : 0;
406
+ const resetSec = Math.ceil(resetMs / 1000);
407
+ lines.push(
408
+ theme.fg("muted", `Rate: ${rateInfo.used}/${rateInfo.max}`) +
409
+ (resetMs > 0 ? theme.fg("dim", ` (resets in ${resetSec}s)`) : ""),
410
+ );
411
+
412
+ ctx.ui.setWidget("web-activity", lines);
413
+ }
414
+
415
+ function formatEntryLine(
416
+ entry: ActivityEntry,
417
+ theme: ExtensionTheme,
418
+ ): string {
419
+ const typeStr = entry.type === "api" ? "API" : "GET";
420
+ const target =
421
+ entry.type === "api"
422
+ ? `"${truncateToWidth(entry.query || "", 28, "")}"`
423
+ : truncateToWidth(entry.url?.replace(/^https?:\/\//, "") || "", 30, "");
424
+
425
+ const duration = entry.endTime
426
+ ? `${((entry.endTime - entry.startTime) / 1000).toFixed(1)}s`
427
+ : `${((Date.now() - entry.startTime) / 1000).toFixed(1)}s`;
428
+
429
+ let statusStr: string;
430
+ let indicator: string;
431
+ if (entry.error) {
432
+ statusStr = "err";
433
+ indicator = theme.fg("error", "✗");
434
+ } else if (entry.status === null) {
435
+ statusStr = "...";
436
+ indicator = theme.fg("warning", "⋯");
437
+ } else if (entry.status === 0) {
438
+ statusStr = "abort";
439
+ indicator = theme.fg("muted", "○");
440
+ } else {
441
+ statusStr = String(entry.status);
442
+ indicator = entry.status >= 200 && entry.status < 300 ? theme.fg("success", "✓") : theme.fg("error", "✗");
443
+ }
444
+
445
+ return `${typeStr.padEnd(4)} ${target.padEnd(32)} ${statusStr.padStart(5)} ${duration.padStart(5)} ${indicator}`;
446
+ }
447
+
448
+ function handleSessionChange(ctx: ExtensionContext): void {
449
+ abortPendingFetches();
450
+ clearCloneCache();
451
+ sessionActive = true;
452
+ restoreFromSession(ctx);
453
+ // Unsubscribe before clear() to avoid callback with stale ctx
454
+ widgetUnsubscribe?.();
455
+ widgetUnsubscribe = null;
456
+ activityMonitor.clear();
457
+ if (widgetVisible) {
458
+ // Re-subscribe with new ctx
459
+ widgetUnsubscribe = activityMonitor.onUpdate(() => updateWidget(ctx));
460
+ updateWidget(ctx);
461
+ }
462
+ }
463
+
464
+ export default function (pi: ExtensionAPI) {
465
+ const initConfig = loadConfigForExtensionInit();
466
+ installGlobalProxyFetch();
467
+ const toolNames = resolveToolNames(initConfig);
468
+ const webSearchEnabled = isToolEnabled(initConfig, "webSearch");
469
+ const sourceCheckEnabled = isToolEnabled(initConfig, "sourceCheck");
470
+ const fetchContentEnabled = isToolEnabled(initConfig, "fetchContent");
471
+ const getSearchContentEnabled = isToolEnabled(initConfig, "getSearchContent");
472
+ // Names as registered this session, so fetch failure guidance never points
473
+ // at tools that are disabled or were renamed after init.
474
+ const registeredToolNames = {
475
+ ...(webSearchEnabled ? { webSearch: toolNames.webSearch } : {}),
476
+ ...(fetchContentEnabled ? { fetchContent: toolNames.fetchContent } : {}),
477
+ };
478
+ const storedContentSources = joinToolNames([
479
+ ...(webSearchEnabled ? [toolNames.webSearch] : []),
480
+ ...(sourceCheckEnabled ? [toolNames.sourceCheck] : []),
481
+ ...(fetchContentEnabled ? [toolNames.fetchContent] : []),
482
+ ]);
483
+ const searchQueryDescription = webSearchEnabled
484
+ ? `Get content for this query (${toolNames.webSearch})`
485
+ : "Get content for a stored search query";
486
+ const fetchContentStorageNote = getSearchContentEnabled
487
+ ? `Full original content is stored for retrieval with ${toolNames.getSearchContent}.`
488
+ : "Full original content is stored internally, but the retrieval tool is not registered.";
489
+ const activityKey = initConfig.shortcuts?.activity || DEFAULT_SHORTCUTS.activity;
490
+
491
+ function startBackgroundFetch(urls: string[], proxy?: string): string | null {
492
+ if (urls.length === 0) return null;
493
+ const fetchId = generateId();
494
+ const controller = new AbortController();
495
+ pendingFetches.set(fetchId, controller);
496
+ runWithProxy(proxy, () => fetchAllContent(urls, controller.signal, withRegisteredFetchOptions(undefined, registeredToolNames, proxy)))
497
+ .then((fetched) => {
498
+ if (!sessionActive || !pendingFetches.has(fetchId)) return;
499
+ const data = {
500
+ id: fetchId,
501
+ type: "fetch",
502
+ timestamp: Date.now(),
503
+ urls: stripThumbnails(fetched),
504
+ } satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
505
+ pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
506
+ const ok = fetched.filter(f => !f.error).length;
507
+ const availability = ok === fetched.length
508
+ ? "Full page content now available."
509
+ : ok > 0
510
+ ? "Partial page content now available."
511
+ : "No page content was fetched. Stored fetch diagnostics are available.";
512
+ pi.sendMessage(
513
+ {
514
+ customType: "web-search-content-ready",
515
+ content: `Content fetched for ${ok}/${fetched.length} URLs [${fetchId}]. ${availability}`,
516
+ display: true,
517
+ },
518
+ { triggerTurn: true },
519
+ );
520
+ })
521
+ .catch((err) => {
522
+ if (!sessionActive || !pendingFetches.has(fetchId)) return;
523
+ const message = err instanceof Error ? err.message : String(err);
524
+ const isAbort = (err instanceof Error && err.name === "AbortError") || message.toLowerCase().includes("abort");
525
+ if (!isAbort) {
526
+ pi.sendMessage(
527
+ {
528
+ customType: "web-search-error",
529
+ content: `Content fetch failed [${fetchId}]: ${message}`,
530
+ display: true,
531
+ },
532
+ { triggerTurn: false },
533
+ );
534
+ }
535
+ })
536
+ .finally(() => { pendingFetches.delete(fetchId); });
537
+ return fetchId;
538
+ }
539
+
540
+ function storeAndPublishSearch(results: QueryResultData[]): string {
541
+ const id = generateId();
542
+ const data: StoredSearchData = {
543
+ id, type: "search", timestamp: Date.now(), queries: results,
544
+ };
545
+ storeResult(id, data);
546
+ pi.appendEntry("web-search-results", data);
547
+ return id;
548
+ }
549
+
550
+ interface SearchReturnOptions {
551
+ queryList: string[];
552
+ results: QueryResultData[];
553
+ urls: string[];
554
+ includeContent: boolean;
555
+ inlineContent?: ExtractedContent[];
556
+ proxy?: string;
557
+ }
558
+
559
+
560
+ function buildSearchReturn(opts: SearchReturnOptions): AgentToolResult<Record<string, unknown>> {
561
+ const sc = opts.results.filter(r => !r.error).length;
562
+ const tr = opts.results.reduce((sum, r) => sum + r.results.length, 0);
563
+
564
+ let output = "";
565
+ for (const { query, answer, results, error } of opts.results) {
566
+ if (opts.queryList.length > 1) {
567
+ output += `## Query: "${query}"\n\n`;
568
+ }
569
+ if (error) output += `Error: ${error}\n\n`;
570
+ else output += formatSearchSummary(results, answer) + "\n\n";
571
+ }
572
+
573
+ const hasInlineReady = hasFullInlineCoverage(opts.urls, opts.inlineContent);
574
+ let fetchId: string | null = null;
575
+ if (hasInlineReady && opts.inlineContent) {
576
+ fetchId = generateId();
577
+ const data = {
578
+ id: fetchId,
579
+ type: "fetch",
580
+ timestamp: Date.now(),
581
+ urls: opts.inlineContent,
582
+ } satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
583
+ pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
584
+ output += `---\nFull content for ${opts.inlineContent.length} sources available [${fetchId}].`;
585
+ } else if (opts.includeContent) {
586
+ fetchId = startBackgroundFetch(opts.urls, opts.proxy);
587
+ if (fetchId) {
588
+ output += `---\nContent fetching in background [${fetchId}]. Will notify when ready.`;
589
+ }
590
+ }
591
+
592
+ const searchId = storeAndPublishSearch(opts.results);
593
+ const isBackgroundFetch = fetchId !== null && !hasInlineReady;
594
+
595
+ return {
596
+ content: [{ type: "text", text: output.trim() }],
597
+ details: {
598
+ queries: opts.queryList,
599
+ queryCount: opts.queryList.length,
600
+ successfulQueries: sc,
601
+ totalResults: tr,
602
+ includeContent: opts.includeContent,
603
+ fetchId,
604
+ fetchUrls: isBackgroundFetch ? opts.urls : undefined,
605
+ searchId,
606
+ },
607
+ };
608
+ }
609
+
610
+
611
+ pi.registerShortcut(activityKey, {
612
+ description: "Toggle web search activity",
613
+ handler: async (ctx) => {
614
+ widgetVisible = !widgetVisible;
615
+ if (widgetVisible) {
616
+ widgetUnsubscribe = activityMonitor.onUpdate(() => updateWidget(ctx));
617
+ updateWidget(ctx);
618
+ } else {
619
+ widgetUnsubscribe?.();
620
+ widgetUnsubscribe = null;
621
+ ctx.ui.setWidget("web-activity", undefined);
622
+ }
623
+ },
624
+ });
625
+
626
+ pi.on("session_start", async (_event, ctx) => handleSessionChange(ctx));
627
+ pi.on("session_tree", async (_event, ctx) => handleSessionChange(ctx));
628
+
629
+ pi.on("session_shutdown", () => {
630
+ sessionActive = false;
631
+ abortPendingFetches();
632
+ clearCloneCache();
633
+ clearResults();
634
+ // Unsubscribe before clear() to avoid callback with stale ctx
635
+ widgetUnsubscribe?.();
636
+ widgetUnsubscribe = null;
637
+ activityMonitor.clear();
638
+ widgetVisible = false;
639
+ });
640
+
641
+ if (webSearchEnabled) pi.registerTool({
642
+ name: toolNames.webSearch,
643
+ label: "Web Search",
644
+ description:
645
+ `Search the web using Exa. Returns an AI-synthesized answer with source citations. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background.`,
646
+ promptSnippet:
647
+ "Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage.",
648
+ parameters: Type.Object({
649
+ query: Type.Optional(Type.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
650
+ queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched in sequence, each returning its own synthesized answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
651
+ numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)" })),
652
+ includeContent: Type.Optional(Type.Boolean({ description: "Fetch full page content (async)" })),
653
+ recencyFilter: Type.Optional(
654
+ StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" }),
655
+ ),
656
+ domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
657
+ proxy: Type.Optional(Type.String({
658
+ description: "http(s) proxy URL (e.g. http://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.",
659
+ })),
660
+ }),
661
+
662
+ async execute(callId, params, signal, onUpdate, ctx) {
663
+ return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
664
+ const rawQueryList: unknown[] = Array.isArray(params.queries)
665
+ ? params.queries
666
+ : (params.query !== undefined ? [params.query] : []);
667
+ const queryList = normalizeQueryList(rawQueryList);
668
+ const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
669
+
670
+ if (queryList.length === 0) {
671
+ return {
672
+ content: [{ type: "text", text: "Error: No query provided. Use 'query' or 'queries' parameter." }],
673
+ details: { error: "No query provided" },
674
+ };
675
+ }
676
+
677
+ const searchResults: QueryResultData[] = [];
678
+ const allUrls: string[] = [];
679
+ const allInlineContent: ExtractedContent[] = [];
680
+
681
+ for (let i = 0; i < queryList.length; i++) {
682
+ const query = queryList[i];
683
+
684
+ onUpdate?.({
685
+ content: [{ type: "text", text: `Searching ${i + 1}/${queryList.length}: "${query}"...` }],
686
+ details: { phase: "search", progress: i / queryList.length, currentQuery: query },
687
+ });
688
+
689
+ try {
690
+ const { answer, results, inlineContent, provider } = await search(query, {
691
+ numResults: params.numResults,
692
+ recencyFilter,
693
+ domainFilter: params.domainFilter,
694
+ includeContent: params.includeContent,
695
+ signal,
696
+ });
697
+
698
+ searchResults.push({ query, answer, results, error: null, provider });
699
+ for (const r of results) {
700
+ if (!allUrls.includes(r.url)) {
701
+ allUrls.push(r.url);
702
+ }
703
+ }
704
+ if (inlineContent) allInlineContent.push(...inlineContent);
705
+ } catch (err) {
706
+ if (signal?.aborted || isAbortError(err)) throw err;
707
+ const message = err instanceof Error ? err.message : String(err);
708
+ searchResults.push({ query, answer: "", results: [], error: message, provider: "exa" });
709
+ }
710
+ }
711
+
712
+ return buildSearchReturn({
713
+ queryList,
714
+ results: searchResults,
715
+ urls: allUrls,
716
+ includeContent: params.includeContent ?? false,
717
+ inlineContent: allInlineContent.length > 0 ? allInlineContent : undefined,
718
+ proxy: typeof params.proxy === "string" ? params.proxy : undefined,
719
+ });
720
+ });
721
+ },
722
+
723
+ renderCall(args, theme) {
724
+ const input = args as { query?: unknown; queries?: unknown };
725
+ const rawQueryList: unknown[] = Array.isArray(input.queries)
726
+ ? input.queries
727
+ : (input.query !== undefined ? [input.query] : []);
728
+ const queryList = normalizeQueryList(rawQueryList);
729
+ if (queryList.length === 0) {
730
+ return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("error", "(no query)"), 0, 0);
731
+ }
732
+ if (queryList.length === 1) {
733
+ const q = queryList[0];
734
+ const display = q.length > 60 ? q.slice(0, 57) + "..." : q;
735
+ return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("accent", `"${display}"`), 0, 0);
736
+ }
737
+ const lines = [theme.fg("toolTitle", theme.bold("search ")) + theme.fg("accent", `${queryList.length} queries`)];
738
+ for (const q of queryList.slice(0, 5)) {
739
+ const display = q.length > 50 ? q.slice(0, 47) + "..." : q;
740
+ lines.push(theme.fg("muted", ` "${display}"`));
741
+ }
742
+ if (queryList.length > 5) {
743
+ lines.push(theme.fg("muted", ` ... and ${queryList.length - 5} more`));
744
+ }
745
+ return new Text(lines.join("\n"), 0, 0);
746
+ },
747
+
748
+ renderResult(result, { expanded, isPartial }, theme) {
749
+ const details = result.details as {
750
+ queryCount?: number;
751
+ successfulQueries?: number;
752
+ totalResults?: number;
753
+ error?: string;
754
+ fetchId?: string;
755
+ fetchUrls?: string[];
756
+ phase?: string;
757
+ progress?: number;
758
+ currentQuery?: string;
759
+ cancelled?: boolean;
760
+ cancelReason?: string;
761
+ browserConnected?: boolean;
762
+ lastHeartbeatAgeMs?: number | null;
763
+ cancelledQueries?: import("./render-search-error.ts").CancelledQueryDetail[];
764
+ };
765
+
766
+ if (isPartial) {
767
+ if (details?.phase === "searching") {
768
+ const progress = details?.progress ?? 0;
769
+ const bar = "\u2588".repeat(Math.floor(progress * 10)) + "\u2591".repeat(10 - Math.floor(progress * 10));
770
+ const query = details?.currentQuery || "";
771
+ const display = query.length > 40 ? query.slice(0, 37) + "..." : query;
772
+ return new Text(theme.fg("accent", `[${bar}] ${display}`), 0, 0);
773
+ }
774
+ const progress = details?.progress ?? 0;
775
+ const bar = "\u2588".repeat(Math.floor(progress * 10)) + "\u2591".repeat(10 - Math.floor(progress * 10));
776
+ return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "searching"}`), 0, 0);
777
+ }
778
+
779
+ if (details?.error) {
780
+ // Expandable Ctrl+O diagnostics: which queries completed, per-query errors,
781
+ // browser connection state, cancel reason. See render-search-error.ts.
782
+ const plan = buildSearchErrorPlan(details as SearchErrorDetails);
783
+ if (plan) return renderSearchErrorPlan(plan, expanded, theme);
784
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
785
+ }
786
+
787
+ let statusLine: string;
788
+ const queryInfo = details?.queryCount === 1 ? "" : `${details?.successfulQueries}/${details?.queryCount} queries, `;
789
+ statusLine = theme.fg("success", `${queryInfo}${details?.totalResults ?? 0} sources`);
790
+ if (details?.fetchId && details?.fetchUrls) {
791
+ statusLine += theme.fg("muted", ` (fetching ${details.fetchUrls.length} URLs)`);
792
+ } else if (details?.fetchId) {
793
+ statusLine += theme.fg("muted", " (content ready)");
794
+ }
795
+
796
+ // Build expanded lines first so collapsed view can reference total count
797
+ const lines = [statusLine];
798
+ {
799
+ const textContent = result.content.find((c) => c.type === "text")?.text || "";
800
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
801
+ for (const line of preview.split("\n")) {
802
+ lines.push(theme.fg("dim", line));
803
+ }
804
+ }
805
+
806
+ if (details?.fetchUrls && details.fetchUrls.length > 0) {
807
+ lines.push(theme.fg("muted", "Fetching:"));
808
+ for (const u of details.fetchUrls.slice(0, 5)) {
809
+ const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
810
+ lines.push(theme.fg("dim", " " + display));
811
+ }
812
+ if (details.fetchUrls.length > 5) {
813
+ lines.push(theme.fg("dim", ` ... and ${details.fetchUrls.length - 5} more`));
814
+ }
815
+ }
816
+
817
+ const totalLines = lines.length;
818
+
819
+ if (!expanded) {
820
+ const box = new Box(1, 0);
821
+ box.addChild(new Text(statusLine, 0, 0));
822
+
823
+ let collapsedLines = 1; // statusLine
824
+ {
825
+ const textContent = result.content.find((c) => c.type === "text")?.text || "";
826
+ const firstContentLine = textContent.split("\n").find(l => {
827
+ const t = l.trim();
828
+ return t && !t.startsWith("[") && !t.startsWith("#") && !t.startsWith("---");
829
+ });
830
+ const fallbackLine = (firstContentLine?.trim() || "").replace(/\*\*/g, "");
831
+ if (fallbackLine) {
832
+ const preview = fallbackLine.length > 120 ? fallbackLine.slice(0, 117) + "..." : fallbackLine;
833
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
834
+ collapsedLines++;
835
+ }
836
+ }
837
+ const moreLines = Math.max(0, totalLines - collapsedLines);
838
+ if (moreLines > 0) {
839
+ box.addChild(new Text(theme.fg("muted", `\n... (${moreLines} more lines, ${totalLines} total, ctrl+o to expand)`), 0, 0));
840
+ }
841
+ return box;
842
+ }
843
+
844
+ return new Text(lines.join("\n"), 0, 0);
845
+ },
846
+ });
847
+
848
+ if (sourceCheckEnabled) pi.registerTool({
849
+ name: toolNames.sourceCheck,
850
+ label: "Source Check",
851
+ description: "Check a claim against web sources and return a bounded machine-readable research artifact with exact passage citations.",
852
+ promptSnippet: "Verify a claim with structured source evidence and passage-level citations.",
853
+ parameters: Type.Object({
854
+ claim: Type.String({ description: "The assertion to check against web sources." }),
855
+ queries: Type.Optional(Type.Array(Type.String(), { description: "Search queries (default: the claim)." })),
856
+ numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)." })),
857
+ fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
858
+ recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
859
+ domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
860
+ proxy: Type.Optional(Type.String({
861
+ description: "http(s) proxy URL (e.g. http://host:port) used for every outbound request in this call (search APIs and result-page fetches). Empty string forces direct access.",
862
+ })),
863
+ }),
864
+ async execute(_callId, params, signal, _onUpdate, ctx) {
865
+ return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
866
+ const claim = typeof params.claim === "string" ? params.claim.trim() : "";
867
+ if (!claim) {
868
+ return { content: [{ type: "text", text: "Error: 'claim' is required." }], details: { error: "Missing claim" } };
869
+ }
870
+
871
+ const requestedQueries = Array.isArray(params.queries)
872
+ ? params.queries.filter((query): query is string => typeof query === "string").map((query) => query.trim()).filter(Boolean)
873
+ : [];
874
+ const queries = (requestedQueries.length > 0 ? requestedQueries : [claim]).slice(0, 8);
875
+ const numResults = typeof params.numResults === "number" && Number.isFinite(params.numResults)
876
+ ? Math.min(20, Math.max(1, Math.floor(params.numResults)))
877
+ : 5;
878
+ const domainFilter = Array.isArray(params.domainFilter)
879
+ ? params.domainFilter.filter((domain): domain is string => typeof domain === "string")
880
+ : undefined;
881
+ const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
882
+ const resultsByUrl = new Map<string, SearchResult>();
883
+ const summaries: string[] = [];
884
+ const errors: Array<{ query: string; error: string }> = [];
885
+ let provider: string | undefined;
886
+
887
+ for (const query of queries) {
888
+ if (signal?.aborted) break;
889
+ try {
890
+ const response = await search(query, {
891
+ numResults,
892
+ recencyFilter,
893
+ domainFilter,
894
+ signal,
895
+ });
896
+ if (signal?.aborted) break;
897
+ provider ??= response.provider;
898
+ if (response.answer) summaries.push(`${query}: ${response.answer}`);
899
+ for (const result of response.results) {
900
+ if (!resultsByUrl.has(result.url)) resultsByUrl.set(result.url, result);
901
+ }
902
+ } catch (err) {
903
+ if (signal?.aborted || isAbortError(err)) break;
904
+ errors.push({ query, error: err instanceof Error ? err.message : String(err) });
905
+ }
906
+ }
907
+
908
+ const results = [...resultsByUrl.values()].slice(0, 20).map((result, index) => ({ ...result, rank: index + 1 }));
909
+ let fetched: ExtractedContent[] = [];
910
+ if (params.fetchContent && results.length > 0) {
911
+ const urls = results.slice(0, 5).map((result) => result.url);
912
+ try {
913
+ fetched = await fetchAllContent(urls, signal, withRegisteredFetchOptions(undefined, registeredToolNames, typeof params.proxy === "string" ? params.proxy : undefined));
914
+ } catch (err) {
915
+ if (signal?.aborted || isAbortError(err)) throw err;
916
+ fetched = urls.map((url) => ({ url, title: "", content: "", error: err instanceof Error ? err.message : String(err) }));
917
+ }
918
+ }
919
+ const artifact = withClaimAssessment(buildResearchArtifact({
920
+ query: claim,
921
+ provider,
922
+ summary: summaries.length > 0 ? summaries.join("\n\n") : undefined,
923
+ results,
924
+ fetched,
925
+ recency: recencyFilter,
926
+ domainFilter,
927
+ }), [claim]);
928
+ if (errors.length > 0) artifact.errors = errors;
929
+ storeResearchArtifact(artifact);
930
+ pi.appendEntry("web-search-results", {
931
+ id: artifact.id,
932
+ type: "research",
933
+ timestamp: artifact.timestamp,
934
+ artifact,
935
+ });
936
+ return {
937
+ content: [{ type: "text", text: formatSourceCheckResult(artifact, getSearchContentEnabled ? toolNames.getSearchContent : null) }],
938
+ details: { responseId: artifact.id, artifact, sourceCount: artifact.sources.length, passageCount: artifact.passages.length },
939
+ };
940
+ });
941
+ },
942
+ });
943
+
944
+ if (fetchContentEnabled) pi.registerTool({
945
+ name: toolNames.fetchContent,
946
+ label: "Fetch Content",
947
+ description: `Fetch URL(s) and extract readable content as markdown. Use mode "raw" for exact textual HTTP response bodies or mode "answer" with prompt to answer using only fetched content. Direct image URLs return resized image content. Supports YouTube transcripts, GitHub repositories, PDFs, and local videos. ${fetchContentStorageNote}`,
948
+ promptSnippet:
949
+ "Use to fetch readable or raw URL content, direct images, GitHub repos, and videos. Mode answer answers a prompt using only the fetched source.",
950
+ parameters: Type.Object({
951
+ url: Type.Optional(Type.String({ description: "Single URL to fetch" })),
952
+ urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs (parallel)" })),
953
+ forceClone: Type.Optional(Type.Boolean({
954
+ description: "Force cloning large GitHub repositories that exceed the size threshold",
955
+ })),
956
+ prompt: Type.Optional(Type.String({
957
+ description: "Question or instruction for video analysis, or the page-local question required by mode answer.",
958
+ })),
959
+ mode: Type.Optional(StringEnum(["readable", "raw", "answer"], {
960
+ description: "Fetch mode: readable (default extraction), raw (exact textual HTTP body), or answer (answer prompt using only fetched content).",
961
+ })),
962
+ answerModel: Type.Optional(Type.String({
963
+ description: "Optional provider/model-id override for mode answer. Defaults to the current Pi model.",
964
+ })),
965
+ timestamp: Type.Optional(Type.String({
966
+ description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame.",
967
+ })),
968
+ frames: Type.Optional(Type.Integer({
969
+ minimum: 1,
970
+ maximum: 12,
971
+ description: "Number of frames to extract. Use with timestamp range for custom density, with single timestamp to get N frames at 5s intervals, or alone to sample across the entire video. Requires yt-dlp + ffmpeg for YouTube, ffmpeg for local video.",
972
+ })),
973
+ model: Type.Optional(Type.String({
974
+ description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-3.6-flash'). Defaults to config or gemini-3.6-flash.",
975
+ })),
976
+ auth: Type.Optional(Type.Union([Type.String(), Type.Boolean()], {
977
+ description: "Opt into an authFetch profile for local browser-cookie fetching. Use a profile name, or true only when exactly one profile exists.",
978
+ })),
979
+ proxy: Type.Optional(Type.String({
980
+ description: "http(s) proxy URL (e.g. http://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.",
981
+ })),
982
+ }),
983
+
984
+ async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<Record<string, unknown>>> {
985
+ let normalized: ReturnType<typeof normalizeFetchContentParams>;
986
+ try {
987
+ normalized = normalizeFetchContentParams(params);
988
+ } catch (err) {
989
+ const error = err instanceof Error ? err.message : String(err);
990
+ return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
991
+ }
992
+ const { urlList, options } = normalized;
993
+ return runWithProxy(options.proxy, async () => {
994
+ const mode = options.mode ?? "readable";
995
+ if (mode === "answer" && !options.prompt) {
996
+ return { content: [{ type: "text", text: "Error: mode answer requires prompt." }], details: { error: "mode answer requires prompt" } };
997
+ }
998
+ if (mode === "raw" && (options.forceClone === true || options.timestamp || options.frames || options.prompt || options.model || options.answerModel)) {
999
+ 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" } };
1000
+ }
1001
+ if (mode !== "answer" && options.answerModel) {
1002
+ return { content: [{ type: "text", text: "Error: answerModel requires mode answer." }], details: { error: "answerModel requires mode answer" } };
1003
+ }
1004
+ if (mode === "answer" && options.model) {
1005
+ return { content: [{ type: "text", text: "Error: use answerModel, not model, with mode answer." }], details: { error: "model is incompatible with mode answer" } };
1006
+ }
1007
+ if (mode === "answer" && options.auth !== undefined) {
1008
+ return { content: [{ type: "text", text: "Error: auth cannot be combined with mode answer." }], details: { error: "auth cannot be combined with mode answer" } };
1009
+ }
1010
+ let authFetchProfile: AuthFetchProfile | undefined;
1011
+ if (options.auth !== undefined) {
1012
+ try {
1013
+ authFetchProfile = resolveAuthFetchProfile(options.auth);
1014
+ } catch (err) {
1015
+ const error = err instanceof Error ? err.message : String(err);
1016
+ return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
1017
+ }
1018
+ }
1019
+ if (urlList.length === 0) {
1020
+ return {
1021
+ content: [{ type: "text", text: "Error: No URL provided." }],
1022
+ details: { error: "No URL provided" },
1023
+ };
1024
+ }
1025
+
1026
+ onUpdate?.({
1027
+ content: [{ type: "text", text: `Fetching ${urlList.length} URL(s)...` }],
1028
+ details: { phase: "fetch", progress: 0 },
1029
+ });
1030
+
1031
+ const { answerModel: _answerModel, auth: _auth, ...extractionOptions } = options;
1032
+ const fetchOptions = mode === "answer"
1033
+ ? (() => {
1034
+ const { prompt: _prompt, ...rest } = extractionOptions;
1035
+ return { ...rest, ...(authFetchProfile ? { authFetchProfile } : {}) };
1036
+ })()
1037
+ : { ...extractionOptions, ...(authFetchProfile ? { authFetchProfile } : {}) };
1038
+ const fetchResults = await fetchAllContent(urlList, signal, withRegisteredFetchOptions(fetchOptions, registeredToolNames, options.proxy));
1039
+ const presentedResults = mode === "answer"
1040
+ ? await Promise.all(fetchResults.map(async result => {
1041
+ if (result.error) return result;
1042
+ if (result.thumbnail || result.mimeType?.startsWith("image/")) {
1043
+ return { ...result, error: "Page answer requires textual fetched content" };
1044
+ }
1045
+ try {
1046
+ const answer = await answerFromPage({
1047
+ question: options.prompt!,
1048
+ pageText: result.content,
1049
+ sourceUrl: result.url,
1050
+ ...(options.answerModel ? { model: options.answerModel } : {}),
1051
+ }, ctx, signal);
1052
+ return { ...result, content: answer.text };
1053
+ } catch (err) {
1054
+ return { ...result, error: `Page answer failed: ${err instanceof Error ? err.message : String(err)}` };
1055
+ }
1056
+ }))
1057
+ : fetchResults;
1058
+ const successful = presentedResults.filter((r) => !r.error).length;
1059
+ const totalChars = presentedResults.reduce((sum, r) => sum + r.content.length, 0);
1060
+
1061
+ const responseId = generateId();
1062
+ const data = {
1063
+ id: responseId,
1064
+ type: "fetch",
1065
+ timestamp: Date.now(),
1066
+ urls: stripThumbnails(fetchResults),
1067
+ } satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
1068
+ const storedContent = storeFetchResult(pi, responseId, data, authFetchProfile);
1069
+
1070
+ if (urlList.length === 1) {
1071
+ const result = presentedResults[0];
1072
+ if (result.error) {
1073
+ return {
1074
+ content: [{ type: "text", text: `Error: ${result.error}` }],
1075
+ details: { urls: urlList, urlCount: 1, successful: 0, error: result.error, ...(storedContent ? { responseId } : {}), prompt: params.prompt, timestamp: params.timestamp, frames: params.frames },
1076
+ };
1077
+ }
1078
+
1079
+ const fullLength = result.content.length;
1080
+ const slice = initialContentSlice(result.content, getMaxInlineContentChars());
1081
+ const truncated = slice.endOffset < fullLength;
1082
+ let output = slice.text;
1083
+
1084
+ if (truncated) {
1085
+ output += `\n\n---\nShowing ${slice.endOffset} of ${fullLength} chars, ${slice.shownBytes} of ${slice.totalBytes} bytes, and ${slice.shownLines} of ${slice.totalLines} lines. `;
1086
+ output += storedContent
1087
+ ? getSearchContentEnabled
1088
+ ? `Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0, offset: ${slice.endOffset} }) for the next slice.`
1089
+ : "Content retrieval is not registered."
1090
+ : "Authenticated fetch cache is off; repeat the fetch to read more.";
1091
+ }
1092
+
1093
+ const content: Array<TextContent | ImageContent> = [];
1094
+ if (result.frames?.length) {
1095
+ for (const frame of result.frames) {
1096
+ content.push({ type: "image", data: frame.data, mimeType: frame.mimeType });
1097
+ content.push({ type: "text", text: `Frame at ${frame.timestamp}` });
1098
+ }
1099
+ } else if (result.thumbnail) {
1100
+ content.push({ type: "image", data: result.thumbnail.data, mimeType: result.thumbnail.mimeType });
1101
+ }
1102
+ content.push({ type: "text", text: output });
1103
+
1104
+ const imageCount = (result.frames?.length ?? 0) + (result.thumbnail ? 1 : 0);
1105
+ return {
1106
+ content,
1107
+ details: {
1108
+ urls: urlList,
1109
+ urlCount: 1,
1110
+ successful: 1,
1111
+ totalChars: fullLength,
1112
+ title: result.title,
1113
+ ...(storedContent ? { responseId } : {}),
1114
+ truncated,
1115
+ hasImage: imageCount > 0,
1116
+ imageCount,
1117
+ prompt: params.prompt,
1118
+ timestamp: params.timestamp,
1119
+ frames: params.frames,
1120
+ duration: result.duration,
1121
+ mode,
1122
+ mimeType: result.mimeType,
1123
+ status: result.status,
1124
+ totalBytes: slice.totalBytes,
1125
+ totalLines: slice.totalLines,
1126
+ shownBytes: slice.shownBytes,
1127
+ shownLines: slice.shownLines,
1128
+ },
1129
+ };
1130
+ }
1131
+
1132
+ let output = "## Fetched URLs\n\n";
1133
+ for (const { url, title, content, error } of presentedResults) {
1134
+ if (error) {
1135
+ output += `- ${url}: Error - ${error}\n`;
1136
+ } else {
1137
+ output += `- ${title || url} (${content.length} chars)\n`;
1138
+ }
1139
+ }
1140
+ output += storedContent
1141
+ ? getSearchContentEnabled
1142
+ ? `\n---\nUse ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 }) to retrieve bounded content slices.`
1143
+ : "\n---\nContent retrieval is not registered."
1144
+ : "\n---\nAuthenticated fetch cache is off; repeat the fetch to read content.";
1145
+
1146
+ return {
1147
+ content: [{ type: "text", text: output }],
1148
+ details: { urls: urlList, urlCount: urlList.length, successful, totalChars, ...(storedContent ? { responseId } : {}) },
1149
+ };
1150
+ });
1151
+ },
1152
+
1153
+ renderCall(args, theme) {
1154
+ const { urlList, options } = normalizeFetchContentParams(args);
1155
+ const { prompt, timestamp, frames, model, mode, answerModel, auth } = options;
1156
+ if (urlList.length === 0) {
1157
+ return new Text(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("error", "(no URL)"), 0, 0);
1158
+ }
1159
+ const lines: string[] = [];
1160
+ if (urlList.length === 1) {
1161
+ const display = urlList[0].length > 60 ? urlList[0].slice(0, 57) + "..." : urlList[0];
1162
+ lines.push(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("accent", display));
1163
+ } else {
1164
+ lines.push(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("accent", `${urlList.length} URLs`));
1165
+ for (const u of urlList.slice(0, 5)) {
1166
+ const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
1167
+ lines.push(theme.fg("muted", " " + display));
1168
+ }
1169
+ if (urlList.length > 5) {
1170
+ lines.push(theme.fg("muted", ` ... and ${urlList.length - 5} more`));
1171
+ }
1172
+ }
1173
+ if (mode && mode !== "readable") {
1174
+ lines.push(theme.fg("dim", " mode: ") + theme.fg("warning", mode));
1175
+ }
1176
+ if (timestamp) {
1177
+ lines.push(theme.fg("dim", " timestamp: ") + theme.fg("warning", timestamp));
1178
+ }
1179
+ if (typeof frames === "number") {
1180
+ lines.push(theme.fg("dim", " frames: ") + theme.fg("warning", String(frames)));
1181
+ }
1182
+ if (prompt) {
1183
+ const display = prompt.length > 250 ? prompt.slice(0, 247) + "..." : prompt;
1184
+ lines.push(theme.fg("dim", " prompt: ") + theme.fg("muted", `"${display}"`));
1185
+ }
1186
+ if (model) {
1187
+ lines.push(theme.fg("dim", " model: ") + theme.fg("warning", model));
1188
+ }
1189
+ if (answerModel) {
1190
+ lines.push(theme.fg("dim", " answer model: ") + theme.fg("warning", answerModel));
1191
+ }
1192
+ if (auth !== undefined) {
1193
+ lines.push(theme.fg("dim", " auth: ") + theme.fg("warning", auth === true ? "true" : auth));
1194
+ }
1195
+ return new Text(lines.join("\n"), 0, 0);
1196
+ },
1197
+
1198
+ renderResult(result, { expanded, isPartial }, theme) {
1199
+ const details = result.details as {
1200
+ urlCount?: number;
1201
+ successful?: number;
1202
+ totalChars?: number;
1203
+ error?: string;
1204
+ title?: string;
1205
+ truncated?: boolean;
1206
+ responseId?: string;
1207
+ phase?: string;
1208
+ progress?: number;
1209
+ hasImage?: boolean;
1210
+ imageCount?: number;
1211
+ prompt?: string;
1212
+ timestamp?: string;
1213
+ frames?: number;
1214
+ duration?: number;
1215
+ };
1216
+
1217
+ if (isPartial) {
1218
+ const progress = details?.progress ?? 0;
1219
+ const bar = "\u2588".repeat(Math.floor(progress * 10)) + "\u2591".repeat(10 - Math.floor(progress * 10));
1220
+ return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "fetching"}`), 0, 0);
1221
+ }
1222
+
1223
+ if (details?.error) {
1224
+ const fd = details as typeof details & { urls?: string[] };
1225
+ const extras: string[] = [];
1226
+ if (typeof fd.urlCount === "number" || typeof fd.successful === "number") {
1227
+ extras.push(`urls: ${fd.successful ?? 0}/${fd.urlCount ?? 0} succeeded`);
1228
+ }
1229
+ if (fd.responseId) extras.push(`response id: ${fd.responseId}`);
1230
+ if (fd.urls && fd.urls.length > 0) {
1231
+ for (const u of fd.urls.slice(0, 8)) extras.push(` \u25b8 ${u}`);
1232
+ if (fd.urls.length > 8) extras.push(` ... and ${fd.urls.length - 8} more`);
1233
+ }
1234
+ const plan = buildSearchErrorPlan({ error: details.error, extraLines: extras });
1235
+ if (plan) return renderSearchErrorPlan(plan, expanded, theme);
1236
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
1237
+ }
1238
+
1239
+ if (details?.urlCount === 1) {
1240
+ const title = details?.title || "Untitled";
1241
+ const imgCount = details?.imageCount ?? (details?.hasImage ? 1 : 0);
1242
+ const imageBadge = imgCount > 1
1243
+ ? theme.fg("accent", ` [${imgCount} images]`)
1244
+ : imgCount === 1
1245
+ ? theme.fg("accent", " [image]")
1246
+ : "";
1247
+ let statusLine = theme.fg("success", title) + theme.fg("muted", ` (${details?.totalChars ?? 0} chars)`) + imageBadge;
1248
+ if (details?.truncated) {
1249
+ statusLine += theme.fg("warning", " [truncated]");
1250
+ }
1251
+ if (typeof details?.duration === "number") {
1252
+ statusLine += theme.fg("muted", ` | ${formatSeconds(Math.floor(details.duration))} total`);
1253
+ }
1254
+ const textContent = result.content.find((c) => c.type === "text")?.text || "";
1255
+ if (!expanded) {
1256
+ const brief = textContent.length > 200 ? textContent.slice(0, 200) + "..." : textContent;
1257
+ return new Text(statusLine + "\n" + theme.fg("dim", brief), 0, 0);
1258
+ }
1259
+ const lines = [statusLine];
1260
+ if (details?.prompt) {
1261
+ const display = details.prompt.length > 250 ? details.prompt.slice(0, 247) + "..." : details.prompt;
1262
+ lines.push(theme.fg("dim", ` prompt: "${display}"`));
1263
+ }
1264
+ if (details?.timestamp) {
1265
+ lines.push(theme.fg("dim", ` timestamp: ${details.timestamp}`));
1266
+ }
1267
+ if (typeof details?.frames === "number") {
1268
+ lines.push(theme.fg("dim", ` frames: ${details.frames}`));
1269
+ }
1270
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
1271
+ lines.push(theme.fg("dim", preview));
1272
+ return new Text(lines.join("\n"), 0, 0);
1273
+ }
1274
+
1275
+ const countColor = (details?.successful ?? 0) > 0 ? "success" : "error";
1276
+ const statusLine = theme.fg(countColor, `${details?.successful}/${details?.urlCount} URLs`) + theme.fg("muted", getSearchContentEnabled ? " (content stored)" : " (content fetched)");
1277
+ if (!expanded) {
1278
+ return new Text(statusLine, 0, 0);
1279
+ }
1280
+ const textContent = result.content.find((c) => c.type === "text")?.text || "";
1281
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
1282
+ return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
1283
+ },
1284
+ });
1285
+
1286
+ if (getSearchContentEnabled) {
1287
+ const maxInlineContentChars = getMaxInlineContentChars(initConfig);
1288
+ pi.registerTool({
1289
+ name: toolNames.getSearchContent,
1290
+ label: "Get Search Content",
1291
+ description: `Retrieve bounded content slices or find matching passages in a previous ${storedContentSources} call.`,
1292
+ promptSnippet:
1293
+ `Use after ${storedContentSources} to retrieve stored content via responseId. Use findText to locate passages without paging through the full content.`,
1294
+ parameters: Type.Object({
1295
+ responseId: Type.String({ description: `The responseId from ${storedContentSources}` }),
1296
+ query: Type.Optional(Type.String({ description: searchQueryDescription })),
1297
+ queryIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for query at index" })),
1298
+ url: Type.Optional(Type.String({ description: "Get content for this URL" })),
1299
+ urlIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for URL at index" })),
1300
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset for fetched URL content slices (default 0). Ignored when findText is supplied." })),
1301
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "Maximum characters to return for fetched URL content slices (default and max are set by maxInlineContentChars). Ignored when findText is supplied." })),
1302
+ findText: Type.Optional(Type.Union([
1303
+ Type.String({ minLength: 1, maxLength: 500 }),
1304
+ Type.Array(Type.String({ minLength: 1, maxLength: 500 }), { minItems: 1, maxItems: 10 }),
1305
+ ], { description: "Text or texts to find in the selected stored content. When supplied, offset and limit are ignored." })),
1306
+ findMode: Type.Optional(StringEnum(["exact", "case-insensitive", "fuzzy"], { description: "Matching mode for findText (default: case-insensitive). Requires findText." })),
1307
+ }),
1308
+
1309
+ async execute(_toolCallId, rawParams): Promise<AgentToolResult<Record<string, unknown>>> {
1310
+ const params = normalizeGetSearchContentParams(rawParams);
1311
+ if (params.findMode !== undefined && params.findText === undefined) {
1312
+ return {
1313
+ content: [{ type: "text", text: `findMode ${formatInputValue(params.findMode)} requires findText; provide findText or omit findMode.` }],
1314
+ details: { error: "findMode requires findText" },
1315
+ };
1316
+ }
1317
+ const data = getResult(params.responseId);
1318
+ if (!data) {
1319
+ return {
1320
+ content: [{ type: "text", text: `Error: No stored results for responseId ${formatInputValue(params.responseId)}. Use a responseId returned by ${storedContentSources}.` }],
1321
+ details: { error: "Not found", responseId: params.responseId },
1322
+ };
1323
+ }
1324
+
1325
+ if (data.type === "research") {
1326
+ const artifact = getResearchArtifact(params.responseId);
1327
+ if (!artifact) {
1328
+ return {
1329
+ content: [{ type: "text", text: `Error: stored research artifact for responseId ${formatInputValue(params.responseId)} was not found. Use a responseId returned by ${storedContentSources}.` }],
1330
+ details: { error: "Artifact not found", responseId: params.responseId },
1331
+ };
1332
+ }
1333
+ const serialized = JSON.stringify(artifact, null, 2);
1334
+ if (params.findText !== undefined) {
1335
+ try {
1336
+ const found = findContent(serialized, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
1337
+ const { text, ...findDetails } = found;
1338
+ return {
1339
+ content: [{ type: "text", text }],
1340
+ details: { responseId: artifact.id, type: "research", contentLength: serialized.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
1341
+ };
1342
+ } catch (err) {
1343
+ const error = err instanceof Error ? err.message : String(err);
1344
+ return {
1345
+ 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.` }],
1346
+ details: { error, responseId: params.responseId, type: "research" },
1347
+ };
1348
+ }
1349
+ }
1350
+ const offset = params.offset ?? 0;
1351
+ const limit = params.limit ?? maxInlineContentChars;
1352
+ if (!Number.isInteger(offset) || offset < 0) {
1353
+ return {
1354
+ 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.` }],
1355
+ details: { error: "Invalid offset", offset },
1356
+ };
1357
+ }
1358
+ if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
1359
+ return {
1360
+ 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.` }],
1361
+ details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
1362
+ };
1363
+ }
1364
+ if (offset > serialized.length) {
1365
+ return {
1366
+ 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.` }],
1367
+ details: { error: "Offset out of range", offset, contentLength: serialized.length },
1368
+ };
1369
+ }
1370
+ const endOffset = Math.min(offset + limit, serialized.length);
1371
+ const artifactSlice = serialized.slice(offset, endOffset);
1372
+ const hasMore = endOffset < serialized.length;
1373
+ return {
1374
+ content: [{ type: "text", text: artifactSlice }],
1375
+ details: { responseId: artifact.id, type: "research", contentLength: serialized.length, offset, limit, returnedChars: artifactSlice.length, nextOffset: hasMore ? endOffset : null, truncated: hasMore },
1376
+ };
1377
+ }
1378
+
1379
+ if (data.type === "search" && data.queries) {
1380
+ let queryData: QueryResultData | undefined;
1381
+
1382
+ if (params.query !== undefined) {
1383
+ queryData = data.queries.find((q) => q.query === params.query);
1384
+ if (!queryData) {
1385
+ const available = data.queries.map((q) => `"${q.query}"`).join(", ");
1386
+ return {
1387
+ 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.` }],
1388
+ details: { error: "Query not found" },
1389
+ };
1390
+ }
1391
+ } else if (params.queryIndex !== undefined) {
1392
+ queryData = data.queries[params.queryIndex];
1393
+ if (!queryData) {
1394
+ const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
1395
+ return {
1396
+ 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.` }],
1397
+ details: { error: "Index out of range" },
1398
+ };
1399
+ }
1400
+ } else {
1401
+ const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
1402
+ return {
1403
+ content: [{ type: "text", text: `Specify query or queryIndex for responseId ${formatInputValue(params.responseId)}. Available queries: ${available || "none"}.` }],
1404
+ details: { error: "No query specified" },
1405
+ };
1406
+ }
1407
+
1408
+ if (queryData.error) {
1409
+ return {
1410
+ 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.` }],
1411
+ details: { error: queryData.error, query: queryData.query },
1412
+ };
1413
+ }
1414
+
1415
+ const fullResults = formatFullResults(queryData);
1416
+ if (params.findText !== undefined) {
1417
+ try {
1418
+ const found = findContent(fullResults, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
1419
+ const { text, ...findDetails } = found;
1420
+ return {
1421
+ content: [{ type: "text", text }],
1422
+ details: { query: queryData.query, resultCount: queryData.results.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
1423
+ };
1424
+ } catch (err) {
1425
+ const error = err instanceof Error ? err.message : String(err);
1426
+ return {
1427
+ 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.` }],
1428
+ details: { error, query: queryData.query },
1429
+ };
1430
+ }
1431
+ }
1432
+
1433
+ return {
1434
+ content: [{ type: "text", text: fullResults }],
1435
+ details: { query: queryData.query, resultCount: queryData.results.length },
1436
+ };
1437
+ }
1438
+
1439
+ if (data.type === "fetch" && data.urls) {
1440
+ let urlData: ExtractedContent | undefined;
1441
+ let selectedUrlIndex = -1;
1442
+
1443
+ if (params.url !== undefined) {
1444
+ selectedUrlIndex = data.urls.findIndex((u) => u.url === params.url);
1445
+ urlData = data.urls[selectedUrlIndex];
1446
+ if (!urlData) {
1447
+ const available = data.urls.map((u) => u.url).join("\n ");
1448
+ return {
1449
+ 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.` }],
1450
+ details: { error: "URL not found" },
1451
+ };
1452
+ }
1453
+ } else if (params.urlIndex !== undefined) {
1454
+ selectedUrlIndex = params.urlIndex;
1455
+ urlData = data.urls[selectedUrlIndex];
1456
+ if (!urlData) {
1457
+ const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
1458
+ return {
1459
+ 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.` }],
1460
+ details: { error: "Index out of range" },
1461
+ };
1462
+ }
1463
+ } else {
1464
+ const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
1465
+ return {
1466
+ content: [{ type: "text", text: `Specify url or urlIndex for responseId ${formatInputValue(params.responseId)}. Available URLs:\n ${available || " none"}` }],
1467
+ details: { error: "No URL specified" },
1468
+ };
1469
+ }
1470
+
1471
+ if (urlData.error) {
1472
+ return {
1473
+ 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.` }],
1474
+ details: { error: urlData.error, url: urlData.url },
1475
+ };
1476
+ }
1477
+
1478
+ if (params.findText !== undefined) {
1479
+ try {
1480
+ const found = findContent(urlData.content, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
1481
+ const { text, ...findDetails } = found;
1482
+ return {
1483
+ content: [{ type: "text", text: `# ${urlData.title || urlData.url}\n\n${text}` }],
1484
+ details: { url: urlData.url, title: urlData.title, contentLength: urlData.content.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
1485
+ };
1486
+ } catch (err) {
1487
+ const error = err instanceof Error ? err.message : String(err);
1488
+ return {
1489
+ 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.` }],
1490
+ details: { error, url: urlData.url },
1491
+ };
1492
+ }
1493
+ }
1494
+
1495
+ const offset = params.offset ?? 0;
1496
+ const limit = params.limit ?? maxInlineContentChars;
1497
+ if (!Number.isInteger(offset) || offset < 0) {
1498
+ return {
1499
+ 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.` }],
1500
+ details: { error: "Invalid offset", offset },
1501
+ };
1502
+ }
1503
+ if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
1504
+ return {
1505
+ 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.` }],
1506
+ details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
1507
+ };
1508
+ }
1509
+ if (offset > urlData.content.length) {
1510
+ return {
1511
+ 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.` }],
1512
+ details: { error: "Offset out of range", offset, contentLength: urlData.content.length },
1513
+ };
1514
+ }
1515
+
1516
+ const endOffset = Math.min(offset + limit, urlData.content.length);
1517
+ const contentSlice = urlData.content.slice(offset, endOffset);
1518
+ const hasMore = endOffset < urlData.content.length;
1519
+ let text = `# ${urlData.title || urlData.url}\n\n${contentSlice}`;
1520
+ if (hasMore || offset > 0) {
1521
+ text += `\n\n---\nShowing chars ${offset}-${endOffset} of ${urlData.content.length}.`;
1522
+ if (hasMore) {
1523
+ text += ` Use ${toolNames.getSearchContent}({ responseId: "${params.responseId}", urlIndex: ${selectedUrlIndex}, offset: ${endOffset}, limit: ${limit} }) for the next slice.`;
1524
+ }
1525
+ }
1526
+
1527
+ return {
1528
+ content: [{ type: "text", text }],
1529
+ details: {
1530
+ url: urlData.url,
1531
+ title: urlData.title,
1532
+ contentLength: urlData.content.length,
1533
+ offset,
1534
+ limit,
1535
+ returnedChars: contentSlice.length,
1536
+ nextOffset: hasMore ? endOffset : null,
1537
+ truncated: hasMore,
1538
+ },
1539
+ };
1540
+ }
1541
+
1542
+ return {
1543
+ content: [{ type: "text", text: `Invalid stored data for responseId ${formatInputValue(params.responseId)}: received type ${formatInputValue(data.type)}. Use a responseId returned by ${storedContentSources}.` }],
1544
+ details: { error: "Invalid data" },
1545
+ };
1546
+ },
1547
+
1548
+ renderCall(args, theme) {
1549
+ const { responseId, query, queryIndex, url, urlIndex, offset, findText } = args as {
1550
+ responseId: string;
1551
+ query?: string;
1552
+ queryIndex?: number;
1553
+ url?: string;
1554
+ urlIndex?: number;
1555
+ offset?: number;
1556
+ findText?: string | string[];
1557
+ };
1558
+ let target = "";
1559
+ if (query) target = `query="${query}"`;
1560
+ else if (queryIndex !== undefined) target = `queryIndex=${queryIndex}`;
1561
+ else if (url) target = url.length > 30 ? url.slice(0, 27) + "..." : url;
1562
+ else if (urlIndex !== undefined) target = `urlIndex=${urlIndex}`;
1563
+ if (offset !== undefined) target += target ? ` @ ${offset}` : `offset=${offset}`;
1564
+ if (findText !== undefined) {
1565
+ const queries = Array.isArray(findText) ? findText : [findText];
1566
+ target += `${target ? " · " : ""}find ${queries.length}`;
1567
+ }
1568
+ return new Text(theme.fg("toolTitle", theme.bold("get_content ")) + theme.fg("accent", target || responseId.slice(0, 8)), 0, 0);
1569
+ },
1570
+
1571
+ renderResult(result, { expanded }, theme) {
1572
+ const details = result.details as {
1573
+ error?: string;
1574
+ query?: string;
1575
+ url?: string;
1576
+ title?: string;
1577
+ resultCount?: number;
1578
+ contentLength?: number;
1579
+ offset?: number;
1580
+ returnedChars?: number;
1581
+ nextOffset?: number | null;
1582
+ matchCount?: number;
1583
+ returnedMatches?: number;
1584
+ };
1585
+
1586
+ if (details?.error) {
1587
+ const extras: string[] = [];
1588
+ if (details.query) extras.push(`query: ${details.query}`);
1589
+ if (details.url) extras.push(`url: ${details.url}`);
1590
+ else if (details.title) extras.push(`resource: ${details.title}`);
1591
+ const plan = buildSearchErrorPlan({ error: details.error, extraLines: extras });
1592
+ if (plan) return renderSearchErrorPlan(plan, expanded, theme);
1593
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
1594
+ }
1595
+
1596
+ let statusLine: string;
1597
+ if (typeof details?.matchCount === "number") {
1598
+ statusLine = theme.fg("success", details?.title || details?.query || "Content") + theme.fg("muted", ` (${details.matchCount} matches, ${details.returnedMatches ?? 0} shown)`);
1599
+ } else if (details?.query) {
1600
+ statusLine = theme.fg("success", `"${details.query}"`) + theme.fg("muted", ` (${details.resultCount} results)`);
1601
+ } else {
1602
+ const start = details?.offset ?? 0;
1603
+ const returned = details?.returnedChars ?? details?.contentLength ?? 0;
1604
+ const end = start + returned;
1605
+ const slice = details?.nextOffset !== undefined || start > 0
1606
+ ? `, showing ${start}-${end}`
1607
+ : "";
1608
+ statusLine = theme.fg("success", details?.title || "Content") + theme.fg("muted", ` (${details?.contentLength ?? 0} chars${slice})`);
1609
+ }
1610
+
1611
+ if (!expanded) {
1612
+ return new Text(statusLine, 0, 0);
1613
+ }
1614
+
1615
+ const textContent = result.content.find((c) => c.type === "text")?.text || "";
1616
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
1617
+ return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
1618
+ },
1619
+ });
1620
+ }
1621
+
1622
+ if (isCommandEnabled(initConfig, "google-account")) pi.registerCommand("google-account", {
1623
+ description: "Show the active Google account for Gemini Web",
1624
+ handler: async () => {
1625
+ if (!isBrowserCookieAccessAllowed()) {
1626
+ pi.sendMessage({
1627
+ customType: "google-account",
1628
+ content: [{ type: "text", text: `Gemini Web browser cookie access is disabled. Set allowBrowserCookies: true in ${WEB_SEARCH_CONFIG_PATH} to enable it.` }],
1629
+ display: true,
1630
+ details: { available: false, cookieAccessAllowed: false },
1631
+ }, { triggerTurn: true, deliverAs: "followUp" });
1632
+ return;
1633
+ }
1634
+
1635
+ const cookies = await isGeminiWebAvailable();
1636
+ if (!cookies) {
1637
+ const diagnostic = getGeminiWebAvailabilityDiagnostic();
1638
+ const diagnosticDetails = getGeminiWebAvailabilityDiagnosticDetails();
1639
+ const attempted = formatCookieAttempts(diagnosticDetails?.attempts ?? []);
1640
+ const text = diagnostic
1641
+ ? `Gemini Web is unavailable: ${diagnostic}${attempted ? ` Attempted browser profiles: ${attempted}.` : ""}`
1642
+ : "Gemini Web is unavailable. Sign into gemini.google.com in a supported Chromium-based browser.";
1643
+ pi.sendMessage({
1644
+ customType: "google-account",
1645
+ content: [{ type: "text", text }],
1646
+ display: true,
1647
+ details: { available: false, cookieAccessAllowed: true, diagnostic, cookieDiagnostic: diagnosticDetails },
1648
+ }, { triggerTurn: true, deliverAs: "followUp" });
1649
+ return;
1650
+ }
1651
+
1652
+ const email = await getActiveGoogleEmail(cookies);
1653
+ const text = email
1654
+ ? `Active Google account: ${email}`
1655
+ : "Gemini Web is available, but the active Google account could not be determined.";
1656
+
1657
+ pi.sendMessage({
1658
+ customType: "google-account",
1659
+ content: [{ type: "text", text }],
1660
+ display: true,
1661
+ details: { available: true, email: email ?? null },
1662
+ }, { triggerTurn: true, deliverAs: "followUp" });
1663
+ },
1664
+ });
1665
+
1666
+ function formatCookieAttempts(attempts: { browser: string; profile: string; status: string }[]): string {
1667
+ return attempts.map(({ browser, profile, status }) => `${browser}/${profile} (${status})`).join(", ");
1668
+ }
1669
+
1670
+ if (isCommandEnabled(initConfig, "search")) pi.registerCommand("search", {
1671
+ description: "Browse stored web search results",
1672
+ handler: async (_args, ctx) => {
1673
+ const results = getAllResults();
1674
+
1675
+ if (results.length === 0) {
1676
+ ctx.ui.notify("No stored search results", "info");
1677
+ return;
1678
+ }
1679
+
1680
+ const options = results.map((r) => {
1681
+ const age = Math.floor((Date.now() - r.timestamp) / 60000);
1682
+ const ageStr = age < 60 ? `${age}m ago` : `${Math.floor(age / 60)}h ago`;
1683
+ if (r.type === "search" && r.queries) {
1684
+ const query = r.queries[0]?.query || "unknown";
1685
+ return `[${r.id.slice(0, 6)}] "${query}" (${r.queries.length} queries) - ${ageStr}`;
1686
+ }
1687
+ if (r.type === "fetch" && (r.urls || r.urlMetadata)) {
1688
+ return `[${r.id.slice(0, 6)}] ${(r.urls ?? r.urlMetadata ?? []).length} URLs fetched - ${ageStr}`;
1689
+ }
1690
+ return `[${r.id.slice(0, 6)}] ${r.type} - ${ageStr}`;
1691
+ });
1692
+
1693
+ const choice = await ctx.ui.select("Stored Search Results", options);
1694
+ if (!choice) return;
1695
+
1696
+ const match = choice.match(/^\[([a-z0-9]+)\]/);
1697
+ if (!match) return;
1698
+
1699
+ const selected = results.find((r) => r.id.startsWith(match[1]));
1700
+ if (!selected) return;
1701
+
1702
+ const actions = ["View details", "Delete"];
1703
+ const action = await ctx.ui.select(`Result ${selected.id.slice(0, 6)}`, actions);
1704
+
1705
+ if (action === "Delete") {
1706
+ deleteResult(selected.id);
1707
+ ctx.ui.notify(`Deleted ${selected.id.slice(0, 6)}`, "info");
1708
+ } else if (action === "View details") {
1709
+ let info = `ID: ${selected.id}\nType: ${selected.type}\nAge: ${Math.floor((Date.now() - selected.timestamp) / 60000)}m\n\n`;
1710
+ if (selected.type === "search" && selected.queries) {
1711
+ info += "Queries:\n";
1712
+ const queries = selected.queries.slice(0, 10);
1713
+ for (const q of queries) {
1714
+ info += `- "${q.query}" (${q.results.length} results)\n`;
1715
+ }
1716
+ if (selected.queries.length > 10) {
1717
+ info += `... and ${selected.queries.length - 10} more\n`;
1718
+ }
1719
+ }
1720
+ if (selected.type === "fetch" && (selected.urls || selected.urlMetadata)) {
1721
+ info += "URLs:\n";
1722
+ const urlItems = selected.urls ?? selected.urlMetadata ?? [];
1723
+ const urls = urlItems.slice(0, 10);
1724
+ for (const u of urls) {
1725
+ const urlDisplay = u.url.length > 50 ? u.url.slice(0, 47) + "..." : u.url;
1726
+ const contentLength = "content" in u ? u.content.length : u.contentLength;
1727
+ info += `- ${urlDisplay} (${u.error || `${contentLength} chars`})\n`;
1728
+ }
1729
+ if (urlItems.length > 10) {
1730
+ info += `... and ${urlItems.length - 10} more\n`;
1731
+ }
1732
+ }
1733
+ ctx.ui.notify(info, "info");
1734
+ }
1735
+ },
1736
+ });
1737
+ }