@asterxsk/kiln 0.3.0 → 0.4.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 (267) hide show
  1. package/README.md +2 -2
  2. package/agent/version.txt +1 -1
  3. package/bin/kiln.js +10 -8
  4. package/package.json +1 -1
  5. package/agent.bak.20260905-190020/AGENTS.md +0 -67
  6. package/agent.bak.20260905-190020/README.md +0 -5
  7. package/agent.bak.20260905-190020/extensions/ask-user/index.ts +0 -418
  8. package/agent.bak.20260905-190020/extensions/ask-user/install.ps1 +0 -23
  9. package/agent.bak.20260905-190020/extensions/ask-user/install.sh +0 -21
  10. package/agent.bak.20260905-190020/extensions/ask-user/package-lock.json +0 -769
  11. package/agent.bak.20260905-190020/extensions/ask-user/package.json +0 -19
  12. package/agent.bak.20260905-190020/extensions/ask-user/prompt.ts +0 -45
  13. package/agent.bak.20260905-190020/extensions/ask-user/tsconfig.json +0 -7
  14. package/agent.bak.20260905-190020/extensions/background-terminals/docs/implementation-guide.md +0 -942
  15. package/agent.bak.20260905-190020/extensions/background-terminals/index.ts +0 -629
  16. package/agent.bak.20260905-190020/extensions/background-terminals/install.ps1 +0 -23
  17. package/agent.bak.20260905-190020/extensions/background-terminals/install.sh +0 -21
  18. package/agent.bak.20260905-190020/extensions/background-terminals/package-lock.json +0 -769
  19. package/agent.bak.20260905-190020/extensions/background-terminals/package.json +0 -17
  20. package/agent.bak.20260905-190020/extensions/background-terminals/src/domain.ts +0 -87
  21. package/agent.bak.20260905-190020/extensions/background-terminals/src/manager.ts +0 -907
  22. package/agent.bak.20260905-190020/extensions/background-terminals/src/output.ts +0 -84
  23. package/agent.bak.20260905-190020/extensions/background-terminals/src/prompt.ts +0 -142
  24. package/agent.bak.20260905-190020/extensions/background-terminals/src/result-delivery.ts +0 -27
  25. package/agent.bak.20260905-190020/extensions/background-terminals/src/runtime.ts +0 -36
  26. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/output-view.ts +0 -79
  27. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/ps.ts +0 -621
  28. package/agent.bak.20260905-190020/extensions/background-terminals/src/widget-order.ts +0 -121
  29. package/agent.bak.20260905-190020/extensions/background-terminals/test/manager.test.ts +0 -735
  30. package/agent.bak.20260905-190020/extensions/background-terminals/test/output.test.ts +0 -109
  31. package/agent.bak.20260905-190020/extensions/background-terminals/test/prompt.test.ts +0 -125
  32. package/agent.bak.20260905-190020/extensions/background-terminals/test/ps.test.ts +0 -82
  33. package/agent.bak.20260905-190020/extensions/background-terminals/test/result-delivery.test.ts +0 -44
  34. package/agent.bak.20260905-190020/extensions/background-terminals/tsconfig.json +0 -7
  35. package/agent.bak.20260905-190020/extensions/destructive/README.md +0 -34
  36. package/agent.bak.20260905-190020/extensions/destructive/index.ts +0 -124
  37. package/agent.bak.20260905-190020/extensions/destructive/install.ps1 +0 -23
  38. package/agent.bak.20260905-190020/extensions/destructive/install.sh +0 -21
  39. package/agent.bak.20260905-190020/extensions/file-search/index.ts +0 -459
  40. package/agent.bak.20260905-190020/extensions/file-search/install.ps1 +0 -23
  41. package/agent.bak.20260905-190020/extensions/file-search/install.sh +0 -21
  42. package/agent.bak.20260905-190020/extensions/file-search/package-lock.json +0 -2253
  43. package/agent.bak.20260905-190020/extensions/file-search/package.json +0 -23
  44. package/agent.bak.20260905-190020/extensions/file-search/src/args.ts +0 -122
  45. package/agent.bak.20260905-190020/extensions/file-search/src/binaries.ts +0 -422
  46. package/agent.bak.20260905-190020/extensions/file-search/src/output.ts +0 -126
  47. package/agent.bak.20260905-190020/extensions/file-search/src/process.ts +0 -146
  48. package/agent.bak.20260905-190020/extensions/file-search/src/prompt.ts +0 -52
  49. package/agent.bak.20260905-190020/extensions/file-search/test/index.spec.ts +0 -443
  50. package/agent.bak.20260905-190020/extensions/file-search/tsconfig.json +0 -7
  51. package/agent.bak.20260905-190020/extensions/goal/README.md +0 -50
  52. package/agent.bak.20260905-190020/extensions/goal/index.ts +0 -155
  53. package/agent.bak.20260905-190020/extensions/goal/install.ps1 +0 -23
  54. package/agent.bak.20260905-190020/extensions/goal/install.sh +0 -21
  55. package/agent.bak.20260905-190020/extensions/kiln-update/README.md +0 -25
  56. package/agent.bak.20260905-190020/extensions/kiln-update/index.ts +0 -99
  57. package/agent.bak.20260905-190020/extensions/kiln-update/install.ps1 +0 -23
  58. package/agent.bak.20260905-190020/extensions/kiln-update/install.sh +0 -21
  59. package/agent.bak.20260905-190020/extensions/modelconf/PLAN.md +0 -915
  60. package/agent.bak.20260905-190020/extensions/modelconf/README.md +0 -66
  61. package/agent.bak.20260905-190020/extensions/modelconf/index.ts +0 -296
  62. package/agent.bak.20260905-190020/extensions/modelconf/install.ps1 +0 -23
  63. package/agent.bak.20260905-190020/extensions/modelconf/install.sh +0 -21
  64. package/agent.bak.20260905-190020/extensions/modelconf/package-lock.json +0 -1809
  65. package/agent.bak.20260905-190020/extensions/modelconf/package.json +0 -13
  66. package/agent.bak.20260905-190020/extensions/modelconf/src/fuzzy.ts +0 -26
  67. package/agent.bak.20260905-190020/extensions/modelconf/src/glob.ts +0 -13
  68. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.test.ts +0 -46
  69. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.ts +0 -164
  70. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.test.ts +0 -75
  71. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.ts +0 -1101
  72. package/agent.bak.20260905-190020/extensions/modelconf/tsconfig.json +0 -15
  73. package/agent.bak.20260905-190020/extensions/pi-web-access/CHANGELOG.md +0 -690
  74. package/agent.bak.20260905-190020/extensions/pi-web-access/LICENSE +0 -21
  75. package/agent.bak.20260905-190020/extensions/pi-web-access/README.md +0 -470
  76. package/agent.bak.20260905-190020/extensions/pi-web-access/SECURITY.md +0 -5
  77. package/agent.bak.20260905-190020/extensions/pi-web-access/activity.ts +0 -101
  78. package/agent.bak.20260905-190020/extensions/pi-web-access/auth-fetch.ts +0 -148
  79. package/agent.bak.20260905-190020/extensions/pi-web-access/banner.png +0 -0
  80. package/agent.bak.20260905-190020/extensions/pi-web-access/brightdata-unlocker.ts +0 -272
  81. package/agent.bak.20260905-190020/extensions/pi-web-access/chrome-cookies.ts +0 -669
  82. package/agent.bak.20260905-190020/extensions/pi-web-access/content-find.ts +0 -139
  83. package/agent.bak.20260905-190020/extensions/pi-web-access/credential-source.ts +0 -191
  84. package/agent.bak.20260905-190020/extensions/pi-web-access/data-uri-sanitize.ts +0 -406
  85. package/agent.bak.20260905-190020/extensions/pi-web-access/datalab-pdf-extract.ts +0 -568
  86. package/agent.bak.20260905-190020/extensions/pi-web-access/declared-web-links.ts +0 -173
  87. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +0 -496
  88. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/contract-probe.mjs +0 -140
  89. package/agent.bak.20260905-190020/extensions/pi-web-access/exa.ts +0 -526
  90. package/agent.bak.20260905-190020/extensions/pi-web-access/extract.ts +0 -1196
  91. package/agent.bak.20260905-190020/extensions/pi-web-access/feature-config.ts +0 -29
  92. package/agent.bak.20260905-190020/extensions/pi-web-access/fetch-params.ts +0 -111
  93. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-adc.ts +0 -298
  94. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-api.ts +0 -353
  95. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-pdf-extract.ts +0 -108
  96. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-search.ts +0 -21
  97. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-url-context.ts +0 -128
  98. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web-config.ts +0 -101
  99. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web.ts +0 -487
  100. package/agent.bak.20260905-190020/extensions/pi-web-access/github-api.ts +0 -197
  101. package/agent.bak.20260905-190020/extensions/pi-web-access/github-extract.ts +0 -746
  102. package/agent.bak.20260905-190020/extensions/pi-web-access/github-issue-pr.ts +0 -700
  103. package/agent.bak.20260905-190020/extensions/pi-web-access/index.ts +0 -1737
  104. package/agent.bak.20260905-190020/extensions/pi-web-access/package-lock.json +0 -5808
  105. package/agent.bak.20260905-190020/extensions/pi-web-access/package.json +0 -64
  106. package/agent.bak.20260905-190020/extensions/pi-web-access/page-query.ts +0 -96
  107. package/agent.bak.20260905-190020/extensions/pi-web-access/pdf-extract.ts +0 -409
  108. package/agent.bak.20260905-190020/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  109. package/agent.bak.20260905-190020/extensions/pi-web-access/promise-try.d.ts +0 -7
  110. package/agent.bak.20260905-190020/extensions/pi-web-access/query-rewrite.ts +0 -51
  111. package/agent.bak.20260905-190020/extensions/pi-web-access/render-search-error.ts +0 -170
  112. package/agent.bak.20260905-190020/extensions/pi-web-access/rsc-extract.ts +0 -338
  113. package/agent.bak.20260905-190020/extensions/pi-web-access/search-types.ts +0 -20
  114. package/agent.bak.20260905-190020/extensions/pi-web-access/source-check.ts +0 -282
  115. package/agent.bak.20260905-190020/extensions/pi-web-access/ssrf-protection.ts +0 -526
  116. package/agent.bak.20260905-190020/extensions/pi-web-access/storage.ts +0 -521
  117. package/agent.bak.20260905-190020/extensions/pi-web-access/summary-model-scope.ts +0 -125
  118. package/agent.bak.20260905-190020/extensions/pi-web-access/test/auth-fetch.test.mjs +0 -208
  119. package/agent.bak.20260905-190020/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +0 -840
  120. package/agent.bak.20260905-190020/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +0 -441
  121. package/agent.bak.20260905-190020/extensions/pi-web-access/test/config-path.test.mjs +0 -283
  122. package/agent.bak.20260905-190020/extensions/pi-web-access/test/content-find.test.mjs +0 -25
  123. package/agent.bak.20260905-190020/extensions/pi-web-access/test/credential-source.test.mjs +0 -118
  124. package/agent.bak.20260905-190020/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +0 -210
  125. package/agent.bak.20260905-190020/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +0 -552
  126. package/agent.bak.20260905-190020/extensions/pi-web-access/test/declared-web-links.test.mjs +0 -212
  127. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +0 -40
  128. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +0 -334
  129. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +0 -95
  130. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-modes.test.mjs +0 -53
  131. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +0 -92
  132. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-params.test.mjs +0 -86
  133. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-render-call.test.mjs +0 -34
  134. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-routing.test.mjs +0 -173
  135. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +0 -257
  136. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-api-transport.test.mjs +0 -170
  137. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +0 -133
  138. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +0 -178
  139. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +0 -148
  140. package/agent.bak.20260905-190020/extensions/pi-web-access/test/get-search-content.test.mjs +0 -223
  141. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-extract.test.mjs +0 -378
  142. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-issue-pr.test.mjs +0 -565
  143. package/agent.bak.20260905-190020/extensions/pi-web-access/test/inline-content-config.test.mjs +0 -99
  144. package/agent.bak.20260905-190020/extensions/pi-web-access/test/lazy-extract-load.test.mjs +0 -118
  145. package/agent.bak.20260905-190020/extensions/pi-web-access/test/local-video-oversize.test.mjs +0 -52
  146. package/agent.bak.20260905-190020/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +0 -50
  147. package/agent.bak.20260905-190020/extensions/pi-web-access/test/page-query.test.mjs +0 -51
  148. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-config.test.mjs +0 -140
  149. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-extract.test.mjs +0 -500
  150. package/agent.bak.20260905-190020/extensions/pi-web-access/test/proxy-transport.test.mjs +0 -286
  151. package/agent.bak.20260905-190020/extensions/pi-web-access/test/query-rewrite.test.mjs +0 -52
  152. package/agent.bak.20260905-190020/extensions/pi-web-access/test/rsc-fallback.test.mjs +0 -102
  153. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-error-render.test.mjs +0 -152
  154. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-providers.test.mjs +0 -274
  155. package/agent.bak.20260905-190020/extensions/pi-web-access/test/source-check.test.mjs +0 -179
  156. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +0 -205
  157. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-protection.test.mjs +0 -456
  158. package/agent.bak.20260905-190020/extensions/pi-web-access/test/summary-model-scope.test.mjs +0 -106
  159. package/agent.bak.20260905-190020/extensions/pi-web-access/test/tool-registration-config.test.mjs +0 -182
  160. package/agent.bak.20260905-190020/extensions/pi-web-access/test/web-search-answer-render.test.mjs +0 -66
  161. package/agent.bak.20260905-190020/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +0 -64
  162. package/agent.bak.20260905-190020/extensions/pi-web-access/tsconfig.json +0 -11
  163. package/agent.bak.20260905-190020/extensions/pi-web-access/utils.ts +0 -451
  164. package/agent.bak.20260905-190020/extensions/pi-web-access/video-extract.ts +0 -392
  165. package/agent.bak.20260905-190020/extensions/pi-web-access/youtube-extract.ts +0 -328
  166. package/agent.bak.20260905-190020/extensions/shared/activity-status.ts +0 -31
  167. package/agent.bak.20260905-190020/extensions/shared/child-session.ts +0 -148
  168. package/agent.bak.20260905-190020/extensions/shared/context-utilization.ts +0 -47
  169. package/agent.bak.20260905-190020/extensions/shared/dashboard-state.ts +0 -99
  170. package/agent.bak.20260905-190020/extensions/shared/install.ps1 +0 -23
  171. package/agent.bak.20260905-190020/extensions/shared/install.sh +0 -21
  172. package/agent.bak.20260905-190020/extensions/shared/test/child-session.test.ts +0 -270
  173. package/agent.bak.20260905-190020/extensions/shared/test/context-utilization.test.ts +0 -48
  174. package/agent.bak.20260905-190020/extensions/shared/test/tool-call-timeout.test.ts +0 -117
  175. package/agent.bak.20260905-190020/extensions/shared/test/widget-order.test.ts +0 -167
  176. package/agent.bak.20260905-190020/extensions/shared/tool-call-timeout.ts +0 -104
  177. package/agent.bak.20260905-190020/extensions/skillsconf/README.md +0 -74
  178. package/agent.bak.20260905-190020/extensions/skillsconf/index.ts +0 -92
  179. package/agent.bak.20260905-190020/extensions/skillsconf/install.ps1 +0 -23
  180. package/agent.bak.20260905-190020/extensions/skillsconf/install.sh +0 -21
  181. package/agent.bak.20260905-190020/extensions/skillsconf/package-lock.json +0 -1809
  182. package/agent.bak.20260905-190020/extensions/skillsconf/package.json +0 -18
  183. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.test.ts +0 -64
  184. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.ts +0 -45
  185. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.test.ts +0 -80
  186. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.ts +0 -74
  187. package/agent.bak.20260905-190020/extensions/skillsconf/src/fuzzy.ts +0 -26
  188. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.test.ts +0 -72
  189. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.ts +0 -169
  190. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.test.ts +0 -337
  191. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.ts +0 -758
  192. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/text-input.ts +0 -91
  193. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/tui-helpers.ts +0 -144
  194. package/agent.bak.20260905-190020/extensions/skillsconf/tsconfig.json +0 -15
  195. package/agent.bak.20260905-190020/extensions/statusline/index.ts +0 -282
  196. package/agent.bak.20260905-190020/extensions/statusline/install.ps1 +0 -23
  197. package/agent.bak.20260905-190020/extensions/statusline/install.sh +0 -21
  198. package/agent.bak.20260905-190020/extensions/subagents/docs/design-plan.md +0 -568
  199. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-extension-guide.md +0 -354
  200. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-notes.md +0 -571
  201. package/agent.bak.20260905-190020/extensions/subagents/index.ts +0 -826
  202. package/agent.bak.20260905-190020/extensions/subagents/install.ps1 +0 -23
  203. package/agent.bak.20260905-190020/extensions/subagents/install.sh +0 -21
  204. package/agent.bak.20260905-190020/extensions/subagents/package-lock.json +0 -2244
  205. package/agent.bak.20260905-190020/extensions/subagents/package.json +0 -19
  206. package/agent.bak.20260905-190020/extensions/subagents/src/backend.ts +0 -73
  207. package/agent.bak.20260905-190020/extensions/subagents/src/backends/claude.ts +0 -701
  208. package/agent.bak.20260905-190020/extensions/subagents/src/backends/codex.ts +0 -1060
  209. package/agent.bak.20260905-190020/extensions/subagents/src/backends/pi.ts +0 -575
  210. package/agent.bak.20260905-190020/extensions/subagents/src/backends/stub.ts +0 -300
  211. package/agent.bak.20260905-190020/extensions/subagents/src/by-the-way.ts +0 -21
  212. package/agent.bak.20260905-190020/extensions/subagents/src/domain.ts +0 -253
  213. package/agent.bak.20260905-190020/extensions/subagents/src/format.ts +0 -74
  214. package/agent.bak.20260905-190020/extensions/subagents/src/manager.ts +0 -736
  215. package/agent.bak.20260905-190020/extensions/subagents/src/prompt.ts +0 -92
  216. package/agent.bak.20260905-190020/extensions/subagents/src/result-delivery.ts +0 -20
  217. package/agent.bak.20260905-190020/extensions/subagents/src/runtime.ts +0 -53
  218. package/agent.bak.20260905-190020/extensions/subagents/src/ui/takeover.ts +0 -583
  219. package/agent.bak.20260905-190020/extensions/subagents/src/ui/transcript.ts +0 -201
  220. package/agent.bak.20260905-190020/extensions/subagents/src/widget-order.ts +0 -121
  221. package/agent.bak.20260905-190020/extensions/subagents/test/by-the-way.test.ts +0 -29
  222. package/agent.bak.20260905-190020/extensions/subagents/test/claude.test.ts +0 -119
  223. package/agent.bak.20260905-190020/extensions/subagents/test/codex.test.ts +0 -102
  224. package/agent.bak.20260905-190020/extensions/subagents/test/context-usage.test.ts +0 -107
  225. package/agent.bak.20260905-190020/extensions/subagents/test/manager.test.ts +0 -276
  226. package/agent.bak.20260905-190020/extensions/subagents/test/result-delivery.test.ts +0 -27
  227. package/agent.bak.20260905-190020/extensions/subagents/test/takeover.test.ts +0 -29
  228. package/agent.bak.20260905-190020/extensions/subagents/tsconfig.json +0 -7
  229. package/agent.bak.20260905-190020/extensions/taste/index.ts +0 -443
  230. package/agent.bak.20260905-190020/extensions/taste/install.ps1 +0 -23
  231. package/agent.bak.20260905-190020/extensions/taste/install.sh +0 -21
  232. package/agent.bak.20260905-190020/extensions/todo/AGENTS.md +0 -38
  233. package/agent.bak.20260905-190020/extensions/todo/LICENSE +0 -21
  234. package/agent.bak.20260905-190020/extensions/todo/config.ts +0 -55
  235. package/agent.bak.20260905-190020/extensions/todo/index.ts +0 -151
  236. package/agent.bak.20260905-190020/extensions/todo/install.ps1 +0 -23
  237. package/agent.bak.20260905-190020/extensions/todo/install.sh +0 -21
  238. package/agent.bak.20260905-190020/extensions/todo/locales/de.json +0 -17
  239. package/agent.bak.20260905-190020/extensions/todo/locales/en.json +0 -15
  240. package/agent.bak.20260905-190020/extensions/todo/locales/es.json +0 -17
  241. package/agent.bak.20260905-190020/extensions/todo/locales/fr.json +0 -17
  242. package/agent.bak.20260905-190020/extensions/todo/locales/pt-BR.json +0 -17
  243. package/agent.bak.20260905-190020/extensions/todo/locales/pt.json +0 -17
  244. package/agent.bak.20260905-190020/extensions/todo/locales/ru.json +0 -17
  245. package/agent.bak.20260905-190020/extensions/todo/locales/uk.json +0 -17
  246. package/agent.bak.20260905-190020/extensions/todo/locales/zh.json +0 -17
  247. package/agent.bak.20260905-190020/extensions/todo/package-lock.json +0 -3358
  248. package/agent.bak.20260905-190020/extensions/todo/package.json +0 -67
  249. package/agent.bak.20260905-190020/extensions/todo/state/i18n-bridge.ts +0 -64
  250. package/agent.bak.20260905-190020/extensions/todo/state/invariants.ts +0 -20
  251. package/agent.bak.20260905-190020/extensions/todo/state/replay.ts +0 -38
  252. package/agent.bak.20260905-190020/extensions/todo/state/selectors.ts +0 -107
  253. package/agent.bak.20260905-190020/extensions/todo/state/state-reducer.ts +0 -326
  254. package/agent.bak.20260905-190020/extensions/todo/state/state.ts +0 -18
  255. package/agent.bak.20260905-190020/extensions/todo/state/store.ts +0 -82
  256. package/agent.bak.20260905-190020/extensions/todo/state/task-graph.ts +0 -57
  257. package/agent.bak.20260905-190020/extensions/todo/todo-overlay.ts +0 -204
  258. package/agent.bak.20260905-190020/extensions/todo/todo.ts +0 -155
  259. package/agent.bak.20260905-190020/extensions/todo/tool/response-envelope.ts +0 -109
  260. package/agent.bak.20260905-190020/extensions/todo/tool/types.ts +0 -206
  261. package/agent.bak.20260905-190020/extensions/todo/view/format.ts +0 -177
  262. package/agent.bak.20260905-190020/extensions/todo/widget-order.ts +0 -121
  263. package/agent.bak.20260905-190020/extensions/trim-context/README.md +0 -54
  264. package/agent.bak.20260905-190020/extensions/trim-context/index.ts +0 -487
  265. package/agent.bak.20260905-190020/extensions/trim-context/install.ps1 +0 -23
  266. package/agent.bak.20260905-190020/extensions/trim-context/install.sh +0 -21
  267. package/agent.bak.20260905-190020/keybindings.json +0 -7
@@ -1,1737 +0,0 @@
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
- }