@ashx-j/lunr-dev 0.2.13-dev.8.1 → 0.2.16-dev.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/builtin-extensions/ashxj-tui.d.ts +14 -0
  3. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  4. package/dist/builtin-extensions/ashxj-tui.js +37 -3
  5. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  6. package/dist/builtin-extensions/index.d.ts +1 -15
  7. package/dist/builtin-extensions/index.d.ts.map +1 -1
  8. package/dist/builtin-extensions/index.js +7 -21
  9. package/dist/builtin-extensions/index.js.map +1 -1
  10. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.d.ts.map +1 -1
  11. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js +6 -2
  12. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js.map +1 -1
  13. package/dist/builtin-extensions/pi-lsp-extension/src/index.d.ts +4 -0
  14. package/dist/builtin-extensions/pi-lsp-extension/src/index.d.ts.map +1 -1
  15. package/dist/builtin-extensions/pi-lsp-extension/src/index.js +163 -169
  16. package/dist/builtin-extensions/pi-lsp-extension/src/index.js.map +1 -1
  17. package/dist/builtin-extensions/pi-lsp-extension/src/runtime.d.ts +46 -0
  18. package/dist/builtin-extensions/pi-lsp-extension/src/runtime.d.ts.map +1 -0
  19. package/dist/builtin-extensions/pi-lsp-extension/src/runtime.js +109 -0
  20. package/dist/builtin-extensions/pi-lsp-extension/src/runtime.js.map +1 -0
  21. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.d.ts +7 -0
  22. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.d.ts.map +1 -0
  23. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.js +211 -0
  24. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.js.map +1 -0
  25. package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.d.ts +0 -5
  26. package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.d.ts.map +1 -1
  27. package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.js +0 -202
  28. package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.js.map +1 -1
  29. package/dist/builtin-extensions/pi-mcp-adapter/index.d.ts.map +1 -1
  30. package/dist/builtin-extensions/pi-mcp-adapter/index.js +251 -94
  31. package/dist/builtin-extensions/pi-mcp-adapter/index.js.map +1 -1
  32. package/dist/builtin-extensions/pi-mcp-adapter/init.d.ts +4 -1
  33. package/dist/builtin-extensions/pi-mcp-adapter/init.d.ts.map +1 -1
  34. package/dist/builtin-extensions/pi-mcp-adapter/init.js +10 -3
  35. package/dist/builtin-extensions/pi-mcp-adapter/init.js.map +1 -1
  36. package/dist/builtin-extensions/pi-mcp-adapter/metadata-cache.d.ts.map +1 -1
  37. package/dist/builtin-extensions/pi-mcp-adapter/metadata-cache.js +6 -1
  38. package/dist/builtin-extensions/pi-mcp-adapter/metadata-cache.js.map +1 -1
  39. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.d.ts.map +1 -1
  40. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.js +2 -1
  41. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.js.map +1 -1
  42. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.d.ts +1 -1
  43. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.d.ts.map +1 -1
  44. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.js +1 -1
  45. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.js.map +1 -1
  46. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts +1 -1
  47. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  48. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +38 -14
  49. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  50. package/dist/builtin-extensions/pi-subagents/src/shared/fork-context.d.ts.map +1 -1
  51. package/dist/builtin-extensions/pi-subagents/src/shared/fork-context.js +1 -1
  52. package/dist/builtin-extensions/pi-subagents/src/shared/fork-context.js.map +1 -1
  53. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts +1 -1
  54. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
  55. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +1 -1
  56. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
  57. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +1 -1
  58. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  59. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +2 -1
  60. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  61. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.d.ts +1 -1
  62. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.d.ts.map +1 -1
  63. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.js +2 -1
  64. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.js.map +1 -1
  65. package/dist/builtin-extensions/pi-web-access/extract.d.ts.map +1 -1
  66. package/dist/builtin-extensions/pi-web-access/extract.js +105 -54
  67. package/dist/builtin-extensions/pi-web-access/extract.js.map +1 -1
  68. package/dist/builtin-extensions/pi-web-access/gemini-search.d.ts +1 -1
  69. package/dist/builtin-extensions/pi-web-access/gemini-search.d.ts.map +1 -1
  70. package/dist/builtin-extensions/pi-web-access/gemini-search.js +82 -65
  71. package/dist/builtin-extensions/pi-web-access/gemini-search.js.map +1 -1
  72. package/dist/builtin-extensions/pi-web-access/github-extract.d.ts.map +1 -1
  73. package/dist/builtin-extensions/pi-web-access/github-extract.js +2 -0
  74. package/dist/builtin-extensions/pi-web-access/github-extract.js.map +1 -1
  75. package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
  76. package/dist/builtin-extensions/pi-web-access/index.js +167 -77
  77. package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
  78. package/dist/builtin-extensions/pi-web-access/lazy.d.ts +18 -0
  79. package/dist/builtin-extensions/pi-web-access/lazy.d.ts.map +1 -0
  80. package/dist/builtin-extensions/pi-web-access/lazy.js +19 -0
  81. package/dist/builtin-extensions/pi-web-access/lazy.js.map +1 -0
  82. package/dist/builtin-extensions/pi-web-access/session-cleanup.d.ts +6 -0
  83. package/dist/builtin-extensions/pi-web-access/session-cleanup.d.ts.map +1 -0
  84. package/dist/builtin-extensions/pi-web-access/session-cleanup.js +17 -0
  85. package/dist/builtin-extensions/pi-web-access/session-cleanup.js.map +1 -0
  86. package/dist/catalog/publication.json +12 -12
  87. package/dist/cli-runtime.d.ts +3 -0
  88. package/dist/cli-runtime.d.ts.map +1 -0
  89. package/dist/cli-runtime.js +73 -0
  90. package/dist/cli-runtime.js.map +1 -0
  91. package/dist/cli.d.ts.map +1 -1
  92. package/dist/cli.js +14 -53
  93. package/dist/cli.js.map +1 -1
  94. package/dist/config.d.ts +1 -0
  95. package/dist/config.d.ts.map +1 -1
  96. package/dist/config.js +1 -0
  97. package/dist/config.js.map +1 -1
  98. package/dist/core/agent-session.d.ts +3 -0
  99. package/dist/core/agent-session.d.ts.map +1 -1
  100. package/dist/core/agent-session.js +56 -12
  101. package/dist/core/agent-session.js.map +1 -1
  102. package/dist/core/compaction/compaction.d.ts.map +1 -1
  103. package/dist/core/compaction/compaction.js +10 -5
  104. package/dist/core/compaction/compaction.js.map +1 -1
  105. package/dist/core/extensions/host-static.d.ts.map +1 -1
  106. package/dist/core/extensions/host-static.js +34 -12
  107. package/dist/core/extensions/host-static.js.map +1 -1
  108. package/dist/core/extensions/loader.d.ts +1 -2
  109. package/dist/core/extensions/loader.d.ts.map +1 -1
  110. package/dist/core/extensions/loader.js +10 -64
  111. package/dist/core/extensions/loader.js.map +1 -1
  112. package/dist/core/extensions/types.d.ts +2 -2
  113. package/dist/core/extensions/types.d.ts.map +1 -1
  114. package/dist/core/extensions/types.js.map +1 -1
  115. package/dist/core/footer-data-provider.d.ts.map +1 -1
  116. package/dist/core/footer-data-provider.js +2 -0
  117. package/dist/core/footer-data-provider.js.map +1 -1
  118. package/dist/core/permissions.d.ts +0 -2
  119. package/dist/core/permissions.d.ts.map +1 -1
  120. package/dist/core/permissions.js +10 -18
  121. package/dist/core/permissions.js.map +1 -1
  122. package/dist/core/sdk.d.ts +1 -1
  123. package/dist/core/sdk.d.ts.map +1 -1
  124. package/dist/core/sdk.js +1 -1
  125. package/dist/core/sdk.js.map +1 -1
  126. package/dist/core/slash-commands.d.ts.map +1 -1
  127. package/dist/core/slash-commands.js +0 -1
  128. package/dist/core/slash-commands.js.map +1 -1
  129. package/dist/core/timings.d.ts +2 -1
  130. package/dist/core/timings.d.ts.map +1 -1
  131. package/dist/core/timings.js +17 -2
  132. package/dist/core/timings.js.map +1 -1
  133. package/dist/gateway/commands.d.ts +1 -1
  134. package/dist/gateway/commands.d.ts.map +1 -1
  135. package/dist/gateway/commands.js +1 -18
  136. package/dist/gateway/commands.js.map +1 -1
  137. package/dist/main.d.ts +2 -2
  138. package/dist/main.d.ts.map +1 -1
  139. package/dist/main.js +57 -48
  140. package/dist/main.js.map +1 -1
  141. package/dist/modes/interactive/interactive-mode.d.ts +12 -21
  142. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  143. package/dist/modes/interactive/interactive-mode.js +120 -144
  144. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  145. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  146. package/dist/modes/interactive/theme/theme.js +95 -84
  147. package/dist/modes/interactive/theme/theme.js.map +1 -1
  148. package/dist/node-runtime/agent-session-services-IFJMSX7Z.js +36 -0
  149. package/dist/node-runtime/benchmark-request-UC7PD3V3.js +57 -0
  150. package/dist/node-runtime/brave-UWDA3I22.js +143 -0
  151. package/dist/node-runtime/builtin-extensions-LYZ3BFWQ.js +28 -0
  152. package/dist/node-runtime/chunk-2QKLKMKC.js +173 -0
  153. package/dist/node-runtime/chunk-3GHXVUAK.js +21 -0
  154. package/dist/node-runtime/chunk-3RCWXWAY.js +466 -0
  155. package/dist/node-runtime/chunk-3WFAEVKS.js +115 -0
  156. package/dist/node-runtime/chunk-3X3KZKMI.js +49 -0
  157. package/dist/node-runtime/chunk-4HXHDDCF.js +16 -0
  158. package/dist/node-runtime/chunk-4TLO4PO2.js +3427 -0
  159. package/dist/node-runtime/chunk-4ZWGEEAS.js +69 -0
  160. package/dist/node-runtime/chunk-53YJI7JU.js +137 -0
  161. package/dist/node-runtime/chunk-5P7PZMRU.js +610 -0
  162. package/dist/node-runtime/chunk-7N3YHTYF.js +26 -0
  163. package/dist/node-runtime/chunk-7NBZ5P7I.js +24 -0
  164. package/dist/node-runtime/chunk-A2KQXUJD.js +105 -0
  165. package/dist/node-runtime/chunk-AD6VXAO7.js +342 -0
  166. package/dist/node-runtime/chunk-AHSTJBIB.js +64 -0
  167. package/dist/node-runtime/chunk-AJX357CL.js +20211 -0
  168. package/dist/node-runtime/chunk-ARC4KOTI.js +11178 -0
  169. package/dist/node-runtime/chunk-AUXVGBO4.js +19 -0
  170. package/dist/node-runtime/chunk-AY6CAJSL.js +30 -0
  171. package/dist/node-runtime/chunk-BHO7O6E5.js +296 -0
  172. package/dist/node-runtime/chunk-BI3DHYCN.js +8845 -0
  173. package/dist/node-runtime/chunk-BR7YDUKA.js +109 -0
  174. package/dist/node-runtime/chunk-BRJX2PRC.js +53 -0
  175. package/dist/node-runtime/chunk-C4O2RSQQ.js +37 -0
  176. package/dist/node-runtime/chunk-CCZJ3PYY.js +17 -0
  177. package/dist/node-runtime/chunk-CDFZEWPU.js +8388 -0
  178. package/dist/node-runtime/chunk-CGBXPL3E.js +1171 -0
  179. package/dist/node-runtime/chunk-CJECDQKP.js +19 -0
  180. package/dist/node-runtime/chunk-DAPBPCBP.js +53 -0
  181. package/dist/node-runtime/chunk-DDGMHUOW.js +305 -0
  182. package/dist/node-runtime/chunk-DMMTJQMQ.js +43 -0
  183. package/dist/node-runtime/chunk-DVGWF6A2.js +1180 -0
  184. package/dist/node-runtime/chunk-ECD5L7YI.js +1369 -0
  185. package/dist/node-runtime/chunk-EDBJNRSM.js +46 -0
  186. package/dist/node-runtime/chunk-F4VQFJI2.js +1269 -0
  187. package/dist/node-runtime/chunk-FVDOXRUD.js +96 -0
  188. package/dist/node-runtime/chunk-GZQHMEWL.js +126 -0
  189. package/dist/node-runtime/chunk-H2EOZYMP.js +57 -0
  190. package/dist/node-runtime/chunk-HQCKFM2O.js +56 -0
  191. package/dist/node-runtime/chunk-HYHJIRDW.js +19 -0
  192. package/dist/node-runtime/chunk-I6R76AG7.js +22 -0
  193. package/dist/node-runtime/chunk-IPA627GP.js +296 -0
  194. package/dist/node-runtime/chunk-JHYKXIMM.js +378 -0
  195. package/dist/node-runtime/chunk-KKSIVROT.js +88 -0
  196. package/dist/node-runtime/chunk-LZAW4ZX3.js +633 -0
  197. package/dist/node-runtime/chunk-M3LZLQW2.js +98 -0
  198. package/dist/node-runtime/chunk-MGWCVY6Y.js +47 -0
  199. package/dist/node-runtime/chunk-MJQDX3BA.js +72 -0
  200. package/dist/node-runtime/chunk-MKKH27PH.js +8956 -0
  201. package/dist/node-runtime/chunk-MX3GJNXJ.js +986 -0
  202. package/dist/node-runtime/chunk-N77MY65L.js +966 -0
  203. package/dist/node-runtime/chunk-NUIFPZWX.js +209 -0
  204. package/dist/node-runtime/chunk-OBMWP3I5.js +68 -0
  205. package/dist/node-runtime/chunk-OR4L3IB2.js +42 -0
  206. package/dist/node-runtime/chunk-PBC2Y6GS.js +903 -0
  207. package/dist/node-runtime/chunk-PCGNAY2O.js +1324 -0
  208. package/dist/node-runtime/chunk-PCIGF2XK.js +250 -0
  209. package/dist/node-runtime/chunk-PGAJ5MO5.js +7558 -0
  210. package/dist/node-runtime/chunk-PL6JKXWI.js +3622 -0
  211. package/dist/node-runtime/chunk-PVCDKGR6.js +55 -0
  212. package/dist/node-runtime/chunk-Q4Q6D4IY.js +364 -0
  213. package/dist/node-runtime/chunk-Q5ZU2VRT.js +83 -0
  214. package/dist/node-runtime/chunk-QFWG2RVL.js +5477 -0
  215. package/dist/node-runtime/chunk-QH42CDX3.js +9451 -0
  216. package/dist/node-runtime/chunk-RQ2Q5IYR.js +72 -0
  217. package/dist/node-runtime/chunk-RQ5DNUNG.js +429 -0
  218. package/dist/node-runtime/chunk-RRIZE6QQ.js +17 -0
  219. package/dist/node-runtime/chunk-SN6G4OU6.js +4753 -0
  220. package/dist/node-runtime/chunk-SWSN6HAB.js +53 -0
  221. package/dist/node-runtime/chunk-TQNSPERU.js +949 -0
  222. package/dist/node-runtime/chunk-VRB2IYGG.js +53 -0
  223. package/dist/node-runtime/chunk-VSELYKWU.js +772 -0
  224. package/dist/node-runtime/chunk-W3CVH5RX.js +62 -0
  225. package/dist/node-runtime/chunk-WJVFOIB5.js +24921 -0
  226. package/dist/node-runtime/chunk-WOVTOV5P.js +5653 -0
  227. package/dist/node-runtime/chunk-XBWPBZBN.js +125 -0
  228. package/dist/node-runtime/chunk-XTHQ7XR7.js +509 -0
  229. package/dist/node-runtime/chunk-YF63P5ZT.js +864 -0
  230. package/dist/node-runtime/chunk-YSKSB3BJ.js +295 -0
  231. package/dist/node-runtime/chunk-ZCHMJ34K.js +130 -0
  232. package/dist/node-runtime/chunk-ZKMFUD3Q.js +2834 -0
  233. package/dist/node-runtime/cli-runtime.js +81 -0
  234. package/dist/node-runtime/clipboard-OZ63BBC6.js +12 -0
  235. package/dist/node-runtime/clipboard-image-7QR6GXVA.js +13 -0
  236. package/dist/node-runtime/command-L3LAHDIH.js +13 -0
  237. package/dist/node-runtime/commands-RL5YBW5I.js +291 -0
  238. package/dist/node-runtime/config-FMEOZSWE.js +77 -0
  239. package/dist/node-runtime/curator-server-XCM43VEF.js +3816 -0
  240. package/dist/node-runtime/customize-XEXL2DGQ.js +12 -0
  241. package/dist/node-runtime/direct-tool-executor-JVDXTO7A.js +188 -0
  242. package/dist/node-runtime/exa-LL64QZR2.js +329 -0
  243. package/dist/node-runtime/export-html-KR26FOUZ.js +150 -0
  244. package/dist/node-runtime/extension-input-RV5N6APL.js +14 -0
  245. package/dist/node-runtime/extension-selector-CPD5LWDD.js +14 -0
  246. package/dist/node-runtime/extract-XLQSJN4V.js +17 -0
  247. package/dist/node-runtime/file-sync-HCOP2QVF.js +117 -0
  248. package/dist/node-runtime/gateway-X2TI5X7F.js +2484 -0
  249. package/dist/node-runtime/gemini-api-QE7ANVDZ.js +29 -0
  250. package/dist/node-runtime/gemini-search-U7SUMPQA.js +330 -0
  251. package/dist/node-runtime/gemini-url-context-PXWCBQHK.js +103 -0
  252. package/dist/node-runtime/gemini-web-MM446SEH.js +14 -0
  253. package/dist/node-runtime/github-extract-F64RZOEA.js +619 -0
  254. package/dist/node-runtime/goal-CZQ7FFQJ.js +1831 -0
  255. package/dist/node-runtime/index.js +859 -0
  256. package/dist/node-runtime/init-NJV2T55N.js +28 -0
  257. package/dist/node-runtime/install-cli-TH7IJJQQ.js +308 -0
  258. package/dist/node-runtime/interactive-mode-KOM35YF2.js +56 -0
  259. package/dist/node-runtime/interactive-view-U6OILKF3.js +249 -0
  260. package/dist/node-runtime/lsp-manager-3ASUSO4T.js +1104 -0
  261. package/dist/node-runtime/lunr-cron-YJ5UHYRD.js +268 -0
  262. package/dist/node-runtime/main.js +54 -0
  263. package/dist/node-runtime/mcp-auth-flow-QIXHW6QN.js +32 -0
  264. package/dist/node-runtime/mcp-panel-IIGMXQGP.js +501 -0
  265. package/dist/node-runtime/mcp-setup-panel-FEK7663I.js +355 -0
  266. package/dist/node-runtime/memory-cap-3S4XRLNP.js +20 -0
  267. package/dist/node-runtime/model-tiers-NJLHUEGK.js +20 -0
  268. package/dist/node-runtime/openai-search-B3MSNCQA.js +288 -0
  269. package/dist/node-runtime/package-manager-cli-TZZHJCVI.js +749 -0
  270. package/dist/node-runtime/parallel-AI2MNSCU.js +265 -0
  271. package/dist/node-runtime/parser-manager-TQHWYDH7.js +152 -0
  272. package/dist/node-runtime/pdf-extract-IPE6H4HP.js +57 -0
  273. package/dist/node-runtime/perplexity-QVJW3I35.js +12 -0
  274. package/dist/node-runtime/pi-intercom-EEMBGQID.js +2215 -0
  275. package/dist/node-runtime/pi-mcp-adapter-QFB37AVN.js +422 -0
  276. package/dist/node-runtime/pi-ollama-cloud-4VZRMULP.js +1440 -0
  277. package/dist/node-runtime/pi-prompt-template-model-4QT4LLXF.js +3946 -0
  278. package/dist/node-runtime/pi-subagents-7XJ4H53P.js +6023 -0
  279. package/dist/node-runtime/pi-web-access-DPGAVKOA.js +1864 -0
  280. package/dist/node-runtime/print-mode-VWIRFDAC.js +12 -0
  281. package/dist/node-runtime/proxy-modes-HEEFK4KC.js +685 -0
  282. package/dist/node-runtime/rpc-entry.js +62 -0
  283. package/dist/node-runtime/rpc-mode-SJNSOA4G.js +13 -0
  284. package/dist/node-runtime/runtime-bridges-PPHNBFWL.js +14 -0
  285. package/dist/node-runtime/session-manager-MQNBYACL.js +35 -0
  286. package/dist/node-runtime/session-selector-5AZLJS6U.js +14 -0
  287. package/dist/node-runtime/settings-manager-5LF2VQWC.js +21 -0
  288. package/dist/node-runtime/src-Y7JSCK64.js +2407 -0
  289. package/dist/node-runtime/subagent-executor-5RNDEPIJ.js +8483 -0
  290. package/dist/node-runtime/summary-review-YSMXKITO.js +228 -0
  291. package/dist/node-runtime/tavily-WEZFVFP7.js +154 -0
  292. package/dist/node-runtime/timings-UAD5BEAS.js +16 -0
  293. package/dist/node-runtime/tool-renderer-MV5O452K.js +204 -0
  294. package/dist/node-runtime/update-cli-A253SZ7P.js +73 -0
  295. package/dist/node-runtime/v4-5MNC3H7G.js +424 -0
  296. package/dist/node-runtime/video-extract-OYT5WKS5.js +304 -0
  297. package/dist/node-runtime/workspace-index-6DBXSTJ6.js +163 -0
  298. package/dist/node-runtime/youtube-extract-TQYKGFGS.js +264 -0
  299. package/dist/startup/benchmark-request.d.ts +3 -0
  300. package/dist/startup/benchmark-request.d.ts.map +1 -0
  301. package/dist/startup/benchmark-request.js +65 -0
  302. package/dist/startup/benchmark-request.js.map +1 -0
  303. package/dist/startup/interactive-view.d.ts +55 -0
  304. package/dist/startup/interactive-view.d.ts.map +1 -0
  305. package/dist/startup/interactive-view.js +332 -0
  306. package/dist/startup/interactive-view.js.map +1 -0
  307. package/dist/startup/launch-routing.d.ts.map +1 -1
  308. package/dist/startup/launch-routing.js +9 -10
  309. package/dist/startup/launch-routing.js.map +1 -1
  310. package/dist/startup/startup-milestones.d.ts +1 -1
  311. package/dist/startup/startup-milestones.d.ts.map +1 -1
  312. package/dist/startup/startup-milestones.js +3 -2
  313. package/dist/startup/startup-milestones.js.map +1 -1
  314. package/dist/utils/await-with-abort.d.ts +2 -0
  315. package/dist/utils/await-with-abort.d.ts.map +1 -0
  316. package/dist/utils/await-with-abort.js +19 -0
  317. package/dist/utils/await-with-abort.js.map +1 -0
  318. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  319. package/dist/utils/syntax-highlight.js +10 -4
  320. package/dist/utils/syntax-highlight.js.map +1 -1
  321. package/docs/compaction.md +5 -3
  322. package/docs/development.md +5 -1
  323. package/docs/features.md +0 -1
  324. package/docs/interactive-startup.md +98 -0
  325. package/docs/usage.md +0 -1
  326. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  327. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  328. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  329. package/examples/extensions/gondolin/package-lock.json +2 -2
  330. package/examples/extensions/gondolin/package.json +1 -1
  331. package/examples/extensions/sandbox/package-lock.json +2 -2
  332. package/examples/extensions/sandbox/package.json +1 -1
  333. package/examples/extensions/with-deps/package-lock.json +2 -2
  334. package/examples/extensions/with-deps/package.json +1 -1
  335. package/package.json +20 -20
  336. package/dist/core/swarm.d.ts +0 -23
  337. package/dist/core/swarm.d.ts.map +0 -1
  338. package/dist/core/swarm.js +0 -45
  339. package/dist/core/swarm.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { type AgentTool, type StreamFn, type ThinkingLevel } from "@ashx-j/lunr-agent";
