onbuzz 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +425 -0
  3. package/bin/cli.js +556 -0
  4. package/bin/loxia-terminal-v2.js +162 -0
  5. package/bin/loxia-terminal.js +90 -0
  6. package/bin/start-with-terminal.js +200 -0
  7. package/node_modules/@isaacs/balanced-match/LICENSE.md +23 -0
  8. package/node_modules/@isaacs/balanced-match/README.md +60 -0
  9. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.d.ts +9 -0
  10. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.d.ts.map +1 -0
  11. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.js +59 -0
  12. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.js.map +1 -0
  13. package/node_modules/@isaacs/balanced-match/dist/commonjs/package.json +3 -0
  14. package/node_modules/@isaacs/balanced-match/dist/esm/index.d.ts +9 -0
  15. package/node_modules/@isaacs/balanced-match/dist/esm/index.d.ts.map +1 -0
  16. package/node_modules/@isaacs/balanced-match/dist/esm/index.js +54 -0
  17. package/node_modules/@isaacs/balanced-match/dist/esm/index.js.map +1 -0
  18. package/node_modules/@isaacs/balanced-match/dist/esm/package.json +3 -0
  19. package/node_modules/@isaacs/balanced-match/package.json +79 -0
  20. package/node_modules/@isaacs/brace-expansion/LICENSE +23 -0
  21. package/node_modules/@isaacs/brace-expansion/README.md +97 -0
  22. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.d.ts +6 -0
  23. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.d.ts.map +1 -0
  24. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js +199 -0
  25. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +1 -0
  26. package/node_modules/@isaacs/brace-expansion/dist/commonjs/package.json +3 -0
  27. package/node_modules/@isaacs/brace-expansion/dist/esm/index.d.ts +6 -0
  28. package/node_modules/@isaacs/brace-expansion/dist/esm/index.d.ts.map +1 -0
  29. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js +195 -0
  30. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +1 -0
  31. package/node_modules/@isaacs/brace-expansion/dist/esm/package.json +3 -0
  32. package/node_modules/@isaacs/brace-expansion/package.json +60 -0
  33. package/node_modules/glob/LICENSE.md +63 -0
  34. package/node_modules/glob/README.md +1177 -0
  35. package/node_modules/glob/dist/commonjs/glob.d.ts +388 -0
  36. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -0
  37. package/node_modules/glob/dist/commonjs/glob.js +247 -0
  38. package/node_modules/glob/dist/commonjs/glob.js.map +1 -0
  39. package/node_modules/glob/dist/commonjs/has-magic.d.ts +14 -0
  40. package/node_modules/glob/dist/commonjs/has-magic.d.ts.map +1 -0
  41. package/node_modules/glob/dist/commonjs/has-magic.js +27 -0
  42. package/node_modules/glob/dist/commonjs/has-magic.js.map +1 -0
  43. package/node_modules/glob/dist/commonjs/ignore.d.ts +24 -0
  44. package/node_modules/glob/dist/commonjs/ignore.d.ts.map +1 -0
  45. package/node_modules/glob/dist/commonjs/ignore.js +119 -0
  46. package/node_modules/glob/dist/commonjs/ignore.js.map +1 -0
  47. package/node_modules/glob/dist/commonjs/index.d.ts +97 -0
  48. package/node_modules/glob/dist/commonjs/index.d.ts.map +1 -0
  49. package/node_modules/glob/dist/commonjs/index.js +68 -0
  50. package/node_modules/glob/dist/commonjs/index.js.map +1 -0
  51. package/node_modules/glob/dist/commonjs/index.min.js +4 -0
  52. package/node_modules/glob/dist/commonjs/index.min.js.map +7 -0
  53. package/node_modules/glob/dist/commonjs/package.json +3 -0
  54. package/node_modules/glob/dist/commonjs/pattern.d.ts +76 -0
  55. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -0
  56. package/node_modules/glob/dist/commonjs/pattern.js +219 -0
  57. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -0
  58. package/node_modules/glob/dist/commonjs/processor.d.ts +59 -0
  59. package/node_modules/glob/dist/commonjs/processor.d.ts.map +1 -0
  60. package/node_modules/glob/dist/commonjs/processor.js +301 -0
  61. package/node_modules/glob/dist/commonjs/processor.js.map +1 -0
  62. package/node_modules/glob/dist/commonjs/walker.d.ts +97 -0
  63. package/node_modules/glob/dist/commonjs/walker.d.ts.map +1 -0
  64. package/node_modules/glob/dist/commonjs/walker.js +387 -0
  65. package/node_modules/glob/dist/commonjs/walker.js.map +1 -0
  66. package/node_modules/glob/dist/esm/glob.d.ts +388 -0
  67. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -0
  68. package/node_modules/glob/dist/esm/glob.js +243 -0
  69. package/node_modules/glob/dist/esm/glob.js.map +1 -0
  70. package/node_modules/glob/dist/esm/has-magic.d.ts +14 -0
  71. package/node_modules/glob/dist/esm/has-magic.d.ts.map +1 -0
  72. package/node_modules/glob/dist/esm/has-magic.js +23 -0
  73. package/node_modules/glob/dist/esm/has-magic.js.map +1 -0
  74. package/node_modules/glob/dist/esm/ignore.d.ts +24 -0
  75. package/node_modules/glob/dist/esm/ignore.d.ts.map +1 -0
  76. package/node_modules/glob/dist/esm/ignore.js +115 -0
  77. package/node_modules/glob/dist/esm/ignore.js.map +1 -0
  78. package/node_modules/glob/dist/esm/index.d.ts +97 -0
  79. package/node_modules/glob/dist/esm/index.d.ts.map +1 -0
  80. package/node_modules/glob/dist/esm/index.js +55 -0
  81. package/node_modules/glob/dist/esm/index.js.map +1 -0
  82. package/node_modules/glob/dist/esm/index.min.js +4 -0
  83. package/node_modules/glob/dist/esm/index.min.js.map +7 -0
  84. package/node_modules/glob/dist/esm/package.json +3 -0
  85. package/node_modules/glob/dist/esm/pattern.d.ts +76 -0
  86. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -0
  87. package/node_modules/glob/dist/esm/pattern.js +215 -0
  88. package/node_modules/glob/dist/esm/pattern.js.map +1 -0
  89. package/node_modules/glob/dist/esm/processor.d.ts +59 -0
  90. package/node_modules/glob/dist/esm/processor.d.ts.map +1 -0
  91. package/node_modules/glob/dist/esm/processor.js +294 -0
  92. package/node_modules/glob/dist/esm/processor.js.map +1 -0
  93. package/node_modules/glob/dist/esm/walker.d.ts +97 -0
  94. package/node_modules/glob/dist/esm/walker.d.ts.map +1 -0
  95. package/node_modules/glob/dist/esm/walker.js +381 -0
  96. package/node_modules/glob/dist/esm/walker.js.map +1 -0
  97. package/node_modules/glob/node_modules/minimatch/LICENSE.md +55 -0
  98. package/node_modules/glob/node_modules/minimatch/README.md +453 -0
  99. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +2 -0
  100. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -0
  101. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js +14 -0
  102. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -0
  103. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +20 -0
  104. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -0
  105. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +591 -0
  106. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -0
  107. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts +8 -0
  108. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -0
  109. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +152 -0
  110. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -0
  111. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +15 -0
  112. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -0
  113. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +30 -0
  114. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -0
  115. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +94 -0
  116. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -0
  117. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +1029 -0
  118. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -0
  119. package/node_modules/glob/node_modules/minimatch/dist/commonjs/package.json +3 -0
  120. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +22 -0
  121. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -0
  122. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +38 -0
  123. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -0
  124. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +2 -0
  125. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -0
  126. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js +10 -0
  127. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -0
  128. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +20 -0
  129. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -0
  130. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +587 -0
  131. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -0
  132. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts +8 -0
  133. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -0
  134. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +148 -0
  135. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -0
  136. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +15 -0
  137. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -0
  138. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +26 -0
  139. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -0
  140. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +94 -0
  141. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -0
  142. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +1016 -0
  143. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -0
  144. package/node_modules/glob/node_modules/minimatch/dist/esm/package.json +3 -0
  145. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +22 -0
  146. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -0
  147. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +34 -0
  148. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -0
  149. package/node_modules/glob/node_modules/minimatch/package.json +67 -0
  150. package/node_modules/glob/package.json +101 -0
  151. package/node_modules/minipass/LICENSE +15 -0
  152. package/node_modules/minipass/README.md +825 -0
  153. package/node_modules/minipass/dist/commonjs/index.d.ts +549 -0
  154. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -0
  155. package/node_modules/minipass/dist/commonjs/index.js +1028 -0
  156. package/node_modules/minipass/dist/commonjs/index.js.map +1 -0
  157. package/node_modules/minipass/dist/commonjs/package.json +3 -0
  158. package/node_modules/minipass/dist/esm/index.d.ts +549 -0
  159. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -0
  160. package/node_modules/minipass/dist/esm/index.js +1018 -0
  161. package/node_modules/minipass/dist/esm/index.js.map +1 -0
  162. package/node_modules/minipass/dist/esm/package.json +3 -0
  163. package/node_modules/minipass/package.json +82 -0
  164. package/node_modules/package-json-from-dist/LICENSE.md +63 -0
  165. package/node_modules/package-json-from-dist/README.md +110 -0
  166. package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts +89 -0
  167. package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts.map +1 -0
  168. package/node_modules/package-json-from-dist/dist/commonjs/index.js +134 -0
  169. package/node_modules/package-json-from-dist/dist/commonjs/index.js.map +1 -0
  170. package/node_modules/package-json-from-dist/dist/commonjs/package.json +3 -0
  171. package/node_modules/package-json-from-dist/dist/esm/index.d.ts +89 -0
  172. package/node_modules/package-json-from-dist/dist/esm/index.d.ts.map +1 -0
  173. package/node_modules/package-json-from-dist/dist/esm/index.js +129 -0
  174. package/node_modules/package-json-from-dist/dist/esm/index.js.map +1 -0
  175. package/node_modules/package-json-from-dist/dist/esm/package.json +3 -0
  176. package/node_modules/package-json-from-dist/package.json +68 -0
  177. package/node_modules/path-scurry/LICENSE.md +55 -0
  178. package/node_modules/path-scurry/README.md +636 -0
  179. package/node_modules/path-scurry/dist/commonjs/index.d.ts +1115 -0
  180. package/node_modules/path-scurry/dist/commonjs/index.d.ts.map +1 -0
  181. package/node_modules/path-scurry/dist/commonjs/index.js +2018 -0
  182. package/node_modules/path-scurry/dist/commonjs/index.js.map +1 -0
  183. package/node_modules/path-scurry/dist/commonjs/package.json +3 -0
  184. package/node_modules/path-scurry/dist/esm/index.d.ts +1115 -0
  185. package/node_modules/path-scurry/dist/esm/index.d.ts.map +1 -0
  186. package/node_modules/path-scurry/dist/esm/index.js +1983 -0
  187. package/node_modules/path-scurry/dist/esm/index.js.map +1 -0
  188. package/node_modules/path-scurry/dist/esm/package.json +3 -0
  189. package/node_modules/path-scurry/node_modules/lru-cache/LICENSE.md +55 -0
  190. package/node_modules/path-scurry/node_modules/lru-cache/README.md +383 -0
  191. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +1323 -0
  192. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -0
  193. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +1589 -0
  194. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -0
  195. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +2 -0
  196. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +7 -0
  197. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/package.json +3 -0
  198. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +1323 -0
  199. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -0
  200. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +1585 -0
  201. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -0
  202. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +2 -0
  203. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +7 -0
  204. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/package.json +3 -0
  205. package/node_modules/path-scurry/node_modules/lru-cache/package.json +101 -0
  206. package/node_modules/path-scurry/package.json +88 -0
  207. package/node_modules/rimraf/LICENSE.md +55 -0
  208. package/node_modules/rimraf/README.md +226 -0
  209. package/node_modules/rimraf/dist/commonjs/default-tmp.d.ts +3 -0
  210. package/node_modules/rimraf/dist/commonjs/default-tmp.d.ts.map +1 -0
  211. package/node_modules/rimraf/dist/commonjs/default-tmp.js +58 -0
  212. package/node_modules/rimraf/dist/commonjs/default-tmp.js.map +1 -0
  213. package/node_modules/rimraf/dist/commonjs/error.d.ts +6 -0
  214. package/node_modules/rimraf/dist/commonjs/error.d.ts.map +1 -0
  215. package/node_modules/rimraf/dist/commonjs/error.js +10 -0
  216. package/node_modules/rimraf/dist/commonjs/error.js.map +1 -0
  217. package/node_modules/rimraf/dist/commonjs/fix-eperm.d.ts +3 -0
  218. package/node_modules/rimraf/dist/commonjs/fix-eperm.d.ts.map +1 -0
  219. package/node_modules/rimraf/dist/commonjs/fix-eperm.js +38 -0
  220. package/node_modules/rimraf/dist/commonjs/fix-eperm.js.map +1 -0
  221. package/node_modules/rimraf/dist/commonjs/fs.d.ts +15 -0
  222. package/node_modules/rimraf/dist/commonjs/fs.d.ts.map +1 -0
  223. package/node_modules/rimraf/dist/commonjs/fs.js +33 -0
  224. package/node_modules/rimraf/dist/commonjs/fs.js.map +1 -0
  225. package/node_modules/rimraf/dist/commonjs/ignore-enoent.d.ts +3 -0
  226. package/node_modules/rimraf/dist/commonjs/ignore-enoent.d.ts.map +1 -0
  227. package/node_modules/rimraf/dist/commonjs/ignore-enoent.js +24 -0
  228. package/node_modules/rimraf/dist/commonjs/ignore-enoent.js.map +1 -0
  229. package/node_modules/rimraf/dist/commonjs/index.d.ts +50 -0
  230. package/node_modules/rimraf/dist/commonjs/index.d.ts.map +1 -0
  231. package/node_modules/rimraf/dist/commonjs/index.js +78 -0
  232. package/node_modules/rimraf/dist/commonjs/index.js.map +1 -0
  233. package/node_modules/rimraf/dist/commonjs/opt-arg.d.ts +34 -0
  234. package/node_modules/rimraf/dist/commonjs/opt-arg.d.ts.map +1 -0
  235. package/node_modules/rimraf/dist/commonjs/opt-arg.js +53 -0
  236. package/node_modules/rimraf/dist/commonjs/opt-arg.js.map +1 -0
  237. package/node_modules/rimraf/dist/commonjs/package.json +3 -0
  238. package/node_modules/rimraf/dist/commonjs/path-arg.d.ts +4 -0
  239. package/node_modules/rimraf/dist/commonjs/path-arg.d.ts.map +1 -0
  240. package/node_modules/rimraf/dist/commonjs/path-arg.js +48 -0
  241. package/node_modules/rimraf/dist/commonjs/path-arg.js.map +1 -0
  242. package/node_modules/rimraf/dist/commonjs/readdir-or-error.d.ts +3 -0
  243. package/node_modules/rimraf/dist/commonjs/readdir-or-error.d.ts.map +1 -0
  244. package/node_modules/rimraf/dist/commonjs/readdir-or-error.js +19 -0
  245. package/node_modules/rimraf/dist/commonjs/readdir-or-error.js.map +1 -0
  246. package/node_modules/rimraf/dist/commonjs/retry-busy.d.ts +8 -0
  247. package/node_modules/rimraf/dist/commonjs/retry-busy.d.ts.map +1 -0
  248. package/node_modules/rimraf/dist/commonjs/retry-busy.js +65 -0
  249. package/node_modules/rimraf/dist/commonjs/retry-busy.js.map +1 -0
  250. package/node_modules/rimraf/dist/commonjs/rimraf-manual.d.ts +3 -0
  251. package/node_modules/rimraf/dist/commonjs/rimraf-manual.d.ts.map +1 -0
  252. package/node_modules/rimraf/dist/commonjs/rimraf-manual.js +8 -0
  253. package/node_modules/rimraf/dist/commonjs/rimraf-manual.js.map +1 -0
  254. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.d.ts +4 -0
  255. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.d.ts.map +1 -0
  256. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.js +138 -0
  257. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.js.map +1 -0
  258. package/node_modules/rimraf/dist/commonjs/rimraf-native.d.ts +4 -0
  259. package/node_modules/rimraf/dist/commonjs/rimraf-native.d.ts.map +1 -0
  260. package/node_modules/rimraf/dist/commonjs/rimraf-native.js +24 -0
  261. package/node_modules/rimraf/dist/commonjs/rimraf-native.js.map +1 -0
  262. package/node_modules/rimraf/dist/commonjs/rimraf-posix.d.ts +4 -0
  263. package/node_modules/rimraf/dist/commonjs/rimraf-posix.d.ts.map +1 -0
  264. package/node_modules/rimraf/dist/commonjs/rimraf-posix.js +103 -0
  265. package/node_modules/rimraf/dist/commonjs/rimraf-posix.js.map +1 -0
  266. package/node_modules/rimraf/dist/commonjs/rimraf-windows.d.ts +4 -0
  267. package/node_modules/rimraf/dist/commonjs/rimraf-windows.d.ts.map +1 -0
  268. package/node_modules/rimraf/dist/commonjs/rimraf-windows.js +159 -0
  269. package/node_modules/rimraf/dist/commonjs/rimraf-windows.js.map +1 -0
  270. package/node_modules/rimraf/dist/commonjs/use-native.d.ts +4 -0
  271. package/node_modules/rimraf/dist/commonjs/use-native.d.ts.map +1 -0
  272. package/node_modules/rimraf/dist/commonjs/use-native.js +18 -0
  273. package/node_modules/rimraf/dist/commonjs/use-native.js.map +1 -0
  274. package/node_modules/rimraf/dist/esm/bin.d.mts +3 -0
  275. package/node_modules/rimraf/dist/esm/bin.d.mts.map +1 -0
  276. package/node_modules/rimraf/dist/esm/bin.mjs +250 -0
  277. package/node_modules/rimraf/dist/esm/bin.mjs.map +1 -0
  278. package/node_modules/rimraf/dist/esm/default-tmp.d.ts +3 -0
  279. package/node_modules/rimraf/dist/esm/default-tmp.d.ts.map +1 -0
  280. package/node_modules/rimraf/dist/esm/default-tmp.js +55 -0
  281. package/node_modules/rimraf/dist/esm/default-tmp.js.map +1 -0
  282. package/node_modules/rimraf/dist/esm/error.d.ts +6 -0
  283. package/node_modules/rimraf/dist/esm/error.d.ts.map +1 -0
  284. package/node_modules/rimraf/dist/esm/error.js +5 -0
  285. package/node_modules/rimraf/dist/esm/error.js.map +1 -0
  286. package/node_modules/rimraf/dist/esm/fix-eperm.d.ts +3 -0
  287. package/node_modules/rimraf/dist/esm/fix-eperm.d.ts.map +1 -0
  288. package/node_modules/rimraf/dist/esm/fix-eperm.js +33 -0
  289. package/node_modules/rimraf/dist/esm/fix-eperm.js.map +1 -0
  290. package/node_modules/rimraf/dist/esm/fs.d.ts +15 -0
  291. package/node_modules/rimraf/dist/esm/fs.d.ts.map +1 -0
  292. package/node_modules/rimraf/dist/esm/fs.js +18 -0
  293. package/node_modules/rimraf/dist/esm/fs.js.map +1 -0
  294. package/node_modules/rimraf/dist/esm/ignore-enoent.d.ts +3 -0
  295. package/node_modules/rimraf/dist/esm/ignore-enoent.d.ts.map +1 -0
  296. package/node_modules/rimraf/dist/esm/ignore-enoent.js +19 -0
  297. package/node_modules/rimraf/dist/esm/ignore-enoent.js.map +1 -0
  298. package/node_modules/rimraf/dist/esm/index.d.ts +50 -0
  299. package/node_modules/rimraf/dist/esm/index.d.ts.map +1 -0
  300. package/node_modules/rimraf/dist/esm/index.js +70 -0
  301. package/node_modules/rimraf/dist/esm/index.js.map +1 -0
  302. package/node_modules/rimraf/dist/esm/opt-arg.d.ts +34 -0
  303. package/node_modules/rimraf/dist/esm/opt-arg.d.ts.map +1 -0
  304. package/node_modules/rimraf/dist/esm/opt-arg.js +46 -0
  305. package/node_modules/rimraf/dist/esm/opt-arg.js.map +1 -0
  306. package/node_modules/rimraf/dist/esm/package.json +3 -0
  307. package/node_modules/rimraf/dist/esm/path-arg.d.ts +4 -0
  308. package/node_modules/rimraf/dist/esm/path-arg.d.ts.map +1 -0
  309. package/node_modules/rimraf/dist/esm/path-arg.js +46 -0
  310. package/node_modules/rimraf/dist/esm/path-arg.js.map +1 -0
  311. package/node_modules/rimraf/dist/esm/readdir-or-error.d.ts +3 -0
  312. package/node_modules/rimraf/dist/esm/readdir-or-error.d.ts.map +1 -0
  313. package/node_modules/rimraf/dist/esm/readdir-or-error.js +14 -0
  314. package/node_modules/rimraf/dist/esm/readdir-or-error.js.map +1 -0
  315. package/node_modules/rimraf/dist/esm/retry-busy.d.ts +8 -0
  316. package/node_modules/rimraf/dist/esm/retry-busy.d.ts.map +1 -0
  317. package/node_modules/rimraf/dist/esm/retry-busy.js +60 -0
  318. package/node_modules/rimraf/dist/esm/retry-busy.js.map +1 -0
  319. package/node_modules/rimraf/dist/esm/rimraf-manual.d.ts +3 -0
  320. package/node_modules/rimraf/dist/esm/rimraf-manual.d.ts.map +1 -0
  321. package/node_modules/rimraf/dist/esm/rimraf-manual.js +5 -0
  322. package/node_modules/rimraf/dist/esm/rimraf-manual.js.map +1 -0
  323. package/node_modules/rimraf/dist/esm/rimraf-move-remove.d.ts +4 -0
  324. package/node_modules/rimraf/dist/esm/rimraf-move-remove.d.ts.map +1 -0
  325. package/node_modules/rimraf/dist/esm/rimraf-move-remove.js +133 -0
  326. package/node_modules/rimraf/dist/esm/rimraf-move-remove.js.map +1 -0
  327. package/node_modules/rimraf/dist/esm/rimraf-native.d.ts +4 -0
  328. package/node_modules/rimraf/dist/esm/rimraf-native.d.ts.map +1 -0
  329. package/node_modules/rimraf/dist/esm/rimraf-native.js +19 -0
  330. package/node_modules/rimraf/dist/esm/rimraf-native.js.map +1 -0
  331. package/node_modules/rimraf/dist/esm/rimraf-posix.d.ts +4 -0
  332. package/node_modules/rimraf/dist/esm/rimraf-posix.d.ts.map +1 -0
  333. package/node_modules/rimraf/dist/esm/rimraf-posix.js +98 -0
  334. package/node_modules/rimraf/dist/esm/rimraf-posix.js.map +1 -0
  335. package/node_modules/rimraf/dist/esm/rimraf-windows.d.ts +4 -0
  336. package/node_modules/rimraf/dist/esm/rimraf-windows.d.ts.map +1 -0
  337. package/node_modules/rimraf/dist/esm/rimraf-windows.js +154 -0
  338. package/node_modules/rimraf/dist/esm/rimraf-windows.js.map +1 -0
  339. package/node_modules/rimraf/dist/esm/use-native.d.ts +4 -0
  340. package/node_modules/rimraf/dist/esm/use-native.d.ts.map +1 -0
  341. package/node_modules/rimraf/dist/esm/use-native.js +15 -0
  342. package/node_modules/rimraf/dist/esm/use-native.js.map +1 -0
  343. package/node_modules/rimraf/package.json +92 -0
  344. package/package.json +152 -0
  345. package/scripts/install-scanners.js +258 -0
  346. package/scripts/watchdog.js +147 -0
  347. package/src/analyzers/CSSAnalyzer.js +297 -0
  348. package/src/analyzers/ConfigValidator.js +690 -0
  349. package/src/analyzers/ESLintAnalyzer.js +320 -0
  350. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  351. package/src/analyzers/PrettierFormatter.js +247 -0
  352. package/src/analyzers/PythonAnalyzer.js +283 -0
  353. package/src/analyzers/SecurityAnalyzer.js +729 -0
  354. package/src/analyzers/SparrowAnalyzer.js +341 -0
  355. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  356. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  357. package/src/analyzers/codeCloneDetector/detector.js +250 -0
  358. package/src/analyzers/codeCloneDetector/index.js +192 -0
  359. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  360. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  361. package/src/analyzers/codeCloneDetector/scanner.js +88 -0
  362. package/src/core/agentPool.js +1957 -0
  363. package/src/core/agentScheduler.js +3212 -0
  364. package/src/core/contextManager.js +709 -0
  365. package/src/core/flowExecutor.js +928 -0
  366. package/src/core/messageProcessor.js +808 -0
  367. package/src/core/orchestrator.js +584 -0
  368. package/src/core/stateManager.js +1500 -0
  369. package/src/index.js +972 -0
  370. package/src/interfaces/cli.js +553 -0
  371. package/src/interfaces/terminal/__tests__/smoke/advancedFeatures.test.js +208 -0
  372. package/src/interfaces/terminal/__tests__/smoke/agentControl.test.js +236 -0
  373. package/src/interfaces/terminal/__tests__/smoke/agents.test.js +138 -0
  374. package/src/interfaces/terminal/__tests__/smoke/components.test.js +137 -0
  375. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +350 -0
  376. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -0
  377. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +332 -0
  378. package/src/interfaces/terminal/__tests__/smoke/messages.test.js +256 -0
  379. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +388 -0
  380. package/src/interfaces/terminal/api/apiClient.js +299 -0
  381. package/src/interfaces/terminal/api/messageRouter.js +262 -0
  382. package/src/interfaces/terminal/api/session.js +266 -0
  383. package/src/interfaces/terminal/api/websocket.js +497 -0
  384. package/src/interfaces/terminal/components/AgentCreator.js +705 -0
  385. package/src/interfaces/terminal/components/AgentEditor.js +678 -0
  386. package/src/interfaces/terminal/components/AgentSwitcher.js +330 -0
  387. package/src/interfaces/terminal/components/ErrorBoundary.js +92 -0
  388. package/src/interfaces/terminal/components/ErrorPanel.js +264 -0
  389. package/src/interfaces/terminal/components/Header.js +28 -0
  390. package/src/interfaces/terminal/components/HelpPanel.js +231 -0
  391. package/src/interfaces/terminal/components/InputBox.js +118 -0
  392. package/src/interfaces/terminal/components/Layout.js +603 -0
  393. package/src/interfaces/terminal/components/LoadingSpinner.js +71 -0
  394. package/src/interfaces/terminal/components/MessageList.js +281 -0
  395. package/src/interfaces/terminal/components/MultilineTextInput.js +251 -0
  396. package/src/interfaces/terminal/components/SearchPanel.js +265 -0
  397. package/src/interfaces/terminal/components/SettingsPanel.js +415 -0
  398. package/src/interfaces/terminal/components/StatusBar.js +65 -0
  399. package/src/interfaces/terminal/components/TextInput.js +127 -0
  400. package/src/interfaces/terminal/config/agentEditorConstants.js +227 -0
  401. package/src/interfaces/terminal/config/constants.js +393 -0
  402. package/src/interfaces/terminal/index.js +168 -0
  403. package/src/interfaces/terminal/state/useAgentControl.js +496 -0
  404. package/src/interfaces/terminal/state/useAgents.js +537 -0
  405. package/src/interfaces/terminal/state/useConnection.js +444 -0
  406. package/src/interfaces/terminal/state/useMessages.js +630 -0
  407. package/src/interfaces/terminal/state/useTools.js +554 -0
  408. package/src/interfaces/terminal/utils/debugLogger.js +44 -0
  409. package/src/interfaces/terminal/utils/settingsStorage.js +232 -0
  410. package/src/interfaces/terminal/utils/theme.js +85 -0
  411. package/src/interfaces/webServer.js +5457 -0
  412. package/src/modules/fileExplorer/controller.js +413 -0
  413. package/src/modules/fileExplorer/index.js +37 -0
  414. package/src/modules/fileExplorer/middleware.js +92 -0
  415. package/src/modules/fileExplorer/routes.js +158 -0
  416. package/src/modules/fileExplorer/types.js +44 -0
  417. package/src/services/agentActivityService.js +399 -0
  418. package/src/services/aiService.js +2618 -0
  419. package/src/services/apiKeyManager.js +334 -0
  420. package/src/services/benchmarkService.js +196 -0
  421. package/src/services/budgetService.js +565 -0
  422. package/src/services/contextInjectionService.js +268 -0
  423. package/src/services/conversationCompactionService.js +1103 -0
  424. package/src/services/credentialVault.js +685 -0
  425. package/src/services/errorHandler.js +810 -0
  426. package/src/services/fileAttachmentService.js +547 -0
  427. package/src/services/flowContextService.js +189 -0
  428. package/src/services/memoryService.js +521 -0
  429. package/src/services/modelRouterService.js +365 -0
  430. package/src/services/modelsService.js +323 -0
  431. package/src/services/ollamaService.js +452 -0
  432. package/src/services/portRegistry.js +336 -0
  433. package/src/services/portTracker.js +223 -0
  434. package/src/services/projectDetector.js +404 -0
  435. package/src/services/promptService.js +372 -0
  436. package/src/services/qualityInspector.js +796 -0
  437. package/src/services/scheduleService.js +725 -0
  438. package/src/services/serviceRegistry.js +386 -0
  439. package/src/services/skillsService.js +486 -0
  440. package/src/services/telegramService.js +920 -0
  441. package/src/services/tokenCountingService.js +316 -0
  442. package/src/services/visualEditorBridge.js +1033 -0
  443. package/src/services/visualEditorServer.js +1727 -0
  444. package/src/services/whatsappService.js +663 -0
  445. package/src/tools/__tests__/webTool.e2e.test.js +569 -0
  446. package/src/tools/__tests__/webTool.unit.test.js +195 -0
  447. package/src/tools/agentCommunicationTool.js +1343 -0
  448. package/src/tools/agentDelayTool.js +498 -0
  449. package/src/tools/asyncToolManager.js +604 -0
  450. package/src/tools/baseTool.js +887 -0
  451. package/src/tools/browserTool.js +897 -0
  452. package/src/tools/cloneDetectionTool.js +581 -0
  453. package/src/tools/codeMapTool.js +857 -0
  454. package/src/tools/dependencyResolverTool.js +1212 -0
  455. package/src/tools/docxTool.js +623 -0
  456. package/src/tools/excelTool.js +636 -0
  457. package/src/tools/fileContentReplaceTool.js +840 -0
  458. package/src/tools/fileTreeTool.js +833 -0
  459. package/src/tools/filesystemTool.js +1217 -0
  460. package/src/tools/helpTool.js +198 -0
  461. package/src/tools/imageTool.js +1034 -0
  462. package/src/tools/importAnalyzerTool.js +1056 -0
  463. package/src/tools/jobDoneTool.js +388 -0
  464. package/src/tools/memoryTool.js +554 -0
  465. package/src/tools/pdfTool.js +627 -0
  466. package/src/tools/seekTool.js +883 -0
  467. package/src/tools/skillsTool.js +276 -0
  468. package/src/tools/staticAnalysisTool.js +2146 -0
  469. package/src/tools/taskManagerTool.js +2836 -0
  470. package/src/tools/terminalTool.js +2486 -0
  471. package/src/tools/userPromptTool.js +474 -0
  472. package/src/tools/videoTool.js +1139 -0
  473. package/src/tools/visionTool.js +507 -0
  474. package/src/tools/visualEditorTool.js +1175 -0
  475. package/src/tools/webTool.js +3114 -0
  476. package/src/tools/whatsappTool.js +457 -0
  477. package/src/types/agent.js +519 -0
  478. package/src/types/contextReference.js +972 -0
  479. package/src/types/conversation.js +730 -0
  480. package/src/types/toolCommand.js +747 -0
  481. package/src/utilities/attachmentValidator.js +288 -0
  482. package/src/utilities/browserStealth.js +630 -0
  483. package/src/utilities/configManager.js +618 -0
  484. package/src/utilities/constants.js +870 -0
  485. package/src/utilities/directoryAccessManager.js +566 -0
  486. package/src/utilities/fileProcessor.js +307 -0
  487. package/src/utilities/humanBehavior.js +453 -0
  488. package/src/utilities/jsonRepair.js +242 -0
  489. package/src/utilities/logger.js +436 -0
  490. package/src/utilities/platformUtils.js +255 -0
  491. package/src/utilities/platformUtils.test.js +98 -0
  492. package/src/utilities/stealthConstants.js +377 -0
  493. package/src/utilities/structuredFileValidator.js +699 -0
  494. package/src/utilities/tagParser.js +878 -0
  495. package/src/utilities/toolConstants.js +415 -0
  496. package/src/utilities/userDataDir.js +300 -0
  497. package/web-ui/build/brands/autopilot/favicon.svg +1 -0
  498. package/web-ui/build/brands/autopilot/logo.webp +0 -0
  499. package/web-ui/build/brands/onbuzz/favicon.svg +1 -0
  500. package/web-ui/build/brands/onbuzz/logo-text.webp +0 -0
  501. package/web-ui/build/brands/onbuzz/logo.webp +0 -0
  502. package/web-ui/build/index.html +15 -0
  503. package/web-ui/build/logo.png +0 -0
  504. package/web-ui/build/logo2.png +0 -0
  505. package/web-ui/build/static/index-SmQFfvBs.js +746 -0
  506. package/web-ui/build/static/index-V2ySwjHp.css +1 -0
@@ -0,0 +1,3114 @@
1
+ /**
2
+ * WebTool - Web browsing and automation with Puppeteer
3
+ *
4
+ * Purpose:
5
+ * - Search the web using known search engines
6
+ * - Fetch web content in various formats
7
+ * - Interactive browser automation with command chaining
8
+ * - Tab management with agent isolation
9
+ * - Screenshot capture and AI-powered analysis
10
+ * - Mouse and keyboard event simulation
11
+ *
12
+ * NOTE: This tool replaces the deprecated BrowserTool (December 2024).
13
+ * WebTool provides equivalent functionality with a singleton browser instance
14
+ * architecture for better resource management.
15
+ *
16
+ * ============================================================================
17
+ * TODO: FEATURES TO ADD (migrated from deprecated BrowserTool)
18
+ * ============================================================================
19
+ *
20
+ * TODO 1: TIME-BASED WAIT ACTION
21
+ * -----------------------------------------------------------------------------
22
+ * Add a time-based wait capability alongside the existing 'wait-for-selector'.
23
+ * The deprecated BrowserTool supported: { "action": "wait", "waitTime": 3000 }
24
+ *
25
+ * Implementation details:
26
+ * - Add new action type: 'wait' or 'delay' (distinct from 'wait-for-selector')
27
+ * - Accept 'waitTime' parameter in milliseconds (e.g., 1000 = 1 second)
28
+ * - Use: await new Promise(resolve => setTimeout(resolve, waitTime))
29
+ * - Max limit suggestion: 30000ms (30 seconds) to prevent abuse
30
+ * - Use case: Wait for JavaScript-rendered content, animations, or rate limiting
31
+ *
32
+ * Example invocation:
33
+ * ```json
34
+ * {
35
+ * "toolId": "web",
36
+ * "operation": "interactive",
37
+ * "tabName": "my-tab",
38
+ * "commands": [
39
+ * { "action": "navigate", "url": "https://example.com" },
40
+ * { "action": "wait", "waitTime": 2000 },
41
+ * { "action": "screenshot" }
42
+ * ]
43
+ * }
44
+ * ```
45
+ *
46
+ * TODO 2: IMPROVED TYPE ACTION WITH ELEMENT WAITING AND CLEARING
47
+ * -----------------------------------------------------------------------------
48
+ * Enhance the 'type' action to automatically wait for the element and optionally
49
+ * clear existing content before typing. The deprecated BrowserTool had better
50
+ * handling for form inputs.
51
+ *
52
+ * Implementation details:
53
+ * - Before typing, wait for the selector to be visible (with configurable timeout)
54
+ * - Add 'clearFirst' option (boolean, default: true) to clear input before typing
55
+ * - Add 'delay' option for human-like typing speed (ms between keystrokes)
56
+ * - Use page.click(selector) to focus, then page.keyboard.type() for realistic input
57
+ * - Consider using page.$eval to clear: element.value = ''
58
+ *
59
+ * Example invocation:
60
+ * ```json
61
+ * {
62
+ * "action": "type",
63
+ * "selector": "#search-input",
64
+ * "text": "search query",
65
+ * "clearFirst": true,
66
+ * "delay": 50,
67
+ * "waitForSelector": true,
68
+ * "timeout": 5000
69
+ * }
70
+ * ```
71
+ *
72
+ * TODO 3: FLAT ACTION STRUCTURE (NO WRAPPER REQUIRED)
73
+ * -----------------------------------------------------------------------------
74
+ * Allow executing single actions without requiring the 'open-tab' or 'interactive'
75
+ * wrapper structure. This makes simple operations more concise.
76
+ *
77
+ * Implementation details:
78
+ * - Detect when params contain a single 'action' at the top level
79
+ * - Auto-create or reuse a default tab for the agent
80
+ * - Execute the action directly without requiring explicit tab management
81
+ * - Useful for quick one-off operations like screenshots or navigation
82
+ *
83
+ * Example invocation (simplified):
84
+ * ```json
85
+ * {
86
+ * "toolId": "web",
87
+ * "action": "navigate",
88
+ * "url": "https://example.com"
89
+ * }
90
+ * ```
91
+ * Instead of:
92
+ * ```json
93
+ * {
94
+ * "toolId": "web",
95
+ * "operation": "interactive",
96
+ * "tabName": "default",
97
+ * "commands": [{ "action": "navigate", "url": "https://example.com" }]
98
+ * }
99
+ * ```
100
+ *
101
+ * TODO 4: HUMAN-LIKE BROWSING DELAYS
102
+ * -----------------------------------------------------------------------------
103
+ * Add configurable delays between actions to make browsing appear more human-like.
104
+ * This helps avoid bot detection and rate limiting on websites.
105
+ *
106
+ * Implementation details:
107
+ * - Add 'humanMode' or 'naturalDelay' option to interactive operations
108
+ * - When enabled, add random delays (e.g., 500-2000ms) between commands
109
+ * - Add random mouse movements before clicks
110
+ * - Vary typing speed with random delays between keystrokes
111
+ * - Consider adding scroll jitter and viewport variations
112
+ *
113
+ * Example:
114
+ * ```json
115
+ * {
116
+ * "toolId": "web",
117
+ * "operation": "interactive",
118
+ * "humanMode": true,
119
+ * "commands": [...]
120
+ * }
121
+ * ```
122
+ * ============================================================================
123
+ */
124
+
125
+ import { BaseTool } from './baseTool.js';
126
+ import TagParser from '../utilities/tagParser.js';
127
+ import path from 'path';
128
+ import fs from 'fs/promises';
129
+ import os from 'os';
130
+
131
+ import {
132
+ TOOL_STATUS,
133
+ SYSTEM_DEFAULTS
134
+ } from '../utilities/constants.js';
135
+
136
+ // Stealth browser and human behavior utilities
137
+ import {
138
+ createStealthBrowser,
139
+ createStealthPage,
140
+ getRandomViewport
141
+ } from '../utilities/browserStealth.js';
142
+
143
+ import {
144
+ createHumanCursor,
145
+ humanType,
146
+ humanWait,
147
+ humanScroll,
148
+ humanSubmit
149
+ } from '../utilities/humanBehavior.js';
150
+
151
+ import { BROWSER_CONFIG, KNOWN_SITES, LOGIN_FIELD_PATTERNS } from '../utilities/stealthConstants.js';
152
+
153
+ // Credential vault for secure authentication
154
+ import { getCredentialVault } from '../services/credentialVault.js';
155
+
156
+ class WebTool extends BaseTool {
157
+ constructor(config = {}, logger = null) {
158
+ super(config, logger);
159
+
160
+ // Tool metadata
161
+ this.requiresProject = false;
162
+ this.isAsync = true;
163
+ this.builtinDelay = 3000; // 3 second delay after browser operations
164
+
165
+ // Browser instance (singleton per system)
166
+ this.browser = null;
167
+ this.browserInitializing = false;
168
+
169
+ // Tab tracking: Map<agentId, Map<tabName, tabInfo>>
170
+ this.agentTabs = new Map();
171
+
172
+ // Known search engines
173
+ this.searchEngines = [
174
+ {
175
+ name: 'google',
176
+ url: 'https://www.google.com/search?q=',
177
+ searchSelector: 'input[name="q"]',
178
+ submitSelector: 'input[type="submit"], button[type="submit"]',
179
+ // Google's DOM varies by region/bot-detection — use broad selectors
180
+ resultsSelector: '#search .g a[href], #rso a[href], .g a[href], div[data-hveid] a[href]',
181
+ waitSelector: '#search, #rso, #main'
182
+ },
183
+ {
184
+ name: 'bing',
185
+ url: 'https://www.bing.com/search?q=',
186
+ searchSelector: 'input[name="q"]',
187
+ submitSelector: 'input[type="submit"]',
188
+ resultsSelector: '.b_algo a, .b_algo h2 a, li.b_algo a',
189
+ waitSelector: '#b_results, #b_content'
190
+ },
191
+ {
192
+ name: 'duckduckgo',
193
+ url: 'https://duckduckgo.com/?q=',
194
+ searchSelector: 'input[name="q"]',
195
+ submitSelector: 'button[type="submit"]',
196
+ resultsSelector: '.result__a, a[data-testid="result-title-a"], article a[href]',
197
+ waitSelector: '#links, [data-testid="mainline"], .results'
198
+ }
199
+ ];
200
+
201
+ // Configuration
202
+ this.TAB_IDLE_TIMEOUT = config.tabIdleTimeout || 60 * 60 * 1000; // 1 hour
203
+ this.CLEANUP_INTERVAL = config.cleanupInterval || 5 * 60 * 1000; // 5 minutes
204
+ this.DEFAULT_TIMEOUT = config.defaultTimeout || 60000; // 60 seconds
205
+ this.TEMP_DIR = config.tempDir || path.join(os.tmpdir(), 'webtool-screenshots');
206
+
207
+ // Start cleanup timer
208
+ this.cleanupTimer = null;
209
+ this.startCleanupTimer();
210
+
211
+ // Ensure temp directory exists
212
+ this.ensureTempDir();
213
+ }
214
+
215
+ /**
216
+ * Get tool description for LLM consumption
217
+ * @returns {string} Tool description
218
+ */
219
+ getDescription() {
220
+ return `
221
+ Web Tool: Browse, search, and automate web interactions using Puppeteer.
222
+
223
+ ## AUTHENTICATED BROWSING WORKFLOW (IMPORTANT - READ FIRST)
224
+
225
+ When you need to browse a site that requires login, follow this workflow:
226
+
227
+ ### Option A: Keep tab open (RECOMMENDED for immediate follow-up browsing)
228
+ \`\`\`json
229
+ Step 1: Authenticate with keepTabOpen
230
+ {"toolId": "web", "operation": "authenticate", "site": "mysite", "loginUrl": "https://example.com/login", "tabName": "session", "keepTabOpen": true}
231
+
232
+ Step 2: Continue browsing in same tab (MUST use stealthLevel: "maximum")
233
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
234
+ {"type": "switch-tab", "name": "session"},
235
+ {"type": "navigate", "url": "https://example.com/dashboard"},
236
+ {"type": "extract-text", "selector": ".content"}
237
+ ]}
238
+ \`\`\`
239
+
240
+ ### Option B: Open new tab after authentication (cookies auto-restored)
241
+ \`\`\`json
242
+ Step 1: Authenticate (tab closes after login, cookies saved)
243
+ {"toolId": "web", "operation": "authenticate", "site": "mysite", "loginUrl": "https://example.com/login"}
244
+
245
+ Step 2: Open new tab - cookies are automatically restored (MUST use stealthLevel: "maximum")
246
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
247
+ {"type": "open-tab", "name": "browsing", "url": "https://example.com/dashboard", "nestedActions": [
248
+ {"type": "extract-text", "selector": ".content"}
249
+ ]}
250
+ ]}
251
+ \`\`\`
252
+
253
+ **CRITICAL RULES:**
254
+ 1. ALWAYS use stealthLevel: "maximum" after authentication (changing levels restarts browser)
255
+ 2. NEVER type passwords with interactive - always use authenticate operation
256
+ 3. The agent is PAUSED while waiting for user credentials - this is normal
257
+
258
+ ## AUTHENTICATION DETAILS
259
+
260
+ A secure modal appears for user to enter credentials. You NEVER see or type credentials.
261
+
262
+ ### Pre-configured sites (no selectors needed):
263
+ linkedin, github, google, twitter
264
+ \`\`\`json
265
+ {"toolId": "web", "operation": "authenticate", "site": "linkedin"}
266
+ \`\`\`
267
+
268
+ ### Custom sites (provide selectors):
269
+ First FETCH the login page to identify form selectors, then authenticate:
270
+ \`\`\`json
271
+ {"toolId": "web", "operation": "authenticate", "site": "customsite", "loginUrl": "https://example.com/login", "usernameSelector": "input[name='email']", "passwordSelector": "input[type='password']", "submitSelector": "button[type='submit']"}
272
+ \`\`\`
273
+
274
+ ## STEALTH LEVELS
275
+ - "standard" (default): Headless browser, invisible. Good for public pages.
276
+ - "maximum": Visible Chrome window. REQUIRED for authenticated browsing and bot detection bypass.
277
+
278
+ ## OPERATIONS
279
+
280
+ 1. SEARCH - Search the web
281
+ \`\`\`json
282
+ {"toolId": "web", "operation": "search", "query": "search terms", "engine": "google", "maxResults": 10}
283
+ \`\`\`
284
+
285
+ 2. FETCH - Get page content (public pages only)
286
+ \`\`\`json
287
+ {"toolId": "web", "operation": "fetch", "url": "https://example.com", "formats": ["title", "text", "links"]}
288
+ \`\`\`
289
+
290
+ 3. INTERACTIVE - Browser automation
291
+ \`\`\`json
292
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
293
+ {"type": "open-tab", "name": "main", "url": "https://example.com", "nestedActions": [
294
+ {"type": "wait-for", "selector": ".content"},
295
+ {"type": "click", "selector": "button"},
296
+ {"type": "extract-text", "selector": "#content"},
297
+ {"type": "screenshot", "format": "file", "path": "screenshot.png"}
298
+ ]}
299
+ ]}
300
+ \`\`\`
301
+
302
+ ## INTERACTIVE ACTIONS
303
+ - Tab: open-tab, close-tab, switch-tab, list-tabs
304
+ - Navigation: navigate, wait-for, wait (time-based delay)
305
+ - Input: click, type, press, hover, scroll, select, submit
306
+ - Extract: extract-text, extract-links, get-source, screenshot, get-field-values, evaluate
307
+
308
+ ## FORM FILLING WORKFLOW (RECOMMENDED)
309
+
310
+ When filling and submitting web forms, follow this pattern:
311
+
312
+ ### Step 1: Discover the form — fetch page text, then get-source to find selectors
313
+ \`\`\`json
314
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
315
+ {"type": "open-tab", "name": "form", "url": "https://example.com/signup", "nestedActions": [
316
+ {"type": "extract-text", "selector": "body"},
317
+ {"type": "get-source"}
318
+ ]}
319
+ ]}
320
+ \`\`\`
321
+
322
+ ### Step 2: Fill the form — type into inputs, click checkboxes, select dropdowns
323
+ \`\`\`json
324
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
325
+ {"type": "open-tab", "name": "form", "nestedActions": [
326
+ {"type": "type", "selector": "#name", "text": "John Doe"},
327
+ {"type": "type", "selector": "#email", "text": "john@example.com"},
328
+ {"type": "select", "selector": "#country", "value": "US"},
329
+ {"type": "click", "selector": "input[name='agree']"},
330
+ {"type": "type", "selector": "#phone", "text": "+1234567890"}
331
+ ]}
332
+ ]}
333
+ \`\`\`
334
+ NOTE: For checkboxes, use attribute selectors like input[name="agree"] or input[value="Option text"] instead of dynamic IDs.
335
+ NOTE: click on checkbox/radio returns { checked: true/false } so you can verify the toggle state.
336
+
337
+ ### Step 3: Verify form state before submitting
338
+ \`\`\`json
339
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
340
+ {"type": "open-tab", "name": "form", "nestedActions": [
341
+ {"type": "get-field-values", "selectors": ["#name", "#email", "#country", "input[name='agree']"]}
342
+ ]}
343
+ ]}
344
+ \`\`\`
345
+ Returns: { fields: { "#name": { value: "John Doe" }, "input[name='agree']": { checked: true } } }
346
+
347
+ ### Step 4: Submit and check result
348
+ \`\`\`json
349
+ {"toolId": "web", "operation": "interactive", "stealthLevel": "maximum", "actions": [
350
+ {"type": "open-tab", "name": "form", "nestedActions": [
351
+ {"type": "submit", "selector": "button[type='submit']"}
352
+ ]}
353
+ ]}
354
+ \`\`\`
355
+ Submit returns: { submitConfirmed: true/false, successMessage: "...", networkResponse: [...], formErrors: [...] }
356
+ If submitConfirmed is false, check formErrors for validation messages.
357
+ If submit button is disabled, it returns an error — use get-field-values to find unfilled required fields.
358
+
359
+ ## KEY ACTIONS REFERENCE
360
+
361
+ **evaluate** — Run arbitrary JavaScript in the page. Use for custom logic the other actions can't cover.
362
+ \`\`\`json
363
+ {"type": "evaluate", "script": "return document.querySelector('#myField').value"}
364
+ {"type": "evaluate", "script": "return document.querySelectorAll('.item').length"}
365
+ \`\`\`
366
+
367
+ **select** — Select a dropdown option by value or visible text. Works with native <select> and custom dropdowns.
368
+ \`\`\`json
369
+ {"type": "select", "selector": "#country", "value": "United States"}
370
+ \`\`\`
371
+
372
+ **get-field-values** — Read current values of multiple form fields at once (inputs, checkboxes, selects).
373
+ \`\`\`json
374
+ {"type": "get-field-values", "selectors": ["#name", "#email", "input[type='checkbox']", "select#country"]}
375
+ \`\`\`
376
+
377
+ ## BOT DETECTION
378
+ If blocked (CAPTCHA, access denied), use stealthLevel: "maximum" (visible browser).
379
+ `;
380
+ }
381
+
382
+ /**
383
+ * Parse parameters from tool command content
384
+ * @param {string} content - Raw tool command content
385
+ * @returns {Object} Parsed parameters
386
+ */
387
+ parseParameters(content) {
388
+ try {
389
+ // Try JSON first
390
+ if (content.trim().startsWith('{')) {
391
+ return JSON.parse(content);
392
+ }
393
+
394
+ // Parse XML-style tags
395
+ const params = {};
396
+
397
+ // Extract operation
398
+ const operationMatches = TagParser.extractContent(content, 'operation');
399
+ if (operationMatches.length > 0) {
400
+ params.operation = operationMatches[0].trim();
401
+ }
402
+
403
+ // Extract based on operation
404
+ switch (params.operation) {
405
+ case 'search':
406
+ params.query = TagParser.extractContent(content, 'query')[0]?.trim();
407
+ params.engine = TagParser.extractContent(content, 'engine')[0]?.trim() || 'google';
408
+ const maxResults = TagParser.extractContent(content, 'max-results')[0]?.trim();
409
+ params.maxResults = maxResults ? parseInt(maxResults, 10) : 10;
410
+ // stealthLevel for search (default standard, use maximum if getting blocked)
411
+ params.stealthLevel = TagParser.extractContent(content, 'stealthLevel')[0]?.trim() || 'standard';
412
+ break;
413
+
414
+ case 'fetch':
415
+ params.url = TagParser.extractContent(content, 'url')[0]?.trim();
416
+ const formatStr = TagParser.extractContent(content, 'format')[0]?.trim();
417
+ params.formats = formatStr ? formatStr.split(',').map(f => f.trim()) : ['title', 'text'];
418
+ // stealthLevel for fetch
419
+ params.stealthLevel = TagParser.extractContent(content, 'stealthLevel')[0]?.trim() || 'standard';
420
+ break;
421
+
422
+ case 'interactive':
423
+ // Extract stealthLevel (standard = headless, maximum = visible window)
424
+ const stealthLevelStr = TagParser.extractContent(content, 'stealthLevel')[0]?.trim();
425
+ params.stealthLevel = stealthLevelStr || 'standard';
426
+
427
+ // Legacy headless parameter - maps to stealthLevel for backwards compatibility
428
+ const headlessStr = TagParser.extractContent(content, 'headless')[0]?.trim();
429
+ if (headlessStr === 'false' && !stealthLevelStr) {
430
+ params.stealthLevel = 'maximum'; // headless:false = maximum stealth
431
+ }
432
+ params.headless = params.stealthLevel === 'standard';
433
+
434
+ // Extract humanMode (default true for anti-detection)
435
+ const humanModeStr = TagParser.extractContent(content, 'humanMode')[0]?.trim();
436
+ params.humanMode = humanModeStr !== 'false'; // Default true
437
+
438
+ // Extract actions block
439
+ const actionsContent = TagParser.extractContent(content, 'actions')[0];
440
+ if (actionsContent) {
441
+ params.actions = this.parseActions(actionsContent);
442
+ }
443
+ break;
444
+
445
+ case 'authenticate':
446
+ // Site ID for credential lookup
447
+ params.siteId = TagParser.extractContent(content, 'site')[0]?.trim() ||
448
+ TagParser.extractContent(content, 'siteId')[0]?.trim();
449
+ // Optional custom login URL (required for custom sites)
450
+ params.loginUrl = TagParser.extractContent(content, 'loginUrl')[0]?.trim();
451
+ // Optional tab name (to keep open after auth for continued browsing)
452
+ params.tabName = TagParser.extractContent(content, 'tabName')[0]?.trim();
453
+ // stealthLevel - default to 'maximum' for login pages (visible browser)
454
+ params.stealthLevel = TagParser.extractContent(content, 'stealthLevel')[0]?.trim() || 'maximum';
455
+ // Custom selectors - agent can provide these after analyzing the login page
456
+ params.usernameSelector = TagParser.extractContent(content, 'usernameSelector')[0]?.trim();
457
+ params.passwordSelector = TagParser.extractContent(content, 'passwordSelector')[0]?.trim();
458
+ params.submitSelector = TagParser.extractContent(content, 'submitSelector')[0]?.trim();
459
+ // Keep tab open for continued browsing (requires tabName)
460
+ const keepTabOpenStr = TagParser.extractContent(content, 'keepTabOpen')[0]?.trim()?.toLowerCase();
461
+ params.keepTabOpen = keepTabOpenStr === 'true' || keepTabOpenStr === '1';
462
+ break;
463
+ }
464
+
465
+ params.rawContent = content.trim();
466
+ return params;
467
+
468
+ } catch (error) {
469
+ throw new Error(`Failed to parse web tool parameters: ${error.message}`);
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Parse actions from XML content
475
+ * @param {string} content - Actions XML content
476
+ * @returns {Array} Parsed actions
477
+ * @private
478
+ */
479
+ parseActions(content) {
480
+ const actions = [];
481
+
482
+ // Parse open-tab actions
483
+ const openTabRegex = /<open-tab[^>]*name="([^"]+)"[^>]*>([\s\S]*?)<\/open-tab>/g;
484
+ let match;
485
+
486
+ while ((match = openTabRegex.exec(content)) !== null) {
487
+ const [, name, nestedContent] = match;
488
+ const url = TagParser.extractContent(nestedContent, 'navigate')[0]?.trim();
489
+
490
+ actions.push({
491
+ type: 'open-tab',
492
+ name,
493
+ url,
494
+ nestedActions: this.parseNestedActions(nestedContent)
495
+ });
496
+ }
497
+
498
+ // Parse other actions (close-tab, switch-tab, list-tabs, etc.)
499
+ const simpleActions = [
500
+ 'close-tab', 'switch-tab', 'list-tabs', 'navigate',
501
+ 'click', 'type', 'press', 'wait-for', 'screenshot',
502
+ 'analyze-screenshot', 'extract-text', 'extract-links',
503
+ 'get-source', 'get-console', 'scroll', 'hover', 'mouse-move',
504
+ 'wait', 'delay', 'submit', 'evaluate', 'get-field-values', 'select'
505
+ ];
506
+
507
+ for (const actionType of simpleActions) {
508
+ const regex = new RegExp(`<${actionType}([^>]*)>([^<]*)<\/${actionType}>`, 'g');
509
+ let actionMatch;
510
+
511
+ while ((actionMatch = regex.exec(content)) !== null) {
512
+ const [, attrs, value] = actionMatch;
513
+ const action = { type: actionType };
514
+
515
+ // Parse attributes
516
+ const attrRegex = /(\w+(?:-\w+)*)="([^"]*)"/g;
517
+ let attrMatch;
518
+ while ((attrMatch = attrRegex.exec(attrs)) !== null) {
519
+ action[attrMatch[1]] = attrMatch[2];
520
+ }
521
+
522
+ // Add value if present
523
+ if (value && value.trim()) {
524
+ action.value = value.trim();
525
+ }
526
+
527
+ actions.push(action);
528
+ }
529
+ }
530
+
531
+ return actions;
532
+ }
533
+
534
+ /**
535
+ * Parse nested actions within a tab
536
+ * @param {string} content - Nested actions content
537
+ * @returns {Array} Parsed nested actions
538
+ * @private
539
+ */
540
+ parseNestedActions(content) {
541
+ const actions = [];
542
+
543
+ const actionTypes = [
544
+ 'navigate', 'click', 'type', 'press', 'wait-for', 'screenshot',
545
+ 'analyze-screenshot', 'extract-text', 'extract-links',
546
+ 'get-source', 'get-console', 'scroll', 'hover', 'mouse-move',
547
+ 'wait', 'delay', 'submit', 'evaluate', 'get-field-values', 'select'
548
+ ];
549
+
550
+ for (const actionType of actionTypes) {
551
+ const regex = new RegExp(`<${actionType}([^>]*)>([^<]*)<\/${actionType}>|<${actionType}([^>]*)\/>`, 'g');
552
+ let match;
553
+
554
+ while ((match = regex.exec(content)) !== null) {
555
+ const [, attrs1, value, attrs2] = match;
556
+ const attrs = attrs1 || attrs2 || '';
557
+ const action = { type: actionType };
558
+
559
+ // Parse attributes
560
+ const attrRegex = /(\w+(?:-\w+)*)="([^"]*)"/g;
561
+ let attrMatch;
562
+ while ((attrMatch = attrRegex.exec(attrs)) !== null) {
563
+ action[attrMatch[1]] = attrMatch[2];
564
+ }
565
+
566
+ // Add value if present
567
+ if (value && value.trim()) {
568
+ action.value = value.trim();
569
+ }
570
+
571
+ actions.push(action);
572
+ }
573
+ }
574
+
575
+ return actions;
576
+ }
577
+
578
+ /**
579
+ * Get required parameters based on operation
580
+ * @returns {Array<string>} Array of required parameter names
581
+ */
582
+ getRequiredParameters() {
583
+ return ['operation'];
584
+ }
585
+
586
+ /**
587
+ * Custom parameter validation
588
+ * @param {Object} params - Parameters to validate
589
+ * @returns {Object} Validation result
590
+ */
591
+ customValidateParameters(params) {
592
+ const errors = [];
593
+
594
+ if (!['search', 'fetch', 'interactive', 'authenticate'].includes(params.operation)) {
595
+ errors.push('operation must be one of: search, fetch, interactive, authenticate');
596
+ return { valid: false, errors };
597
+ }
598
+
599
+ switch (params.operation) {
600
+ case 'search':
601
+ if (!params.query) {
602
+ errors.push('query is required for search operation');
603
+ }
604
+ break;
605
+
606
+ case 'fetch':
607
+ if (!params.url) {
608
+ errors.push('url is required for fetch operation');
609
+ }
610
+ break;
611
+
612
+ case 'interactive':
613
+ if (!params.actions || !Array.isArray(params.actions) || params.actions.length === 0) {
614
+ errors.push('actions array is required for interactive operation');
615
+ }
616
+ break;
617
+
618
+ case 'authenticate':
619
+ if (!params.siteId && !params.site) {
620
+ errors.push('site or siteId is required for authenticate operation');
621
+ }
622
+ break;
623
+ }
624
+
625
+ return {
626
+ valid: errors.length === 0,
627
+ errors
628
+ };
629
+ }
630
+
631
+ /**
632
+ * Execute tool with parsed parameters
633
+ * @param {Object} params - Parsed parameters
634
+ * @param {Object} context - Execution context
635
+ * @returns {Promise<Object>} Execution result
636
+ */
637
+ async execute(params, context) {
638
+ // Unwrap tag parser format {value, attributes} for all params
639
+ const unwrappedParams = {};
640
+ for (const [key, value] of Object.entries(params)) {
641
+ if (value && typeof value === 'object' && 'value' in value) {
642
+ unwrappedParams[key] = value.value;
643
+ } else {
644
+ unwrappedParams[key] = value;
645
+ }
646
+ }
647
+ params = unwrappedParams;
648
+
649
+ const { operation } = params;
650
+ const { agentId } = context;
651
+
652
+ try {
653
+ let result;
654
+
655
+ // Fix 1: Compute effective stealth level BEFORE ensuring browser
656
+ // This preserves current browser stealth level instead of defaulting to 'standard'
657
+ // which prevents browser restart when agent forgets to specify stealthLevel after authenticate
658
+ const currentBrowserStealth = this.browser?._stealthConfig?.stealthLevel;
659
+ const effectiveStealthLevel = params.stealthLevel || currentBrowserStealth || 'standard';
660
+
661
+ // Ensure browser is initialized with the correct stealth level
662
+ // Note: Each operation also calls ensureBrowser internally, but this pre-check
663
+ // ensures we don't unnecessarily restart the browser
664
+ await this.ensureBrowser({ stealthLevel: effectiveStealthLevel });
665
+
666
+ switch (operation) {
667
+ case 'search':
668
+ result = await this.search(params.query, {
669
+ engine: params.engine || 'google',
670
+ maxResults: params.maxResults || 10,
671
+ stealthLevel: effectiveStealthLevel,
672
+ agentId
673
+ });
674
+ break;
675
+
676
+ case 'fetch':
677
+ result = await this.fetch(params.url, {
678
+ formats: params.formats || ['title', 'text'],
679
+ stealthLevel: effectiveStealthLevel,
680
+ agentId
681
+ });
682
+ break;
683
+
684
+ case 'interactive':
685
+ result = await this.interactive(params.actions, {
686
+ stealthLevel: effectiveStealthLevel,
687
+ humanMode: params.humanMode !== false, // Default true for anti-detection
688
+ agentId,
689
+ context
690
+ });
691
+ break;
692
+
693
+ case 'authenticate':
694
+ // Accept both 'site' and 'siteId' for flexibility
695
+ const siteId = params.siteId || params.site;
696
+ if (!siteId) {
697
+ throw new Error('site or siteId is required for authenticate operation');
698
+ }
699
+ result = await this.authenticate(siteId, {
700
+ loginUrl: params.loginUrl,
701
+ tabName: params.tabName,
702
+ stealthLevel: params.stealthLevel || 'maximum', // Default maximum for login
703
+ keepTabOpen: params.keepTabOpen || false, // Keep tab open for continued browsing
704
+ agentId,
705
+ context,
706
+ // Custom selectors - agent can provide these after analyzing the login page
707
+ customSelectors: (params.usernameSelector || params.passwordSelector || params.submitSelector) ? {
708
+ username: params.usernameSelector,
709
+ password: params.passwordSelector,
710
+ submit: params.submitSelector
711
+ } : null
712
+ });
713
+ break;
714
+
715
+ default:
716
+ throw new Error(`Unknown operation: ${operation}`);
717
+ }
718
+
719
+ // Flatten result for easier access (result.title instead of result.result.title)
720
+ // IMPORTANT: Respect the operation's own success flag — don't override it to true
721
+ const operationSuccess = result.success !== undefined ? result.success : true;
722
+ return {
723
+ success: operationSuccess,
724
+ operation,
725
+ toolUsed: 'web',
726
+ // Spread operation-specific data at top level
727
+ data: result,
728
+ // Also keep common properties at top level for convenience
729
+ ...(result.title !== undefined && { title: result.title }),
730
+ ...(result.text !== undefined && { text: result.text }),
731
+ ...(result.url !== undefined && { url: result.url }),
732
+ ...(result.results !== undefined && { results: result.results }),
733
+ ...(result.resultsCount !== undefined && { resultsCount: result.resultsCount }),
734
+ ...(result.error !== undefined && { error: result.error }),
735
+ ...(result.warning !== undefined && { warning: result.warning }),
736
+ ...(result.httpStatus !== undefined && { httpStatus: result.httpStatus }),
737
+ ...(result.suggestion !== undefined && { suggestion: result.suggestion }),
738
+ // Surface page-level errors for agent awareness
739
+ ...(result.jsErrors?.length > 0 && { jsErrors: result.jsErrors }),
740
+ ...(result.networkFailures?.length > 0 && { networkFailures: result.networkFailures }),
741
+ ...(result.httpErrors?.length > 0 && { httpErrors: result.httpErrors })
742
+ };
743
+
744
+ } catch (error) {
745
+ this.logger?.error('Web tool execution failed', {
746
+ operation,
747
+ error: error.message,
748
+ agentId
749
+ });
750
+
751
+ // Fix 3: Provide better error context for common browser issues
752
+ let enhancedError = error.message;
753
+ let suggestion = null;
754
+
755
+ // Detect browser connection issues
756
+ if (error.message.includes('Connection closed') ||
757
+ error.message.includes('Target closed') ||
758
+ error.message.includes('Protocol error') ||
759
+ error.message.includes('Session closed')) {
760
+ enhancedError = `Browser connection lost: ${error.message}`;
761
+ suggestion = 'The browser or tab was closed unexpectedly. Try the operation again.';
762
+ }
763
+
764
+ // Detect stealth level mismatch errors (from Fix 2)
765
+ if (error.message.includes('Cannot change stealth level')) {
766
+ suggestion = 'Use the same stealthLevel as your authenticated session, or close tabs first.';
767
+ }
768
+
769
+ // Detect tab not found errors
770
+ if (error.message.includes('not found for agent')) {
771
+ enhancedError = `Tab lost: ${error.message}`;
772
+ suggestion = 'The tab may have been closed or the browser was restarted. Re-authenticate and try again.';
773
+ }
774
+
775
+ // Detect navigation timeout
776
+ if (error.message.includes('Navigation timeout') || error.message.includes('Timeout')) {
777
+ suggestion = 'The page took too long to load. Check your internet connection or try again.';
778
+ }
779
+
780
+ return {
781
+ success: false,
782
+ operation,
783
+ error: enhancedError,
784
+ ...(suggestion && { suggestion }),
785
+ toolUsed: 'web'
786
+ };
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Ensure browser is initialized
792
+ * @private
793
+ */
794
+ /**
795
+ * Ensure browser is running with the specified stealth level
796
+ * @param {Object} options - Browser options
797
+ * @param {string} options.stealthLevel - 'standard' (headless) or 'maximum' (visible window)
798
+ */
799
+ async ensureBrowser(options = {}) {
800
+ const { stealthLevel = 'standard' } = options;
801
+ const requestedLevel = stealthLevel.toLowerCase();
802
+
803
+ // Check if browser exists and matches requested stealth level
804
+ if (this.browser && this.browser.isConnected()) {
805
+ const currentLevel = this.browser._stealthConfig?.stealthLevel || 'standard';
806
+
807
+ // If stealth level matches, reuse browser
808
+ if (currentLevel === requestedLevel) {
809
+ return;
810
+ }
811
+
812
+ // Stealth level changed - check for active tabs before restarting
813
+ // Fix 2: Warn/block stealth level change when there are active tabs
814
+ let totalActiveTabs = 0;
815
+ const agentsWithTabs = [];
816
+ for (const [agentId, tabsMap] of this.agentTabs.entries()) {
817
+ if (tabsMap.size > 0) {
818
+ totalActiveTabs += tabsMap.size;
819
+ agentsWithTabs.push({ agentId, tabCount: tabsMap.size, tabs: Array.from(tabsMap.keys()) });
820
+ }
821
+ }
822
+
823
+ if (totalActiveTabs > 0) {
824
+ // Throw an error with clear context about what would happen
825
+ const tabDetails = agentsWithTabs.map(a => `${a.agentId}: [${a.tabs.join(', ')}]`).join('; ');
826
+ throw new Error(
827
+ `Cannot change stealth level from '${currentLevel}' to '${requestedLevel}': ` +
828
+ `${totalActiveTabs} active tab(s) would be destroyed. ` +
829
+ `Active tabs: ${tabDetails}. ` +
830
+ `Either close tabs first with close-tab action, or use stealthLevel: "${currentLevel}" to preserve session.`
831
+ );
832
+ }
833
+
834
+ this.logger?.info('[WebTool] Stealth level changed, restarting browser (no active tabs)', {
835
+ from: currentLevel,
836
+ to: requestedLevel
837
+ });
838
+
839
+ await this.closeBrowser();
840
+ }
841
+
842
+ if (this.browserInitializing) {
843
+ // Wait for browser to finish initializing
844
+ while (this.browserInitializing) {
845
+ await new Promise(resolve => setTimeout(resolve, 100));
846
+ }
847
+ // After waiting, check if the initialized browser matches our stealth level
848
+ if (this.browser && this.browser._stealthConfig?.stealthLevel === requestedLevel) {
849
+ return;
850
+ }
851
+ // Otherwise, close and reinitialize
852
+ if (this.browser) {
853
+ await this.closeBrowser();
854
+ }
855
+ }
856
+
857
+ this.browserInitializing = true;
858
+
859
+ try {
860
+ this.logger?.info('[WebTool] Initializing stealth browser', {
861
+ stealthLevel: requestedLevel,
862
+ headless: requestedLevel === 'standard' ? 'new' : false
863
+ });
864
+
865
+ // Use stealth browser with specified level
866
+ this.browser = await createStealthBrowser({
867
+ stealthLevel: requestedLevel,
868
+ logger: this.logger
869
+ });
870
+
871
+ this.logger?.info('[WebTool] Stealth browser initialized successfully', {
872
+ stealthLevel: requestedLevel
873
+ });
874
+
875
+ } catch (error) {
876
+ this.logger?.error('[WebTool] Failed to initialize browser', { error: error.message });
877
+ throw new Error(`Browser initialization failed: ${error.message}`);
878
+ } finally {
879
+ this.browserInitializing = false;
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Create a stealth page with human-like cursor
885
+ * @param {Object} options - Page options
886
+ * @param {boolean} options.humanMode - Enable human-like behavior
887
+ * @returns {Promise<Object>} Object with page and optional cursor
888
+ */
889
+ async createPage(options = {}) {
890
+ const { humanMode = false } = options;
891
+
892
+ // Note: ensureBrowser() is NOT called here because the caller should have already
893
+ // called it with the appropriate stealth level. This prevents accidentally
894
+ // resetting the browser to 'standard' stealth when 'maximum' is needed.
895
+ if (!this.browser || !this.browser.isConnected()) {
896
+ throw new Error('Browser not initialized. Call ensureBrowser() first.');
897
+ }
898
+
899
+ const page = await createStealthPage(this.browser, {
900
+ logger: this.logger
901
+ });
902
+
903
+ let cursor = null;
904
+ if (humanMode) {
905
+ cursor = createHumanCursor(page);
906
+ this.logger?.info('[WebTool] Human-like cursor enabled for page');
907
+ }
908
+
909
+ return { page, cursor };
910
+ }
911
+
912
+ /**
913
+ * Search the web using a known search engine
914
+ * @param {string} query - Search query
915
+ * @param {Object} options - Search options
916
+ * @returns {Promise<Object>} Search results
917
+ */
918
+ async search(query, options = {}) {
919
+ const { engine = 'google', maxResults = 10, agentId, humanMode = true, stealthLevel = 'standard' } = options;
920
+
921
+ // Validate query
922
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
923
+ throw new Error('Search query is required and must be a non-empty string');
924
+ }
925
+
926
+ const searchEngine = this.searchEngines.find(e => e.name === engine);
927
+ if (!searchEngine) {
928
+ throw new Error(`Unknown search engine: ${engine}. Available: ${this.searchEngines.map(e => e.name).join(', ')}`);
929
+ }
930
+
931
+ this.logger?.info('[WebTool] Performing web search', { query, engine, agentId, humanMode, stealthLevel });
932
+
933
+ // Ensure browser with specified stealth level
934
+ await this.ensureBrowser({ stealthLevel });
935
+
936
+ // Create stealth page with optional human-like cursor
937
+ const { page, cursor } = await this.createPage({ humanMode });
938
+
939
+ try {
940
+ // Navigate to search engine
941
+ const searchUrl = `${searchEngine.url}${encodeURIComponent(query)}`;
942
+ const searchNavResponse = await page.goto(searchUrl, {
943
+ waitUntil: BROWSER_CONFIG.WAIT_UNTIL,
944
+ timeout: this.DEFAULT_TIMEOUT
945
+ });
946
+ const searchHttpStatus = searchNavResponse ? searchNavResponse.status() : null;
947
+ if (searchHttpStatus && searchHttpStatus >= 400) {
948
+ return {
949
+ success: false,
950
+ query,
951
+ engine,
952
+ httpStatus: searchHttpStatus,
953
+ error: `Search engine returned HTTP ${searchHttpStatus}. The search engine may be blocking automated requests.`,
954
+ suggestion: 'Try a different search engine or increase stealth level.'
955
+ };
956
+ }
957
+
958
+ // Human-like wait after navigation
959
+ if (humanMode) {
960
+ await humanWait('navigation');
961
+ }
962
+
963
+ // Wait for results — try each selector with a shorter timeout, proceed even if none match
964
+ const waitSelectors = searchEngine.waitSelector.split(',').map(s => s.trim());
965
+ let waitResolved = false;
966
+ for (const ws of waitSelectors) {
967
+ try {
968
+ await page.waitForSelector(ws, { timeout: 10000 });
969
+ waitResolved = true;
970
+ break;
971
+ } catch {
972
+ // Try next selector
973
+ }
974
+ }
975
+ if (!waitResolved) {
976
+ // Last resort: wait a bit and try to extract whatever is on the page
977
+ this.logger?.warn('[WebTool] No wait selector matched, attempting extraction anyway', { engine });
978
+ await new Promise(r => setTimeout(r, 3000));
979
+ }
980
+
981
+ // Extract results
982
+ const results = await page.evaluate((selector, max) => {
983
+ const links = Array.from(document.querySelectorAll(selector));
984
+ return links.slice(0, max).map(link => ({
985
+ url: link.href,
986
+ title: link.textContent.trim(),
987
+ description: link.closest('.g, .b_algo, .result')?.textContent.trim() || ''
988
+ })).filter(result => result.url && result.url.startsWith('http'));
989
+ }, searchEngine.resultsSelector, maxResults);
990
+
991
+ this.logger?.info('[WebTool] Search completed', { resultsCount: results.length, agentId });
992
+
993
+ const searchResult = {
994
+ success: true,
995
+ query,
996
+ engine,
997
+ resultsCount: results.length,
998
+ results
999
+ };
1000
+
1001
+ // Warn when 0 results — may indicate selector mismatch or CAPTCHA
1002
+ if (results.length === 0) {
1003
+ searchResult.warning = 'Search returned 0 results. This may indicate: (1) no matching results exist, (2) the search engine blocked the request (CAPTCHA), or (3) the page layout changed and results could not be extracted.';
1004
+ // Grab page title for context
1005
+ try { searchResult.pageTitle = await page.title(); } catch {}
1006
+ }
1007
+
1008
+ return searchResult;
1009
+
1010
+ } finally {
1011
+ try { await page.close(); } catch {}
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * Fetch web content in various formats
1017
+ * @param {string} url - URL to fetch
1018
+ * @param {Object} options - Fetch options
1019
+ * @returns {Promise<Object>} Fetched content
1020
+ */
1021
+ async fetch(url, options = {}) {
1022
+ const { formats = ['title', 'text'], agentId, stealthLevel = 'standard' } = options;
1023
+
1024
+ // Validate URL before doing anything expensive
1025
+ if (!url || typeof url !== 'string' || url.trim().length === 0) {
1026
+ return { success: false, error: 'URL is required for fetch operation' };
1027
+ }
1028
+ try {
1029
+ new URL(url);
1030
+ } catch {
1031
+ return { success: false, error: `Invalid URL format: "${url}". Must include protocol (e.g. https://example.com)` };
1032
+ }
1033
+
1034
+ // Ensure browser with specified stealth level
1035
+ await this.ensureBrowser({ stealthLevel });
1036
+
1037
+ this.logger?.info('Fetching web content', { url, formats, agentId, stealthLevel });
1038
+
1039
+ // Create temporary page
1040
+ const page = await this.browser.newPage();
1041
+
1042
+ try {
1043
+ // Listen for console messages if requested
1044
+ const consoleMessages = [];
1045
+ if (formats.includes('console')) {
1046
+ page.on('console', msg => {
1047
+ consoleMessages.push({
1048
+ type: msg.type(),
1049
+ text: msg.text()
1050
+ });
1051
+ });
1052
+ }
1053
+
1054
+ // Navigate to URL and capture HTTP response
1055
+ const fetchResponse = await page.goto(url, { waitUntil: 'networkidle2', timeout: this.DEFAULT_TIMEOUT });
1056
+ const fetchStatus = fetchResponse ? fetchResponse.status() : null;
1057
+
1058
+ if (fetchStatus && fetchStatus >= 400) {
1059
+ const errorResult = {
1060
+ success: false,
1061
+ url,
1062
+ httpStatus: fetchStatus,
1063
+ error: `Fetch failed with HTTP ${fetchStatus} (${fetchStatus >= 500 ? 'server error' : fetchStatus === 404 ? 'page not found' : fetchStatus === 403 ? 'access forbidden' : 'client error'})`
1064
+ };
1065
+ // Still try to get title for context
1066
+ try { errorResult.title = await page.title(); } catch {}
1067
+ // Don't close page here — finally block handles it
1068
+ return errorResult;
1069
+ }
1070
+
1071
+ const result = { url, httpStatus: fetchStatus };
1072
+
1073
+ // Extract requested formats
1074
+ for (const format of formats) {
1075
+ switch (format) {
1076
+ case 'title':
1077
+ result.title = await page.title();
1078
+ break;
1079
+
1080
+ case 'text':
1081
+ result.text = await page.evaluate(() => document.body.innerText);
1082
+ break;
1083
+
1084
+ case 'links':
1085
+ result.links = await page.evaluate(() => {
1086
+ return Array.from(document.querySelectorAll('a[href]')).map(a => ({
1087
+ href: a.href,
1088
+ text: a.textContent.trim()
1089
+ }));
1090
+ });
1091
+ break;
1092
+
1093
+ case 'html':
1094
+ result.html = await page.content();
1095
+ break;
1096
+
1097
+ case 'console':
1098
+ result.consoleMessages = consoleMessages;
1099
+ break;
1100
+ }
1101
+ }
1102
+
1103
+ this.logger?.info('Fetch completed', { url, formats, agentId });
1104
+
1105
+ return {
1106
+ success: true,
1107
+ ...result
1108
+ };
1109
+
1110
+ } finally {
1111
+ try { await page.close(); } catch {}
1112
+ }
1113
+ }
1114
+
1115
+ /**
1116
+ * Interactive browser automation with command chaining
1117
+ * @param {Array} actions - Array of actions to execute
1118
+ * @param {Object} options - Options
1119
+ * @returns {Promise<Object>} Results of all actions
1120
+ */
1121
+ async interactive(actions, options = {}) {
1122
+ const { stealthLevel = 'standard', agentId, context, humanMode = true } = options;
1123
+
1124
+ // Derive headless from stealthLevel (standard = headless, maximum = visible)
1125
+ const headless = stealthLevel === 'standard';
1126
+
1127
+ // Ensure browser with specified stealth level
1128
+ await this.ensureBrowser({ stealthLevel });
1129
+
1130
+ this.logger?.info('[WebTool] Starting interactive session', {
1131
+ actionsCount: actions.length,
1132
+ stealthLevel,
1133
+ humanMode,
1134
+ agentId
1135
+ });
1136
+
1137
+ const results = [];
1138
+
1139
+ // Initialize agent tabs if not exists
1140
+ if (!this.agentTabs.has(agentId)) {
1141
+ this.agentTabs.set(agentId, new Map());
1142
+ }
1143
+
1144
+ const agentTabsMap = this.agentTabs.get(agentId);
1145
+
1146
+ for (const action of actions) {
1147
+ // Accept both "type" and "action" as the action discriminator for resilience
1148
+ const actionType = action.type || action.action;
1149
+ try {
1150
+ let actionResult;
1151
+
1152
+ switch (actionType) {
1153
+ case 'open-tab':
1154
+ actionResult = await this.openTab(
1155
+ agentId, action.name, action.url, headless,
1156
+ action.nestedActions || action.actions, context, { humanMode }
1157
+ );
1158
+ break;
1159
+
1160
+ case 'close-tab':
1161
+ actionResult = await this.closeTab(agentId, action.name);
1162
+ break;
1163
+
1164
+ case 'switch-tab':
1165
+ actionResult = await this.switchTab(agentId, action.name);
1166
+ break;
1167
+
1168
+ case 'list-tabs':
1169
+ actionResult = await this.listTabs(agentId);
1170
+ break;
1171
+
1172
+ default:
1173
+ // For actions that need a tab context, we need to specify which tab
1174
+ // For now, we'll skip these at the top level
1175
+ actionResult = {
1176
+ success: false,
1177
+ error: `Action ${actionType} must be executed within a tab context (use open-tab with nestedActions)`
1178
+ };
1179
+ }
1180
+
1181
+ results.push({
1182
+ action: actionType,
1183
+ ...actionResult
1184
+ });
1185
+
1186
+ } catch (error) {
1187
+ this.logger?.error('Action failed', {
1188
+ action: actionType,
1189
+ error: error.message,
1190
+ agentId
1191
+ });
1192
+
1193
+ results.push({
1194
+ action: actionType,
1195
+ success: false,
1196
+ error: error.message
1197
+ });
1198
+ }
1199
+ }
1200
+
1201
+ return {
1202
+ success: results.every(r => r.success !== false),
1203
+ actionsExecuted: results.length,
1204
+ results
1205
+ };
1206
+ }
1207
+
1208
+ /**
1209
+ * Authenticate to a website using stored credentials
1210
+ * The agent never sees the actual credentials - they are retrieved from the vault
1211
+ *
1212
+ * @param {string} siteId - Site identifier (e.g., 'linkedin', 'github')
1213
+ * @param {Object} options - Authentication options
1214
+ * @param {string} options.loginUrl - Custom login URL (optional)
1215
+ * @param {string} options.tabName - Tab name to reuse (optional)
1216
+ * @param {string} options.agentId - Agent identifier
1217
+ * @param {Object} options.context - Execution context
1218
+ * @returns {Promise<Object>} Authentication result (success/failure, no credentials exposed)
1219
+ */
1220
+ async authenticate(siteId, options = {}) {
1221
+ const { loginUrl, tabName, agentId, context = {}, stealthLevel = 'maximum', customSelectors, keepTabOpen = false } = options;
1222
+
1223
+ // Validate siteId
1224
+ if (!siteId || typeof siteId !== 'string') {
1225
+ return {
1226
+ success: false,
1227
+ error: 'siteId is required and must be a string (e.g., "linkedin", "github")',
1228
+ requiresCredentials: false
1229
+ };
1230
+ }
1231
+
1232
+ const normalizedSiteId = siteId.toLowerCase().trim();
1233
+
1234
+ this.logger?.info('[WebTool] Authentication requested', {
1235
+ siteId: normalizedSiteId,
1236
+ agentId,
1237
+ stealthLevel
1238
+ });
1239
+
1240
+ // Get credential vault
1241
+ const vault = getCredentialVault(this.logger);
1242
+ await vault.initialize();
1243
+
1244
+ // Check if we have stored credentials
1245
+ let credentials = vault.getCredentials(normalizedSiteId);
1246
+
1247
+ // If no credentials stored, we need to request them from the user
1248
+ if (!credentials) {
1249
+ // Check if we have a webSocketManager to request credentials
1250
+ const wsManager = global.loxiaWebServer;
1251
+
1252
+ this.logger?.info('[WebTool] No stored credentials, checking for WebSocket manager', {
1253
+ hasWsManager: !!wsManager,
1254
+ sessionId: context?.sessionId,
1255
+ siteId: normalizedSiteId
1256
+ });
1257
+
1258
+ if (!wsManager) {
1259
+ this.logger?.warn('[WebTool] No WebSocket manager available - cannot request credentials from UI');
1260
+ return {
1261
+ success: false,
1262
+ error: `No credentials stored for ${normalizedSiteId}. Please add credentials in Settings > Saved Logins.`,
1263
+ requiresCredentials: true,
1264
+ siteId: normalizedSiteId
1265
+ };
1266
+ }
1267
+
1268
+ // Create a credential request
1269
+ const { requestInfo, promise } = vault.createCredentialRequest(normalizedSiteId, {
1270
+ loginUrl,
1271
+ agentId
1272
+ });
1273
+
1274
+ this.logger?.info('[WebTool] Broadcasting credential request to UI', {
1275
+ requestId: requestInfo.requestId,
1276
+ siteId: requestInfo.siteId,
1277
+ sessionId: context?.sessionId
1278
+ });
1279
+
1280
+ // Block scheduling while waiting for user to enter credentials.
1281
+ // We set awaitingUserInput (checked by agentActivityService) instead of
1282
+ // changing status to PAUSED, so the agent stays visible to other agents.
1283
+ const agentPool = context?.agentPool;
1284
+ let agent = null;
1285
+
1286
+ if (agentPool && agentId) {
1287
+ try {
1288
+ agent = await agentPool.getAgent(agentId);
1289
+ if (agent) {
1290
+ agent.awaitingUserInput = {
1291
+ type: 'credentials',
1292
+ siteId: normalizedSiteId,
1293
+ requestId: requestInfo.requestId,
1294
+ startedAt: new Date().toISOString()
1295
+ };
1296
+ await agentPool.persistAgentState(agentId);
1297
+ this.logger?.info('[WebTool] Agent awaiting credentials (scheduling blocked)', {
1298
+ agentId,
1299
+ siteId: normalizedSiteId
1300
+ });
1301
+
1302
+ // Notify UI that agent is paused awaiting user input
1303
+ if (wsManager?.broadcastToSession) {
1304
+ wsManager.broadcastToSession(context.sessionId, {
1305
+ type: 'agent_awaiting_input',
1306
+ data: {
1307
+ agentId,
1308
+ inputType: 'credentials',
1309
+ siteId: normalizedSiteId,
1310
+ message: `Waiting for ${normalizedSiteId} credentials...`,
1311
+ timestamp: new Date().toISOString()
1312
+ }
1313
+ });
1314
+ }
1315
+ }
1316
+ } catch (pauseError) {
1317
+ this.logger?.warn('[WebTool] Failed to pause agent (non-fatal):', pauseError.message);
1318
+ }
1319
+ }
1320
+
1321
+ // Broadcast credential request to UI
1322
+ wsManager.broadcastCredentialRequest(requestInfo, context.sessionId);
1323
+
1324
+ // Wait for credentials to be submitted (or timeout/cancel)
1325
+ try {
1326
+ const result = await promise;
1327
+ credentials = {
1328
+ ...vault.getCredentials(normalizedSiteId),
1329
+ ...result.credentials
1330
+ };
1331
+ } catch (error) {
1332
+ // RESUME the agent even on error
1333
+ if (agent && agentPool) {
1334
+ try {
1335
+ delete agent.awaitingUserInput;
1336
+ await agentPool.persistAgentState(agentId);
1337
+ this.logger?.info('[WebTool] Agent resumed after credential error', { agentId });
1338
+
1339
+ // Notify UI that agent is no longer waiting
1340
+ if (wsManager?.broadcastToSession) {
1341
+ wsManager.broadcastToSession(context.sessionId, {
1342
+ type: 'agent_input_complete',
1343
+ data: {
1344
+ agentId,
1345
+ inputType: 'credentials',
1346
+ success: false,
1347
+ reason: error.message.includes('cancelled') ? 'cancelled' : 'timeout',
1348
+ timestamp: new Date().toISOString()
1349
+ }
1350
+ });
1351
+ }
1352
+ } catch (resumeError) {
1353
+ this.logger?.warn('[WebTool] Failed to resume agent (non-fatal):', resumeError.message);
1354
+ }
1355
+ }
1356
+
1357
+ return {
1358
+ success: false,
1359
+ error: error.message,
1360
+ cancelled: error.message.includes('cancelled'),
1361
+ timedOut: error.message.includes('timed out'),
1362
+ siteId: normalizedSiteId
1363
+ };
1364
+ }
1365
+
1366
+ // RESUME the agent after credentials received
1367
+ if (agent && agentPool) {
1368
+ try {
1369
+ delete agent.awaitingUserInput;
1370
+ await agentPool.persistAgentState(agentId);
1371
+ this.logger?.info('[WebTool] Agent resumed after credentials received', { agentId });
1372
+
1373
+ // Notify UI that agent is no longer waiting
1374
+ if (wsManager?.broadcastToSession) {
1375
+ wsManager.broadcastToSession(context.sessionId, {
1376
+ type: 'agent_input_complete',
1377
+ data: {
1378
+ agentId,
1379
+ inputType: 'credentials',
1380
+ success: true,
1381
+ timestamp: new Date().toISOString()
1382
+ }
1383
+ });
1384
+ }
1385
+ } catch (resumeError) {
1386
+ this.logger?.warn('[WebTool] Failed to resume agent (non-fatal):', resumeError.message);
1387
+ }
1388
+ }
1389
+ }
1390
+
1391
+ // Get site configuration
1392
+ const knownSite = KNOWN_SITES[normalizedSiteId] || {};
1393
+ const actualLoginUrl = loginUrl || credentials.loginUrl || knownSite.loginUrl;
1394
+
1395
+ if (!actualLoginUrl) {
1396
+ return {
1397
+ success: false,
1398
+ error: `No login URL configured for ${normalizedSiteId}. Provide loginUrl parameter.`,
1399
+ siteId: normalizedSiteId
1400
+ };
1401
+ }
1402
+
1403
+ // Resolve selectors - priority: customSelectors > credentials.selectors > knownSite.selectors
1404
+ const selectors = customSelectors && (customSelectors.username || customSelectors.password)
1405
+ ? {
1406
+ username: customSelectors.username,
1407
+ password: customSelectors.password,
1408
+ submit: customSelectors.submit || 'button[type="submit"], input[type="submit"]',
1409
+ loginSuccess: knownSite.selectors?.loginSuccess,
1410
+ loginError: knownSite.selectors?.loginError
1411
+ }
1412
+ : (credentials.selectors || knownSite.selectors);
1413
+
1414
+ if (!selectors || !selectors.username || !selectors.password) {
1415
+ return {
1416
+ success: false,
1417
+ error: `No login form selectors for ${normalizedSiteId}. Provide usernameSelector and passwordSelector parameters, or use a supported site (linkedin, github, google, twitter).`,
1418
+ siteId: normalizedSiteId
1419
+ };
1420
+ }
1421
+
1422
+ this.logger?.info('[WebTool] Using selectors for authentication', {
1423
+ siteId: normalizedSiteId,
1424
+ usernameSelector: selectors.username,
1425
+ passwordSelector: selectors.password,
1426
+ submitSelector: selectors.submit,
1427
+ isCustom: !!customSelectors
1428
+ });
1429
+
1430
+ // Check for existing session cookies
1431
+ const existingSession = vault.getSession(normalizedSiteId);
1432
+
1433
+ // Create or reuse a tab for authentication with specified stealth level
1434
+ // Default to 'maximum' (visible browser) for login pages to avoid detection
1435
+ await this.ensureBrowser({ stealthLevel });
1436
+ const { page, cursor } = await this.createPage({ humanMode: true });
1437
+
1438
+ try {
1439
+ // If we have existing session cookies, try them first
1440
+ if (existingSession && existingSession.cookies) {
1441
+ this.logger?.info('[WebTool] Attempting session restore', { siteId: normalizedSiteId });
1442
+
1443
+ await page.setCookie(...existingSession.cookies);
1444
+ await page.goto(actualLoginUrl, {
1445
+ waitUntil: BROWSER_CONFIG.WAIT_UNTIL,
1446
+ timeout: this.DEFAULT_TIMEOUT
1447
+ });
1448
+ await humanWait('navigation');
1449
+
1450
+ // Check if we're already logged in
1451
+ if (selectors.loginSuccess) {
1452
+ try {
1453
+ await page.waitForSelector(selectors.loginSuccess, { timeout: 5000 });
1454
+ this.logger?.info('[WebTool] Session restore successful', { siteId: normalizedSiteId });
1455
+
1456
+ await page.close();
1457
+ return {
1458
+ success: true,
1459
+ message: `Already logged into ${credentials.name || normalizedSiteId} (session restored)`,
1460
+ siteId: normalizedSiteId,
1461
+ method: 'session_restore'
1462
+ };
1463
+ } catch {
1464
+ // Session invalid, need to login
1465
+ this.logger?.info('[WebTool] Session expired, performing fresh login', { siteId: normalizedSiteId });
1466
+ }
1467
+ }
1468
+ } else {
1469
+ // Navigate to login page
1470
+ await page.goto(actualLoginUrl, {
1471
+ waitUntil: BROWSER_CONFIG.WAIT_UNTIL,
1472
+ timeout: this.DEFAULT_TIMEOUT
1473
+ });
1474
+ await humanWait('navigation');
1475
+ }
1476
+
1477
+ // Wait for login form
1478
+ await page.waitForSelector(selectors.username, { timeout: this.DEFAULT_TIMEOUT });
1479
+
1480
+ // Fill in credentials with human-like typing
1481
+ await humanType(page, selectors.username, credentials.username, {
1482
+ clearFirst: true,
1483
+ simulateTypos: false
1484
+ });
1485
+ await humanWait('action');
1486
+
1487
+ // Handle multi-step login (e.g., Google)
1488
+ if (knownSite.multiStep && selectors.submitEmail) {
1489
+ await humanSubmit(page, selectors.submitEmail, { cursor, waitForNavigation: true });
1490
+ await humanWait('navigation');
1491
+ await page.waitForSelector(selectors.password, { timeout: this.DEFAULT_TIMEOUT });
1492
+ }
1493
+
1494
+ // Enter password
1495
+ await humanType(page, selectors.password, credentials.password, {
1496
+ clearFirst: true,
1497
+ simulateTypos: false
1498
+ });
1499
+ await humanWait('action');
1500
+
1501
+ // Submit the form
1502
+ const submitSelector = selectors.submitPassword || selectors.submit;
1503
+ await humanSubmit(page, submitSelector, { cursor, waitForNavigation: true });
1504
+ await humanWait('afterSubmit');
1505
+
1506
+ // Check for success or error
1507
+ let loginSuccess = false;
1508
+ let loginError = null;
1509
+
1510
+ // Wait for either success or error indicator
1511
+ try {
1512
+ if (selectors.loginSuccess) {
1513
+ await page.waitForSelector(selectors.loginSuccess, { timeout: 15000 });
1514
+ loginSuccess = true;
1515
+ } else {
1516
+ // No explicit success selector — check if URL changed (common indicator of successful login)
1517
+ await humanWait('navigation');
1518
+ const postLoginUrl = page.url();
1519
+ const urlChanged = postLoginUrl !== actualLoginUrl;
1520
+
1521
+ // Also check if error indicator appeared
1522
+ let errorVisible = false;
1523
+ if (selectors.loginError) {
1524
+ try {
1525
+ const errorEl = await page.$(selectors.loginError);
1526
+ if (errorEl) {
1527
+ errorVisible = true;
1528
+ loginError = await page.evaluate(el => el.textContent?.trim(), errorEl);
1529
+ }
1530
+ } catch {}
1531
+ }
1532
+
1533
+ if (errorVisible) {
1534
+ loginSuccess = false;
1535
+ } else if (urlChanged) {
1536
+ loginSuccess = true;
1537
+ } else {
1538
+ // URL didn't change and no error selector found — ambiguous
1539
+ loginSuccess = false;
1540
+ loginError = 'Login result is ambiguous: no success indicator found and URL did not change after submit. The login may have failed silently. Provide a loginSuccess selector for reliable detection.';
1541
+ }
1542
+ }
1543
+ } catch (waitErr) {
1544
+ // Success selector wait timed out — login likely failed
1545
+ loginSuccess = false;
1546
+ loginError = `Login success indicator "${selectors.loginSuccess}" not found within 15s.`;
1547
+
1548
+ // Also check for explicit error message
1549
+ if (selectors.loginError) {
1550
+ try {
1551
+ const errorElement = await page.$(selectors.loginError);
1552
+ if (errorElement) {
1553
+ const extractedError = await page.evaluate(el => el.textContent?.trim(), errorElement);
1554
+ if (extractedError) {
1555
+ loginError = `Login failed: ${extractedError}`;
1556
+ }
1557
+ }
1558
+ } catch (errExtract) {
1559
+ this.logger?.warn('[WebTool] Error extraction failed', { error: errExtract.message });
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ if (loginSuccess) {
1565
+ // Save session cookies for future use
1566
+ const cookies = await page.cookies();
1567
+ await vault.saveSession(normalizedSiteId, cookies);
1568
+
1569
+ this.logger?.info('[WebTool] Login successful', { siteId: normalizedSiteId });
1570
+
1571
+ // Optionally keep the tab open for continued browsing
1572
+ if (keepTabOpen && tabName && agentId) {
1573
+ // Initialize agent tabs if needed
1574
+ if (!this.agentTabs.has(agentId)) {
1575
+ this.agentTabs.set(agentId, new Map());
1576
+ }
1577
+ const agentTabsMap = this.agentTabs.get(agentId);
1578
+
1579
+ // Store the authenticated page as a named tab
1580
+ agentTabsMap.set(tabName, {
1581
+ page,
1582
+ url: actualLoginUrl,
1583
+ lastActivity: Date.now(),
1584
+ headless: stealthLevel === 'standard',
1585
+ consoleMessages: [],
1586
+ name: tabName,
1587
+ humanMode: true,
1588
+ authenticated: true,
1589
+ siteId: normalizedSiteId
1590
+ });
1591
+
1592
+ this.logger?.info('[WebTool] Tab kept open after authentication', {
1593
+ tabName,
1594
+ siteId: normalizedSiteId
1595
+ });
1596
+
1597
+ return {
1598
+ success: true,
1599
+ message: `Successfully logged into ${credentials.name || normalizedSiteId}. Tab '${tabName}' is ready for continued browsing.`,
1600
+ siteId: normalizedSiteId,
1601
+ method: 'credentials',
1602
+ tabName,
1603
+ tabKeptOpen: true
1604
+ };
1605
+ }
1606
+
1607
+ await page.close();
1608
+ return {
1609
+ success: true,
1610
+ message: `Successfully logged into ${credentials.name || normalizedSiteId}`,
1611
+ siteId: normalizedSiteId,
1612
+ method: 'credentials'
1613
+ };
1614
+ } else {
1615
+ await page.close();
1616
+ return {
1617
+ success: false,
1618
+ error: loginError || `Login failed for ${normalizedSiteId}`,
1619
+ siteId: normalizedSiteId
1620
+ };
1621
+ }
1622
+
1623
+ } catch (error) {
1624
+ await page.close();
1625
+ this.logger?.error('[WebTool] Authentication failed', {
1626
+ siteId: normalizedSiteId,
1627
+ error: error.message
1628
+ });
1629
+
1630
+ return {
1631
+ success: false,
1632
+ error: `Authentication failed: ${error.message}`,
1633
+ siteId: normalizedSiteId
1634
+ };
1635
+ }
1636
+ }
1637
+
1638
+ /**
1639
+ * Open a new tab with nested actions
1640
+ * @param {string} agentId - Agent identifier
1641
+ * @param {string} tabName - Unique tab name
1642
+ * @param {string} url - Initial URL
1643
+ * @param {boolean} headless - Headless mode
1644
+ * @param {Array} nestedActions - Actions to execute in this tab
1645
+ * @param {Object} context - Execution context
1646
+ * @param {Object} options - Additional options
1647
+ * @param {boolean} options.humanMode - Enable human-like behavior
1648
+ * @returns {Promise<Object>} Result
1649
+ */
1650
+
1651
+ /**
1652
+ * Deduplicate an array of error strings, returning "error x N" for repeats.
1653
+ * Keeps output compact for the agent.
1654
+ * @param {string[]} errors - Array of error message strings
1655
+ * @returns {string[]} Deduplicated array with counts
1656
+ * @private
1657
+ */
1658
+ static _dedupeErrors(errors) {
1659
+ if (!errors || errors.length === 0) return [];
1660
+ const counts = new Map();
1661
+ for (const e of errors) {
1662
+ counts.set(e, (counts.get(e) || 0) + 1);
1663
+ }
1664
+ return Array.from(counts.entries()).map(([msg, count]) =>
1665
+ count > 1 ? `${msg} (x${count})` : msg
1666
+ );
1667
+ }
1668
+
1669
+ async openTab(agentId, tabName, url, headless, nestedActions = [], context = {}, options = {}) {
1670
+ const { humanMode = true } = options; // Default to human mode
1671
+
1672
+ // Initialize agent tabs if not exists
1673
+ if (!this.agentTabs.has(agentId)) {
1674
+ this.agentTabs.set(agentId, new Map());
1675
+ }
1676
+
1677
+ const agentTabsMap = this.agentTabs.get(agentId);
1678
+
1679
+ // If tab already exists, reuse it — execute nested actions on the existing page
1680
+ if (agentTabsMap.has(tabName)) {
1681
+ const existingTab = agentTabsMap.get(tabName);
1682
+ existingTab.lastActivity = Date.now();
1683
+ const results = [];
1684
+
1685
+ if (url) {
1686
+ const reuseNavResponse = await existingTab.page.goto(url, {
1687
+ waitUntil: BROWSER_CONFIG.WAIT_UNTIL,
1688
+ timeout: BROWSER_CONFIG.DEFAULT_TIMEOUT_MS
1689
+ });
1690
+ const reuseNavStatus = reuseNavResponse ? reuseNavResponse.status() : null;
1691
+ if (humanMode) await humanWait('navigation');
1692
+ if (reuseNavStatus && reuseNavStatus >= 400) {
1693
+ results.push({
1694
+ action: 'navigate',
1695
+ success: false,
1696
+ url: existingTab.page.url(),
1697
+ httpStatus: reuseNavStatus,
1698
+ error: `Navigation got HTTP ${reuseNavStatus}`
1699
+ });
1700
+ }
1701
+ }
1702
+
1703
+ for (const nestedAction of (nestedActions || [])) {
1704
+ // Snapshot error counts before action
1705
+ const prePageErrors = (existingTab.pageErrors || []).length;
1706
+ const preNetworkFailures = (existingTab.networkFailures || []).length;
1707
+ const preHttpErrors = (existingTab.httpErrors || []).length;
1708
+
1709
+ const actionResult = await this.executeTabAction(existingTab.page, nestedAction, existingTab, context);
1710
+
1711
+ const actionEntry = { action: nestedAction.type || nestedAction.action, ...actionResult };
1712
+
1713
+ // Attach errors that occurred during this action
1714
+ const newPageErrors = (existingTab.pageErrors || []).slice(prePageErrors);
1715
+ const newNetworkFails = (existingTab.networkFailures || []).slice(preNetworkFailures);
1716
+ const newHttpErrs = (existingTab.httpErrors || []).slice(preHttpErrors);
1717
+ const dJs = WebTool._dedupeErrors(newPageErrors.map(e => e.message));
1718
+ const dNet = WebTool._dedupeErrors(newNetworkFails.map(f => `${f.method} ${f.url} → ${f.errorText}`));
1719
+ const dHttp = WebTool._dedupeErrors(newHttpErrs.map(e => `${e.method} ${e.url} → ${e.status}`));
1720
+ if (dJs.length > 0) actionEntry.jsErrors = dJs;
1721
+ if (dNet.length > 0) actionEntry.networkFailures = dNet;
1722
+ if (dHttp.length > 0) actionEntry.httpErrors = dHttp;
1723
+
1724
+ results.push(actionEntry);
1725
+ existingTab.lastActivity = Date.now();
1726
+ if (humanMode) await humanWait('action');
1727
+ }
1728
+
1729
+ const anyFailed = results.some(r => r.success === false);
1730
+ return {
1731
+ success: !anyFailed,
1732
+ tabName,
1733
+ url: existingTab.page.url(),
1734
+ actionsExecuted: results.length,
1735
+ results,
1736
+ reused: true,
1737
+ ...(anyFailed && { warning: `${results.filter(r => r.success === false).length} of ${results.length} action(s) failed` }),
1738
+ ...((existingTab.pageErrors || []).length > 0 && { jsErrors: WebTool._dedupeErrors(existingTab.pageErrors.map(e => e.message)) }),
1739
+ ...((existingTab.networkFailures || []).length > 0 && { networkFailures: WebTool._dedupeErrors(existingTab.networkFailures.map(f => `${f.method} ${f.url} → ${f.errorText}`)) }),
1740
+ ...((existingTab.httpErrors || []).length > 0 && { httpErrors: WebTool._dedupeErrors(existingTab.httpErrors.map(e => `${e.method} ${e.url} → ${e.status}`)) })
1741
+ };
1742
+ }
1743
+
1744
+ this.logger?.info('[WebTool] Opening stealth tab', { agentId, tabName, url, headless, humanMode });
1745
+
1746
+ // Create stealth page with optional human cursor
1747
+ const { page, cursor } = await this.createPage({ humanMode });
1748
+
1749
+ // RESTORE SESSION COOKIES if available for this domain
1750
+ // This enables authenticated browsing after using the authenticate operation
1751
+ if (url) {
1752
+ try {
1753
+ const vault = getCredentialVault(this.logger);
1754
+ await vault.initialize();
1755
+
1756
+ // Extract domain from URL to find matching sessions
1757
+ const urlObj = new URL(url);
1758
+ const domain = urlObj.hostname.replace(/^www\./, '');
1759
+
1760
+ // Check all stored sessions for matching domain
1761
+ const allSessions = vault.getAllSessions ? vault.getAllSessions() : {};
1762
+ for (const [siteId, session] of Object.entries(allSessions)) {
1763
+ if (session.cookies && session.cookies.length > 0) {
1764
+ // Check if any cookie domain matches our target URL
1765
+ const hasMatchingCookies = session.cookies.some(cookie => {
1766
+ const cookieDomain = (cookie.domain || '').replace(/^\./, '');
1767
+ return domain.includes(cookieDomain) || cookieDomain.includes(domain);
1768
+ });
1769
+
1770
+ if (hasMatchingCookies) {
1771
+ this.logger?.info('[WebTool] Restoring session cookies for domain', {
1772
+ siteId,
1773
+ domain,
1774
+ cookieCount: session.cookies.length
1775
+ });
1776
+
1777
+ // Filter cookies that match this domain
1778
+ const relevantCookies = session.cookies.filter(cookie => {
1779
+ const cookieDomain = (cookie.domain || '').replace(/^\./, '');
1780
+ return domain.includes(cookieDomain) || cookieDomain.includes(domain);
1781
+ });
1782
+
1783
+ if (relevantCookies.length > 0) {
1784
+ await page.setCookie(...relevantCookies);
1785
+ this.logger?.debug('[WebTool] Session cookies restored', {
1786
+ domain,
1787
+ restoredCount: relevantCookies.length
1788
+ });
1789
+ }
1790
+ break; // Only restore from first matching session
1791
+ }
1792
+ }
1793
+ }
1794
+ } catch (cookieError) {
1795
+ this.logger?.warn('[WebTool] Failed to restore session cookies (non-fatal)', {
1796
+ error: cookieError.message
1797
+ });
1798
+ }
1799
+ }
1800
+
1801
+ // Track console messages
1802
+ const consoleMessages = [];
1803
+ page.on('console', msg => {
1804
+ consoleMessages.push({
1805
+ type: msg.type(),
1806
+ text: msg.text(),
1807
+ timestamp: Date.now()
1808
+ });
1809
+ });
1810
+
1811
+ // Track JS errors (uncaught exceptions in the page)
1812
+ const pageErrors = [];
1813
+ page.on('pageerror', err => {
1814
+ pageErrors.push({
1815
+ message: err.message || String(err),
1816
+ timestamp: Date.now()
1817
+ });
1818
+ });
1819
+
1820
+ // Track failed network requests (DNS, CORS, timeouts, connection refused, etc.)
1821
+ const networkFailures = [];
1822
+ page.on('requestfailed', req => {
1823
+ const failure = req.failure();
1824
+ networkFailures.push({
1825
+ url: req.url(),
1826
+ method: req.method(),
1827
+ resourceType: req.resourceType(),
1828
+ errorText: failure ? failure.errorText : 'unknown',
1829
+ timestamp: Date.now()
1830
+ });
1831
+ });
1832
+
1833
+ // Track failed HTTP responses on XHR/fetch (API errors that don't throw but return 4xx/5xx)
1834
+ const httpErrors = [];
1835
+ page.on('response', res => {
1836
+ const status = res.status();
1837
+ const resourceType = res.request().resourceType();
1838
+ // Only track XHR/fetch failures, not static assets like images/css (too noisy)
1839
+ if (status >= 400 && (resourceType === 'xhr' || resourceType === 'fetch')) {
1840
+ httpErrors.push({
1841
+ url: res.url(),
1842
+ status,
1843
+ method: res.request().method(),
1844
+ timestamp: Date.now()
1845
+ });
1846
+ }
1847
+ });
1848
+
1849
+ // Store tab info with cursor for human-like actions
1850
+ const tabInfo = {
1851
+ page,
1852
+ url,
1853
+ lastActivity: Date.now(),
1854
+ headless,
1855
+ consoleMessages,
1856
+ pageErrors,
1857
+ networkFailures,
1858
+ httpErrors,
1859
+ name: tabName,
1860
+ humanMode,
1861
+ cursor // Store cursor for reuse in actions
1862
+ };
1863
+
1864
+ agentTabsMap.set(tabName, tabInfo);
1865
+
1866
+ const results = [];
1867
+
1868
+ try {
1869
+ // Navigate to initial URL if provided
1870
+ if (url) {
1871
+ const openTabResponse = await page.goto(url, {
1872
+ waitUntil: BROWSER_CONFIG.WAIT_UNTIL,
1873
+ timeout: this.DEFAULT_TIMEOUT
1874
+ });
1875
+ tabInfo.url = page.url();
1876
+ tabInfo.lastActivity = Date.now();
1877
+ const openTabStatus = openTabResponse ? openTabResponse.status() : null;
1878
+ tabInfo.httpStatus = openTabStatus;
1879
+
1880
+ // Human-like wait after navigation
1881
+ if (humanMode) {
1882
+ await humanWait('navigation');
1883
+ }
1884
+
1885
+ // Warn on HTTP errors but don't fail — tab is still usable
1886
+ if (openTabStatus && openTabStatus >= 400) {
1887
+ results.push({
1888
+ action: 'navigate',
1889
+ success: false,
1890
+ url: tabInfo.url,
1891
+ httpStatus: openTabStatus,
1892
+ error: `Initial navigation got HTTP ${openTabStatus}`
1893
+ });
1894
+ }
1895
+ }
1896
+
1897
+ // Execute nested actions — drain page/network errors after each action
1898
+ for (const action of nestedActions) {
1899
+ // Snapshot error counts before action
1900
+ const prePageErrors = pageErrors.length;
1901
+ const preNetworkFailures = networkFailures.length;
1902
+ const preHttpErrors = httpErrors.length;
1903
+ const preConsoleErrors = consoleMessages.filter(m => m.type === 'error').length;
1904
+
1905
+ const actionResult = await this.executeTabAction(page, action, tabInfo, context);
1906
+
1907
+ // Collect errors that occurred during this action
1908
+ const newPageErrors = pageErrors.slice(prePageErrors);
1909
+ const newNetworkFailures = networkFailures.slice(preNetworkFailures);
1910
+ const newHttpErrors = httpErrors.slice(preHttpErrors);
1911
+ const newConsoleErrors = consoleMessages.filter(m => m.type === 'error').slice(preConsoleErrors);
1912
+
1913
+ const actionEntry = {
1914
+ action: action.type || action.action,
1915
+ ...actionResult
1916
+ };
1917
+
1918
+ // Attach detected issues to the action result (deduplicated)
1919
+ const dedupedJs = WebTool._dedupeErrors(newPageErrors.map(e => e.message));
1920
+ const dedupedNet = WebTool._dedupeErrors(newNetworkFailures.map(f => `${f.method} ${f.url} → ${f.errorText}`));
1921
+ const dedupedHttp = WebTool._dedupeErrors(newHttpErrors.map(e => `${e.method} ${e.url} → ${e.status}`));
1922
+ const dedupedConsole = WebTool._dedupeErrors(newConsoleErrors.map(e => e.text));
1923
+ if (dedupedJs.length > 0) actionEntry.jsErrors = dedupedJs;
1924
+ if (dedupedNet.length > 0) actionEntry.networkFailures = dedupedNet;
1925
+ if (dedupedHttp.length > 0) actionEntry.httpErrors = dedupedHttp;
1926
+ if (dedupedConsole.length > 0) actionEntry.consoleErrors = dedupedConsole;
1927
+
1928
+ results.push(actionEntry);
1929
+ tabInfo.lastActivity = Date.now();
1930
+
1931
+ // Human-like delay between actions
1932
+ if (humanMode) {
1933
+ await humanWait('action');
1934
+ }
1935
+ }
1936
+
1937
+ const anyFailed = results.some(r => r.success === false);
1938
+ // Summarize all page issues for the agent (deduplicated)
1939
+ const allJsErrors = WebTool._dedupeErrors(pageErrors.map(e => e.message));
1940
+ const allNetworkFails = WebTool._dedupeErrors(networkFailures.map(f => `${f.method} ${f.url} → ${f.errorText}`));
1941
+ const allHttpErrs = WebTool._dedupeErrors(httpErrors.map(e => `${e.method} ${e.url} → ${e.status}`));
1942
+
1943
+ return {
1944
+ success: !anyFailed,
1945
+ tabName,
1946
+ url: tabInfo.url,
1947
+ actionsExecuted: results.length,
1948
+ results,
1949
+ ...(anyFailed && { warning: `${results.filter(r => r.success === false).length} of ${results.length} action(s) failed` }),
1950
+ // Surface all detected issues at the top level for agent awareness
1951
+ ...(allJsErrors.length > 0 && { jsErrors: allJsErrors }),
1952
+ ...(allNetworkFails.length > 0 && { networkFailures: allNetworkFails }),
1953
+ ...(allHttpErrs.length > 0 && { httpErrors: allHttpErrs })
1954
+ };
1955
+
1956
+ } catch (error) {
1957
+ this.logger?.error('Failed to open tab', {
1958
+ agentId,
1959
+ tabName,
1960
+ error: error.message
1961
+ });
1962
+
1963
+ // Check if the page is still usable before deciding to clean up
1964
+ let pageStillAlive = false;
1965
+ try {
1966
+ await page.evaluate(() => document.readyState);
1967
+ pageStillAlive = true;
1968
+ } catch { /* page is dead */ }
1969
+
1970
+ if (pageStillAlive) {
1971
+ // Keep the tab alive so subsequent operations can reuse it
1972
+ // (the error was likely in a nested action, not in navigation itself)
1973
+ this.logger?.warn('[WebTool] Tab kept alive despite error — page is still usable', { tabName });
1974
+ return {
1975
+ success: false,
1976
+ tabName,
1977
+ url: tabInfo.url || page.url(),
1978
+ actionsExecuted: results.length,
1979
+ results,
1980
+ error: error.message,
1981
+ warning: 'Tab is still open and reusable despite the error above.'
1982
+ };
1983
+ } else {
1984
+ // Page is genuinely dead — clean up
1985
+ try { await page.close(); } catch { /* ignore close errors */ }
1986
+ agentTabsMap.delete(tabName);
1987
+ throw error;
1988
+ }
1989
+ }
1990
+ }
1991
+
1992
+ /**
1993
+ * Execute an action in a tab context
1994
+ * @param {Page} page - Puppeteer page
1995
+ * @param {Object} action - Action to execute
1996
+ * @param {Object} tabInfo - Tab information (includes humanMode and cursor)
1997
+ * @param {Object} context - Execution context
1998
+ * @returns {Promise<Object>} Action result
1999
+ * @private
2000
+ */
2001
+ async executeTabAction(page, action, tabInfo, context) {
2002
+ const { humanMode = false, cursor = null } = tabInfo;
2003
+ // Accept both "type" and "action" as the action discriminator
2004
+ if (!action.type && action.action) action.type = action.action;
2005
+
2006
+ switch (action.type) {
2007
+ case 'navigate': {
2008
+ const navUrl = action.value || action.url;
2009
+ // Validate URL format
2010
+ if (!navUrl || typeof navUrl !== 'string') {
2011
+ return { success: false, error: 'URL is required for navigate action' };
2012
+ }
2013
+ try {
2014
+ new URL(navUrl);
2015
+ } catch {
2016
+ return { success: false, error: `Invalid URL format: "${navUrl}". Must include protocol (https://)` };
2017
+ }
2018
+ const navResponse = await page.goto(navUrl, {
2019
+ waitUntil: BROWSER_CONFIG.WAIT_UNTIL,
2020
+ timeout: this.DEFAULT_TIMEOUT
2021
+ });
2022
+ tabInfo.url = page.url();
2023
+ // Check HTTP status
2024
+ const navStatus = navResponse ? navResponse.status() : null;
2025
+ if (humanMode) {
2026
+ await humanWait('navigation');
2027
+ }
2028
+ if (navStatus && navStatus >= 400) {
2029
+ return {
2030
+ success: false,
2031
+ url: tabInfo.url,
2032
+ httpStatus: navStatus,
2033
+ error: `Navigation failed with HTTP ${navStatus} (${navStatus >= 500 ? 'server error' : navStatus === 404 ? 'page not found' : navStatus === 403 ? 'access forbidden' : 'client error'})`
2034
+ };
2035
+ }
2036
+ return { success: true, url: tabInfo.url, httpStatus: navStatus };
2037
+ }
2038
+
2039
+ case 'click': {
2040
+ if (!action.selector) {
2041
+ return { success: false, error: 'selector is required for click action' };
2042
+ }
2043
+ // Pre-check selector exists
2044
+ const clickTarget = await page.$(action.selector);
2045
+ if (!clickTarget) {
2046
+ const pageUrl = page.url();
2047
+ return {
2048
+ success: false,
2049
+ error: `Element not found: "${action.selector}" on ${pageUrl}`,
2050
+ selector: action.selector,
2051
+ suggestion: 'The element may not exist on this page, may have a different selector, or may not have loaded yet. Try wait-for first, or use extract-text/extract-links to inspect available elements.'
2052
+ };
2053
+ }
2054
+ // Scroll element into view before interacting (prevents offscreen failures)
2055
+ await page.evaluate((sel) => {
2056
+ const el = document.querySelector(sel);
2057
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2058
+ }, action.selector);
2059
+ await new Promise(r => setTimeout(r, 300)); // Brief settle after scroll
2060
+ if (humanMode && cursor) {
2061
+ try {
2062
+ await cursor.click(action.selector);
2063
+ } catch (cursorErr) {
2064
+ // Fallback to standard Puppeteer click if ghost-cursor fails
2065
+ // (e.g., element obscured by overlay, zero-size, or offscreen)
2066
+ this.logger?.warn('[WebTool] ghost-cursor click failed, falling back to standard click', {
2067
+ selector: action.selector,
2068
+ error: cursorErr.message
2069
+ });
2070
+ try {
2071
+ await page.click(action.selector, { button: action.button || 'left' });
2072
+ } catch (puppeteerClickErr) {
2073
+ // Final fallback: use JavaScript .click() for hidden/styled elements
2074
+ // (e.g., custom checkboxes, radio buttons with display:none inputs)
2075
+ this.logger?.warn('[WebTool] Puppeteer click also failed, using JS click fallback', {
2076
+ selector: action.selector,
2077
+ error: puppeteerClickErr.message
2078
+ });
2079
+ await page.evaluate((sel) => {
2080
+ const el = document.querySelector(sel);
2081
+ if (el) {
2082
+ // For checkboxes/radios, try clicking the associated label first
2083
+ if (el.type === 'checkbox' || el.type === 'radio') {
2084
+ const label = el.id ? document.querySelector(`label[for="${el.id}"]`) : null;
2085
+ if (label) {
2086
+ label.click();
2087
+ } else {
2088
+ el.click();
2089
+ }
2090
+ } else {
2091
+ el.click();
2092
+ }
2093
+ }
2094
+ }, action.selector);
2095
+ }
2096
+ }
2097
+ } else {
2098
+ try {
2099
+ await page.click(action.selector, {
2100
+ button: action.button || 'left'
2101
+ });
2102
+ } catch (clickErr) {
2103
+ // JS fallback for non-humanMode too
2104
+ this.logger?.warn('[WebTool] Standard click failed, using JS click fallback', {
2105
+ selector: action.selector,
2106
+ error: clickErr.message
2107
+ });
2108
+ await page.evaluate((sel) => {
2109
+ const el = document.querySelector(sel);
2110
+ if (el) {
2111
+ if (el.type === 'checkbox' || el.type === 'radio') {
2112
+ const label = el.id ? document.querySelector(`label[for="${el.id}"]`) : null;
2113
+ if (label) { label.click(); } else { el.click(); }
2114
+ } else {
2115
+ el.click();
2116
+ }
2117
+ }
2118
+ }, action.selector);
2119
+ }
2120
+ }
2121
+ // Verify checkbox/radio state after click for agent awareness
2122
+ const clickedElState = await page.evaluate((sel) => {
2123
+ const el = document.querySelector(sel);
2124
+ if (!el) return null;
2125
+ if (el.type === 'checkbox' || el.type === 'radio') {
2126
+ return { isToggle: true, checked: el.checked, type: el.type };
2127
+ }
2128
+ return { isToggle: false };
2129
+ }, action.selector);
2130
+ return {
2131
+ success: true,
2132
+ selector: action.selector,
2133
+ ...(clickedElState?.isToggle && { checked: clickedElState.checked, elementType: clickedElState.type })
2134
+ };
2135
+ }
2136
+
2137
+ case 'type': {
2138
+ if (!action.selector) {
2139
+ return { success: false, error: 'selector is required for type action' };
2140
+ }
2141
+ const typeText = action.text || action.value || '';
2142
+ // Pre-check selector exists
2143
+ const typeTarget = await page.$(action.selector);
2144
+ if (!typeTarget) {
2145
+ return {
2146
+ success: false,
2147
+ error: `Element not found: "${action.selector}" on ${page.url()}`,
2148
+ selector: action.selector,
2149
+ suggestion: 'The input element may not exist or may have a different selector. Use extract-text to inspect the page.'
2150
+ };
2151
+ }
2152
+ // Scroll element into view before interacting (prevents offscreen failures)
2153
+ await page.evaluate((sel) => {
2154
+ const el = document.querySelector(sel);
2155
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2156
+ }, action.selector);
2157
+ await new Promise(r => setTimeout(r, 300)); // Brief settle after scroll
2158
+ if (humanMode) {
2159
+ try {
2160
+ await humanType(page, action.selector, typeText, {
2161
+ clearFirst: action.clearFirst !== false,
2162
+ simulateTypos: action.simulateTypos || false
2163
+ });
2164
+ } catch (humanTypeErr) {
2165
+ // Fallback to standard Puppeteer typing if humanType fails
2166
+ this.logger?.warn('[WebTool] humanType failed, falling back to standard type', {
2167
+ selector: action.selector,
2168
+ error: humanTypeErr.message
2169
+ });
2170
+ if (action.clearFirst !== false) {
2171
+ await page.click(action.selector, { clickCount: 3 });
2172
+ await page.keyboard.press('Backspace');
2173
+ }
2174
+ await page.type(action.selector, typeText);
2175
+ }
2176
+ } else {
2177
+ if (action.clearFirst !== false) {
2178
+ await page.click(action.selector, { clickCount: 3 });
2179
+ await page.keyboard.press('Backspace');
2180
+ }
2181
+ await page.type(action.selector, typeText);
2182
+ }
2183
+ // Verify the value was set — fallback to direct value injection if needed
2184
+ let verificationNote = null;
2185
+ try {
2186
+ const currentVal = await page.evaluate((sel) => {
2187
+ const el = document.querySelector(sel);
2188
+ return el ? (el.value || el.textContent) : null;
2189
+ }, action.selector);
2190
+ if (currentVal !== null && !currentVal.includes(typeText.slice(0, 5)) && typeText.length > 0) {
2191
+ this.logger?.warn('[WebTool] Type verification failed, using direct value injection', { selector: action.selector });
2192
+ await page.evaluate((sel, val) => {
2193
+ const el = document.querySelector(sel);
2194
+ if (el) {
2195
+ el.value = val;
2196
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2197
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2198
+ }
2199
+ }, action.selector, typeText);
2200
+ verificationNote = 'Keyboard typing did not produce expected value; used direct value injection as fallback.';
2201
+ }
2202
+ } catch (verifyErr) {
2203
+ verificationNote = `Type verification check failed: ${verifyErr.message}`;
2204
+ }
2205
+ return {
2206
+ success: true,
2207
+ selector: action.selector,
2208
+ text: action.text,
2209
+ ...(verificationNote && { warning: verificationNote })
2210
+ };
2211
+ }
2212
+
2213
+ case 'press':
2214
+ await page.keyboard.press(action.key || action.value);
2215
+ return { success: true, key: action.key };
2216
+
2217
+ case 'wait-for': {
2218
+ if (!action.selector) return { success: false, error: 'selector is required for wait-for action' };
2219
+ const waitTimeout = action.timeout ? parseInt(action.timeout, 10) : this.DEFAULT_TIMEOUT;
2220
+ try {
2221
+ await page.waitForSelector(action.selector, { timeout: waitTimeout });
2222
+ return { success: true, selector: action.selector };
2223
+ } catch (waitErr) {
2224
+ return {
2225
+ success: false,
2226
+ selector: action.selector,
2227
+ error: `Element "${action.selector}" did not appear within ${Math.round(waitTimeout / 1000)}s on ${page.url()}`,
2228
+ suggestion: 'The element may not exist on this page, may use a different selector, or may require user interaction first.'
2229
+ };
2230
+ }
2231
+ }
2232
+
2233
+ case 'screenshot':
2234
+ return await this.takeScreenshot(page, action, context);
2235
+
2236
+ case 'analyze-screenshot':
2237
+ return await this.analyzeScreenshot(page, action.value, context);
2238
+
2239
+ case 'extract-text': {
2240
+ if (!action.selector) {
2241
+ return { success: false, error: 'selector is required for extract-text action' };
2242
+ }
2243
+ const extractedText = await page.evaluate((sel) => {
2244
+ const element = document.querySelector(sel);
2245
+ return element ? element.innerText : null;
2246
+ }, action.selector);
2247
+ if (extractedText === null) {
2248
+ return {
2249
+ success: false,
2250
+ selector: action.selector,
2251
+ error: `Element not found: "${action.selector}" on ${page.url()}`,
2252
+ text: null
2253
+ };
2254
+ }
2255
+ return { success: true, selector: action.selector, text: extractedText };
2256
+ }
2257
+
2258
+ case 'extract-links':
2259
+ const links = await page.evaluate((sel) => {
2260
+ const elements = document.querySelectorAll(sel);
2261
+ return Array.from(elements).map(a => ({
2262
+ href: a.href,
2263
+ text: a.textContent.trim()
2264
+ }));
2265
+ }, action.selector);
2266
+ return { success: true, selector: action.selector, links };
2267
+
2268
+ case 'get-source':
2269
+ const html = await page.content();
2270
+ return { success: true, html };
2271
+
2272
+ case 'get-console':
2273
+ return {
2274
+ success: true,
2275
+ consoleMessages: [...tabInfo.consoleMessages]
2276
+ };
2277
+
2278
+ case 'scroll':
2279
+ if (humanMode) {
2280
+ // Human-like smooth scroll
2281
+ await humanScroll(page, {
2282
+ direction: action.direction || 'down',
2283
+ distance: action.distance || 300
2284
+ });
2285
+ } else {
2286
+ await page.evaluate((sel) => {
2287
+ if (sel) {
2288
+ document.querySelector(sel)?.scrollIntoView();
2289
+ } else {
2290
+ window.scrollTo(0, document.body.scrollHeight);
2291
+ }
2292
+ }, action.selector);
2293
+ }
2294
+ return { success: true };
2295
+
2296
+ case 'hover': {
2297
+ if (!action.selector) return { success: false, error: 'selector is required for hover action' };
2298
+ const hoverTarget = await page.$(action.selector);
2299
+ if (!hoverTarget) {
2300
+ return { success: false, error: `Element not found: "${action.selector}" on ${page.url()}`, selector: action.selector };
2301
+ }
2302
+ // Scroll into view first
2303
+ await page.evaluate((sel) => {
2304
+ const el = document.querySelector(sel);
2305
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2306
+ }, action.selector);
2307
+ await new Promise(r => setTimeout(r, 300));
2308
+ if (humanMode && cursor) {
2309
+ try {
2310
+ await cursor.hover(action.selector);
2311
+ } catch (cursorErr) {
2312
+ this.logger?.warn('[WebTool] ghost-cursor hover failed, falling back to standard hover', {
2313
+ selector: action.selector, error: cursorErr.message
2314
+ });
2315
+ await page.hover(action.selector);
2316
+ }
2317
+ } else {
2318
+ await page.hover(action.selector);
2319
+ }
2320
+ return { success: true, selector: action.selector };
2321
+ }
2322
+
2323
+ case 'mouse-move': {
2324
+ if (!action.selector) return { success: false, error: 'selector is required for mouse-move action' };
2325
+ const moveTarget = await page.$(action.selector);
2326
+ if (!moveTarget) {
2327
+ return { success: false, error: `Element not found: "${action.selector}" on ${page.url()}`, selector: action.selector };
2328
+ }
2329
+ // Scroll into view first
2330
+ await page.evaluate((sel) => {
2331
+ const el = document.querySelector(sel);
2332
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2333
+ }, action.selector);
2334
+ await new Promise(r => setTimeout(r, 300));
2335
+ if (humanMode && cursor) {
2336
+ try {
2337
+ await cursor.moveTo(action.selector);
2338
+ } catch (cursorErr) {
2339
+ this.logger?.warn('[WebTool] ghost-cursor moveTo failed, falling back to standard hover', {
2340
+ selector: action.selector, error: cursorErr.message
2341
+ });
2342
+ await page.hover(action.selector);
2343
+ }
2344
+ } else {
2345
+ await page.hover(action.selector);
2346
+ }
2347
+ return { success: true, selector: action.selector };
2348
+ }
2349
+
2350
+ case 'wait':
2351
+ case 'delay':
2352
+ // Time-based wait (max 30 seconds to prevent abuse)
2353
+ const waitTime = Math.min(action.waitTime || action.value || 1000, 30000);
2354
+ await new Promise(resolve => setTimeout(resolve, waitTime));
2355
+ return { success: true, waited: waitTime };
2356
+
2357
+ case 'submit': {
2358
+ if (!action.selector) return { success: false, error: 'selector is required for submit action' };
2359
+ const submitTarget = await page.$(action.selector);
2360
+ if (!submitTarget) {
2361
+ return { success: false, error: `Element not found: "${action.selector}" on ${page.url()}`, selector: action.selector };
2362
+ }
2363
+
2364
+ // Check if submit button is disabled
2365
+ const submitDisabled = await page.evaluate((sel) => {
2366
+ const el = document.querySelector(sel);
2367
+ return el?.disabled || el?.getAttribute('aria-disabled') === 'true';
2368
+ }, action.selector);
2369
+ if (submitDisabled) {
2370
+ return {
2371
+ success: false,
2372
+ selector: action.selector,
2373
+ error: `Submit button "${action.selector}" is disabled — required fields may be empty or invalid.`,
2374
+ suggestion: 'Use get-field-values to inspect form state, or check for unfilled required fields and unchecked agreement checkboxes.'
2375
+ };
2376
+ }
2377
+
2378
+ // Scroll submit button into view
2379
+ await page.evaluate((sel) => {
2380
+ const el = document.querySelector(sel);
2381
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2382
+ }, action.selector);
2383
+ await new Promise(r => setTimeout(r, 300));
2384
+
2385
+ // Capture pre-submit state for change detection
2386
+ const preSubmitBodyText = await page.evaluate(() => document.body.innerText.substring(0, 500));
2387
+ const preSubmitUrl = page.url();
2388
+
2389
+ // Set up network POST listener to capture form submission response
2390
+ const submitNetworkResults = [];
2391
+ const submitResponseHandler = async (response) => {
2392
+ const req = response.request();
2393
+ if (req.method() === 'POST') {
2394
+ let respBody = null;
2395
+ try { respBody = await response.text(); } catch {}
2396
+ submitNetworkResults.push({
2397
+ url: response.url(),
2398
+ status: response.status(),
2399
+ body: respBody?.substring(0, 1000)
2400
+ });
2401
+ }
2402
+ };
2403
+ page.on('response', submitResponseHandler);
2404
+
2405
+ // Determine if selector points to a form or a button
2406
+ let isFormElement = false;
2407
+ try {
2408
+ isFormElement = await page.evaluate((sel) => {
2409
+ const el = document.querySelector(sel);
2410
+ return el?.tagName?.toLowerCase() === 'form';
2411
+ }, action.selector);
2412
+ } catch (evalErr) {
2413
+ this.logger?.warn('[WebTool] Form detection failed', { error: evalErr.message });
2414
+ }
2415
+
2416
+ let navigationWarning = null;
2417
+ if (isFormElement) {
2418
+ await Promise.all([
2419
+ page.waitForNavigation({ timeout: 15000, waitUntil: BROWSER_CONFIG.WAIT_UNTIL }).catch((e) => {
2420
+ navigationWarning = `Navigation after submit did not complete: ${e.message}`;
2421
+ }),
2422
+ page.evaluate((sel) => document.querySelector(sel).submit(), action.selector)
2423
+ ]);
2424
+ } else if (humanMode && cursor) {
2425
+ try {
2426
+ await humanSubmit(page, action.selector, {
2427
+ cursor,
2428
+ waitForNavigation: action.waitForNavigation !== false
2429
+ });
2430
+ } catch (submitCursorErr) {
2431
+ // Fallback to standard click for submit
2432
+ this.logger?.warn('[WebTool] humanSubmit failed, falling back to standard click', {
2433
+ selector: action.selector, error: submitCursorErr.message
2434
+ });
2435
+ await page.click(action.selector);
2436
+ }
2437
+ } else {
2438
+ await Promise.all([
2439
+ action.waitForNavigation !== false
2440
+ ? page.waitForNavigation({ timeout: 15000, waitUntil: BROWSER_CONFIG.WAIT_UNTIL }).catch((e) => {
2441
+ navigationWarning = `Navigation after submit did not complete: ${e.message}`;
2442
+ })
2443
+ : Promise.resolve(),
2444
+ page.click(action.selector)
2445
+ ]);
2446
+ }
2447
+
2448
+ // Wait for AJAX responses to arrive (most forms submit via AJAX)
2449
+ await new Promise(r => setTimeout(r, 3000));
2450
+
2451
+ // Remove listener
2452
+ page.off('response', submitResponseHandler);
2453
+
2454
+ // Detect success indicators on the page
2455
+ const submitDetection = await page.evaluate(() => {
2456
+ const body = document.body.innerText;
2457
+ const successKeywords = ['thank', 'success', 'submitted', 'received', 'confirmation', 'we will contact'];
2458
+ const errorKeywords = ['error', 'failed', 'invalid', 'required', 'please fill', 'try again'];
2459
+ const foundSuccess = successKeywords.filter(kw => body.toLowerCase().includes(kw));
2460
+ const foundErrors = errorKeywords.filter(kw => body.toLowerCase().includes(kw));
2461
+
2462
+ // Look for success/error DOM elements
2463
+ const successEls = document.querySelectorAll('[class*="success"], [class*="thank"], [class*="confirmation"], [data-success]');
2464
+ const errorEls = document.querySelectorAll('[class*="error"], [class*="invalid"], [class*="alert-danger"], .field-error');
2465
+ const successMessages = Array.from(successEls).map(el => el.innerText?.trim()).filter(t => t && t.length < 200);
2466
+ const errorMessages = Array.from(errorEls).map(el => el.innerText?.trim()).filter(t => t && t.length < 200 && t.length > 0);
2467
+
2468
+ return { foundSuccess, foundErrors, successMessages, errorMessages };
2469
+ });
2470
+
2471
+ const postSubmitUrl = page.url();
2472
+ const urlChanged = postSubmitUrl !== preSubmitUrl;
2473
+
2474
+ // Determine if submission was confirmed
2475
+ const submitConfirmed = submitDetection.successMessages.length > 0 ||
2476
+ submitDetection.foundSuccess.length > 0 ||
2477
+ urlChanged ||
2478
+ submitNetworkResults.some(r => r.status >= 200 && r.status < 300);
2479
+
2480
+ return {
2481
+ success: true,
2482
+ selector: action.selector,
2483
+ url: postSubmitUrl,
2484
+ ...(urlChanged && { urlChanged: true, previousUrl: preSubmitUrl }),
2485
+ ...(navigationWarning && { warning: navigationWarning }),
2486
+ // Submission detection results
2487
+ submitConfirmed,
2488
+ ...(submitDetection.successMessages.length > 0 && { successMessage: submitDetection.successMessages[0] }),
2489
+ ...(submitDetection.errorMessages.length > 0 && { formErrors: submitDetection.errorMessages }),
2490
+ ...(submitNetworkResults.length > 0 && {
2491
+ networkResponse: submitNetworkResults.map(r => ({
2492
+ url: r.url.substring(0, 150),
2493
+ status: r.status,
2494
+ ...(r.body && r.body.length < 500 && { body: r.body })
2495
+ }))
2496
+ })
2497
+ };
2498
+ }
2499
+
2500
+ case 'evaluate': {
2501
+ // Execute arbitrary JavaScript in the page context
2502
+ const script = action.script || action.value;
2503
+ if (!script) {
2504
+ return { success: false, error: 'script is required for evaluate action. Provide JS code to execute in the page context.' };
2505
+ }
2506
+ try {
2507
+ const evalResult = await page.evaluate((code) => {
2508
+ try {
2509
+ // Wrap in Function to allow return statements, or eval directly
2510
+ const fn = new Function(code);
2511
+ const result = fn();
2512
+ // Serialize result (handle DOM elements, circular refs)
2513
+ if (result instanceof HTMLElement) {
2514
+ return { __type: 'HTMLElement', tagName: result.tagName, id: result.id, className: result.className?.toString(), text: result.innerText?.substring(0, 500) };
2515
+ }
2516
+ return JSON.parse(JSON.stringify(result ?? null));
2517
+ } catch (e) {
2518
+ return { __error: e.message };
2519
+ }
2520
+ }, script);
2521
+
2522
+ if (evalResult && evalResult.__error) {
2523
+ return { success: false, error: `Script execution error: ${evalResult.__error}` };
2524
+ }
2525
+ return { success: true, result: evalResult };
2526
+ } catch (evalErr) {
2527
+ return { success: false, error: `Evaluate failed: ${evalErr.message}` };
2528
+ }
2529
+ }
2530
+
2531
+ case 'get-field-values': {
2532
+ // Read current values of form fields — essential for verifying form state
2533
+ const selectors = action.selectors || (action.selector ? [action.selector] : null);
2534
+ if (!selectors || !Array.isArray(selectors) || selectors.length === 0) {
2535
+ return { success: false, error: 'selectors array is required for get-field-values action.' };
2536
+ }
2537
+ const fieldValues = await page.evaluate((sels) => {
2538
+ const results = {};
2539
+ for (const sel of sels) {
2540
+ const el = document.querySelector(sel);
2541
+ if (!el) {
2542
+ results[sel] = { found: false };
2543
+ continue;
2544
+ }
2545
+ const entry = { found: true, tagName: el.tagName.toLowerCase() };
2546
+ if (el.type === 'checkbox' || el.type === 'radio') {
2547
+ entry.checked = el.checked;
2548
+ entry.value = el.value;
2549
+ } else if (el.tagName === 'SELECT') {
2550
+ entry.value = el.value;
2551
+ entry.selectedText = el.options[el.selectedIndex]?.text;
2552
+ entry.options = Array.from(el.options).map(o => ({ value: o.value, text: o.text, selected: o.selected }));
2553
+ } else {
2554
+ entry.value = el.value || el.innerText?.substring(0, 500) || '';
2555
+ }
2556
+ entry.disabled = el.disabled || false;
2557
+ entry.required = el.required || el.hasAttribute('required') || false;
2558
+ results[sel] = entry;
2559
+ }
2560
+ return results;
2561
+ }, selectors);
2562
+ return { success: true, fields: fieldValues };
2563
+ }
2564
+
2565
+ case 'select': {
2566
+ // Select an option in a <select> dropdown or custom dropdown
2567
+ if (!action.selector) {
2568
+ return { success: false, error: 'selector is required for select action' };
2569
+ }
2570
+ const selectValue = action.value || action.text;
2571
+ if (selectValue === undefined || selectValue === null) {
2572
+ return { success: false, error: 'value or text is required for select action' };
2573
+ }
2574
+ const selectEl = await page.$(action.selector);
2575
+ if (!selectEl) {
2576
+ return {
2577
+ success: false,
2578
+ error: `Element not found: "${action.selector}" on ${page.url()}`,
2579
+ selector: action.selector,
2580
+ suggestion: 'Use get-source or extract-text to find the correct select element selector.'
2581
+ };
2582
+ }
2583
+ // Scroll into view
2584
+ await page.evaluate((sel) => {
2585
+ const el = document.querySelector(sel);
2586
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2587
+ }, action.selector);
2588
+ await new Promise(r => setTimeout(r, 300));
2589
+
2590
+ const isNativeSelect = await page.evaluate((sel) => {
2591
+ return document.querySelector(sel)?.tagName?.toLowerCase() === 'select';
2592
+ }, action.selector);
2593
+
2594
+ if (isNativeSelect) {
2595
+ // Native <select> — use Puppeteer's select method
2596
+ try {
2597
+ // Try matching by value first, then by visible text
2598
+ let selected = await page.select(action.selector, selectValue);
2599
+ if (!selected || selected.length === 0) {
2600
+ // Try matching by text content
2601
+ selected = await page.evaluate((sel, text) => {
2602
+ const selectEl = document.querySelector(sel);
2603
+ if (!selectEl) return [];
2604
+ const option = Array.from(selectEl.options).find(o =>
2605
+ o.text.toLowerCase().includes(text.toLowerCase()) ||
2606
+ o.value.toLowerCase().includes(text.toLowerCase())
2607
+ );
2608
+ if (option) {
2609
+ selectEl.value = option.value;
2610
+ selectEl.dispatchEvent(new Event('change', { bubbles: true }));
2611
+ selectEl.dispatchEvent(new Event('input', { bubbles: true }));
2612
+ return [option.value];
2613
+ }
2614
+ return [];
2615
+ }, action.selector, selectValue);
2616
+ }
2617
+ const currentValue = await page.evaluate((sel) => {
2618
+ const el = document.querySelector(sel);
2619
+ return { value: el?.value, text: el?.options?.[el?.selectedIndex]?.text };
2620
+ }, action.selector);
2621
+ return {
2622
+ success: true,
2623
+ selector: action.selector,
2624
+ selectedValue: currentValue.value,
2625
+ selectedText: currentValue.text
2626
+ };
2627
+ } catch (selectErr) {
2628
+ return { success: false, error: `Select failed: ${selectErr.message}`, selector: action.selector };
2629
+ }
2630
+ } else {
2631
+ // Custom dropdown — click to open, then click matching option
2632
+ if (humanMode && cursor) {
2633
+ try { await cursor.click(action.selector); } catch { await page.click(action.selector); }
2634
+ } else {
2635
+ await page.click(action.selector);
2636
+ }
2637
+ await new Promise(r => setTimeout(r, 500)); // Wait for dropdown to open
2638
+
2639
+ // Find and click the matching option
2640
+ const optionClicked = await page.evaluate((text) => {
2641
+ // Common dropdown option selectors
2642
+ const optionSelectors = [
2643
+ '[role="option"]', '[role="listbox"] li', '.dropdown-item', '.select-option',
2644
+ '[class*="option"]', '[class*="dropdown"] li', '[class*="menu"] li',
2645
+ 'ul[class*="select"] li', 'div[class*="select"] div[class*="option"]'
2646
+ ];
2647
+ for (const optSel of optionSelectors) {
2648
+ const options = document.querySelectorAll(optSel);
2649
+ for (const opt of options) {
2650
+ if (opt.innerText?.toLowerCase().includes(text.toLowerCase())) {
2651
+ opt.click();
2652
+ return { found: true, text: opt.innerText.trim() };
2653
+ }
2654
+ }
2655
+ }
2656
+ return { found: false };
2657
+ }, selectValue);
2658
+
2659
+ if (optionClicked.found) {
2660
+ return { success: true, selector: action.selector, selectedText: optionClicked.text };
2661
+ } else {
2662
+ return {
2663
+ success: false,
2664
+ selector: action.selector,
2665
+ error: `Could not find option matching "${selectValue}" in dropdown`,
2666
+ suggestion: 'Use extract-text on the dropdown or get-source to inspect available options.'
2667
+ };
2668
+ }
2669
+ }
2670
+ }
2671
+
2672
+ default:
2673
+ throw new Error(`Unknown action type: ${action.type}`);
2674
+ }
2675
+ }
2676
+
2677
+ /**
2678
+ * Take screenshot of page
2679
+ * @param {Page} page - Puppeteer page
2680
+ * @param {Object} options - Screenshot options
2681
+ * @param {Object} context - Execution context
2682
+ * @returns {Promise<Object>} Screenshot result
2683
+ * @private
2684
+ */
2685
+ async takeScreenshot(page, options, context) {
2686
+ const format = options.format || 'file';
2687
+ const screenshotPath = options.path;
2688
+
2689
+ if (format === 'base64') {
2690
+ const screenshot = await page.screenshot({ encoding: 'base64' });
2691
+ return {
2692
+ success: true,
2693
+ format: 'base64',
2694
+ screenshot
2695
+ };
2696
+ }
2697
+
2698
+ // File format
2699
+ let filePath;
2700
+
2701
+ if (screenshotPath) {
2702
+ // Save to project directory if path is provided
2703
+ const projectDir = context.directoryAccess?.workingDirectory || context.projectDir || process.cwd();
2704
+ filePath = path.isAbsolute(screenshotPath)
2705
+ ? screenshotPath
2706
+ : path.join(projectDir, screenshotPath);
2707
+ } else {
2708
+ // Save to temp directory
2709
+ const filename = `screenshot-${Date.now()}.png`;
2710
+ filePath = path.join(this.TEMP_DIR, filename);
2711
+ }
2712
+
2713
+ await page.screenshot({ path: filePath });
2714
+
2715
+ return {
2716
+ success: true,
2717
+ format: 'file',
2718
+ path: filePath
2719
+ };
2720
+ }
2721
+
2722
+ /**
2723
+ * Analyze screenshot using AI vision model
2724
+ * @param {Page} page - Puppeteer page
2725
+ * @param {string} question - Question for AI
2726
+ * @param {Object} context - Execution context
2727
+ * @returns {Promise<Object>} Analysis result
2728
+ * @private
2729
+ */
2730
+ async analyzeScreenshot(page, question, context) {
2731
+ // Take screenshot as base64
2732
+ const screenshot = await page.screenshot({ encoding: 'base64' });
2733
+
2734
+ // Get AI service from context
2735
+ const aiService = context.aiService;
2736
+ if (!aiService) {
2737
+ throw new Error('AI service not available for screenshot analysis');
2738
+ }
2739
+
2740
+ this.logger?.info('Analyzing screenshot with AI', {
2741
+ question: question.substring(0, 100),
2742
+ agentId: context.agentId
2743
+ });
2744
+
2745
+ try {
2746
+ // Use vision model (prefer o3 if available, fallback to gpt-4-vision)
2747
+ const model = 'o3'; // Will be mapped by AI service
2748
+
2749
+ // Create message with image
2750
+ const response = await aiService.sendMessage(
2751
+ model,
2752
+ question,
2753
+ {
2754
+ agentId: context.agentId,
2755
+ images: [`data:image/png;base64,${screenshot}`],
2756
+ apiKey: context.apiKey,
2757
+ customApiKeys: context.customApiKeys,
2758
+ platformProvided: context.platformProvided
2759
+ }
2760
+ );
2761
+
2762
+ return {
2763
+ success: true,
2764
+ question,
2765
+ analysis: response.content,
2766
+ model: response.model || model
2767
+ };
2768
+
2769
+ } catch (error) {
2770
+ this.logger?.error('Screenshot analysis failed', {
2771
+ error: error.message,
2772
+ agentId: context.agentId
2773
+ });
2774
+
2775
+ throw new Error(`Screenshot analysis failed: ${error.message}`);
2776
+ }
2777
+ }
2778
+
2779
+ /**
2780
+ * Close a tab
2781
+ * @param {string} agentId - Agent identifier
2782
+ * @param {string} tabName - Tab name to close
2783
+ * @returns {Promise<Object>} Result
2784
+ */
2785
+ async closeTab(agentId, tabName) {
2786
+ const agentTabsMap = this.agentTabs.get(agentId);
2787
+ if (!agentTabsMap || !agentTabsMap.has(tabName)) {
2788
+ throw new Error(`Tab '${tabName}' not found for agent ${agentId}`);
2789
+ }
2790
+
2791
+ const tabInfo = agentTabsMap.get(tabName);
2792
+
2793
+ this.logger?.info('Closing tab', { agentId, tabName });
2794
+
2795
+ await tabInfo.page.close();
2796
+ agentTabsMap.delete(tabName);
2797
+
2798
+ return {
2799
+ success: true,
2800
+ tabName,
2801
+ message: `Tab '${tabName}' closed`
2802
+ };
2803
+ }
2804
+
2805
+ /**
2806
+ * Switch to an existing tab
2807
+ * @param {string} agentId - Agent identifier
2808
+ * @param {string} tabName - Tab name to switch to
2809
+ * @returns {Promise<Object>} Result
2810
+ */
2811
+ async switchTab(agentId, tabName) {
2812
+ const agentTabsMap = this.agentTabs.get(agentId);
2813
+ if (!agentTabsMap || !agentTabsMap.has(tabName)) {
2814
+ throw new Error(`Tab '${tabName}' not found for agent ${agentId}`);
2815
+ }
2816
+
2817
+ const tabInfo = agentTabsMap.get(tabName);
2818
+ tabInfo.lastActivity = Date.now();
2819
+
2820
+ return {
2821
+ success: true,
2822
+ tabName,
2823
+ url: tabInfo.url,
2824
+ message: `Switched to tab '${tabName}'`
2825
+ };
2826
+ }
2827
+
2828
+ /**
2829
+ * List all active tabs for an agent
2830
+ * @param {string} agentId - Agent identifier
2831
+ * @returns {Promise<Object>} List of tabs
2832
+ */
2833
+ async listTabs(agentId) {
2834
+ const agentTabsMap = this.agentTabs.get(agentId);
2835
+
2836
+ if (!agentTabsMap || agentTabsMap.size === 0) {
2837
+ return {
2838
+ success: true,
2839
+ tabCount: 0,
2840
+ tabs: [],
2841
+ message: 'No active tabs'
2842
+ };
2843
+ }
2844
+
2845
+ const tabs = [];
2846
+ for (const [name, info] of agentTabsMap.entries()) {
2847
+ tabs.push({
2848
+ name,
2849
+ url: info.url,
2850
+ idleTime: Date.now() - info.lastActivity,
2851
+ headless: info.headless
2852
+ });
2853
+ }
2854
+
2855
+ return {
2856
+ success: true,
2857
+ tabCount: tabs.length,
2858
+ tabs
2859
+ };
2860
+ }
2861
+
2862
+ /**
2863
+ * Start cleanup timer for idle tabs
2864
+ * @private
2865
+ */
2866
+ startCleanupTimer() {
2867
+ if (this.cleanupTimer) {
2868
+ clearInterval(this.cleanupTimer);
2869
+ }
2870
+
2871
+ this.cleanupTimer = setInterval(() => {
2872
+ this.cleanupIdleTabs();
2873
+ }, this.CLEANUP_INTERVAL);
2874
+ }
2875
+
2876
+ /**
2877
+ * Cleanup idle tabs (1-hour timeout)
2878
+ * @private
2879
+ */
2880
+ async cleanupIdleTabs() {
2881
+ const now = Date.now();
2882
+ const tabsToClose = [];
2883
+
2884
+ for (const [agentId, agentTabsMap] of this.agentTabs.entries()) {
2885
+ for (const [tabName, tabInfo] of agentTabsMap.entries()) {
2886
+ const idleTime = now - tabInfo.lastActivity;
2887
+
2888
+ if (idleTime > this.TAB_IDLE_TIMEOUT) {
2889
+ tabsToClose.push({ agentId, tabName, tabInfo });
2890
+ }
2891
+ }
2892
+ }
2893
+
2894
+ if (tabsToClose.length > 0) {
2895
+ this.logger?.info('Cleaning up idle tabs', {
2896
+ count: tabsToClose.length
2897
+ });
2898
+
2899
+ for (const { agentId, tabName, tabInfo } of tabsToClose) {
2900
+ try {
2901
+ await tabInfo.page.close();
2902
+ this.agentTabs.get(agentId).delete(tabName);
2903
+ this.logger?.debug('Closed idle tab', { agentId, tabName });
2904
+ } catch (error) {
2905
+ this.logger?.error('Failed to close idle tab', {
2906
+ agentId,
2907
+ tabName,
2908
+ error: error.message
2909
+ });
2910
+ }
2911
+ }
2912
+ }
2913
+ }
2914
+
2915
+ /**
2916
+ * Cleanup all tabs for an agent (called when agent is deleted)
2917
+ * @param {string} agentId - Agent identifier
2918
+ * @returns {Promise<Object>} Cleanup result
2919
+ */
2920
+ async cleanupAgent(agentId) {
2921
+ const agentTabsMap = this.agentTabs.get(agentId);
2922
+
2923
+ if (!agentTabsMap) {
2924
+ return {
2925
+ success: true,
2926
+ agentId,
2927
+ closedTabs: 0,
2928
+ message: 'No tabs to clean up'
2929
+ };
2930
+ }
2931
+
2932
+ this.logger?.info('Cleaning up agent tabs', {
2933
+ agentId,
2934
+ tabCount: agentTabsMap.size
2935
+ });
2936
+
2937
+ let closedCount = 0;
2938
+
2939
+ for (const [tabName, tabInfo] of agentTabsMap.entries()) {
2940
+ try {
2941
+ await tabInfo.page.close();
2942
+ closedCount++;
2943
+ } catch (error) {
2944
+ this.logger?.error('Failed to close tab during cleanup', {
2945
+ agentId,
2946
+ tabName,
2947
+ error: error.message
2948
+ });
2949
+ }
2950
+ }
2951
+
2952
+ this.agentTabs.delete(agentId);
2953
+
2954
+ return {
2955
+ success: true,
2956
+ agentId,
2957
+ closedTabs: closedCount,
2958
+ message: `Closed ${closedCount} tabs for agent ${agentId}`
2959
+ };
2960
+ }
2961
+
2962
+ /**
2963
+ * Ensure temp directory exists
2964
+ * @private
2965
+ */
2966
+ async ensureTempDir() {
2967
+ try {
2968
+ await fs.mkdir(this.TEMP_DIR, { recursive: true });
2969
+ } catch (error) {
2970
+ this.logger?.warn('Failed to create temp directory', {
2971
+ path: this.TEMP_DIR,
2972
+ error: error.message
2973
+ });
2974
+ }
2975
+ }
2976
+
2977
+ /**
2978
+ * Fix 4: Save all session cookies from authenticated tabs before browser close
2979
+ * This preserves login sessions even if browser needs to restart
2980
+ * @private
2981
+ * @returns {Promise<number>} Number of sessions saved
2982
+ */
2983
+ async saveAllSessionCookies() {
2984
+ let savedCount = 0;
2985
+ const vault = getCredentialVault();
2986
+
2987
+ for (const [agentId, agentTabsMap] of this.agentTabs.entries()) {
2988
+ for (const [tabName, tabInfo] of agentTabsMap.entries()) {
2989
+ try {
2990
+ // Only save cookies from authenticated tabs or tabs with a meaningful URL
2991
+ if (tabInfo.authenticated || tabInfo.url) {
2992
+ const page = tabInfo.page;
2993
+ if (page && !page.isClosed()) {
2994
+ const cookies = await page.cookies();
2995
+ if (cookies && cookies.length > 0) {
2996
+ // Derive site ID from tab name or URL
2997
+ const siteId = tabInfo.siteId || tabName.replace(/-session$/, '') || this.extractDomainFromUrl(tabInfo.url);
2998
+ if (siteId) {
2999
+ await vault.saveSession(siteId, cookies);
3000
+ savedCount++;
3001
+ this.logger?.info('[WebTool] Saved session cookies before browser close', {
3002
+ agentId,
3003
+ tabName,
3004
+ siteId,
3005
+ cookieCount: cookies.length
3006
+ });
3007
+ }
3008
+ }
3009
+ }
3010
+ }
3011
+ } catch (error) {
3012
+ this.logger?.warn('[WebTool] Failed to save session cookies', {
3013
+ agentId,
3014
+ tabName,
3015
+ error: error.message
3016
+ });
3017
+ }
3018
+ }
3019
+ }
3020
+
3021
+ return savedCount;
3022
+ }
3023
+
3024
+ /**
3025
+ * Extract domain from URL for session identification
3026
+ * @private
3027
+ * @param {string} url - URL to extract domain from
3028
+ * @returns {string|null} Domain or null
3029
+ */
3030
+ extractDomainFromUrl(url) {
3031
+ try {
3032
+ if (!url) return null;
3033
+ const urlObj = new URL(url);
3034
+ // Return domain without www prefix
3035
+ return urlObj.hostname.replace(/^www\./, '');
3036
+ } catch {
3037
+ return null;
3038
+ }
3039
+ }
3040
+
3041
+ /**
3042
+ * Close the browser instance without full cleanup
3043
+ * Useful for changing stealth levels or freeing resources
3044
+ * @returns {Promise<void>}
3045
+ */
3046
+ async closeBrowser() {
3047
+ if (this.browser) {
3048
+ try {
3049
+ // Fix 4: Save session cookies before closing tabs
3050
+ const savedSessions = await this.saveAllSessionCookies();
3051
+ if (savedSessions > 0) {
3052
+ this.logger?.info('[WebTool] Saved session cookies before browser close', { savedSessions });
3053
+ }
3054
+
3055
+ // Close all agent tabs
3056
+ for (const [agentId] of this.agentTabs.entries()) {
3057
+ await this.cleanupAgent(agentId);
3058
+ }
3059
+
3060
+ await this.browser.close();
3061
+ this.browser = null;
3062
+ this.logger?.info('[WebTool] Browser closed');
3063
+ } catch (error) {
3064
+ this.logger?.warn('[WebTool] Error closing browser', { error: error.message });
3065
+ this.browser = null;
3066
+ }
3067
+ }
3068
+ }
3069
+
3070
+ /**
3071
+ * Cleanup resources
3072
+ */
3073
+ async cleanup() {
3074
+ // Stop cleanup timer
3075
+ if (this.cleanupTimer) {
3076
+ clearInterval(this.cleanupTimer);
3077
+ this.cleanupTimer = null;
3078
+ }
3079
+
3080
+ // Fix 4: Save session cookies before cleanup
3081
+ if (this.browser) {
3082
+ try {
3083
+ await this.saveAllSessionCookies();
3084
+ } catch (error) {
3085
+ this.logger?.warn('[WebTool] Failed to save session cookies during cleanup', {
3086
+ error: error.message
3087
+ });
3088
+ }
3089
+ }
3090
+
3091
+ // Close all tabs
3092
+ for (const [agentId] of this.agentTabs.entries()) {
3093
+ await this.cleanupAgent(agentId);
3094
+ }
3095
+
3096
+ // Close browser
3097
+ if (this.browser) {
3098
+ await this.browser.close();
3099
+ this.browser = null;
3100
+ }
3101
+
3102
+ // Clean temp directory
3103
+ try {
3104
+ await fs.rm(this.TEMP_DIR, { recursive: true, force: true });
3105
+ } catch (error) {
3106
+ this.logger?.warn('Failed to clean temp directory', {
3107
+ path: this.TEMP_DIR,
3108
+ error: error.message
3109
+ });
3110
+ }
3111
+ }
3112
+ }
3113
+
3114
+ export default WebTool;