2
- import { type ExtensionContext } from "@ashx-j/lunr-dev";
2
+ import type { ExtensionContext } from "@ashx-j/lunr-dev";
3
3
  import { type Model } from "@ashx-j/lunr-ai/compat";
4
4
  import type { WatchdogReviewFunction } from "./runtime.ts";
5
5
  import { type ResolvedWatchdogConfig } from "./types.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../../../../src/builtin-extensions/pi-subagents/src/watchdog/review.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,EAAqC,KAAK,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAC3G,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAIxE,OAAO,KAAK,EAAE,sBAAsB,EAAyB,MAAM,cAAc,CAAC;AAClF,OAAO,EAIN,KAAK,sBAAsB,EAK3B,MAAM,YAAY,CAAC;AAepB,KAAK,uBAAuB,GAAG,gBAAgB,GAAG,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC,CAAC;AAEvF,KAAK,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;AAEhC,UAAU,kBAAkB;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,4BAA4B;IAC5C,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;CACnD;AAkED,wBAAsB,0BAA0B,CAC/C,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE;IAAE,oBAAoB,CAAC,EAAE,aAAa,CAAA;CAAO,GACpD,OAAO,CAAC,4BAA4B,CAAC,CAiCvC;AAwFD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,OAAO,GAAE,+BAAoC,GAAG,sBAAsB,CAgDjJ","sourcesContent":["// @ts-nocheck\nimport { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport { createReadOnlyTools, convertToLlm, type ExtensionContext } from \"@ashx-j/lunr-dev\";\nimport { streamSimple, type Model } from \"@ashx-j/lunr-ai/compat\";\nimport { Type, type Static } from \"typebox\";\nimport { resolveModelCandidate } from \"../runs/shared/model-fallback.ts\";\nimport { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from \"../shared/model-info.ts\";\nimport type { WatchdogReviewFunction, WatchdogReviewRequest } from \"./runtime.ts\";\nimport {\n\tWATCHDOG_WARNING_CATEGORIES,\n\tWATCHDOG_WARNING_CONFIDENCES,\n\tWATCHDOG_WARNING_SEVERITIES,\n\ttype ResolvedWatchdogConfig,\n\ttype WatchdogCategory,\n\ttype WatchdogConfidence,\n\ttype WatchdogSeverity,\n\ttype WatchdogWarning,\n} from \"./types.ts\";\n\nconst WATCHDOG_ALLOWED_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\", \"ls\", \"watchdog_warn\"]);\n\nconst WatchdogWarnParams = Type.Object({\n\tseverity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: \"concern for actionable risk, blocker for a likely wrong or unsafe outcome\" }),\n\tsummary: Type.String({ description: \"One concise sentence naming the issue.\" }),\n\tevidence: Type.String({ description: \"Specific evidence from the turn delta or inspected files.\" }),\n\trecommendedAction: Type.String({ description: \"Specific action the parent should take before accepting or continuing.\" }),\n\tcategory: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),\n\tconfidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),\n}, { additionalProperties: false });\n\ntype WatchdogWarnParams = Static<typeof WatchdogWarnParams>;\n\ntype WatchdogContextProvider = ExtensionContext | (() => ExtensionContext | undefined);\n\ntype RegistryModel = Model<any>;\n\ninterface WatchdogReviewAuth {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}\n\nexport interface WatchdogReviewModelSelection {\n\tmodel: RegistryModel;\n\tthinkingLevel: ThinkingLevel;\n\tauth: WatchdogReviewAuth;\n\texplicit: boolean;\n}\n\nexport interface CreateMainWatchdogReviewOptions {\n\tstreamFn?: StreamFn;\n\tcreateReadOnlyTools?: (cwd: string) => AgentTool[];\n\tgetThinkingLevel?: () => ThinkingLevel | undefined;\n}\n\nfunction fullModelId(model: Pick<RegistryModel, \"provider\" | \"id\">): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction splitProviderModel(value: string): { provider: string; id: string } | undefined {\n\tconst slashIndex = value.indexOf(\"/\");\n\tif (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;\n\treturn { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };\n}\n\nfunction assertThinkingLevel(value: string, source: string): ThinkingLevel {\n\tif ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;\n\tthrow new Error(`Unsupported watchdog thinking level '${value}' from ${source}; expected ${THINKING_LEVELS.join(\", \")} or false.`);\n}\n\nfunction contextThinkingLevel(ctx: ExtensionContext, currentThinkingLevel: ThinkingLevel | undefined): ThinkingLevel | undefined {\n\tif (currentThinkingLevel) return currentThinkingLevel;\n\tconst value = (ctx as { thinkingLevel?: unknown }).thinkingLevel;\n\treturn typeof value === \"string\" && (THINKING_LEVELS as readonly string[]).includes(value) ? value as ThinkingLevel : undefined;\n}\n\nfunction resolveReviewThinking(input: {\n\tmodelString: string;\n\tconfigThinking: string | false | undefined;\n\tctx: ExtensionContext;\n\tallowContextThinking: boolean;\n\tcurrentThinkingLevel?: ThinkingLevel;\n}): ThinkingLevel {\n\tconst fromModelOrConfig = resolveEffectiveThinking(input.modelString, input.configThinking);\n\tif (fromModelOrConfig) return assertThinkingLevel(fromModelOrConfig, \"watchdog model/config\");\n\tif (input.configThinking === false) return \"off\";\n\tif (input.configThinking !== undefined) return assertThinkingLevel(input.configThinking, \"watchdog config\");\n\tif (input.allowContextThinking) return contextThinkingLevel(input.ctx, input.currentThinkingLevel) ?? \"off\";\n\treturn \"off\";\n}\n\nfunction resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { model: RegistryModel; modelString: string } {\n\tconst availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);\n\tconst preferredProvider = typeof ctx.model?.provider === \"string\" ? ctx.model.provider : undefined;\n\tconst resolved = resolveModelCandidate(rawModel, availableModels, preferredProvider);\n\tconst { baseModel } = splitKnownThinkingSuffix(resolved);\n\tconst named = splitProviderModel(baseModel);\n\tif (!named) {\n\t\tthrow new Error(`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`);\n\t}\n\n\tconst model = ctx.modelRegistry.find(named.provider, named.id);\n\tif (!model) throw new Error(`Configured watchdog model '${rawModel}' was not found as '${baseModel}'.`);\n\tif (!ctx.modelRegistry.hasConfiguredAuth(model)) {\n\t\tthrow new Error(`Configured watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`);\n\t}\n\treturn { model, modelString: resolved };\n}\n\nasync function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {\n\tconst auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);\n\tif (!auth.ok) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);\n\treturn {\n\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t...(auth.headers ? { headers: auth.headers } : {}),\n\t\t...(auth.env ? { env: auth.env } : {}),\n\t};\n}\n\nexport async function resolveWatchdogReviewModel(\n\tctx: ExtensionContext,\n\tconfig: ResolvedWatchdogConfig,\n\toptions: { currentThinkingLevel?: ThinkingLevel } = {},\n): Promise<WatchdogReviewModelSelection> {\n\tif (config.main.model) {\n\t\tconst resolved = resolveConfiguredModel(ctx, config.main.model);\n\t\treturn {\n\t\t\tmodel: resolved.model,\n\t\t\tthinkingLevel: resolveReviewThinking({\n\t\t\t\tmodelString: resolved.modelString,\n\t\t\t\tconfigThinking: config.main.thinking,\n\t\t\t\tctx,\n\t\t\t\tallowContextThinking: false,\n\t\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t\t}),\n\t\t\tauth: await resolveReviewAuth(ctx, resolved.model),\n\t\t\texplicit: true,\n\t\t};\n\t}\n\n\tconst currentModel = ctx.model;\n\tif (!currentModel) {\n\t\tthrow new Error(\"Main watchdog review cannot run because the current Pi session model is unavailable and subagents.watchdog.main.model is not configured.\");\n\t}\n\treturn {\n\t\tmodel: currentModel,\n\t\tthinkingLevel: resolveReviewThinking({\n\t\t\tmodelString: fullModelId(currentModel),\n\t\t\tconfigThinking: config.main.thinking,\n\t\t\tctx,\n\t\t\tallowContextThinking: true,\n\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t}),\n\t\tauth: await resolveReviewAuth(ctx, currentModel),\n\t\texplicit: false,\n\t};\n}\n\nfunction nonEmptyString(value: string, field: string): string {\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(`watchdog_warn.${field} must be a non-empty string.`);\n\treturn trimmed;\n}\n\nfunction toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {\n\treturn {\n\t\tseverity: params.severity as WatchdogSeverity,\n\t\tcategory: (params.category ?? \"other\") as WatchdogCategory,\n\t\tconfidence: (params.confidence ?? \"medium\") as WatchdogConfidence,\n\t\tsource: \"main\",\n\t\tsummary: nonEmptyString(params.summary, \"summary\"),\n\t\tevidence: nonEmptyString(params.evidence, \"evidence\"),\n\t\trecommendedAction: nonEmptyString(params.recommendedAction, \"recommendedAction\"),\n\t};\n}\n\nfunction createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeof WatchdogWarnParams, { accepted: boolean }> {\n\treturn {\n\t\tname: \"watchdog_warn\",\n\t\tlabel: \"Watchdog warning\",\n\t\tdescription: [\n\t\t\t\"Emit one actionable main-session watchdog warning.\",\n\t\t\t\"Use only for medium/high confidence concerns or blockers that the parent should consider before accepting the work.\",\n\t\t\t\"Do not use for nits, praise, informational notes, or clean reviews.\",\n\t\t].join(\" \"),\n\t\tparameters: WatchdogWarnParams,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst warning = toWatchdogWarning(params);\n\t\t\tconst accepted = request.emitWarning(warning);\n\t\t\treturn {\n\t\t\t\tcontent: [{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: accepted\n\t\t\t\t\t\t? \"Watchdog warning recorded.\"\n\t\t\t\t\t\t: \"Watchdog warning was ignored by the runtime guard because it was stale, duplicate, or over budget.\",\n\t\t\t\t}],\n\t\t\t\tdetails: { accepted },\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction buildWatchdogSystemPrompt(ctx: ExtensionContext): string {\n\treturn [\n\t\t\"You are the main-session subagent watchdog for Pi.\",\n\t\t`Working directory: ${ctx.cwd}`,\n\t\t\"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.\",\n\t\t\"You are read-only. You may use read, grep, find, and ls. Do not edit files, run shell commands, spawn agents, or mutate state.\",\n\t\t\"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.\",\n\t\t\"Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.\",\n\t\t\"Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.\",\n\t\t\"If the turn is clean, call no tools and end normally.\",\n\t\t\"Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.\",\n\t].join(\"\\n\");\n}\n\nfunction buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogReviewModelSelection): string {\n\treturn [\n\t\t\"Review this parent-session turn delta for subagent-watchdog-worthy issues.\",\n\t\t`Review id: ${request.reviewId}; epoch: ${request.epoch}; review model: ${fullModelId(selection.model)}; thinking: ${selection.thinkingLevel}.`,\n\t\t\"Call watchdog_warn for each qualifying concern or blocker. Call no tools when clean.\",\n\t\t\"<turn_delta>\",\n\t\trequest.delta,\n\t\t\"</turn_delta>\",\n\t].join(\"\\n\\n\");\n}\n\nfunction finalStopReason(agent: Agent): \"stop\" | \"error\" | \"aborted\" | \"length\" {\n\tfor (let index = agent.state.messages.length - 1; index >= 0; index--) {\n\t\tconst message = agent.state.messages[index];\n\t\tif (message && typeof message === \"object\" && (message as { role?: unknown }).role === \"assistant\") {\n\t\t\tconst stopReason = (message as { stopReason?: unknown }).stopReason;\n\t\t\tif (stopReason === \"error\" || stopReason === \"aborted\" || stopReason === \"length\") return stopReason;\n\t\t\treturn \"stop\";\n\t\t}\n\t}\n\treturn \"stop\";\n}\n\nfunction resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {\n\treturn typeof provider === \"function\" ? provider() : provider;\n}\n\nexport function createMainWatchdogReview(provider: WatchdogContextProvider, options: CreateMainWatchdogReviewOptions = {}): WatchdogReviewFunction {\n\treturn async (request) => {\n\t\tconst ctx = resolveContext(provider);\n\t\tif (!ctx) throw new Error(\"Main watchdog review cannot run without an active Pi extension context.\");\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst selection = await resolveWatchdogReviewModel(ctx, request.config, {\n\t\t\tcurrentThinkingLevel: options.getThinkingLevel?.(),\n\t\t});\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst auth = selection.auth;\n\t\tconst baseStreamFn = options.streamFn ?? streamSimple;\n\t\tconst streamFn: StreamFn = (model, context, streamOptions) => baseStreamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t\tenv: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,\n\t\t\theaders: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },\n\t\t});\n\t\tconst tools = [\n\t\t\t...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== \"watchdog_warn\"),\n\t\t\tcreateWatchdogWarnTool(request),\n\t\t];\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: buildWatchdogSystemPrompt(ctx),\n\t\t\t\tmodel: selection.model,\n\t\t\t\tthinkingLevel: selection.thinkingLevel,\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tstreamFn,\n\t\t\tgetApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,\n\t\t\tbeforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)\n\t\t\t\t? undefined\n\t\t\t\t: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },\n\t\t\ttoolExecution: \"sequential\",\n\t\t});\n\t\tconst abort = () => agent.abort();\n\t\tctx.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\trequest.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\ttry {\n\t\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\t\tawait agent.prompt(buildReviewPrompt(request, selection));\n\t\t} finally {\n\t\t\tctx.signal?.removeEventListener(\"abort\", abort);\n\t\t\trequest.signal?.removeEventListener(\"abort\", abort);\n\t\t}\n\t\treturn { stopReason: finalStopReason(agent) };\n\t};\n}\n"]}
1
+ {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../../../../src/builtin-extensions/pi-subagents/src/watchdog/review.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGxE,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAIxE,OAAO,KAAK,EAAE,sBAAsB,EAAyB,MAAM,cAAc,CAAC;AAClF,OAAO,EAIN,KAAK,sBAAsB,EAK3B,MAAM,YAAY,CAAC;AAepB,KAAK,uBAAuB,GAAG,gBAAgB,GAAG,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC,CAAC;AAEvF,KAAK,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;AAEhC,UAAU,kBAAkB;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,4BAA4B;IAC5C,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;CACnD;AAkED,wBAAsB,0BAA0B,CAC/C,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE;IAAE,oBAAoB,CAAC,EAAE,aAAa,CAAA;CAAO,GACpD,OAAO,CAAC,4BAA4B,CAAC,CAiCvC;AAwFD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,OAAO,GAAE,+BAAoC,GAAG,sBAAsB,CAgDjJ","sourcesContent":["// @ts-nocheck\nimport { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport type { ExtensionContext } from \"@ashx-j/lunr-dev\";\nimport { createReadOnlyTools } from \"../../../../core/tools/index.ts\";\nimport { convertToLlm } from \"../../../../core/messages.ts\";\nimport { streamSimple, type Model } from \"@ashx-j/lunr-ai/compat\";\nimport { Type, type Static } from \"typebox\";\nimport { resolveModelCandidate } from \"../runs/shared/model-fallback.ts\";\nimport { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from \"../shared/model-info.ts\";\nimport type { WatchdogReviewFunction, WatchdogReviewRequest } from \"./runtime.ts\";\nimport {\n\tWATCHDOG_WARNING_CATEGORIES,\n\tWATCHDOG_WARNING_CONFIDENCES,\n\tWATCHDOG_WARNING_SEVERITIES,\n\ttype ResolvedWatchdogConfig,\n\ttype WatchdogCategory,\n\ttype WatchdogConfidence,\n\ttype WatchdogSeverity,\n\ttype WatchdogWarning,\n} from \"./types.ts\";\n\nconst WATCHDOG_ALLOWED_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\", \"ls\", \"watchdog_warn\"]);\n\nconst WatchdogWarnParams = Type.Object({\n\tseverity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: \"concern for actionable risk, blocker for a likely wrong or unsafe outcome\" }),\n\tsummary: Type.String({ description: \"One concise sentence naming the issue.\" }),\n\tevidence: Type.String({ description: \"Specific evidence from the turn delta or inspected files.\" }),\n\trecommendedAction: Type.String({ description: \"Specific action the parent should take before accepting or continuing.\" }),\n\tcategory: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),\n\tconfidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),\n}, { additionalProperties: false });\n\ntype WatchdogWarnParams = Static<typeof WatchdogWarnParams>;\n\ntype WatchdogContextProvider = ExtensionContext | (() => ExtensionContext | undefined);\n\ntype RegistryModel = Model<any>;\n\ninterface WatchdogReviewAuth {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}\n\nexport interface WatchdogReviewModelSelection {\n\tmodel: RegistryModel;\n\tthinkingLevel: ThinkingLevel;\n\tauth: WatchdogReviewAuth;\n\texplicit: boolean;\n}\n\nexport interface CreateMainWatchdogReviewOptions {\n\tstreamFn?: StreamFn;\n\tcreateReadOnlyTools?: (cwd: string) => AgentTool[];\n\tgetThinkingLevel?: () => ThinkingLevel | undefined;\n}\n\nfunction fullModelId(model: Pick<RegistryModel, \"provider\" | \"id\">): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction splitProviderModel(value: string): { provider: string; id: string } | undefined {\n\tconst slashIndex = value.indexOf(\"/\");\n\tif (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;\n\treturn { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };\n}\n\nfunction assertThinkingLevel(value: string, source: string): ThinkingLevel {\n\tif ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;\n\tthrow new Error(`Unsupported watchdog thinking level '${value}' from ${source}; expected ${THINKING_LEVELS.join(\", \")} or false.`);\n}\n\nfunction contextThinkingLevel(ctx: ExtensionContext, currentThinkingLevel: ThinkingLevel | undefined): ThinkingLevel | undefined {\n\tif (currentThinkingLevel) return currentThinkingLevel;\n\tconst value = (ctx as { thinkingLevel?: unknown }).thinkingLevel;\n\treturn typeof value === \"string\" && (THINKING_LEVELS as readonly string[]).includes(value) ? value as ThinkingLevel : undefined;\n}\n\nfunction resolveReviewThinking(input: {\n\tmodelString: string;\n\tconfigThinking: string | false | undefined;\n\tctx: ExtensionContext;\n\tallowContextThinking: boolean;\n\tcurrentThinkingLevel?: ThinkingLevel;\n}): ThinkingLevel {\n\tconst fromModelOrConfig = resolveEffectiveThinking(input.modelString, input.configThinking);\n\tif (fromModelOrConfig) return assertThinkingLevel(fromModelOrConfig, \"watchdog model/config\");\n\tif (input.configThinking === false) return \"off\";\n\tif (input.configThinking !== undefined) return assertThinkingLevel(input.configThinking, \"watchdog config\");\n\tif (input.allowContextThinking) return contextThinkingLevel(input.ctx, input.currentThinkingLevel) ?? \"off\";\n\treturn \"off\";\n}\n\nfunction resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { model: RegistryModel; modelString: string } {\n\tconst availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);\n\tconst preferredProvider = typeof ctx.model?.provider === \"string\" ? ctx.model.provider : undefined;\n\tconst resolved = resolveModelCandidate(rawModel, availableModels, preferredProvider);\n\tconst { baseModel } = splitKnownThinkingSuffix(resolved);\n\tconst named = splitProviderModel(baseModel);\n\tif (!named) {\n\t\tthrow new Error(`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`);\n\t}\n\n\tconst model = ctx.modelRegistry.find(named.provider, named.id);\n\tif (!model) throw new Error(`Configured watchdog model '${rawModel}' was not found as '${baseModel}'.`);\n\tif (!ctx.modelRegistry.hasConfiguredAuth(model)) {\n\t\tthrow new Error(`Configured watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`);\n\t}\n\treturn { model, modelString: resolved };\n}\n\nasync function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {\n\tconst auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);\n\tif (!auth.ok) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);\n\treturn {\n\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t...(auth.headers ? { headers: auth.headers } : {}),\n\t\t...(auth.env ? { env: auth.env } : {}),\n\t};\n}\n\nexport async function resolveWatchdogReviewModel(\n\tctx: ExtensionContext,\n\tconfig: ResolvedWatchdogConfig,\n\toptions: { currentThinkingLevel?: ThinkingLevel } = {},\n): Promise<WatchdogReviewModelSelection> {\n\tif (config.main.model) {\n\t\tconst resolved = resolveConfiguredModel(ctx, config.main.model);\n\t\treturn {\n\t\t\tmodel: resolved.model,\n\t\t\tthinkingLevel: resolveReviewThinking({\n\t\t\t\tmodelString: resolved.modelString,\n\t\t\t\tconfigThinking: config.main.thinking,\n\t\t\t\tctx,\n\t\t\t\tallowContextThinking: false,\n\t\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t\t}),\n\t\t\tauth: await resolveReviewAuth(ctx, resolved.model),\n\t\t\texplicit: true,\n\t\t};\n\t}\n\n\tconst currentModel = ctx.model;\n\tif (!currentModel) {\n\t\tthrow new Error(\"Main watchdog review cannot run because the current Pi session model is unavailable and subagents.watchdog.main.model is not configured.\");\n\t}\n\treturn {\n\t\tmodel: currentModel,\n\t\tthinkingLevel: resolveReviewThinking({\n\t\t\tmodelString: fullModelId(currentModel),\n\t\t\tconfigThinking: config.main.thinking,\n\t\t\tctx,\n\t\t\tallowContextThinking: true,\n\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t}),\n\t\tauth: await resolveReviewAuth(ctx, currentModel),\n\t\texplicit: false,\n\t};\n}\n\nfunction nonEmptyString(value: string, field: string): string {\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(`watchdog_warn.${field} must be a non-empty string.`);\n\treturn trimmed;\n}\n\nfunction toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {\n\treturn {\n\t\tseverity: params.severity as WatchdogSeverity,\n\t\tcategory: (params.category ?? \"other\") as WatchdogCategory,\n\t\tconfidence: (params.confidence ?? \"medium\") as WatchdogConfidence,\n\t\tsource: \"main\",\n\t\tsummary: nonEmptyString(params.summary, \"summary\"),\n\t\tevidence: nonEmptyString(params.evidence, \"evidence\"),\n\t\trecommendedAction: nonEmptyString(params.recommendedAction, \"recommendedAction\"),\n\t};\n}\n\nfunction createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeof WatchdogWarnParams, { accepted: boolean }> {\n\treturn {\n\t\tname: \"watchdog_warn\",\n\t\tlabel: \"Watchdog warning\",\n\t\tdescription: [\n\t\t\t\"Emit one actionable main-session watchdog warning.\",\n\t\t\t\"Use only for medium/high confidence concerns or blockers that the parent should consider before accepting the work.\",\n\t\t\t\"Do not use for nits, praise, informational notes, or clean reviews.\",\n\t\t].join(\" \"),\n\t\tparameters: WatchdogWarnParams,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst warning = toWatchdogWarning(params);\n\t\t\tconst accepted = request.emitWarning(warning);\n\t\t\treturn {\n\t\t\t\tcontent: [{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: accepted\n\t\t\t\t\t\t? \"Watchdog warning recorded.\"\n\t\t\t\t\t\t: \"Watchdog warning was ignored by the runtime guard because it was stale, duplicate, or over budget.\",\n\t\t\t\t}],\n\t\t\t\tdetails: { accepted },\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction buildWatchdogSystemPrompt(ctx: ExtensionContext): string {\n\treturn [\n\t\t\"You are the main-session subagent watchdog for Pi.\",\n\t\t`Working directory: ${ctx.cwd}`,\n\t\t\"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.\",\n\t\t\"You are read-only. You may use read, grep, find, and ls. Do not edit files, run shell commands, spawn agents, or mutate state.\",\n\t\t\"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.\",\n\t\t\"Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.\",\n\t\t\"Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.\",\n\t\t\"If the turn is clean, call no tools and end normally.\",\n\t\t\"Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.\",\n\t].join(\"\\n\");\n}\n\nfunction buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogReviewModelSelection): string {\n\treturn [\n\t\t\"Review this parent-session turn delta for subagent-watchdog-worthy issues.\",\n\t\t`Review id: ${request.reviewId}; epoch: ${request.epoch}; review model: ${fullModelId(selection.model)}; thinking: ${selection.thinkingLevel}.`,\n\t\t\"Call watchdog_warn for each qualifying concern or blocker. Call no tools when clean.\",\n\t\t\"<turn_delta>\",\n\t\trequest.delta,\n\t\t\"</turn_delta>\",\n\t].join(\"\\n\\n\");\n}\n\nfunction finalStopReason(agent: Agent): \"stop\" | \"error\" | \"aborted\" | \"length\" {\n\tfor (let index = agent.state.messages.length - 1; index >= 0; index--) {\n\t\tconst message = agent.state.messages[index];\n\t\tif (message && typeof message === \"object\" && (message as { role?: unknown }).role === \"assistant\") {\n\t\t\tconst stopReason = (message as { stopReason?: unknown }).stopReason;\n\t\t\tif (stopReason === \"error\" || stopReason === \"aborted\" || stopReason === \"length\") return stopReason;\n\t\t\treturn \"stop\";\n\t\t}\n\t}\n\treturn \"stop\";\n}\n\nfunction resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {\n\treturn typeof provider === \"function\" ? provider() : provider;\n}\n\nexport function createMainWatchdogReview(provider: WatchdogContextProvider, options: CreateMainWatchdogReviewOptions = {}): WatchdogReviewFunction {\n\treturn async (request) => {\n\t\tconst ctx = resolveContext(provider);\n\t\tif (!ctx) throw new Error(\"Main watchdog review cannot run without an active Pi extension context.\");\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst selection = await resolveWatchdogReviewModel(ctx, request.config, {\n\t\t\tcurrentThinkingLevel: options.getThinkingLevel?.(),\n\t\t});\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst auth = selection.auth;\n\t\tconst baseStreamFn = options.streamFn ?? streamSimple;\n\t\tconst streamFn: StreamFn = (model, context, streamOptions) => baseStreamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t\tenv: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,\n\t\t\theaders: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },\n\t\t});\n\t\tconst tools = [\n\t\t\t...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== \"watchdog_warn\"),\n\t\t\tcreateWatchdogWarnTool(request),\n\t\t];\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: buildWatchdogSystemPrompt(ctx),\n\t\t\t\tmodel: selection.model,\n\t\t\t\tthinkingLevel: selection.thinkingLevel,\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tstreamFn,\n\t\t\tgetApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,\n\t\t\tbeforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)\n\t\t\t\t? undefined\n\t\t\t\t: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },\n\t\t\ttoolExecution: \"sequential\",\n\t\t});\n\t\tconst abort = () => agent.abort();\n\t\tctx.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\trequest.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\ttry {\n\t\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\t\tawait agent.prompt(buildReviewPrompt(request, selection));\n\t\t} finally {\n\t\t\tctx.signal?.removeEventListener(\"abort\", abort);\n\t\t\trequest.signal?.removeEventListener(\"abort\", abort);\n\t\t}\n\t\treturn { stopReason: finalStopReason(agent) };\n\t};\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  // @ts-nocheck
2
2
  import { Agent } from "@ashx-j/lunr-agent";
3
- import { createReadOnlyTools, convertToLlm } from "@ashx-j/lunr-dev";
3
+ import { createReadOnlyTools } from "../../../../core/tools/index.js";
4
+ import { convertToLlm } from "../../../../core/messages.js";
4
5
  import { streamSimple } from "@ashx-j/lunr-ai/compat";
5
6
  import { Type } from "typebox";
6
7
  import { resolveModelCandidate } from "../runs/shared/model-fallback.js";
@@ -1 +1 @@
1
- {"version":3,"file":"review.js","sourceRoot":"","sources":["../../../../../src/builtin-extensions/pi-subagents/src/watchdog/review.ts"],"names":[],"mappings":"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","sourcesContent":["// @ts-nocheck\nimport { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport { createReadOnlyTools, convertToLlm, type ExtensionContext } from \"@ashx-j/lunr-dev\";\nimport { streamSimple, type Model } from \"@ashx-j/lunr-ai/compat\";\nimport { Type, type Static } from \"typebox\";\nimport { resolveModelCandidate } from \"../runs/shared/model-fallback.ts\";\nimport { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from \"../shared/model-info.ts\";\nimport type { WatchdogReviewFunction, WatchdogReviewRequest } from \"./runtime.ts\";\nimport {\n\tWATCHDOG_WARNING_CATEGORIES,\n\tWATCHDOG_WARNING_CONFIDENCES,\n\tWATCHDOG_WARNING_SEVERITIES,\n\ttype ResolvedWatchdogConfig,\n\ttype WatchdogCategory,\n\ttype WatchdogConfidence,\n\ttype WatchdogSeverity,\n\ttype WatchdogWarning,\n} from \"./types.ts\";\n\nconst WATCHDOG_ALLOWED_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\", \"ls\", \"watchdog_warn\"]);\n\nconst WatchdogWarnParams = Type.Object({\n\tseverity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: \"concern for actionable risk, blocker for a likely wrong or unsafe outcome\" }),\n\tsummary: Type.String({ description: \"One concise sentence naming the issue.\" }),\n\tevidence: Type.String({ description: \"Specific evidence from the turn delta or inspected files.\" }),\n\trecommendedAction: Type.String({ description: \"Specific action the parent should take before accepting or continuing.\" }),\n\tcategory: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),\n\tconfidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),\n}, { additionalProperties: false });\n\ntype WatchdogWarnParams = Static<typeof WatchdogWarnParams>;\n\ntype WatchdogContextProvider = ExtensionContext | (() => ExtensionContext | undefined);\n\ntype RegistryModel = Model<any>;\n\ninterface WatchdogReviewAuth {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}\n\nexport interface WatchdogReviewModelSelection {\n\tmodel: RegistryModel;\n\tthinkingLevel: ThinkingLevel;\n\tauth: WatchdogReviewAuth;\n\texplicit: boolean;\n}\n\nexport interface CreateMainWatchdogReviewOptions {\n\tstreamFn?: StreamFn;\n\tcreateReadOnlyTools?: (cwd: string) => AgentTool[];\n\tgetThinkingLevel?: () => ThinkingLevel | undefined;\n}\n\nfunction fullModelId(model: Pick<RegistryModel, \"provider\" | \"id\">): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction splitProviderModel(value: string): { provider: string; id: string } | undefined {\n\tconst slashIndex = value.indexOf(\"/\");\n\tif (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;\n\treturn { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };\n}\n\nfunction assertThinkingLevel(value: string, source: string): ThinkingLevel {\n\tif ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;\n\tthrow new Error(`Unsupported watchdog thinking level '${value}' from ${source}; expected ${THINKING_LEVELS.join(\", \")} or false.`);\n}\n\nfunction contextThinkingLevel(ctx: ExtensionContext, currentThinkingLevel: ThinkingLevel | undefined): ThinkingLevel | undefined {\n\tif (currentThinkingLevel) return currentThinkingLevel;\n\tconst value = (ctx as { thinkingLevel?: unknown }).thinkingLevel;\n\treturn typeof value === \"string\" && (THINKING_LEVELS as readonly string[]).includes(value) ? value as ThinkingLevel : undefined;\n}\n\nfunction resolveReviewThinking(input: {\n\tmodelString: string;\n\tconfigThinking: string | false | undefined;\n\tctx: ExtensionContext;\n\tallowContextThinking: boolean;\n\tcurrentThinkingLevel?: ThinkingLevel;\n}): ThinkingLevel {\n\tconst fromModelOrConfig = resolveEffectiveThinking(input.modelString, input.configThinking);\n\tif (fromModelOrConfig) return assertThinkingLevel(fromModelOrConfig, \"watchdog model/config\");\n\tif (input.configThinking === false) return \"off\";\n\tif (input.configThinking !== undefined) return assertThinkingLevel(input.configThinking, \"watchdog config\");\n\tif (input.allowContextThinking) return contextThinkingLevel(input.ctx, input.currentThinkingLevel) ?? \"off\";\n\treturn \"off\";\n}\n\nfunction resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { model: RegistryModel; modelString: string } {\n\tconst availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);\n\tconst preferredProvider = typeof ctx.model?.provider === \"string\" ? ctx.model.provider : undefined;\n\tconst resolved = resolveModelCandidate(rawModel, availableModels, preferredProvider);\n\tconst { baseModel } = splitKnownThinkingSuffix(resolved);\n\tconst named = splitProviderModel(baseModel);\n\tif (!named) {\n\t\tthrow new Error(`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`);\n\t}\n\n\tconst model = ctx.modelRegistry.find(named.provider, named.id);\n\tif (!model) throw new Error(`Configured watchdog model '${rawModel}' was not found as '${baseModel}'.`);\n\tif (!ctx.modelRegistry.hasConfiguredAuth(model)) {\n\t\tthrow new Error(`Configured watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`);\n\t}\n\treturn { model, modelString: resolved };\n}\n\nasync function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {\n\tconst auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);\n\tif (!auth.ok) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);\n\treturn {\n\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t...(auth.headers ? { headers: auth.headers } : {}),\n\t\t...(auth.env ? { env: auth.env } : {}),\n\t};\n}\n\nexport async function resolveWatchdogReviewModel(\n\tctx: ExtensionContext,\n\tconfig: ResolvedWatchdogConfig,\n\toptions: { currentThinkingLevel?: ThinkingLevel } = {},\n): Promise<WatchdogReviewModelSelection> {\n\tif (config.main.model) {\n\t\tconst resolved = resolveConfiguredModel(ctx, config.main.model);\n\t\treturn {\n\t\t\tmodel: resolved.model,\n\t\t\tthinkingLevel: resolveReviewThinking({\n\t\t\t\tmodelString: resolved.modelString,\n\t\t\t\tconfigThinking: config.main.thinking,\n\t\t\t\tctx,\n\t\t\t\tallowContextThinking: false,\n\t\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t\t}),\n\t\t\tauth: await resolveReviewAuth(ctx, resolved.model),\n\t\t\texplicit: true,\n\t\t};\n\t}\n\n\tconst currentModel = ctx.model;\n\tif (!currentModel) {\n\t\tthrow new Error(\"Main watchdog review cannot run because the current Pi session model is unavailable and subagents.watchdog.main.model is not configured.\");\n\t}\n\treturn {\n\t\tmodel: currentModel,\n\t\tthinkingLevel: resolveReviewThinking({\n\t\t\tmodelString: fullModelId(currentModel),\n\t\t\tconfigThinking: config.main.thinking,\n\t\t\tctx,\n\t\t\tallowContextThinking: true,\n\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t}),\n\t\tauth: await resolveReviewAuth(ctx, currentModel),\n\t\texplicit: false,\n\t};\n}\n\nfunction nonEmptyString(value: string, field: string): string {\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(`watchdog_warn.${field} must be a non-empty string.`);\n\treturn trimmed;\n}\n\nfunction toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {\n\treturn {\n\t\tseverity: params.severity as WatchdogSeverity,\n\t\tcategory: (params.category ?? \"other\") as WatchdogCategory,\n\t\tconfidence: (params.confidence ?? \"medium\") as WatchdogConfidence,\n\t\tsource: \"main\",\n\t\tsummary: nonEmptyString(params.summary, \"summary\"),\n\t\tevidence: nonEmptyString(params.evidence, \"evidence\"),\n\t\trecommendedAction: nonEmptyString(params.recommendedAction, \"recommendedAction\"),\n\t};\n}\n\nfunction createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeof WatchdogWarnParams, { accepted: boolean }> {\n\treturn {\n\t\tname: \"watchdog_warn\",\n\t\tlabel: \"Watchdog warning\",\n\t\tdescription: [\n\t\t\t\"Emit one actionable main-session watchdog warning.\",\n\t\t\t\"Use only for medium/high confidence concerns or blockers that the parent should consider before accepting the work.\",\n\t\t\t\"Do not use for nits, praise, informational notes, or clean reviews.\",\n\t\t].join(\" \"),\n\t\tparameters: WatchdogWarnParams,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst warning = toWatchdogWarning(params);\n\t\t\tconst accepted = request.emitWarning(warning);\n\t\t\treturn {\n\t\t\t\tcontent: [{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: accepted\n\t\t\t\t\t\t? \"Watchdog warning recorded.\"\n\t\t\t\t\t\t: \"Watchdog warning was ignored by the runtime guard because it was stale, duplicate, or over budget.\",\n\t\t\t\t}],\n\t\t\t\tdetails: { accepted },\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction buildWatchdogSystemPrompt(ctx: ExtensionContext): string {\n\treturn [\n\t\t\"You are the main-session subagent watchdog for Pi.\",\n\t\t`Working directory: ${ctx.cwd}`,\n\t\t\"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.\",\n\t\t\"You are read-only. You may use read, grep, find, and ls. Do not edit files, run shell commands, spawn agents, or mutate state.\",\n\t\t\"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.\",\n\t\t\"Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.\",\n\t\t\"Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.\",\n\t\t\"If the turn is clean, call no tools and end normally.\",\n\t\t\"Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.\",\n\t].join(\"\\n\");\n}\n\nfunction buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogReviewModelSelection): string {\n\treturn [\n\t\t\"Review this parent-session turn delta for subagent-watchdog-worthy issues.\",\n\t\t`Review id: ${request.reviewId}; epoch: ${request.epoch}; review model: ${fullModelId(selection.model)}; thinking: ${selection.thinkingLevel}.`,\n\t\t\"Call watchdog_warn for each qualifying concern or blocker. Call no tools when clean.\",\n\t\t\"<turn_delta>\",\n\t\trequest.delta,\n\t\t\"</turn_delta>\",\n\t].join(\"\\n\\n\");\n}\n\nfunction finalStopReason(agent: Agent): \"stop\" | \"error\" | \"aborted\" | \"length\" {\n\tfor (let index = agent.state.messages.length - 1; index >= 0; index--) {\n\t\tconst message = agent.state.messages[index];\n\t\tif (message && typeof message === \"object\" && (message as { role?: unknown }).role === \"assistant\") {\n\t\t\tconst stopReason = (message as { stopReason?: unknown }).stopReason;\n\t\t\tif (stopReason === \"error\" || stopReason === \"aborted\" || stopReason === \"length\") return stopReason;\n\t\t\treturn \"stop\";\n\t\t}\n\t}\n\treturn \"stop\";\n}\n\nfunction resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {\n\treturn typeof provider === \"function\" ? provider() : provider;\n}\n\nexport function createMainWatchdogReview(provider: WatchdogContextProvider, options: CreateMainWatchdogReviewOptions = {}): WatchdogReviewFunction {\n\treturn async (request) => {\n\t\tconst ctx = resolveContext(provider);\n\t\tif (!ctx) throw new Error(\"Main watchdog review cannot run without an active Pi extension context.\");\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst selection = await resolveWatchdogReviewModel(ctx, request.config, {\n\t\t\tcurrentThinkingLevel: options.getThinkingLevel?.(),\n\t\t});\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst auth = selection.auth;\n\t\tconst baseStreamFn = options.streamFn ?? streamSimple;\n\t\tconst streamFn: StreamFn = (model, context, streamOptions) => baseStreamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t\tenv: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,\n\t\t\theaders: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },\n\t\t});\n\t\tconst tools = [\n\t\t\t...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== \"watchdog_warn\"),\n\t\t\tcreateWatchdogWarnTool(request),\n\t\t];\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: buildWatchdogSystemPrompt(ctx),\n\t\t\t\tmodel: selection.model,\n\t\t\t\tthinkingLevel: selection.thinkingLevel,\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tstreamFn,\n\t\t\tgetApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,\n\t\t\tbeforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)\n\t\t\t\t? undefined\n\t\t\t\t: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },\n\t\t\ttoolExecution: \"sequential\",\n\t\t});\n\t\tconst abort = () => agent.abort();\n\t\tctx.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\trequest.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\ttry {\n\t\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\t\tawait agent.prompt(buildReviewPrompt(request, selection));\n\t\t} finally {\n\t\t\tctx.signal?.removeEventListener(\"abort\", abort);\n\t\t\trequest.signal?.removeEventListener(\"abort\", abort);\n\t\t}\n\t\treturn { stopReason: finalStopReason(agent) };\n\t};\n}\n"]}
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../../../../src/builtin-extensions/pi-subagents/src/watchdog/review.ts"],"names":[],"mappings":"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","sourcesContent":["// @ts-nocheck\nimport { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport type { ExtensionContext } from \"@ashx-j/lunr-dev\";\nimport { createReadOnlyTools } from \"../../../../core/tools/index.ts\";\nimport { convertToLlm } from \"../../../../core/messages.ts\";\nimport { streamSimple, type Model } from \"@ashx-j/lunr-ai/compat\";\nimport { Type, type Static } from \"typebox\";\nimport { resolveModelCandidate } from \"../runs/shared/model-fallback.ts\";\nimport { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from \"../shared/model-info.ts\";\nimport type { WatchdogReviewFunction, WatchdogReviewRequest } from \"./runtime.ts\";\nimport {\n\tWATCHDOG_WARNING_CATEGORIES,\n\tWATCHDOG_WARNING_CONFIDENCES,\n\tWATCHDOG_WARNING_SEVERITIES,\n\ttype ResolvedWatchdogConfig,\n\ttype WatchdogCategory,\n\ttype WatchdogConfidence,\n\ttype WatchdogSeverity,\n\ttype WatchdogWarning,\n} from \"./types.ts\";\n\nconst WATCHDOG_ALLOWED_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\", \"ls\", \"watchdog_warn\"]);\n\nconst WatchdogWarnParams = Type.Object({\n\tseverity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: \"concern for actionable risk, blocker for a likely wrong or unsafe outcome\" }),\n\tsummary: Type.String({ description: \"One concise sentence naming the issue.\" }),\n\tevidence: Type.String({ description: \"Specific evidence from the turn delta or inspected files.\" }),\n\trecommendedAction: Type.String({ description: \"Specific action the parent should take before accepting or continuing.\" }),\n\tcategory: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),\n\tconfidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),\n}, { additionalProperties: false });\n\ntype WatchdogWarnParams = Static<typeof WatchdogWarnParams>;\n\ntype WatchdogContextProvider = ExtensionContext | (() => ExtensionContext | undefined);\n\ntype RegistryModel = Model<any>;\n\ninterface WatchdogReviewAuth {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}\n\nexport interface WatchdogReviewModelSelection {\n\tmodel: RegistryModel;\n\tthinkingLevel: ThinkingLevel;\n\tauth: WatchdogReviewAuth;\n\texplicit: boolean;\n}\n\nexport interface CreateMainWatchdogReviewOptions {\n\tstreamFn?: StreamFn;\n\tcreateReadOnlyTools?: (cwd: string) => AgentTool[];\n\tgetThinkingLevel?: () => ThinkingLevel | undefined;\n}\n\nfunction fullModelId(model: Pick<RegistryModel, \"provider\" | \"id\">): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction splitProviderModel(value: string): { provider: string; id: string } | undefined {\n\tconst slashIndex = value.indexOf(\"/\");\n\tif (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;\n\treturn { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };\n}\n\nfunction assertThinkingLevel(value: string, source: string): ThinkingLevel {\n\tif ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;\n\tthrow new Error(`Unsupported watchdog thinking level '${value}' from ${source}; expected ${THINKING_LEVELS.join(\", \")} or false.`);\n}\n\nfunction contextThinkingLevel(ctx: ExtensionContext, currentThinkingLevel: ThinkingLevel | undefined): ThinkingLevel | undefined {\n\tif (currentThinkingLevel) return currentThinkingLevel;\n\tconst value = (ctx as { thinkingLevel?: unknown }).thinkingLevel;\n\treturn typeof value === \"string\" && (THINKING_LEVELS as readonly string[]).includes(value) ? value as ThinkingLevel : undefined;\n}\n\nfunction resolveReviewThinking(input: {\n\tmodelString: string;\n\tconfigThinking: string | false | undefined;\n\tctx: ExtensionContext;\n\tallowContextThinking: boolean;\n\tcurrentThinkingLevel?: ThinkingLevel;\n}): ThinkingLevel {\n\tconst fromModelOrConfig = resolveEffectiveThinking(input.modelString, input.configThinking);\n\tif (fromModelOrConfig) return assertThinkingLevel(fromModelOrConfig, \"watchdog model/config\");\n\tif (input.configThinking === false) return \"off\";\n\tif (input.configThinking !== undefined) return assertThinkingLevel(input.configThinking, \"watchdog config\");\n\tif (input.allowContextThinking) return contextThinkingLevel(input.ctx, input.currentThinkingLevel) ?? \"off\";\n\treturn \"off\";\n}\n\nfunction resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { model: RegistryModel; modelString: string } {\n\tconst availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);\n\tconst preferredProvider = typeof ctx.model?.provider === \"string\" ? ctx.model.provider : undefined;\n\tconst resolved = resolveModelCandidate(rawModel, availableModels, preferredProvider);\n\tconst { baseModel } = splitKnownThinkingSuffix(resolved);\n\tconst named = splitProviderModel(baseModel);\n\tif (!named) {\n\t\tthrow new Error(`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`);\n\t}\n\n\tconst model = ctx.modelRegistry.find(named.provider, named.id);\n\tif (!model) throw new Error(`Configured watchdog model '${rawModel}' was not found as '${baseModel}'.`);\n\tif (!ctx.modelRegistry.hasConfiguredAuth(model)) {\n\t\tthrow new Error(`Configured watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`);\n\t}\n\treturn { model, modelString: resolved };\n}\n\nasync function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {\n\tconst auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);\n\tif (!auth.ok) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);\n\treturn {\n\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t...(auth.headers ? { headers: auth.headers } : {}),\n\t\t...(auth.env ? { env: auth.env } : {}),\n\t};\n}\n\nexport async function resolveWatchdogReviewModel(\n\tctx: ExtensionContext,\n\tconfig: ResolvedWatchdogConfig,\n\toptions: { currentThinkingLevel?: ThinkingLevel } = {},\n): Promise<WatchdogReviewModelSelection> {\n\tif (config.main.model) {\n\t\tconst resolved = resolveConfiguredModel(ctx, config.main.model);\n\t\treturn {\n\t\t\tmodel: resolved.model,\n\t\t\tthinkingLevel: resolveReviewThinking({\n\t\t\t\tmodelString: resolved.modelString,\n\t\t\t\tconfigThinking: config.main.thinking,\n\t\t\t\tctx,\n\t\t\t\tallowContextThinking: false,\n\t\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t\t}),\n\t\t\tauth: await resolveReviewAuth(ctx, resolved.model),\n\t\t\texplicit: true,\n\t\t};\n\t}\n\n\tconst currentModel = ctx.model;\n\tif (!currentModel) {\n\t\tthrow new Error(\"Main watchdog review cannot run because the current Pi session model is unavailable and subagents.watchdog.main.model is not configured.\");\n\t}\n\treturn {\n\t\tmodel: currentModel,\n\t\tthinkingLevel: resolveReviewThinking({\n\t\t\tmodelString: fullModelId(currentModel),\n\t\t\tconfigThinking: config.main.thinking,\n\t\t\tctx,\n\t\t\tallowContextThinking: true,\n\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t}),\n\t\tauth: await resolveReviewAuth(ctx, currentModel),\n\t\texplicit: false,\n\t};\n}\n\nfunction nonEmptyString(value: string, field: string): string {\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(`watchdog_warn.${field} must be a non-empty string.`);\n\treturn trimmed;\n}\n\nfunction toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {\n\treturn {\n\t\tseverity: params.severity as WatchdogSeverity,\n\t\tcategory: (params.category ?? \"other\") as WatchdogCategory,\n\t\tconfidence: (params.confidence ?? \"medium\") as WatchdogConfidence,\n\t\tsource: \"main\",\n\t\tsummary: nonEmptyString(params.summary, \"summary\"),\n\t\tevidence: nonEmptyString(params.evidence, \"evidence\"),\n\t\trecommendedAction: nonEmptyString(params.recommendedAction, \"recommendedAction\"),\n\t};\n}\n\nfunction createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeof WatchdogWarnParams, { accepted: boolean }> {\n\treturn {\n\t\tname: \"watchdog_warn\",\n\t\tlabel: \"Watchdog warning\",\n\t\tdescription: [\n\t\t\t\"Emit one actionable main-session watchdog warning.\",\n\t\t\t\"Use only for medium/high confidence concerns or blockers that the parent should consider before accepting the work.\",\n\t\t\t\"Do not use for nits, praise, informational notes, or clean reviews.\",\n\t\t].join(\" \"),\n\t\tparameters: WatchdogWarnParams,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst warning = toWatchdogWarning(params);\n\t\t\tconst accepted = request.emitWarning(warning);\n\t\t\treturn {\n\t\t\t\tcontent: [{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: accepted\n\t\t\t\t\t\t? \"Watchdog warning recorded.\"\n\t\t\t\t\t\t: \"Watchdog warning was ignored by the runtime guard because it was stale, duplicate, or over budget.\",\n\t\t\t\t}],\n\t\t\t\tdetails: { accepted },\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction buildWatchdogSystemPrompt(ctx: ExtensionContext): string {\n\treturn [\n\t\t\"You are the main-session subagent watchdog for Pi.\",\n\t\t`Working directory: ${ctx.cwd}`,\n\t\t\"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.\",\n\t\t\"You are read-only. You may use read, grep, find, and ls. Do not edit files, run shell commands, spawn agents, or mutate state.\",\n\t\t\"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.\",\n\t\t\"Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.\",\n\t\t\"Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.\",\n\t\t\"If the turn is clean, call no tools and end normally.\",\n\t\t\"Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.\",\n\t].join(\"\\n\");\n}\n\nfunction buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogReviewModelSelection): string {\n\treturn [\n\t\t\"Review this parent-session turn delta for subagent-watchdog-worthy issues.\",\n\t\t`Review id: ${request.reviewId}; epoch: ${request.epoch}; review model: ${fullModelId(selection.model)}; thinking: ${selection.thinkingLevel}.`,\n\t\t\"Call watchdog_warn for each qualifying concern or blocker. Call no tools when clean.\",\n\t\t\"<turn_delta>\",\n\t\trequest.delta,\n\t\t\"</turn_delta>\",\n\t].join(\"\\n\\n\");\n}\n\nfunction finalStopReason(agent: Agent): \"stop\" | \"error\" | \"aborted\" | \"length\" {\n\tfor (let index = agent.state.messages.length - 1; index >= 0; index--) {\n\t\tconst message = agent.state.messages[index];\n\t\tif (message && typeof message === \"object\" && (message as { role?: unknown }).role === \"assistant\") {\n\t\t\tconst stopReason = (message as { stopReason?: unknown }).stopReason;\n\t\t\tif (stopReason === \"error\" || stopReason === \"aborted\" || stopReason === \"length\") return stopReason;\n\t\t\treturn \"stop\";\n\t\t}\n\t}\n\treturn \"stop\";\n}\n\nfunction resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {\n\treturn typeof provider === \"function\" ? provider() : provider;\n}\n\nexport function createMainWatchdogReview(provider: WatchdogContextProvider, options: CreateMainWatchdogReviewOptions = {}): WatchdogReviewFunction {\n\treturn async (request) => {\n\t\tconst ctx = resolveContext(provider);\n\t\tif (!ctx) throw new Error(\"Main watchdog review cannot run without an active Pi extension context.\");\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst selection = await resolveWatchdogReviewModel(ctx, request.config, {\n\t\t\tcurrentThinkingLevel: options.getThinkingLevel?.(),\n\t\t});\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst auth = selection.auth;\n\t\tconst baseStreamFn = options.streamFn ?? streamSimple;\n\t\tconst streamFn: StreamFn = (model, context, streamOptions) => baseStreamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t\tenv: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,\n\t\t\theaders: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },\n\t\t});\n\t\tconst tools = [\n\t\t\t...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== \"watchdog_warn\"),\n\t\t\tcreateWatchdogWarnTool(request),\n\t\t];\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: buildWatchdogSystemPrompt(ctx),\n\t\t\t\tmodel: selection.model,\n\t\t\t\tthinkingLevel: selection.thinkingLevel,\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tstreamFn,\n\t\t\tgetApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,\n\t\t\tbeforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)\n\t\t\t\t? undefined\n\t\t\t\t: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },\n\t\t\ttoolExecution: \"sequential\",\n\t\t});\n\t\tconst abort = () => agent.abort();\n\t\tctx.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\trequest.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\ttry {\n\t\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\t\tawait agent.prompt(buildReviewPrompt(request, selection));\n\t\t} finally {\n\t\t\tctx.signal?.removeEventListener(\"abort\", abort);\n\t\t\trequest.signal?.removeEventListener(\"abort\", abort);\n\t\t}\n\t\treturn { stopReason: finalStopReason(agent) };\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../src/builtin-extensions/pi-web-access/extract.ts"],"names":[],"mappings":"AAcA,OAAO,EAAqC,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAUtF;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAuB9C;AAyBD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3D,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AA6ID,wBAAsB,cAAc,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,gBAAgB,CAAC,CA4P3B;AAsLD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D;AAMD,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B","sourcesContent":["// @ts-nocheck\nimport { Readability } from \"@mozilla/readability\";\nimport { parseHTML } from \"linkedom\";\nimport TurndownService from \"turndown\";\nimport pLimit from \"p-limit\";\nimport { activityMonitor } from \"./activity.ts\";\nimport { extractRSCContent } from \"./rsc-extract.ts\";\nimport { extractPDFToMarkdown, isPDF } from \"./pdf-extract.ts\";\nimport { extractGitHub } from \"./github-extract.ts\";\nimport { isYouTubeURL, isYouTubeEnabled, extractYouTube, extractYouTubeFrame, extractYouTubeFrames, getYouTubeStreamInfo } from \"./youtube-extract.ts\";\nimport { extractWithUrlContext, extractWithGeminiWeb } from \"./gemini-url-context.ts\";\nimport { extractWithParallel, isParallelAvailable } from \"./parallel.ts\";\nimport { isVideoFile, extractVideo, extractVideoFrame, getLocalVideoDuration } from \"./video-extract.ts\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { fetchRemoteUrl, validateRemoteUrl, type Lookup } from \"./ssrf-protection.ts\";\nimport { formatSeconds, getWebSearchConfigPath } from \"./utils.ts\";\n\nconst DEFAULT_TIMEOUT_MS = 30000;\nconst CONCURRENT_LIMIT = 3;\n\nconst NON_RECOVERABLE_ERRORS = [\"Unsupported content type\", \"Response too large\"];\nconst MIN_USEFUL_CONTENT = 500;\nconst WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();\n\n/**\n * Read `ssrf.allowRanges` (CIDR strings) from web-search.json. Returns [] when\n * the file is missing, unreadable, or the key is unset so SSRF protection stays\n * fully on by default. Throws when `ssrf.allowRanges` is present but not an array\n * so a mistyped value (e.g. a bare string instead of a JSON array) fails loudly\n * instead of being silently ignored. Exempts synthetic ranges used by TUN/fake-IP\n * proxies (e.g. 198.18.0.0/15).\n */\nexport function loadSsrfAllowRanges(): string[] {\n\tlet value: unknown;\n\ttry {\n\t\tif (!existsSync(WEB_SEARCH_CONFIG_PATH)) return [];\n\t\tconst raw = readFileSync(WEB_SEARCH_CONFIG_PATH, \"utf-8\");\n\t\tvalue = (JSON.parse(raw) as { ssrf?: { allowRanges?: unknown } })?.ssrf?.allowRanges;\n\t} catch {\n\t\t// Missing/unreadable file or invalid JSON: fail safe with SSRF fully on.\n\t\treturn [];\n\t}\n\tif (value === undefined || value === null) return [];\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(`ssrf.allowRanges in ${WEB_SEARCH_CONFIG_PATH} must be an array of CIDR strings`);\n\t}\n\tconst ranges: string[] = [];\n\tfor (const [index, entry] of value.entries()) {\n\t\tif (typeof entry !== \"string\") {\n\t\t\tthrow new Error(`ssrf.allowRanges in ${WEB_SEARCH_CONFIG_PATH} must contain only CIDR strings; entry ${index + 1} is ${typeof entry}`);\n\t\t}\n\t\tconst trimmed = entry.trim();\n\t\tif (trimmed) ranges.push(trimmed);\n\t}\n\treturn ranges;\n}\n\nfunction errorMessage(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nfunction isConfigParseError(err: unknown): boolean {\n\treturn errorMessage(err).startsWith(\"Failed to parse \");\n}\n\nfunction isAbortError(err: unknown): boolean {\n\treturn errorMessage(err).toLowerCase().includes(\"abort\");\n}\n\nfunction abortedResult(url: string): ExtractedContent {\n\treturn { url, title: \"\", content: \"\", error: \"Aborted\" };\n}\n\nconst turndown = new TurndownService({\n\theadingStyle: \"atx\",\n\tcodeBlockStyle: \"fenced\",\n});\n\nconst fetchLimit = pLimit(CONCURRENT_LIMIT);\n\nexport interface VideoFrame {\n\tdata: string;\n\tmimeType: string;\n\ttimestamp: string;\n}\n\nexport type FrameData = { data: string; mimeType: string };\nexport type FrameResult = FrameData | { error: string };\n\nexport interface ExtractedContent {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\terror: string | null;\n\tthumbnail?: { data: string; mimeType: string };\n\tframes?: VideoFrame[];\n\tduration?: number;\n}\n\nexport interface ExtractOptions {\n\ttimeoutMs?: number;\n\tforceClone?: boolean;\n\tprompt?: string;\n\ttimestamp?: string;\n\tframes?: number;\n\tmodel?: string;\n\t/** Custom DNS resolver used for SSRF validation. Primarily a test seam. */\n\tlookup?: Lookup;\n}\n\nconst JINA_READER_BASE = \"https://r.jina.ai/\";\nconst JINA_TIMEOUT_MS = 30000;\n\nasync function extractWithJinaReader(\n\turl: string,\n\tsignal?: AbortSignal,\n\tlookup?: Lookup,\n): Promise<ExtractedContent | null> {\n\tconst jinaUrl = JINA_READER_BASE + url;\n\n\tconst activityId = activityMonitor.logStart({ type: \"api\", query: `jina: ${url}` });\n\n\ttry {\n\t\tawait validateRemoteUrl(url, { allowRanges: loadSsrfAllowRanges(), lookup });\n\t\tconst res = await fetch(jinaUrl, {\n\t\t\theaders: {\n\t\t\t\t\"Accept\": \"text/markdown\",\n\t\t\t\t\"X-No-Cache\": \"true\",\n\t\t\t},\n\t\t\tsignal: AbortSignal.any([\n\t\t\t\tAbortSignal.timeout(JINA_TIMEOUT_MS),\n\t\t\t\t...(signal ? [signal] : []),\n\t\t\t]),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tactivityMonitor.logComplete(activityId, res.status);\n\t\t\treturn null;\n\t\t}\n\n\t\tconst content = await res.text();\n\t\tactivityMonitor.logComplete(activityId, res.status);\n\n\t\tconst contentStart = content.indexOf(\"Markdown Content:\");\n\t\tif (contentStart < 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst markdownPart = content.slice(contentStart + 17).trim(); // 17 = \"Markdown Content:\".length\n\n\t\t// Check for failed JS rendering or minimal content\n\t\tif (markdownPart.length < 100 ||\n\t\t\tmarkdownPart.startsWith(\"Loading...\") ||\n\t\t\tmarkdownPart.startsWith(\"Please enable JavaScript\")) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst title = extractHeadingTitle(markdownPart) ?? (new URL(url).pathname.split(\"/\").pop() || url);\n\t\treturn { url, title, content: markdownPart, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (message.toLowerCase().includes(\"abort\")) {\n\t\t\tactivityMonitor.logComplete(activityId, 0);\n\t\t} else {\n\t\t\tactivityMonitor.logError(activityId, message);\n\t\t}\n\t\treturn null;\n\t}\n}\n\nfunction parseTimestamp(ts: string): number | null {\n\tconst num = Number(ts);\n\tif (!isNaN(num) && num >= 0) return Math.floor(num);\n\tconst parts = ts.split(\":\").map(Number);\n\tif (parts.some(p => isNaN(p) || p < 0)) return null;\n\tif (parts.length === 3) return Math.floor(parts[0] * 3600 + parts[1] * 60 + parts[2]);\n\tif (parts.length === 2) return Math.floor(parts[0] * 60 + parts[1]);\n\treturn null;\n}\n\ntype TimestampSpec = { type: \"single\"; seconds: number } | { type: \"range\"; start: number; end: number };\n\nfunction parseTimestampSpec(ts: string): TimestampSpec | null {\n\tconst dashIdx = ts.indexOf(\"-\", 1);\n\tif (dashIdx > 0) {\n\t\tconst start = parseTimestamp(ts.slice(0, dashIdx));\n\t\tconst end = parseTimestamp(ts.slice(dashIdx + 1));\n\t\tif (start !== null && end !== null && end > start) return { type: \"range\", start, end };\n\t}\n\tconst seconds = parseTimestamp(ts);\n\treturn seconds !== null ? { type: \"single\", seconds } : null;\n}\n\nconst DEFAULT_RANGE_FRAMES = 6;\nconst MIN_FRAME_INTERVAL = 5;\n\nfunction computeRangeTimestamps(start: number, end: number, maxFrames: number = DEFAULT_RANGE_FRAMES): number[] {\n\tif (maxFrames <= 1) return [start];\n\tconst duration = end - start;\n\tconst idealInterval = duration / (maxFrames - 1);\n\tif (idealInterval < MIN_FRAME_INTERVAL) {\n\t\tconst timestamps: number[] = [];\n\t\tfor (let t = start; t <= end && timestamps.length < maxFrames; t += MIN_FRAME_INTERVAL) {\n\t\t\ttimestamps.push(t);\n\t\t}\n\t\treturn timestamps;\n\t}\n\treturn Array.from({ length: maxFrames }, (_, i) => Math.round(start + i * idealInterval));\n}\n\nfunction buildFrameResult(\n\turl: string, label: string, requestedCount: number,\n\tframes: VideoFrame[], error: string | null, duration?: number,\n): ExtractedContent {\n\tif (frames.length === 0) {\n\t\tconst msg = error ?? \"Frame extraction failed\";\n\t\treturn { url, title: `Frames ${label} (0/${requestedCount})`, content: msg, error: msg };\n\t}\n\treturn {\n\t\turl,\n\t\ttitle: `Frames ${label} (${frames.length}/${requestedCount})`,\n\t\tcontent: `${frames.length} frames extracted from ${label}`,\n\t\terror: null,\n\t\tframes,\n\t\tduration,\n\t};\n}\n\nasync function extractLocalFrames(\n\tfilePath: string, timestamps: number[],\n): Promise<{ frames: VideoFrame[]; error: string | null }> {\n\tconst results = await Promise.all(timestamps.map(async (t) => {\n\t\tconst frame = await extractVideoFrame(filePath, t);\n\t\tif (\"error\" in frame) return { error: frame.error };\n\t\treturn { ...frame, timestamp: formatSeconds(t) };\n\t}));\n\tconst frames = results.filter((f): f is VideoFrame => \"data\" in f);\n\tconst firstError = results.find((f): f is { error: string } => \"error\" in f);\n\treturn { frames, error: frames.length === 0 && firstError ? firstError.error : null };\n}\n\nfunction safeVideoInfo(url: string): { info: ReturnType<typeof isVideoFile>; error?: string } {\n\ttry {\n\t\treturn { info: isVideoFile(url) };\n\t} catch (err) {\n\t\treturn { info: null, error: errorMessage(err) };\n\t}\n}\n\nexport async function extractContent(\n\turl: string,\n\tsignal?: AbortSignal,\n\toptions?: ExtractOptions,\n): Promise<ExtractedContent> {\n\tif (signal?.aborted) {\n\t\treturn { url, title: \"\", content: \"\", error: \"Aborted\" };\n\t}\n\n\tif (options?.frames && !options.timestamp) {\n\t\tconst frameCount = options.frames;\n\t\tconst ytInfo = isYouTubeURL(url);\n\t\tif (ytInfo.isYouTube && ytInfo.videoId) {\n\t\t\tconst streamInfo = await getYouTubeStreamInfo(ytInfo.videoId);\n\t\t\tif (\"error\" in streamInfo) {\n\t\t\t\treturn { url, title: \"Frames\", content: streamInfo.error, error: streamInfo.error };\n\t\t\t}\n\t\t\tif (streamInfo.duration === null) {\n\t\t\t\tconst error = \"Cannot determine video duration. Use a timestamp range instead.\";\n\t\t\t\treturn { url, title: \"Frames\", content: error, error };\n\t\t\t}\n\t\t\tconst dur = Math.floor(streamInfo.duration);\n\t\t\tconst timestamps = computeRangeTimestamps(0, dur, frameCount);\n\t\t\tconst result = await extractYouTubeFrames(ytInfo.videoId, timestamps, streamInfo);\n\t\t\tconst label = `${formatSeconds(0)}-${formatSeconds(dur)}`;\n\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, streamInfo.duration);\n\t\t}\n\n\t\tconst localVideo = safeVideoInfo(url);\n\t\tif (localVideo.error) {\n\t\t\treturn { url, title: \"\", content: \"\", error: localVideo.error };\n\t\t}\n\t\tif (localVideo.info) {\n\t\t\tconst durationResult = await getLocalVideoDuration(localVideo.info.absolutePath);\n\t\t\tif (typeof durationResult !== \"number\") {\n\t\t\t\treturn { url, title: \"Frames\", content: durationResult.error, error: durationResult.error };\n\t\t\t}\n\t\t\tconst dur = Math.floor(durationResult);\n\t\t\tconst timestamps = computeRangeTimestamps(0, dur, frameCount);\n\t\t\tconst result = await extractLocalFrames(localVideo.info.absolutePath, timestamps);\n\t\t\tconst label = `${formatSeconds(0)}-${formatSeconds(dur)}`;\n\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, durationResult);\n\t\t}\n\n\t\treturn { url, title: \"\", content: \"\", error: \"Frame extraction only works with YouTube and local video files\" };\n\t}\n\n\tif (options?.timestamp) {\n\t\tconst spec = parseTimestampSpec(options.timestamp);\n\t\tif (!spec) {\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: `Invalid timestamp format: \"${options.timestamp}\". Use \"H:MM:SS\", \"MM:SS\", \"85\", or \"start-end\".`,\n\t\t\t};\n\t\t}\n\n\t\tconst frameCount = options.frames;\n\t\tconst ytInfo = isYouTubeURL(url);\n\t\tif (ytInfo.isYouTube && ytInfo.videoId) {\n\t\t\tconst streamInfo = await getYouTubeStreamInfo(ytInfo.videoId);\n\t\t\tif (\"error\" in streamInfo) {\n\t\t\t\tif (spec.type === \"range\") {\n\t\t\t\t\tconst label = `${formatSeconds(spec.start)}-${formatSeconds(spec.end)}`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: streamInfo.error, error: streamInfo.error };\n\t\t\t\t}\n\t\t\t\tif (frameCount) {\n\t\t\t\t\tconst end = spec.seconds + (frameCount - 1) * MIN_FRAME_INTERVAL;\n\t\t\t\t\tconst label = `${formatSeconds(spec.seconds)}-${formatSeconds(end)}`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: streamInfo.error, error: streamInfo.error };\n\t\t\t\t}\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: streamInfo.error, error: streamInfo.error };\n\t\t\t}\n\n\t\t\tif (spec.type === \"range\") {\n\t\t\t\tconst label = `${formatSeconds(spec.start)}-${formatSeconds(spec.end)}`;\n\t\t\t\tif (streamInfo.duration !== null && spec.end > streamInfo.duration) {\n\t\t\t\t\tconst error = `Timestamp ${formatSeconds(spec.end)} exceeds video duration (${formatSeconds(Math.floor(streamInfo.duration))})`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: error, error };\n\t\t\t\t}\n\t\t\t\tconst timestamps = frameCount\n\t\t\t\t\t? computeRangeTimestamps(spec.start, spec.end, frameCount)\n\t\t\t\t\t: computeRangeTimestamps(spec.start, spec.end);\n\t\t\t\tconst result = await extractYouTubeFrames(ytInfo.videoId, timestamps, streamInfo);\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, result.duration ?? undefined);\n\t\t\t}\n\n\t\t\tif (frameCount) {\n\t\t\t\tconst end = spec.seconds + (frameCount - 1) * MIN_FRAME_INTERVAL;\n\t\t\t\tconst label = `${formatSeconds(spec.seconds)}-${formatSeconds(end)}`;\n\t\t\t\tif (streamInfo.duration !== null && end > streamInfo.duration) {\n\t\t\t\t\tconst error = `Timestamp ${formatSeconds(end)} exceeds video duration (${formatSeconds(Math.floor(streamInfo.duration))})`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: error, error };\n\t\t\t\t}\n\t\t\t\tconst timestamps = computeRangeTimestamps(spec.seconds, end, frameCount);\n\t\t\t\tconst result = await extractYouTubeFrames(ytInfo.videoId, timestamps, streamInfo);\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, result.duration ?? undefined);\n\t\t\t}\n\n\t\t\tif (streamInfo.duration !== null && spec.seconds > streamInfo.duration) {\n\t\t\t\tconst error = `Timestamp ${formatSeconds(spec.seconds)} exceeds video duration (${formatSeconds(Math.floor(streamInfo.duration))})`;\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: error, error };\n\t\t\t}\n\t\t\tconst frame = await extractYouTubeFrame(ytInfo.videoId, spec.seconds, streamInfo);\n\t\t\tif (\"error\" in frame) {\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: frame.error, error: frame.error };\n\t\t\t}\n\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: `Video frame at ${options.timestamp}`, error: null, thumbnail: frame };\n\t\t}\n\n\t\tconst localVideo = safeVideoInfo(url);\n\t\tif (localVideo.error) {\n\t\t\treturn { url, title: \"\", content: \"\", error: localVideo.error };\n\t\t}\n\t\tif (localVideo.info) {\n\t\t\tif (spec.type === \"range\") {\n\t\t\t\tconst timestamps = frameCount\n\t\t\t\t\t? computeRangeTimestamps(spec.start, spec.end, frameCount)\n\t\t\t\t\t: computeRangeTimestamps(spec.start, spec.end);\n\t\t\t\tconst result = await extractLocalFrames(localVideo.info.absolutePath, timestamps);\n\t\t\t\tconst label = `${formatSeconds(spec.start)}-${formatSeconds(spec.end)}`;\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error);\n\t\t\t}\n\n\t\t\tif (frameCount) {\n\t\t\t\tconst end = spec.seconds + (frameCount - 1) * MIN_FRAME_INTERVAL;\n\t\t\t\tconst timestamps = computeRangeTimestamps(spec.seconds, end, frameCount);\n\t\t\t\tconst result = await extractLocalFrames(localVideo.info.absolutePath, timestamps);\n\t\t\t\tconst label = `${formatSeconds(spec.seconds)}-${formatSeconds(end)}`;\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error);\n\t\t\t}\n\n\t\t\tconst frame = await extractVideoFrame(localVideo.info.absolutePath, spec.seconds);\n\t\t\tif (\"error\" in frame) {\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: frame.error, error: frame.error };\n\t\t\t}\n\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: `Video frame at ${options.timestamp}`, error: null, thumbnail: frame };\n\t\t}\n\n\t\treturn { url, title: \"\", content: \"\", error: \"Timestamp extraction only works with YouTube and local video files\" };\n\t}\n\n\tconst localVideo = safeVideoInfo(url);\n\tif (localVideo.error) {\n\t\treturn { url, title: \"\", content: \"\", error: localVideo.error };\n\t}\n\tif (localVideo.info) {\n\t\ttry {\n\t\t\tconst result = await extractVideo(localVideo.info, signal, options);\n\t\t\tif (signal?.aborted) return abortedResult(url);\n\t\t\treturn result ?? { url, title: \"\", content: \"\", error: `Video analysis requires Gemini access. Either:\\n 1. Sign into gemini.google.com in Chrome (free, uses cookies)\\n 2. Set GEMINI_API_KEY in ${WEB_SEARCH_CONFIG_PATH}` };\n\t\t} catch (err) {\n\t\t\tif (isAbortError(err)) return abortedResult(url);\n\t\t\treturn { url, title: \"\", content: \"\", error: errorMessage(err) };\n\t\t}\n\t}\n\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") {\n\t\t\tawait validateRemoteUrl(parsed, { allowRanges: loadSsrfAllowRanges(), lookup: options?.lookup });\n\t\t}\n\t} catch (err) {\n\t\treturn { url, title: \"\", content: \"\", error: errorMessage(err) };\n\t}\n\n\ttry {\n\t\tconst ghResult = await extractGitHub(url, signal, options?.forceClone);\n\t\tif (ghResult) return ghResult;\n\t\tif (signal?.aborted) return abortedResult(url);\n\t} catch (err) {\n\t\tconst message = errorMessage(err);\n\t\tif (isAbortError(err)) return abortedResult(url);\n\t\tif (isConfigParseError(err)) {\n\t\t\treturn { url, title: \"\", content: \"\", error: message };\n\t\t}\n\t}\n\n\tconst ytInfo = isYouTubeURL(url);\n\tlet youtubeEnabled = false;\n\ttry {\n\t\tyoutubeEnabled = isYouTubeEnabled();\n\t} catch (err) {\n\t\treturn { url, title: \"\", content: \"\", error: errorMessage(err) };\n\t}\n\tif (ytInfo.isYouTube && youtubeEnabled) {\n\t\ttry {\n\t\t\tconst ytResult = await extractYouTube(url, signal, options?.prompt, options?.model);\n\t\t\tif (ytResult) return ytResult;\n\t\t\tif (signal?.aborted) return abortedResult(url);\n\t\t} catch (err) {\n\t\t\tconst message = errorMessage(err);\n\t\t\tif (isAbortError(err)) return abortedResult(url);\n\t\t\treturn { url, title: \"\", content: \"\", error: message };\n\t\t}\n\t\treturn {\n\t\t\turl,\n\t\t\ttitle: \"\",\n\t\t\tcontent: \"\",\n\t\t\terror: \"Could not extract YouTube video content. Sign into Google in Chrome for automatic access, or set GEMINI_API_KEY.\",\n\t\t};\n\t}\n\n\tif (signal?.aborted) return abortedResult(url);\n\n\tconst httpResult = await extractViaHttp(url, signal, options);\n\n\tif (signal?.aborted) return abortedResult(url);\n\tif (!httpResult.error) return httpResult;\n\tif (NON_RECOVERABLE_ERRORS.some(prefix => httpResult.error!.startsWith(prefix))) return httpResult;\n\n\tconst jinaResult = await extractWithJinaReader(url, signal, options?.lookup);\n\tif (jinaResult) return jinaResult;\n\tif (signal?.aborted) return abortedResult(url);\n\n\tlet parallelError: string | null = null;\n\ttry {\n\t\tif (isParallelAvailable()) {\n\t\t\tconst parallelResult = await extractWithParallel(url, signal, options);\n\t\t\tif (parallelResult) return parallelResult;\n\t\t}\n\t} catch (err) {\n\t\tif (isAbortError(err)) return abortedResult(url);\n\t\tparallelError = errorMessage(err);\n\t\tif (isConfigParseError(err)) {\n\t\t\treturn { ...httpResult, error: parallelError };\n\t\t}\n\t}\n\tif (signal?.aborted) return abortedResult(url);\n\n\tlet geminiResult: ExtractedContent | null = null;\n\ttry {\n\t\tgeminiResult = await extractWithUrlContext(url, signal)\n\t\t\t?? await extractWithGeminiWeb(url, signal);\n\t} catch (err) {\n\t\tif (isAbortError(err)) return abortedResult(url);\n\t\tif (isConfigParseError(err)) {\n\t\t\treturn { ...httpResult, error: errorMessage(err) };\n\t\t}\n\t}\n\n\tif (geminiResult) return geminiResult;\n\tif (signal?.aborted) return abortedResult(url);\n\n\tconst guidance = [\n\t\thttpResult.error,\n\t\t...(parallelError ? [`Parallel fallback failed: ${parallelError}`] : []),\n\t\t\"\",\n\t\t\"Fallback options:\",\n\t\t` \\u2022 Set PARALLEL_API_KEY in ${WEB_SEARCH_CONFIG_PATH}`,\n\t\t` \\u2022 Set GEMINI_API_KEY in ${WEB_SEARCH_CONFIG_PATH}`,\n\t\t\" \\u2022 Sign into gemini.google.com in Chrome\",\n\t\t\" \\u2022 Use web_search to find content about this topic\",\n\t].join(\"\\n\");\n\treturn { ...httpResult, error: guidance };\n}\n\nfunction isLikelyJSRendered(html: string): boolean {\n\t// Extract body content\n\tconst bodyMatch = html.match(/<body[^>]*>([\\s\\S]*?)<\\/body>/i);\n\tif (!bodyMatch) return false;\n\n\tconst bodyHtml = bodyMatch[1];\n\n\t// Strip tags to get text content\n\tconst textContent = bodyHtml\n\t\t.replace(/<script[\\s\\S]*?<\\/script>/gi, \"\")\n\t\t.replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n\t\t.replace(/<[^>]+>/g, \"\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\n\t// Count scripts\n\tconst scriptCount = (html.match(/<script/gi) || []).length;\n\n\t// Heuristic: little text content but many scripts suggests JS rendering\n\treturn textContent.length < 500 && scriptCount > 3;\n}\n\nasync function extractViaHttp(\n\turl: string,\n\tsignal?: AbortSignal,\n\toptions?: ExtractOptions,\n): Promise<ExtractedContent> {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst activityId = activityMonitor.logStart({ type: \"fetch\", url });\n\n\tconst controller = new AbortController();\n\tconst timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n\tconst onAbort = () => controller.abort();\n\tsignal?.addEventListener(\"abort\", onAbort);\n\n\ttry {\n\t\tconst response = await fetchRemoteUrl(\n\t\t\turl,\n\t\t\t{\n\t\t\t\tsignal: controller.signal,\n\t\t\t\theaders: {\n\t\t\t\t\t\"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n\t\t\t\t\t\"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8\",\n\t\t\t\t\t\"Accept-Language\": \"en-US,en;q=0.9\",\n\t\t\t\t\t\"Cache-Control\": \"no-cache\",\n\t\t\t\t\t\"Sec-Fetch-Dest\": \"document\",\n\t\t\t\t\t\"Sec-Fetch-Mode\": \"navigate\",\n\t\t\t\t\t\"Sec-Fetch-Site\": \"none\",\n\t\t\t\t\t\"Sec-Fetch-User\": \"?1\",\n\t\t\t\t\t\"Upgrade-Insecure-Requests\": \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ allowRanges: loadSsrfAllowRanges(), lookup: options?.lookup },\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: `HTTP ${response.status}: ${response.statusText}`,\n\t\t\t};\n\t\t}\n\n\t\tconst contentLengthHeader = response.headers.get(\"content-length\");\n\t\tconst contentType = response.headers.get(\"content-type\") || \"\";\n\t\tconst isPDFContent = isPDF(url, contentType);\n\t\tconst maxResponseSize = isPDFContent ? 20 * 1024 * 1024 : 5 * 1024 * 1024;\n\t\tif (contentLengthHeader) {\n\t\t\tconst contentLength = parseInt(contentLengthHeader, 10);\n\t\t\tif (contentLength > maxResponseSize) {\n\t\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\t\treturn {\n\t\t\t\t\turl,\n\t\t\t\t\ttitle: \"\",\n\t\t\t\t\tcontent: \"\",\n\t\t\t\t\terror: `Response too large (${Math.round(contentLength / 1024 / 1024)}MB)`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (isPDFContent) {\n\t\t\ttry {\n\t\t\t\tconst buffer = await response.arrayBuffer();\n\t\t\t\tconst result = await extractPDFToMarkdown(buffer, url);\n\t\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\t\treturn {\n\t\t\t\t\turl,\n\t\t\t\t\ttitle: result.title,\n\t\t\t\t\tcontent: `PDF extracted and saved to: ${result.outputPath}\\n\\nPages: ${result.pages}\\nCharacters: ${result.chars}`,\n\t\t\t\t\terror: null,\n\t\t\t\t};\n\t\t\t} catch (err) {\n\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\tactivityMonitor.logError(activityId, message);\n\t\t\t\treturn { url, title: \"\", content: \"\", error: `PDF extraction failed: ${message}` };\n\t\t\t}\n\t\t}\n\n\t\tif (contentType.includes(\"application/octet-stream\") ||\n\t\t\tcontentType.includes(\"image/\") ||\n\t\t\tcontentType.includes(\"audio/\") ||\n\t\t\tcontentType.includes(\"video/\") ||\n\t\t\tcontentType.includes(\"application/zip\")) {\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: `Unsupported content type: ${contentType.split(\";\")[0]}`,\n\t\t\t};\n\t\t}\n\n\t\tconst text = await response.text();\n\t\tconst isHTML = contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml+xml\");\n\n\t\tif (!isHTML) {\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\tconst title = extractTextTitle(text, url);\n\t\t\treturn { url, title, content: text, error: null };\n\t\t}\n\n\t\tconst { document } = parseHTML(text);\n\t\tconst reader = new Readability(document as unknown as Document);\n\t\tconst article = reader.parse();\n\n\t\tif (!article) {\n\t\t\tconst rscResult = extractRSCContent(text);\n\t\t\tif (rscResult) {\n\t\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\t\treturn { url, title: rscResult.title, content: rscResult.content, error: null };\n\t\t\t}\n\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\n\t\t\t// Provide more specific error message\n\t\t\tconst jsRendered = isLikelyJSRendered(text);\n\t\t\tconst errorMsg = jsRendered\n\t\t\t\t? \"Page appears to be JavaScript-rendered (content loads dynamically)\"\n\t\t\t\t: \"Could not extract readable content from HTML structure\";\n\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: errorMsg,\n\t\t\t};\n\t\t}\n\n\t\tconst markdown = turndown.turndown(article.content);\n\t\tactivityMonitor.logComplete(activityId, response.status);\n\n\t\tif (markdown.length < MIN_USEFUL_CONTENT) {\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: article.title || \"\",\n\t\t\t\tcontent: markdown,\n\t\t\t\terror: isLikelyJSRendered(text)\n\t\t\t\t\t? \"Page appears to be JavaScript-rendered (content loads dynamically)\"\n\t\t\t\t\t: \"Extracted content appears incomplete\",\n\t\t\t};\n\t\t}\n\n\t\treturn { url, title: article.title || \"\", content: markdown, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (message.toLowerCase().includes(\"abort\")) {\n\t\t\tactivityMonitor.logComplete(activityId, 0);\n\t\t} else {\n\t\t\tactivityMonitor.logError(activityId, message);\n\t\t}\n\t\treturn { url, title: \"\", content: \"\", error: message };\n\t} finally {\n\t\tclearTimeout(timeoutId);\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\nexport function extractHeadingTitle(text: string): string | null {\n\tconst match = text.match(/^#{1,2}\\s+(.+)/m);\n\tif (!match) return null;\n\tconst cleaned = match[1].replace(/\\*+/g, \"\").trim();\n\treturn cleaned || null;\n}\n\nfunction extractTextTitle(text: string, url: string): string {\n\treturn extractHeadingTitle(text) ?? (new URL(url).pathname.split(\"/\").pop() || url);\n}\n\nexport async function fetchAllContent(\n\turls: string[],\n\tsignal?: AbortSignal,\n\toptions?: ExtractOptions,\n): Promise<ExtractedContent[]> {\n\treturn Promise.all(urls.map((url) => fetchLimit(() => extractContent(url, signal, options))));\n}\n"]}
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../src/builtin-extensions/pi-web-access/extract.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAqC,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAgDtF;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAuB9C;AAyBD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3D,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAgJD,wBAAsB,cAAc,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,gBAAgB,CAAC,CA0Q3B;AAuLD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D;AAMD,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B","sourcesContent":["// @ts-nocheck\nimport { Readability } from \"@mozilla/readability\";\nimport { parseHTML } from \"linkedom\";\nimport TurndownService from \"turndown\";\nimport pLimit from \"p-limit\";\nimport { activityMonitor } from \"./activity.ts\";\nimport { extractRSCContent } from \"./rsc-extract.ts\";\nimport {\n\tloadGeminiUrlContext,\n\tloadGithubExtract,\n\tloadParallel,\n\tloadPdfExtract,\n\tloadVideoExtract,\n\tloadYoutubeExtract,\n} from \"./lazy.ts\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { fetchRemoteUrl, validateRemoteUrl, type Lookup } from \"./ssrf-protection.ts\";\nimport { formatSeconds, getWebSearchConfigPath } from \"./utils.ts\";\n\nconst YOUTUBE_URL_RE =\n\t/(?:(?:www\\.|m\\.)?youtube\\.com\\/(?:watch\\?.*v=|shorts\\/|live\\/|embed\\/|v\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/;\n\nfunction isYouTubeURL(url: string): { isYouTube: boolean; videoId: string | null } {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (parsed.pathname === \"/playlist\") {\n\t\t\treturn { isYouTube: false, videoId: null };\n\t\t}\n\t} catch {\n\t}\n\tconst match = url.match(YOUTUBE_URL_RE);\n\tif (!match) return { isYouTube: false, videoId: null };\n\treturn { isYouTube: true, videoId: match[1] };\n}\n\nfunction isPDF(url: string, contentType?: string): boolean {\n\tif (contentType?.includes(\"application/pdf\")) return true;\n\ttry {\n\t\treturn new URL(url).pathname.toLowerCase().endsWith(\".pdf\");\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction looksLikeLocalPath(input: string): boolean {\n\treturn input.startsWith(\"/\") || input.startsWith(\"./\") || input.startsWith(\"../\") || input.startsWith(\"file://\");\n}\n\nfunction looksLikeGitHubUrl(url: string): boolean {\n\ttry {\n\t\tconst host = new URL(url).hostname.toLowerCase();\n\t\treturn host === \"github.com\" || host === \"www.github.com\" || host.endsWith(\".github.com\");\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst DEFAULT_TIMEOUT_MS = 30000;\nconst CONCURRENT_LIMIT = 3;\n\nconst NON_RECOVERABLE_ERRORS = [\"Unsupported content type\", \"Response too large\"];\nconst MIN_USEFUL_CONTENT = 500;\nconst WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();\n\n/**\n * Read `ssrf.allowRanges` (CIDR strings) from web-search.json. Returns [] when\n * the file is missing, unreadable, or the key is unset so SSRF protection stays\n * fully on by default. Throws when `ssrf.allowRanges` is present but not an array\n * so a mistyped value (e.g. a bare string instead of a JSON array) fails loudly\n * instead of being silently ignored. Exempts synthetic ranges used by TUN/fake-IP\n * proxies (e.g. 198.18.0.0/15).\n */\nexport function loadSsrfAllowRanges(): string[] {\n\tlet value: unknown;\n\ttry {\n\t\tif (!existsSync(WEB_SEARCH_CONFIG_PATH)) return [];\n\t\tconst raw = readFileSync(WEB_SEARCH_CONFIG_PATH, \"utf-8\");\n\t\tvalue = (JSON.parse(raw) as { ssrf?: { allowRanges?: unknown } })?.ssrf?.allowRanges;\n\t} catch {\n\t\t// Missing/unreadable file or invalid JSON: fail safe with SSRF fully on.\n\t\treturn [];\n\t}\n\tif (value === undefined || value === null) return [];\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(`ssrf.allowRanges in ${WEB_SEARCH_CONFIG_PATH} must be an array of CIDR strings`);\n\t}\n\tconst ranges: string[] = [];\n\tfor (const [index, entry] of value.entries()) {\n\t\tif (typeof entry !== \"string\") {\n\t\t\tthrow new Error(`ssrf.allowRanges in ${WEB_SEARCH_CONFIG_PATH} must contain only CIDR strings; entry ${index + 1} is ${typeof entry}`);\n\t\t}\n\t\tconst trimmed = entry.trim();\n\t\tif (trimmed) ranges.push(trimmed);\n\t}\n\treturn ranges;\n}\n\nfunction errorMessage(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nfunction isConfigParseError(err: unknown): boolean {\n\treturn errorMessage(err).startsWith(\"Failed to parse \");\n}\n\nfunction isAbortError(err: unknown): boolean {\n\treturn errorMessage(err).toLowerCase().includes(\"abort\");\n}\n\nfunction abortedResult(url: string): ExtractedContent {\n\treturn { url, title: \"\", content: \"\", error: \"Aborted\" };\n}\n\nconst turndown = new TurndownService({\n\theadingStyle: \"atx\",\n\tcodeBlockStyle: \"fenced\",\n});\n\nconst fetchLimit = pLimit(CONCURRENT_LIMIT);\n\nexport interface VideoFrame {\n\tdata: string;\n\tmimeType: string;\n\ttimestamp: string;\n}\n\nexport type FrameData = { data: string; mimeType: string };\nexport type FrameResult = FrameData | { error: string };\n\nexport interface ExtractedContent {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\terror: string | null;\n\tthumbnail?: { data: string; mimeType: string };\n\tframes?: VideoFrame[];\n\tduration?: number;\n}\n\nexport interface ExtractOptions {\n\ttimeoutMs?: number;\n\tforceClone?: boolean;\n\tprompt?: string;\n\ttimestamp?: string;\n\tframes?: number;\n\tmodel?: string;\n\t/** Custom DNS resolver used for SSRF validation. Primarily a test seam. */\n\tlookup?: Lookup;\n}\n\nconst JINA_READER_BASE = \"https://r.jina.ai/\";\nconst JINA_TIMEOUT_MS = 30000;\n\nasync function extractWithJinaReader(\n\turl: string,\n\tsignal?: AbortSignal,\n\tlookup?: Lookup,\n): Promise<ExtractedContent | null> {\n\tconst jinaUrl = JINA_READER_BASE + url;\n\n\tconst activityId = activityMonitor.logStart({ type: \"api\", query: `jina: ${url}` });\n\n\ttry {\n\t\tawait validateRemoteUrl(url, { allowRanges: loadSsrfAllowRanges(), lookup });\n\t\tconst res = await fetch(jinaUrl, {\n\t\t\theaders: {\n\t\t\t\t\"Accept\": \"text/markdown\",\n\t\t\t\t\"X-No-Cache\": \"true\",\n\t\t\t},\n\t\t\tsignal: AbortSignal.any([\n\t\t\t\tAbortSignal.timeout(JINA_TIMEOUT_MS),\n\t\t\t\t...(signal ? [signal] : []),\n\t\t\t]),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tactivityMonitor.logComplete(activityId, res.status);\n\t\t\treturn null;\n\t\t}\n\n\t\tconst content = await res.text();\n\t\tactivityMonitor.logComplete(activityId, res.status);\n\n\t\tconst contentStart = content.indexOf(\"Markdown Content:\");\n\t\tif (contentStart < 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst markdownPart = content.slice(contentStart + 17).trim(); // 17 = \"Markdown Content:\".length\n\n\t\t// Check for failed JS rendering or minimal content\n\t\tif (markdownPart.length < 100 ||\n\t\t\tmarkdownPart.startsWith(\"Loading...\") ||\n\t\t\tmarkdownPart.startsWith(\"Please enable JavaScript\")) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst title = extractHeadingTitle(markdownPart) ?? (new URL(url).pathname.split(\"/\").pop() || url);\n\t\treturn { url, title, content: markdownPart, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (message.toLowerCase().includes(\"abort\")) {\n\t\t\tactivityMonitor.logComplete(activityId, 0);\n\t\t} else {\n\t\t\tactivityMonitor.logError(activityId, message);\n\t\t}\n\t\treturn null;\n\t}\n}\n\nfunction parseTimestamp(ts: string): number | null {\n\tconst num = Number(ts);\n\tif (!isNaN(num) && num >= 0) return Math.floor(num);\n\tconst parts = ts.split(\":\").map(Number);\n\tif (parts.some(p => isNaN(p) || p < 0)) return null;\n\tif (parts.length === 3) return Math.floor(parts[0] * 3600 + parts[1] * 60 + parts[2]);\n\tif (parts.length === 2) return Math.floor(parts[0] * 60 + parts[1]);\n\treturn null;\n}\n\ntype TimestampSpec = { type: \"single\"; seconds: number } | { type: \"range\"; start: number; end: number };\n\nfunction parseTimestampSpec(ts: string): TimestampSpec | null {\n\tconst dashIdx = ts.indexOf(\"-\", 1);\n\tif (dashIdx > 0) {\n\t\tconst start = parseTimestamp(ts.slice(0, dashIdx));\n\t\tconst end = parseTimestamp(ts.slice(dashIdx + 1));\n\t\tif (start !== null && end !== null && end > start) return { type: \"range\", start, end };\n\t}\n\tconst seconds = parseTimestamp(ts);\n\treturn seconds !== null ? { type: \"single\", seconds } : null;\n}\n\nconst DEFAULT_RANGE_FRAMES = 6;\nconst MIN_FRAME_INTERVAL = 5;\n\nfunction computeRangeTimestamps(start: number, end: number, maxFrames: number = DEFAULT_RANGE_FRAMES): number[] {\n\tif (maxFrames <= 1) return [start];\n\tconst duration = end - start;\n\tconst idealInterval = duration / (maxFrames - 1);\n\tif (idealInterval < MIN_FRAME_INTERVAL) {\n\t\tconst timestamps: number[] = [];\n\t\tfor (let t = start; t <= end && timestamps.length < maxFrames; t += MIN_FRAME_INTERVAL) {\n\t\t\ttimestamps.push(t);\n\t\t}\n\t\treturn timestamps;\n\t}\n\treturn Array.from({ length: maxFrames }, (_, i) => Math.round(start + i * idealInterval));\n}\n\nfunction buildFrameResult(\n\turl: string, label: string, requestedCount: number,\n\tframes: VideoFrame[], error: string | null, duration?: number,\n): ExtractedContent {\n\tif (frames.length === 0) {\n\t\tconst msg = error ?? \"Frame extraction failed\";\n\t\treturn { url, title: `Frames ${label} (0/${requestedCount})`, content: msg, error: msg };\n\t}\n\treturn {\n\t\turl,\n\t\ttitle: `Frames ${label} (${frames.length}/${requestedCount})`,\n\t\tcontent: `${frames.length} frames extracted from ${label}`,\n\t\terror: null,\n\t\tframes,\n\t\tduration,\n\t};\n}\n\nasync function extractLocalFrames(\n\tfilePath: string, timestamps: number[],\n): Promise<{ frames: VideoFrame[]; error: string | null }> {\n\tconst { extractVideoFrame } = await loadVideoExtract();\n\tconst results = await Promise.all(timestamps.map(async (t) => {\n\t\tconst frame = await extractVideoFrame(filePath, t);\n\t\tif (\"error\" in frame) return { error: frame.error };\n\t\treturn { ...frame, timestamp: formatSeconds(t) };\n\t}));\n\tconst frames = results.filter((f): f is VideoFrame => \"data\" in f);\n\tconst firstError = results.find((f): f is { error: string } => \"error\" in f);\n\treturn { frames, error: frames.length === 0 && firstError ? firstError.error : null };\n}\n\nasync function safeVideoInfo(url: string): Promise<{ info: Awaited<ReturnType<typeof loadVideoExtract>> extends { isVideoFile: infer F } ? (F extends (...args: any) => infer R ? R : null) : null; error?: string }> {\n\tif (!looksLikeLocalPath(url)) return { info: null };\n\ttry {\n\t\tconst { isVideoFile } = await loadVideoExtract();\n\t\treturn { info: isVideoFile(url) };\n\t} catch (err) {\n\t\treturn { info: null, error: errorMessage(err) };\n\t}\n}\n\nexport async function extractContent(\n\turl: string,\n\tsignal?: AbortSignal,\n\toptions?: ExtractOptions,\n): Promise<ExtractedContent> {\n\tif (signal?.aborted) {\n\t\treturn { url, title: \"\", content: \"\", error: \"Aborted\" };\n\t}\n\n\tif (options?.frames && !options.timestamp) {\n\t\tconst frameCount = options.frames;\n\t\tconst ytInfo = isYouTubeURL(url);\n\t\tif (ytInfo.isYouTube && ytInfo.videoId) {\n\t\t\tconst yt = await loadYoutubeExtract();\n\t\t\tconst streamInfo = await yt.getYouTubeStreamInfo(ytInfo.videoId);\n\t\t\tif (\"error\" in streamInfo) {\n\t\t\t\treturn { url, title: \"Frames\", content: streamInfo.error, error: streamInfo.error };\n\t\t\t}\n\t\t\tif (streamInfo.duration === null) {\n\t\t\t\tconst error = \"Cannot determine video duration. Use a timestamp range instead.\";\n\t\t\t\treturn { url, title: \"Frames\", content: error, error };\n\t\t\t}\n\t\t\tconst dur = Math.floor(streamInfo.duration);\n\t\t\tconst timestamps = computeRangeTimestamps(0, dur, frameCount);\n\t\t\tconst result = await yt.extractYouTubeFrames(ytInfo.videoId, timestamps, streamInfo);\n\t\t\tconst label = `${formatSeconds(0)}-${formatSeconds(dur)}`;\n\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, streamInfo.duration);\n\t\t}\n\n\t\tconst localVideo = await safeVideoInfo(url);\n\t\tif (localVideo.error) {\n\t\t\treturn { url, title: \"\", content: \"\", error: localVideo.error };\n\t\t}\n\t\tif (localVideo.info) {\n\t\t\tconst { getLocalVideoDuration } = await loadVideoExtract();\n\t\t\tconst durationResult = await getLocalVideoDuration(localVideo.info.absolutePath);\n\t\t\tif (typeof durationResult !== \"number\") {\n\t\t\t\treturn { url, title: \"Frames\", content: durationResult.error, error: durationResult.error };\n\t\t\t}\n\t\t\tconst dur = Math.floor(durationResult);\n\t\t\tconst timestamps = computeRangeTimestamps(0, dur, frameCount);\n\t\t\tconst result = await extractLocalFrames(localVideo.info.absolutePath, timestamps);\n\t\t\tconst label = `${formatSeconds(0)}-${formatSeconds(dur)}`;\n\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, durationResult);\n\t\t}\n\n\t\treturn { url, title: \"\", content: \"\", error: \"Frame extraction only works with YouTube and local video files\" };\n\t}\n\n\tif (options?.timestamp) {\n\t\tconst spec = parseTimestampSpec(options.timestamp);\n\t\tif (!spec) {\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: `Invalid timestamp format: \"${options.timestamp}\". Use \"H:MM:SS\", \"MM:SS\", \"85\", or \"start-end\".`,\n\t\t\t};\n\t\t}\n\n\t\tconst frameCount = options.frames;\n\t\tconst ytInfo = isYouTubeURL(url);\n\t\tif (ytInfo.isYouTube && ytInfo.videoId) {\n\t\t\tconst yt = await loadYoutubeExtract();\n\t\t\tconst streamInfo = await yt.getYouTubeStreamInfo(ytInfo.videoId);\n\t\t\tif (\"error\" in streamInfo) {\n\t\t\t\tif (spec.type === \"range\") {\n\t\t\t\t\tconst label = `${formatSeconds(spec.start)}-${formatSeconds(spec.end)}`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: streamInfo.error, error: streamInfo.error };\n\t\t\t\t}\n\t\t\t\tif (frameCount) {\n\t\t\t\t\tconst end = spec.seconds + (frameCount - 1) * MIN_FRAME_INTERVAL;\n\t\t\t\t\tconst label = `${formatSeconds(spec.seconds)}-${formatSeconds(end)}`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: streamInfo.error, error: streamInfo.error };\n\t\t\t\t}\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: streamInfo.error, error: streamInfo.error };\n\t\t\t}\n\n\t\t\tif (spec.type === \"range\") {\n\t\t\t\tconst label = `${formatSeconds(spec.start)}-${formatSeconds(spec.end)}`;\n\t\t\t\tif (streamInfo.duration !== null && spec.end > streamInfo.duration) {\n\t\t\t\t\tconst error = `Timestamp ${formatSeconds(spec.end)} exceeds video duration (${formatSeconds(Math.floor(streamInfo.duration))})`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: error, error };\n\t\t\t\t}\n\t\t\t\tconst timestamps = frameCount\n\t\t\t\t\t? computeRangeTimestamps(spec.start, spec.end, frameCount)\n\t\t\t\t\t: computeRangeTimestamps(spec.start, spec.end);\n\t\t\t\tconst result = await yt.extractYouTubeFrames(ytInfo.videoId, timestamps, streamInfo);\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, result.duration ?? undefined);\n\t\t\t}\n\n\t\t\tif (frameCount) {\n\t\t\t\tconst end = spec.seconds + (frameCount - 1) * MIN_FRAME_INTERVAL;\n\t\t\t\tconst label = `${formatSeconds(spec.seconds)}-${formatSeconds(end)}`;\n\t\t\t\tif (streamInfo.duration !== null && end > streamInfo.duration) {\n\t\t\t\t\tconst error = `Timestamp ${formatSeconds(end)} exceeds video duration (${formatSeconds(Math.floor(streamInfo.duration))})`;\n\t\t\t\t\treturn { url, title: `Frames ${label}`, content: error, error };\n\t\t\t\t}\n\t\t\t\tconst timestamps = computeRangeTimestamps(spec.seconds, end, frameCount);\n\t\t\t\tconst result = await yt.extractYouTubeFrames(ytInfo.videoId, timestamps, streamInfo);\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error, result.duration ?? undefined);\n\t\t\t}\n\n\t\t\tif (streamInfo.duration !== null && spec.seconds > streamInfo.duration) {\n\t\t\t\tconst error = `Timestamp ${formatSeconds(spec.seconds)} exceeds video duration (${formatSeconds(Math.floor(streamInfo.duration))})`;\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: error, error };\n\t\t\t}\n\t\t\tconst frame = await yt.extractYouTubeFrame(ytInfo.videoId, spec.seconds, streamInfo);\n\t\t\tif (\"error\" in frame) {\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: frame.error, error: frame.error };\n\t\t\t}\n\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: `Video frame at ${options.timestamp}`, error: null, thumbnail: frame };\n\t\t}\n\n\t\tconst localVideo = await safeVideoInfo(url);\n\t\tif (localVideo.error) {\n\t\t\treturn { url, title: \"\", content: \"\", error: localVideo.error };\n\t\t}\n\t\tif (localVideo.info) {\n\t\t\tif (spec.type === \"range\") {\n\t\t\t\tconst timestamps = frameCount\n\t\t\t\t\t? computeRangeTimestamps(spec.start, spec.end, frameCount)\n\t\t\t\t\t: computeRangeTimestamps(spec.start, spec.end);\n\t\t\t\tconst result = await extractLocalFrames(localVideo.info.absolutePath, timestamps);\n\t\t\t\tconst label = `${formatSeconds(spec.start)}-${formatSeconds(spec.end)}`;\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error);\n\t\t\t}\n\n\t\t\tif (frameCount) {\n\t\t\t\tconst end = spec.seconds + (frameCount - 1) * MIN_FRAME_INTERVAL;\n\t\t\t\tconst timestamps = computeRangeTimestamps(spec.seconds, end, frameCount);\n\t\t\t\tconst result = await extractLocalFrames(localVideo.info.absolutePath, timestamps);\n\t\t\t\tconst label = `${formatSeconds(spec.seconds)}-${formatSeconds(end)}`;\n\t\t\t\treturn buildFrameResult(url, label, timestamps.length, result.frames, result.error);\n\t\t\t}\n\n\t\t\tconst { extractVideoFrame } = await loadVideoExtract();\n\t\t\tconst frame = await extractVideoFrame(localVideo.info.absolutePath, spec.seconds);\n\t\t\tif (\"error\" in frame) {\n\t\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: frame.error, error: frame.error };\n\t\t\t}\n\t\t\treturn { url, title: `Frame at ${options.timestamp}`, content: `Video frame at ${options.timestamp}`, error: null, thumbnail: frame };\n\t\t}\n\n\t\treturn { url, title: \"\", content: \"\", error: \"Timestamp extraction only works with YouTube and local video files\" };\n\t}\n\n\tconst localVideo = await safeVideoInfo(url);\n\tif (localVideo.error) {\n\t\treturn { url, title: \"\", content: \"\", error: localVideo.error };\n\t}\n\tif (localVideo.info) {\n\t\ttry {\n\t\t\tconst { extractVideo } = await loadVideoExtract();\n\t\t\tconst result = await extractVideo(localVideo.info, signal, options);\n\t\t\tif (signal?.aborted) return abortedResult(url);\n\t\t\treturn result ?? { url, title: \"\", content: \"\", error: `Video analysis requires Gemini access. Either:\\n 1. Sign into gemini.google.com in Chrome (free, uses cookies)\\n 2. Set GEMINI_API_KEY in ${WEB_SEARCH_CONFIG_PATH}` };\n\t\t} catch (err) {\n\t\t\tif (isAbortError(err)) return abortedResult(url);\n\t\t\treturn { url, title: \"\", content: \"\", error: errorMessage(err) };\n\t\t}\n\t}\n\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") {\n\t\t\tawait validateRemoteUrl(parsed, { allowRanges: loadSsrfAllowRanges(), lookup: options?.lookup });\n\t\t}\n\t} catch (err) {\n\t\treturn { url, title: \"\", content: \"\", error: errorMessage(err) };\n\t}\n\n\tif (looksLikeGitHubUrl(url)) {\n\t\ttry {\n\t\t\tconst { extractGitHub } = await loadGithubExtract();\n\t\t\tconst ghResult = await extractGitHub(url, signal, options?.forceClone);\n\t\t\tif (ghResult) return ghResult;\n\t\t\tif (signal?.aborted) return abortedResult(url);\n\t\t} catch (err) {\n\t\t\tconst message = errorMessage(err);\n\t\t\tif (isAbortError(err)) return abortedResult(url);\n\t\t\tif (isConfigParseError(err)) {\n\t\t\t\treturn { url, title: \"\", content: \"\", error: message };\n\t\t\t}\n\t\t}\n\t}\n\n\tconst ytInfo = isYouTubeURL(url);\n\tif (ytInfo.isYouTube) {\n\t\tlet youtubeEnabled = false;\n\t\tlet yt;\n\t\ttry {\n\t\t\tyt = await loadYoutubeExtract();\n\t\t\tyoutubeEnabled = yt.isYouTubeEnabled();\n\t\t} catch (err) {\n\t\t\treturn { url, title: \"\", content: \"\", error: errorMessage(err) };\n\t\t}\n\t\tif (youtubeEnabled) {\n\t\t\ttry {\n\t\t\t\tconst ytResult = await yt.extractYouTube(url, signal, options?.prompt, options?.model);\n\t\t\t\tif (ytResult) return ytResult;\n\t\t\t\tif (signal?.aborted) return abortedResult(url);\n\t\t\t} catch (err) {\n\t\t\t\tconst message = errorMessage(err);\n\t\t\t\tif (isAbortError(err)) return abortedResult(url);\n\t\t\t\treturn { url, title: \"\", content: \"\", error: message };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: \"Could not extract YouTube video content. Sign into Google in Chrome for automatic access, or set GEMINI_API_KEY.\",\n\t\t\t};\n\t\t}\n\t}\n\n\tif (signal?.aborted) return abortedResult(url);\n\n\tconst httpResult = await extractViaHttp(url, signal, options);\n\n\tif (signal?.aborted) return abortedResult(url);\n\tif (!httpResult.error) return httpResult;\n\tif (NON_RECOVERABLE_ERRORS.some(prefix => httpResult.error!.startsWith(prefix))) return httpResult;\n\n\tconst jinaResult = await extractWithJinaReader(url, signal, options?.lookup);\n\tif (jinaResult) return jinaResult;\n\tif (signal?.aborted) return abortedResult(url);\n\n\tlet parallelError: string | null = null;\n\ttry {\n\t\tconst parallel = await loadParallel();\n\t\tif (parallel.isParallelAvailable()) {\n\t\t\tconst parallelResult = await parallel.extractWithParallel(url, signal, options);\n\t\t\tif (parallelResult) return parallelResult;\n\t\t}\n\t} catch (err) {\n\t\tif (isAbortError(err)) return abortedResult(url);\n\t\tparallelError = errorMessage(err);\n\t\tif (isConfigParseError(err)) {\n\t\t\treturn { ...httpResult, error: parallelError };\n\t\t}\n\t}\n\tif (signal?.aborted) return abortedResult(url);\n\n\tlet geminiResult: ExtractedContent | null = null;\n\ttry {\n\t\tconst geminiUrl = await loadGeminiUrlContext();\n\t\tgeminiResult = await geminiUrl.extractWithUrlContext(url, signal)\n\t\t\t?? await geminiUrl.extractWithGeminiWeb(url, signal);\n\t} catch (err) {\n\t\tif (isAbortError(err)) return abortedResult(url);\n\t\tif (isConfigParseError(err)) {\n\t\t\treturn { ...httpResult, error: errorMessage(err) };\n\t\t}\n\t}\n\n\tif (geminiResult) return geminiResult;\n\tif (signal?.aborted) return abortedResult(url);\n\n\tconst guidance = [\n\t\thttpResult.error,\n\t\t...(parallelError ? [`Parallel fallback failed: ${parallelError}`] : []),\n\t\t\"\",\n\t\t\"Fallback options:\",\n\t\t` \\u2022 Set PARALLEL_API_KEY in ${WEB_SEARCH_CONFIG_PATH}`,\n\t\t` \\u2022 Set GEMINI_API_KEY in ${WEB_SEARCH_CONFIG_PATH}`,\n\t\t\" \\u2022 Sign into gemini.google.com in Chrome\",\n\t\t\" \\u2022 Use web_search to find content about this topic\",\n\t].join(\"\\n\");\n\treturn { ...httpResult, error: guidance };\n}\n\nfunction isLikelyJSRendered(html: string): boolean {\n\t// Extract body content\n\tconst bodyMatch = html.match(/<body[^>]*>([\\s\\S]*?)<\\/body>/i);\n\tif (!bodyMatch) return false;\n\n\tconst bodyHtml = bodyMatch[1];\n\n\t// Strip tags to get text content\n\tconst textContent = bodyHtml\n\t\t.replace(/<script[\\s\\S]*?<\\/script>/gi, \"\")\n\t\t.replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n\t\t.replace(/<[^>]+>/g, \"\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\n\t// Count scripts\n\tconst scriptCount = (html.match(/<script/gi) || []).length;\n\n\t// Heuristic: little text content but many scripts suggests JS rendering\n\treturn textContent.length < 500 && scriptCount > 3;\n}\n\nasync function extractViaHttp(\n\turl: string,\n\tsignal?: AbortSignal,\n\toptions?: ExtractOptions,\n): Promise<ExtractedContent> {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst activityId = activityMonitor.logStart({ type: \"fetch\", url });\n\n\tconst controller = new AbortController();\n\tconst timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n\tconst onAbort = () => controller.abort();\n\tsignal?.addEventListener(\"abort\", onAbort);\n\n\ttry {\n\t\tconst response = await fetchRemoteUrl(\n\t\t\turl,\n\t\t\t{\n\t\t\t\tsignal: controller.signal,\n\t\t\t\theaders: {\n\t\t\t\t\t\"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n\t\t\t\t\t\"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8\",\n\t\t\t\t\t\"Accept-Language\": \"en-US,en;q=0.9\",\n\t\t\t\t\t\"Cache-Control\": \"no-cache\",\n\t\t\t\t\t\"Sec-Fetch-Dest\": \"document\",\n\t\t\t\t\t\"Sec-Fetch-Mode\": \"navigate\",\n\t\t\t\t\t\"Sec-Fetch-Site\": \"none\",\n\t\t\t\t\t\"Sec-Fetch-User\": \"?1\",\n\t\t\t\t\t\"Upgrade-Insecure-Requests\": \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ allowRanges: loadSsrfAllowRanges(), lookup: options?.lookup },\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: `HTTP ${response.status}: ${response.statusText}`,\n\t\t\t};\n\t\t}\n\n\t\tconst contentLengthHeader = response.headers.get(\"content-length\");\n\t\tconst contentType = response.headers.get(\"content-type\") || \"\";\n\t\tconst isPDFContent = isPDF(url, contentType);\n\t\tconst maxResponseSize = isPDFContent ? 20 * 1024 * 1024 : 5 * 1024 * 1024;\n\t\tif (contentLengthHeader) {\n\t\t\tconst contentLength = parseInt(contentLengthHeader, 10);\n\t\t\tif (contentLength > maxResponseSize) {\n\t\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\t\treturn {\n\t\t\t\t\turl,\n\t\t\t\t\ttitle: \"\",\n\t\t\t\t\tcontent: \"\",\n\t\t\t\t\terror: `Response too large (${Math.round(contentLength / 1024 / 1024)}MB)`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (isPDFContent) {\n\t\t\ttry {\n\t\t\t\tconst buffer = await response.arrayBuffer();\n\t\t\t\tconst { extractPDFToMarkdown } = await loadPdfExtract();\n\t\t\t\tconst result = await extractPDFToMarkdown(buffer, url);\n\t\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\t\treturn {\n\t\t\t\t\turl,\n\t\t\t\t\ttitle: result.title,\n\t\t\t\t\tcontent: `PDF extracted and saved to: ${result.outputPath}\\n\\nPages: ${result.pages}\\nCharacters: ${result.chars}`,\n\t\t\t\t\terror: null,\n\t\t\t\t};\n\t\t\t} catch (err) {\n\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\tactivityMonitor.logError(activityId, message);\n\t\t\t\treturn { url, title: \"\", content: \"\", error: `PDF extraction failed: ${message}` };\n\t\t\t}\n\t\t}\n\n\t\tif (contentType.includes(\"application/octet-stream\") ||\n\t\t\tcontentType.includes(\"image/\") ||\n\t\t\tcontentType.includes(\"audio/\") ||\n\t\t\tcontentType.includes(\"video/\") ||\n\t\t\tcontentType.includes(\"application/zip\")) {\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: `Unsupported content type: ${contentType.split(\";\")[0]}`,\n\t\t\t};\n\t\t}\n\n\t\tconst text = await response.text();\n\t\tconst isHTML = contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml+xml\");\n\n\t\tif (!isHTML) {\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\tconst title = extractTextTitle(text, url);\n\t\t\treturn { url, title, content: text, error: null };\n\t\t}\n\n\t\tconst { document } = parseHTML(text);\n\t\tconst reader = new Readability(document as unknown as Document);\n\t\tconst article = reader.parse();\n\n\t\tif (!article) {\n\t\t\tconst rscResult = extractRSCContent(text);\n\t\t\tif (rscResult) {\n\t\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\t\t\t\treturn { url, title: rscResult.title, content: rscResult.content, error: null };\n\t\t\t}\n\n\t\t\tactivityMonitor.logComplete(activityId, response.status);\n\n\t\t\t// Provide more specific error message\n\t\t\tconst jsRendered = isLikelyJSRendered(text);\n\t\t\tconst errorMsg = jsRendered\n\t\t\t\t? \"Page appears to be JavaScript-rendered (content loads dynamically)\"\n\t\t\t\t: \"Could not extract readable content from HTML structure\";\n\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: \"\",\n\t\t\t\tcontent: \"\",\n\t\t\t\terror: errorMsg,\n\t\t\t};\n\t\t}\n\n\t\tconst markdown = turndown.turndown(article.content);\n\t\tactivityMonitor.logComplete(activityId, response.status);\n\n\t\tif (markdown.length < MIN_USEFUL_CONTENT) {\n\t\t\treturn {\n\t\t\t\turl,\n\t\t\t\ttitle: article.title || \"\",\n\t\t\t\tcontent: markdown,\n\t\t\t\terror: isLikelyJSRendered(text)\n\t\t\t\t\t? \"Page appears to be JavaScript-rendered (content loads dynamically)\"\n\t\t\t\t\t: \"Extracted content appears incomplete\",\n\t\t\t};\n\t\t}\n\n\t\treturn { url, title: article.title || \"\", content: markdown, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (message.toLowerCase().includes(\"abort\")) {\n\t\t\tactivityMonitor.logComplete(activityId, 0);\n\t\t} else {\n\t\t\tactivityMonitor.logError(activityId, message);\n\t\t}\n\t\treturn { url, title: \"\", content: \"\", error: message };\n\t} finally {\n\t\tclearTimeout(timeoutId);\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\nexport function extractHeadingTitle(text: string): string | null {\n\tconst match = text.match(/^#{1,2}\\s+(.+)/m);\n\tif (!match) return null;\n\tconst cleaned = match[1].replace(/\\*+/g, \"\").trim();\n\treturn cleaned || null;\n}\n\nfunction extractTextTitle(text: string, url: string): string {\n\treturn extractHeadingTitle(text) ?? (new URL(url).pathname.split(\"/\").pop() || url);\n}\n\nexport async function fetchAllContent(\n\turls: string[],\n\tsignal?: AbortSignal,\n\toptions?: ExtractOptions,\n): Promise<ExtractedContent[]> {\n\treturn Promise.all(urls.map((url) => fetchLimit(() => extractContent(url, signal, options))));\n}\n"]}