@withbuddi/buddi 0.1.0-pre.51 → 0.1.0-pre.53

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 (503) hide show
  1. package/extension/background.js +1604 -847
  2. package/extension/manifest.json +3 -3
  3. package/extension/popup.css +91 -3
  4. package/extension/popup.html +49 -32
  5. package/extension/popup.js +215 -123
  6. package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
  7. package/node_modules/@buddi/core/dist/.tsbuildinfo +1 -1
  8. package/node_modules/@buddi/core/dist/plugin/index.d.ts +1 -1
  9. package/node_modules/@buddi/core/dist/plugin/index.d.ts.map +1 -1
  10. package/node_modules/@buddi/core/dist/plugin/index.js.map +1 -1
  11. package/node_modules/@buddi/core/dist/questions/store.d.ts +3 -1
  12. package/node_modules/@buddi/core/dist/questions/store.d.ts.map +1 -1
  13. package/node_modules/@buddi/core/dist/questions/store.js +3 -3
  14. package/node_modules/@buddi/core/dist/questions/store.js.map +1 -1
  15. package/node_modules/@buddi/core/dist/questions/types.d.ts +46 -1
  16. package/node_modules/@buddi/core/dist/questions/types.d.ts.map +1 -1
  17. package/node_modules/@buddi/core/dist/questions/types.js +2 -1
  18. package/node_modules/@buddi/core/dist/questions/types.js.map +1 -1
  19. package/node_modules/@buddi/core/dist/secrets/approval.d.ts +54 -0
  20. package/node_modules/@buddi/core/dist/secrets/approval.d.ts.map +1 -1
  21. package/node_modules/@buddi/core/dist/secrets/approval.js +58 -1
  22. package/node_modules/@buddi/core/dist/secrets/approval.js.map +1 -1
  23. package/node_modules/@buddi/core/dist/secrets/settings.d.ts +178 -0
  24. package/node_modules/@buddi/core/dist/secrets/settings.d.ts.map +1 -1
  25. package/node_modules/@buddi/core/dist/secrets/settings.js +63 -6
  26. package/node_modules/@buddi/core/dist/secrets/settings.js.map +1 -1
  27. package/node_modules/@buddi/core/dist/secrets/store.d.ts +31 -0
  28. package/node_modules/@buddi/core/dist/secrets/store.d.ts.map +1 -1
  29. package/node_modules/@buddi/core/dist/secrets/store.js +75 -0
  30. package/node_modules/@buddi/core/dist/secrets/store.js.map +1 -1
  31. package/node_modules/@buddi/core/dist/secrets/use.d.ts.map +1 -1
  32. package/node_modules/@buddi/core/dist/secrets/use.js +68 -3
  33. package/node_modules/@buddi/core/dist/secrets/use.js.map +1 -1
  34. package/node_modules/@buddi/core/dist/tools.d.ts +22 -0
  35. package/node_modules/@buddi/core/dist/tools.d.ts.map +1 -1
  36. package/node_modules/@buddi/core/dist/tools.js.map +1 -1
  37. package/node_modules/@buddi/core/migrations/080_secret_requests.sql +14 -0
  38. package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
  39. package/node_modules/@buddi/gateway/dist/chat/session.d.ts.map +1 -1
  40. package/node_modules/@buddi/gateway/dist/chat/session.js +2 -1
  41. package/node_modules/@buddi/gateway/dist/chat/session.js.map +1 -1
  42. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts.map +1 -1
  43. package/node_modules/@buddi/gateway/dist/missions/execute.js +3 -1
  44. package/node_modules/@buddi/gateway/dist/missions/execute.js.map +1 -1
  45. package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.d.ts +15 -3
  46. package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.d.ts.map +1 -1
  47. package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.js +34 -7
  48. package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.js.map +1 -1
  49. package/node_modules/@buddi/gateway/dist/surfaces/offered-actions.d.ts.map +1 -1
  50. package/node_modules/@buddi/gateway/dist/surfaces/offered-actions.js +8 -2
  51. package/node_modules/@buddi/gateway/dist/surfaces/offered-actions.js.map +1 -1
  52. package/node_modules/@buddi/gateway/dist/surfaces/pending-question.d.ts +3 -1
  53. package/node_modules/@buddi/gateway/dist/surfaces/pending-question.d.ts.map +1 -1
  54. package/node_modules/@buddi/gateway/dist/surfaces/pending-question.js.map +1 -1
  55. package/node_modules/@buddi/gateway/dist/surfaces/secret-request.d.ts +173 -0
  56. package/node_modules/@buddi/gateway/dist/surfaces/secret-request.d.ts.map +1 -0
  57. package/node_modules/@buddi/gateway/dist/surfaces/secret-request.js +332 -0
  58. package/node_modules/@buddi/gateway/dist/surfaces/secret-request.js.map +1 -0
  59. package/node_modules/@buddi/gateway/dist/system-context.js +1 -1
  60. package/node_modules/@buddi/gateway/dist/system-context.js.map +1 -1
  61. package/node_modules/@buddi/gateway/dist/telegram/main.d.ts.map +1 -1
  62. package/node_modules/@buddi/gateway/dist/telegram/main.js +14 -3
  63. package/node_modules/@buddi/gateway/dist/telegram/main.js.map +1 -1
  64. package/node_modules/@buddi/gateway/dist/telegram/surface.d.ts.map +1 -1
  65. package/node_modules/@buddi/gateway/dist/telegram/surface.js +4 -1
  66. package/node_modules/@buddi/gateway/dist/telegram/surface.js.map +1 -1
  67. package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
  68. package/node_modules/@buddi/gateway/dist/web/api-routes.js +9 -1
  69. package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
  70. package/node_modules/@buddi/gateway/dist/web/approval-ask.js +2 -2
  71. package/node_modules/@buddi/gateway/dist/web/approval-ask.js.map +1 -1
  72. package/node_modules/@buddi/gateway/dist/web/browser-capture.d.ts +13 -0
  73. package/node_modules/@buddi/gateway/dist/web/browser-capture.d.ts.map +1 -0
  74. package/node_modules/@buddi/gateway/dist/web/browser-capture.js +24 -0
  75. package/node_modules/@buddi/gateway/dist/web/browser-capture.js.map +1 -0
  76. package/node_modules/@buddi/gateway/dist/web/chat.d.ts +26 -0
  77. package/node_modules/@buddi/gateway/dist/web/chat.d.ts.map +1 -1
  78. package/node_modules/@buddi/gateway/dist/web/chat.js +87 -2
  79. package/node_modules/@buddi/gateway/dist/web/chat.js.map +1 -1
  80. package/node_modules/@buddi/gateway/dist/web/extension.d.ts +30 -0
  81. package/node_modules/@buddi/gateway/dist/web/extension.d.ts.map +1 -1
  82. package/node_modules/@buddi/gateway/dist/web/extension.js +86 -6
  83. package/node_modules/@buddi/gateway/dist/web/extension.js.map +1 -1
  84. package/node_modules/@buddi/gateway/dist/web/onboarding.js +1 -1
  85. package/node_modules/@buddi/gateway/dist/web/onboarding.js.map +1 -1
  86. package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts +26 -2
  87. package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts.map +1 -1
  88. package/node_modules/@buddi/gateway/dist/web/remote-hand.js +89 -28
  89. package/node_modules/@buddi/gateway/dist/web/remote-hand.js.map +1 -1
  90. package/node_modules/@buddi/gateway/dist/web/secret-request.d.ts +54 -0
  91. package/node_modules/@buddi/gateway/dist/web/secret-request.d.ts.map +1 -0
  92. package/node_modules/@buddi/gateway/dist/web/secret-request.js +264 -0
  93. package/node_modules/@buddi/gateway/dist/web/secret-request.js.map +1 -0
  94. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts +1 -0
  95. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts.map +1 -1
  96. package/node_modules/@buddi/gateway/dist/web/secrets.js +1 -1
  97. package/node_modules/@buddi/gateway/dist/web/secrets.js.map +1 -1
  98. package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
  99. package/node_modules/@buddi/gateway/dist/web/server.js +72 -3
  100. package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
  101. package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
  102. package/node_modules/@buddi/install/dist/app-layout.d.ts +24 -0
  103. package/node_modules/@buddi/install/dist/app-layout.d.ts.map +1 -1
  104. package/node_modules/@buddi/install/dist/app-layout.js +64 -2
  105. package/node_modules/@buddi/install/dist/app-layout.js.map +1 -1
  106. package/node_modules/@buddi/install/dist/launcher.d.ts.map +1 -1
  107. package/node_modules/@buddi/install/dist/launcher.js +89 -10
  108. package/node_modules/@buddi/install/dist/launcher.js.map +1 -1
  109. package/node_modules/@buddi/install/dist/linux-install.d.ts +41 -0
  110. package/node_modules/@buddi/install/dist/linux-install.d.ts.map +1 -0
  111. package/node_modules/@buddi/install/dist/linux-install.js +86 -0
  112. package/node_modules/@buddi/install/dist/linux-install.js.map +1 -0
  113. package/node_modules/@buddi/install/dist/runtime-info.d.ts +23 -0
  114. package/node_modules/@buddi/install/dist/runtime-info.d.ts.map +1 -0
  115. package/node_modules/@buddi/install/dist/runtime-info.js +69 -0
  116. package/node_modules/@buddi/install/dist/runtime-info.js.map +1 -0
  117. package/node_modules/@buddi/install/dist/supervisor.d.ts.map +1 -1
  118. package/node_modules/@buddi/install/dist/supervisor.js +8 -6
  119. package/node_modules/@buddi/install/dist/supervisor.js.map +1 -1
  120. package/node_modules/@buddi/install/dist/uninstall.d.ts +20 -0
  121. package/node_modules/@buddi/install/dist/uninstall.d.ts.map +1 -1
  122. package/node_modules/@buddi/install/dist/uninstall.js +50 -1
  123. package/node_modules/@buddi/install/dist/uninstall.js.map +1 -1
  124. package/node_modules/@buddi/install/dist/upgrade.d.ts +4 -1
  125. package/node_modules/@buddi/install/dist/upgrade.d.ts.map +1 -1
  126. package/node_modules/@buddi/install/dist/upgrade.js +77 -51
  127. package/node_modules/@buddi/install/dist/upgrade.js.map +1 -1
  128. package/node_modules/@buddi/runtime/dist/.tsbuildinfo +1 -1
  129. package/node_modules/@buddi/runtime/dist/anthropic.d.ts +7 -0
  130. package/node_modules/@buddi/runtime/dist/anthropic.d.ts.map +1 -1
  131. package/node_modules/@buddi/runtime/dist/anthropic.js +19 -1
  132. package/node_modules/@buddi/runtime/dist/anthropic.js.map +1 -1
  133. package/node_modules/@buddi/tool-artifacts/dist/.tsbuildinfo +1 -1
  134. package/node_modules/@buddi/tool-browser/dist/.tsbuildinfo +1 -1
  135. package/node_modules/@buddi/tool-browser/dist/controller.d.ts +16 -1
  136. package/node_modules/@buddi/tool-browser/dist/controller.d.ts.map +1 -1
  137. package/node_modules/@buddi/tool-browser/dist/controller.js +59 -5
  138. package/node_modules/@buddi/tool-browser/dist/controller.js.map +1 -1
  139. package/node_modules/@buddi/tool-browser/dist/driver.d.ts +12 -0
  140. package/node_modules/@buddi/tool-browser/dist/driver.d.ts.map +1 -1
  141. package/node_modules/@buddi/tool-browser/dist/driver.js +41 -2
  142. package/node_modules/@buddi/tool-browser/dist/driver.js.map +1 -1
  143. package/node_modules/@buddi/tool-browser/dist/extension.d.ts +49 -9
  144. package/node_modules/@buddi/tool-browser/dist/extension.d.ts.map +1 -1
  145. package/node_modules/@buddi/tool-browser/dist/extension.js +145 -21
  146. package/node_modules/@buddi/tool-browser/dist/extension.js.map +1 -1
  147. package/node_modules/@buddi/tool-browser/dist/index.d.ts +10 -6
  148. package/node_modules/@buddi/tool-browser/dist/index.d.ts.map +1 -1
  149. package/node_modules/@buddi/tool-browser/dist/index.js +22 -8
  150. package/node_modules/@buddi/tool-browser/dist/index.js.map +1 -1
  151. package/node_modules/@buddi/tool-browser/dist/routes.d.ts +10 -1
  152. package/node_modules/@buddi/tool-browser/dist/routes.d.ts.map +1 -1
  153. package/node_modules/@buddi/tool-browser/dist/routes.js +24 -0
  154. package/node_modules/@buddi/tool-browser/dist/routes.js.map +1 -1
  155. package/node_modules/@buddi/tool-browser/dist/service.d.ts +48 -1
  156. package/node_modules/@buddi/tool-browser/dist/service.d.ts.map +1 -1
  157. package/node_modules/@buddi/tool-browser/dist/service.js +114 -11
  158. package/node_modules/@buddi/tool-browser/dist/service.js.map +1 -1
  159. package/node_modules/@buddi/tool-browser/dist/types.d.ts +54 -3
  160. package/node_modules/@buddi/tool-browser/dist/types.d.ts.map +1 -1
  161. package/node_modules/@buddi/tool-browser/dist/types.js +17 -1
  162. package/node_modules/@buddi/tool-browser/dist/types.js.map +1 -1
  163. package/node_modules/@buddi/tool-email/dist/.tsbuildinfo +1 -1
  164. package/node_modules/@buddi/tool-host/dist/.tsbuildinfo +1 -1
  165. package/node_modules/@buddi/tool-mcp/dist/.tsbuildinfo +1 -1
  166. package/node_modules/@buddi/tool-memory/dist/.tsbuildinfo +1 -1
  167. package/node_modules/@buddi/tool-web/dist/.tsbuildinfo +1 -1
  168. package/node_modules/@hono/node-server/package.json +2 -2
  169. package/node_modules/@types/node/README.md +1 -1
  170. package/node_modules/@types/node/package.json +2 -2
  171. package/node_modules/@types/node/util.d.ts +6 -3
  172. package/node_modules/content-disposition/README.md +105 -35
  173. package/node_modules/content-disposition/dist/index.d.ts +72 -0
  174. package/node_modules/content-disposition/dist/index.js +388 -0
  175. package/node_modules/content-disposition/dist/index.js.map +1 -0
  176. package/node_modules/content-disposition/package.json +31 -19
  177. package/node_modules/docx/dist/index.cjs +182 -80
  178. package/node_modules/docx/dist/index.d.cts +63 -6
  179. package/node_modules/docx/dist/index.d.ts +63 -6
  180. package/node_modules/docx/dist/index.iife.js +182 -80
  181. package/node_modules/docx/dist/index.mjs +182 -80
  182. package/node_modules/docx/dist/index.umd.cjs +182 -80
  183. package/node_modules/docx/dist/layout.cjs +1 -6
  184. package/node_modules/docx/dist/layout.iife.js +1 -6
  185. package/node_modules/docx/dist/layout.mjs +1 -6
  186. package/node_modules/docx/dist/layout.umd.cjs +1 -6
  187. package/node_modules/docx/package.json +4 -3
  188. package/node_modules/express/Readme.md +15 -9
  189. package/node_modules/express/lib/application.js +2 -2
  190. package/node_modules/express/lib/request.js +32 -19
  191. package/node_modules/express/lib/response.js +27 -30
  192. package/node_modules/express/node_modules/content-type/LICENSE +22 -0
  193. package/node_modules/express/node_modules/content-type/README.md +71 -0
  194. package/node_modules/express/node_modules/content-type/dist/index.d.ts +46 -0
  195. package/node_modules/express/node_modules/content-type/dist/index.js +176 -0
  196. package/node_modules/express/node_modules/content-type/dist/index.js.map +1 -0
  197. package/node_modules/express/node_modules/content-type/package.json +52 -0
  198. package/node_modules/express/package.json +10 -10
  199. package/node_modules/imapflow/CHANGELOG.md +48 -0
  200. package/node_modules/imapflow/dist/cjs/commands/append.d.ts +1 -1
  201. package/node_modules/imapflow/dist/cjs/commands/append.js +15 -4
  202. package/node_modules/imapflow/dist/cjs/commands/compress.js +2 -2
  203. package/node_modules/imapflow/dist/cjs/commands/id.js +5 -3
  204. package/node_modules/imapflow/dist/cjs/commands/idle.js +9 -9
  205. package/node_modules/imapflow/dist/cjs/commands/list.js +2 -2
  206. package/node_modules/imapflow/dist/cjs/commands/namespace.js +18 -14
  207. package/node_modules/imapflow/dist/cjs/commands/quota.js +2 -3
  208. package/node_modules/imapflow/dist/cjs/commands/select.d.ts +3 -14
  209. package/node_modules/imapflow/dist/cjs/commands/select.js +1 -4
  210. package/node_modules/imapflow/dist/cjs/commands/status.js +2 -4
  211. package/node_modules/imapflow/dist/cjs/connection-deadline.d.ts +1 -1
  212. package/node_modules/imapflow/dist/cjs/connection-deadline.js +2 -4
  213. package/node_modules/imapflow/dist/cjs/download.js +66 -35
  214. package/node_modules/imapflow/dist/cjs/errors.d.ts +15 -0
  215. package/node_modules/imapflow/dist/cjs/errors.js +22 -0
  216. package/node_modules/imapflow/dist/cjs/handler/imap-compiler.d.ts +4 -4
  217. package/node_modules/imapflow/dist/cjs/handler/imap-compiler.js +18 -18
  218. package/node_modules/imapflow/dist/cjs/handler/imap-parser.js +28 -3
  219. package/node_modules/imapflow/dist/cjs/handler/imap-stream.js +9 -8
  220. package/node_modules/imapflow/dist/cjs/handler/limits.d.ts +1 -1
  221. package/node_modules/imapflow/dist/cjs/handler/limits.js +5 -7
  222. package/node_modules/imapflow/dist/cjs/handler/parser-instance.js +23 -38
  223. package/node_modules/imapflow/dist/cjs/handler/token-parser.d.ts +12 -0
  224. package/node_modules/imapflow/dist/cjs/handler/token-parser.js +44 -104
  225. package/node_modules/imapflow/dist/cjs/imap-flow.d.ts +4 -3
  226. package/node_modules/imapflow/dist/cjs/imap-flow.js +128 -78
  227. package/node_modules/imapflow/dist/cjs/package-info.d.ts +1 -1
  228. package/node_modules/imapflow/dist/cjs/package-info.js +1 -1
  229. package/node_modules/imapflow/dist/cjs/proxy-connection.js +2 -5
  230. package/node_modules/imapflow/dist/cjs/search-compiler.js +2 -3
  231. package/node_modules/imapflow/dist/cjs/tools.d.ts +24 -3
  232. package/node_modules/imapflow/dist/cjs/tools.js +59 -18
  233. package/node_modules/imapflow/dist/cjs/types.d.ts +20 -1
  234. package/node_modules/imapflow/dist/esm/commands/append.d.ts +1 -1
  235. package/node_modules/imapflow/dist/esm/commands/append.js +15 -4
  236. package/node_modules/imapflow/dist/esm/commands/compress.js +2 -2
  237. package/node_modules/imapflow/dist/esm/commands/id.js +6 -4
  238. package/node_modules/imapflow/dist/esm/commands/idle.js +9 -9
  239. package/node_modules/imapflow/dist/esm/commands/list.js +3 -3
  240. package/node_modules/imapflow/dist/esm/commands/namespace.js +19 -15
  241. package/node_modules/imapflow/dist/esm/commands/quota.js +3 -4
  242. package/node_modules/imapflow/dist/esm/commands/select.d.ts +3 -14
  243. package/node_modules/imapflow/dist/esm/commands/select.js +1 -4
  244. package/node_modules/imapflow/dist/esm/commands/status.js +2 -4
  245. package/node_modules/imapflow/dist/esm/connection-deadline.d.ts +1 -1
  246. package/node_modules/imapflow/dist/esm/connection-deadline.js +2 -4
  247. package/node_modules/imapflow/dist/esm/download.js +66 -35
  248. package/node_modules/imapflow/dist/esm/errors.d.ts +15 -0
  249. package/node_modules/imapflow/dist/esm/errors.js +21 -0
  250. package/node_modules/imapflow/dist/esm/handler/imap-compiler.d.ts +4 -4
  251. package/node_modules/imapflow/dist/esm/handler/imap-compiler.js +18 -18
  252. package/node_modules/imapflow/dist/esm/handler/imap-parser.js +28 -3
  253. package/node_modules/imapflow/dist/esm/handler/imap-stream.js +9 -8
  254. package/node_modules/imapflow/dist/esm/handler/limits.d.ts +1 -1
  255. package/node_modules/imapflow/dist/esm/handler/limits.js +5 -7
  256. package/node_modules/imapflow/dist/esm/handler/parser-instance.js +23 -38
  257. package/node_modules/imapflow/dist/esm/handler/token-parser.d.ts +12 -0
  258. package/node_modules/imapflow/dist/esm/handler/token-parser.js +44 -104
  259. package/node_modules/imapflow/dist/esm/imap-flow.d.ts +4 -3
  260. package/node_modules/imapflow/dist/esm/imap-flow.js +130 -80
  261. package/node_modules/imapflow/dist/esm/package-info.d.ts +1 -1
  262. package/node_modules/imapflow/dist/esm/package-info.js +1 -1
  263. package/node_modules/imapflow/dist/esm/proxy-connection.js +2 -5
  264. package/node_modules/imapflow/dist/esm/search-compiler.js +2 -3
  265. package/node_modules/imapflow/dist/esm/tools.d.ts +24 -3
  266. package/node_modules/imapflow/dist/esm/tools.js +58 -19
  267. package/node_modules/imapflow/dist/esm/types.d.ts +20 -1
  268. package/node_modules/imapflow/package.json +6 -3
  269. package/node_modules/playwright/README.md +4 -4
  270. package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +1 -1
  271. package/node_modules/playwright/lib/cli/testActions.js +8 -5
  272. package/node_modules/playwright/lib/common/index.js +37 -23
  273. package/node_modules/playwright/lib/common/index.js.txt +8 -9
  274. package/node_modules/playwright/lib/index.js +16 -4
  275. package/node_modules/playwright/lib/matchers/expect.js +13 -7
  276. package/node_modules/playwright/lib/matchers/expect.js.txt +4 -4
  277. package/node_modules/playwright/lib/program.js +2 -1
  278. package/node_modules/playwright/lib/runner/index.js +438 -244
  279. package/node_modules/playwright/lib/runner/index.js.txt +12 -11
  280. package/node_modules/playwright/lib/transform/babelBundle.js +2612 -2396
  281. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +15 -15
  282. package/node_modules/playwright/lib/transform/babelBundle.js.txt +14 -11
  283. package/node_modules/playwright/lib/transform/esmLoader.js +17 -5
  284. package/node_modules/playwright/lib/transform/esmLoader.js.txt +3 -3
  285. package/node_modules/playwright/lib/worker/workerProcessEntry.js +17 -18
  286. package/node_modules/playwright/package.json +2 -7
  287. package/node_modules/playwright/types/test.d.ts +156 -39
  288. package/node_modules/playwright-core/browsers.json +9 -9
  289. package/node_modules/playwright-core/cli.js +1 -0
  290. package/node_modules/playwright-core/lib/bootstrap.js +61 -0
  291. package/node_modules/playwright-core/lib/coreBundle.js +6393 -4170
  292. package/node_modules/playwright-core/lib/server/electron/loader.js +4 -0
  293. package/node_modules/playwright-core/lib/tools/cli-client/cli.js +2 -1
  294. package/node_modules/playwright-core/lib/tools/cli-client/help.json +99 -12
  295. package/node_modules/playwright-core/lib/tools/cli-client/output.js +17 -16
  296. package/node_modules/playwright-core/lib/tools/cli-client/program.js +8 -4
  297. package/node_modules/playwright-core/lib/tools/cli-client/session.js +2 -0
  298. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +71 -3
  299. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/pr-attachments.md +60 -0
  300. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +4 -0
  301. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +2 -0
  302. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +1 -1
  303. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +76 -3
  304. package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +1 -1
  305. package/node_modules/playwright-core/lib/utilsBundle.js +948 -227
  306. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +6 -6
  307. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +1 -1
  308. package/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CVQWJZAA.js → codeMirrorModule-DK72OMRi.js} +1 -1
  309. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Cn0mmDuv.js +129 -0
  310. package/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  311. package/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-BbkfBe3n.js → codeMirrorModule-BEB2e8M3.js} +1 -1
  312. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-D-g2vrvf.js +184 -0
  313. package/node_modules/playwright-core/lib/vite/traceViewer/assets/{xtermModule-DywYcAf8.js → xtermModule-B794rD2Z.js} +1 -1
  314. package/node_modules/playwright-core/lib/vite/traceViewer/{defaultSettingsView.Bqk9acqE.css → defaultSettingsView.C8BeHuxd.css} +1 -1
  315. package/node_modules/playwright-core/lib/vite/traceViewer/{index.B4cLoZK3.js → index.7-c5LSTW.js} +1 -1
  316. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +3 -3
  317. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -3
  318. package/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.CU5KtEkS.js → uiMode.CYwC_gH1.js} +4 -4
  319. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +3 -3
  320. package/node_modules/playwright-core/package.json +1 -1
  321. package/node_modules/playwright-core/types/protocol.d.ts +468 -337
  322. package/node_modules/playwright-core/types/types.d.ts +572 -37
  323. package/node_modules/process-warning/.github/workflows/ci.yml +1 -1
  324. package/node_modules/process-warning/.github/workflows/lock-threads.yml +1 -1
  325. package/node_modules/process-warning/.github/workflows/release.yml +24 -0
  326. package/node_modules/process-warning/README.md +46 -46
  327. package/node_modules/process-warning/package.json +1 -1
  328. package/package.json +2 -2
  329. package/packages/cli/dist/.tsbuildinfo +1 -1
  330. package/packages/core/dist/.tsbuildinfo +1 -1
  331. package/packages/core/dist/plugin/index.d.ts +1 -1
  332. package/packages/core/dist/plugin/index.d.ts.map +1 -1
  333. package/packages/core/dist/plugin/index.js.map +1 -1
  334. package/packages/core/dist/questions/store.d.ts +3 -1
  335. package/packages/core/dist/questions/store.d.ts.map +1 -1
  336. package/packages/core/dist/questions/store.js +3 -3
  337. package/packages/core/dist/questions/store.js.map +1 -1
  338. package/packages/core/dist/questions/types.d.ts +46 -1
  339. package/packages/core/dist/questions/types.d.ts.map +1 -1
  340. package/packages/core/dist/questions/types.js +2 -1
  341. package/packages/core/dist/questions/types.js.map +1 -1
  342. package/packages/core/dist/secrets/approval.d.ts +54 -0
  343. package/packages/core/dist/secrets/approval.d.ts.map +1 -1
  344. package/packages/core/dist/secrets/approval.js +58 -1
  345. package/packages/core/dist/secrets/approval.js.map +1 -1
  346. package/packages/core/dist/secrets/settings.d.ts +178 -0
  347. package/packages/core/dist/secrets/settings.d.ts.map +1 -1
  348. package/packages/core/dist/secrets/settings.js +63 -6
  349. package/packages/core/dist/secrets/settings.js.map +1 -1
  350. package/packages/core/dist/secrets/store.d.ts +31 -0
  351. package/packages/core/dist/secrets/store.d.ts.map +1 -1
  352. package/packages/core/dist/secrets/store.js +75 -0
  353. package/packages/core/dist/secrets/store.js.map +1 -1
  354. package/packages/core/dist/secrets/use.d.ts.map +1 -1
  355. package/packages/core/dist/secrets/use.js +68 -3
  356. package/packages/core/dist/secrets/use.js.map +1 -1
  357. package/packages/core/dist/tools.d.ts +22 -0
  358. package/packages/core/dist/tools.d.ts.map +1 -1
  359. package/packages/core/dist/tools.js.map +1 -1
  360. package/packages/core/migrations/080_secret_requests.sql +14 -0
  361. package/packages/gateway/dist/.tsbuildinfo +1 -1
  362. package/packages/gateway/dist/chat/session.d.ts.map +1 -1
  363. package/packages/gateway/dist/chat/session.js +2 -1
  364. package/packages/gateway/dist/chat/session.js.map +1 -1
  365. package/packages/gateway/dist/missions/execute.d.ts.map +1 -1
  366. package/packages/gateway/dist/missions/execute.js +3 -1
  367. package/packages/gateway/dist/missions/execute.js.map +1 -1
  368. package/packages/gateway/dist/surfaces/browser-cards.d.ts +15 -3
  369. package/packages/gateway/dist/surfaces/browser-cards.d.ts.map +1 -1
  370. package/packages/gateway/dist/surfaces/browser-cards.js +34 -7
  371. package/packages/gateway/dist/surfaces/browser-cards.js.map +1 -1
  372. package/packages/gateway/dist/surfaces/offered-actions.d.ts.map +1 -1
  373. package/packages/gateway/dist/surfaces/offered-actions.js +8 -2
  374. package/packages/gateway/dist/surfaces/offered-actions.js.map +1 -1
  375. package/packages/gateway/dist/surfaces/pending-question.d.ts +3 -1
  376. package/packages/gateway/dist/surfaces/pending-question.d.ts.map +1 -1
  377. package/packages/gateway/dist/surfaces/pending-question.js.map +1 -1
  378. package/packages/gateway/dist/surfaces/secret-request.d.ts +173 -0
  379. package/packages/gateway/dist/surfaces/secret-request.d.ts.map +1 -0
  380. package/packages/gateway/dist/surfaces/secret-request.js +332 -0
  381. package/packages/gateway/dist/surfaces/secret-request.js.map +1 -0
  382. package/packages/gateway/dist/system-context.js +1 -1
  383. package/packages/gateway/dist/system-context.js.map +1 -1
  384. package/packages/gateway/dist/telegram/main.d.ts.map +1 -1
  385. package/packages/gateway/dist/telegram/main.js +14 -3
  386. package/packages/gateway/dist/telegram/main.js.map +1 -1
  387. package/packages/gateway/dist/telegram/surface.d.ts.map +1 -1
  388. package/packages/gateway/dist/telegram/surface.js +4 -1
  389. package/packages/gateway/dist/telegram/surface.js.map +1 -1
  390. package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
  391. package/packages/gateway/dist/web/api-routes.js +9 -1
  392. package/packages/gateway/dist/web/api-routes.js.map +1 -1
  393. package/packages/gateway/dist/web/approval-ask.js +2 -2
  394. package/packages/gateway/dist/web/approval-ask.js.map +1 -1
  395. package/packages/gateway/dist/web/browser-capture.d.ts +13 -0
  396. package/packages/gateway/dist/web/browser-capture.d.ts.map +1 -0
  397. package/packages/gateway/dist/web/browser-capture.js +24 -0
  398. package/packages/gateway/dist/web/browser-capture.js.map +1 -0
  399. package/packages/gateway/dist/web/chat.d.ts +26 -0
  400. package/packages/gateway/dist/web/chat.d.ts.map +1 -1
  401. package/packages/gateway/dist/web/chat.js +87 -2
  402. package/packages/gateway/dist/web/chat.js.map +1 -1
  403. package/packages/gateway/dist/web/extension.d.ts +30 -0
  404. package/packages/gateway/dist/web/extension.d.ts.map +1 -1
  405. package/packages/gateway/dist/web/extension.js +86 -6
  406. package/packages/gateway/dist/web/extension.js.map +1 -1
  407. package/packages/gateway/dist/web/onboarding.js +1 -1
  408. package/packages/gateway/dist/web/onboarding.js.map +1 -1
  409. package/packages/gateway/dist/web/remote-hand.d.ts +26 -2
  410. package/packages/gateway/dist/web/remote-hand.d.ts.map +1 -1
  411. package/packages/gateway/dist/web/remote-hand.js +89 -28
  412. package/packages/gateway/dist/web/remote-hand.js.map +1 -1
  413. package/packages/gateway/dist/web/secret-request.d.ts +54 -0
  414. package/packages/gateway/dist/web/secret-request.d.ts.map +1 -0
  415. package/packages/gateway/dist/web/secret-request.js +264 -0
  416. package/packages/gateway/dist/web/secret-request.js.map +1 -0
  417. package/packages/gateway/dist/web/secrets.d.ts +1 -0
  418. package/packages/gateway/dist/web/secrets.d.ts.map +1 -1
  419. package/packages/gateway/dist/web/secrets.js +1 -1
  420. package/packages/gateway/dist/web/secrets.js.map +1 -1
  421. package/packages/gateway/dist/web/server.d.ts.map +1 -1
  422. package/packages/gateway/dist/web/server.js +72 -3
  423. package/packages/gateway/dist/web/server.js.map +1 -1
  424. package/packages/install/dist/.tsbuildinfo +1 -1
  425. package/packages/install/dist/app-layout.d.ts +24 -0
  426. package/packages/install/dist/app-layout.d.ts.map +1 -1
  427. package/packages/install/dist/app-layout.js +64 -2
  428. package/packages/install/dist/app-layout.js.map +1 -1
  429. package/packages/install/dist/launcher.d.ts.map +1 -1
  430. package/packages/install/dist/launcher.js +89 -10
  431. package/packages/install/dist/launcher.js.map +1 -1
  432. package/packages/install/dist/linux-install.d.ts +41 -0
  433. package/packages/install/dist/linux-install.d.ts.map +1 -0
  434. package/packages/install/dist/linux-install.js +86 -0
  435. package/packages/install/dist/linux-install.js.map +1 -0
  436. package/packages/install/dist/runtime-info.d.ts +23 -0
  437. package/packages/install/dist/runtime-info.d.ts.map +1 -0
  438. package/packages/install/dist/runtime-info.js +69 -0
  439. package/packages/install/dist/runtime-info.js.map +1 -0
  440. package/packages/install/dist/supervisor.d.ts.map +1 -1
  441. package/packages/install/dist/supervisor.js +8 -6
  442. package/packages/install/dist/supervisor.js.map +1 -1
  443. package/packages/install/dist/uninstall.d.ts +20 -0
  444. package/packages/install/dist/uninstall.d.ts.map +1 -1
  445. package/packages/install/dist/uninstall.js +50 -1
  446. package/packages/install/dist/uninstall.js.map +1 -1
  447. package/packages/install/dist/upgrade.d.ts +4 -1
  448. package/packages/install/dist/upgrade.d.ts.map +1 -1
  449. package/packages/install/dist/upgrade.js +77 -51
  450. package/packages/install/dist/upgrade.js.map +1 -1
  451. package/packages/runtime/dist/.tsbuildinfo +1 -1
  452. package/packages/runtime/dist/anthropic.d.ts +7 -0
  453. package/packages/runtime/dist/anthropic.d.ts.map +1 -1
  454. package/packages/runtime/dist/anthropic.js +19 -1
  455. package/packages/runtime/dist/anthropic.js.map +1 -1
  456. package/packages/tools/artifacts/dist/.tsbuildinfo +1 -1
  457. package/packages/tools/browser/dist/.tsbuildinfo +1 -1
  458. package/packages/tools/browser/dist/controller.d.ts +16 -1
  459. package/packages/tools/browser/dist/controller.d.ts.map +1 -1
  460. package/packages/tools/browser/dist/controller.js +59 -5
  461. package/packages/tools/browser/dist/controller.js.map +1 -1
  462. package/packages/tools/browser/dist/driver.d.ts +12 -0
  463. package/packages/tools/browser/dist/driver.d.ts.map +1 -1
  464. package/packages/tools/browser/dist/driver.js +41 -2
  465. package/packages/tools/browser/dist/driver.js.map +1 -1
  466. package/packages/tools/browser/dist/extension.d.ts +49 -9
  467. package/packages/tools/browser/dist/extension.d.ts.map +1 -1
  468. package/packages/tools/browser/dist/extension.js +145 -21
  469. package/packages/tools/browser/dist/extension.js.map +1 -1
  470. package/packages/tools/browser/dist/index.d.ts +10 -6
  471. package/packages/tools/browser/dist/index.d.ts.map +1 -1
  472. package/packages/tools/browser/dist/index.js +22 -8
  473. package/packages/tools/browser/dist/index.js.map +1 -1
  474. package/packages/tools/browser/dist/routes.d.ts +10 -1
  475. package/packages/tools/browser/dist/routes.d.ts.map +1 -1
  476. package/packages/tools/browser/dist/routes.js +24 -0
  477. package/packages/tools/browser/dist/routes.js.map +1 -1
  478. package/packages/tools/browser/dist/service.d.ts +48 -1
  479. package/packages/tools/browser/dist/service.d.ts.map +1 -1
  480. package/packages/tools/browser/dist/service.js +114 -11
  481. package/packages/tools/browser/dist/service.js.map +1 -1
  482. package/packages/tools/browser/dist/types.d.ts +54 -3
  483. package/packages/tools/browser/dist/types.d.ts.map +1 -1
  484. package/packages/tools/browser/dist/types.js +17 -1
  485. package/packages/tools/browser/dist/types.js.map +1 -1
  486. package/packages/tools/email/dist/.tsbuildinfo +1 -1
  487. package/packages/tools/host/dist/.tsbuildinfo +1 -1
  488. package/packages/tools/mcp/dist/.tsbuildinfo +1 -1
  489. package/packages/tools/memory/dist/.tsbuildinfo +1 -1
  490. package/packages/tools/web/dist/.tsbuildinfo +1 -1
  491. package/packages/web/dist/assets/index-DgpPl0TR.js +155 -0
  492. package/packages/web/dist/assets/index-rMRjibNI.css +1 -0
  493. package/packages/web/dist/assets/{lottie_light-D7CS-IeP.js → lottie_light-twPed1FM.js} +1 -1
  494. package/packages/web/dist/build.json +1 -1
  495. package/packages/web/dist/index.html +2 -2
  496. package/packages/web/dist/precache.json +4 -4
  497. package/node_modules/content-disposition/index.js +0 -536
  498. package/node_modules/playwright/jsx-runtime.js +0 -42
  499. package/node_modules/playwright/jsx-runtime.mjs +0 -21
  500. package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +0 -129
  501. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +0 -182
  502. package/packages/web/dist/assets/index-BibqUZwt.js +0 -155
  503. package/packages/web/dist/assets/index-DBVZzTau.css +0 -1
@@ -1,6 +1,670 @@
1
+ // src/downloads.ts
2
+ var DOWNLOAD_GRACE_MS = 15e3;
3
+ var DOWNLOADS_PERMISSION = "downloads";
4
+ function originOf(url) {
5
+ if (!url) return void 0;
6
+ try {
7
+ const parsed = new URL(url);
8
+ if (parsed.protocol === "blob:") return new URL(parsed.pathname).origin;
9
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.origin : void 0;
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ }
14
+ function basename(file) {
15
+ return file.split(/[\\/]/).pop() || "download";
16
+ }
17
+ var AgentDownloads = class {
18
+ #actions = /* @__PURE__ */ new Map();
19
+ #next = 1;
20
+ #claimed = /* @__PURE__ */ new Map();
21
+ #api;
22
+ #send;
23
+ #now;
24
+ constructor(options) {
25
+ this.#send = options.send;
26
+ this.#now = options.now ?? (() => Date.now());
27
+ }
28
+ /** Chrome's downloads API, once the owner granted it. Listening starts here, once. */
29
+ attach(api) {
30
+ if (!api || this.#api) return;
31
+ this.#api = api;
32
+ api.onCreated.addListener((item) => this.created(item));
33
+ api.onChanged.addListener((delta) => {
34
+ void this.changed(delta).catch(() => void 0);
35
+ });
36
+ }
37
+ get attached() {
38
+ return this.#api !== void 0;
39
+ }
40
+ /** An agent command starts in this session, whose tabs are on these addresses. Returns the command's handle for `end`. */
41
+ begin(session, urls) {
42
+ this.#expire();
43
+ const id = this.#next++;
44
+ const origins = /* @__PURE__ */ new Set();
45
+ for (const url of urls) {
46
+ const origin = originOf(url);
47
+ if (origin) origins.add(origin);
48
+ }
49
+ this.#actions.set(id, { session, origins, running: true, until: 0, at: this.#now() });
50
+ return id;
51
+ }
52
+ /** That command ended; its tabs may have moved on to these. Its downloads still count for a few seconds, its own. */
53
+ end(id, urls = []) {
54
+ const action = this.#actions.get(id);
55
+ if (!action) return;
56
+ for (const url of urls) {
57
+ const origin = originOf(url);
58
+ if (origin) action.origins.add(origin);
59
+ }
60
+ action.running = false;
61
+ action.until = this.#now() + DOWNLOAD_GRACE_MS;
62
+ }
63
+ /** Commands whose few seconds are over count for nothing any more. */
64
+ #expire() {
65
+ const now = this.#now();
66
+ for (const [id, action] of this.#actions) if (!action.running && now > action.until) this.#actions.delete(id);
67
+ }
68
+ /** The session ended or the socket went: nothing more is its. */
69
+ forget(session) {
70
+ if (session === void 0) {
71
+ this.#actions.clear();
72
+ this.#claimed.clear();
73
+ return;
74
+ }
75
+ for (const [id, action] of this.#actions) if (action.session === session) this.#actions.delete(id);
76
+ for (const [id, owner] of this.#claimed) if (owner === session) this.#claimed.delete(id);
77
+ }
78
+ /** Which session's command this download belongs to, if any. */
79
+ #owner(item) {
80
+ if (item.byExtensionId) return void 0;
81
+ const origin = originOf(item.referrer) ?? originOf(item.finalUrl ?? item.url);
82
+ if (!origin) return void 0;
83
+ this.#expire();
84
+ let best;
85
+ for (const action of this.#actions.values()) {
86
+ if (!action.origins.has(origin)) continue;
87
+ if (!best || action.at > best.at) best = action;
88
+ }
89
+ return best?.session;
90
+ }
91
+ created(item) {
92
+ const session = this.#owner(item);
93
+ if (session) this.#claimed.set(item.id, session);
94
+ }
95
+ async changed(delta) {
96
+ const session = this.#claimed.get(delta.id);
97
+ const state = delta.state?.current;
98
+ if (!session || !state) return;
99
+ if (state === "interrupted") {
100
+ this.#claimed.delete(delta.id);
101
+ return;
102
+ }
103
+ if (state !== "complete") return;
104
+ this.#claimed.delete(delta.id);
105
+ const [item] = await (this.#api?.search({ id: delta.id }) ?? Promise.resolve([]));
106
+ if (!item?.filename) return;
107
+ const url = item.finalUrl || item.url || "";
108
+ const size = item.fileSize !== void 0 && item.fileSize >= 0 ? item.fileSize : item.totalBytes !== void 0 && item.totalBytes >= 0 ? item.totalBytes : void 0;
109
+ this.#send({
110
+ type: "download",
111
+ session,
112
+ path: item.filename,
113
+ filename: basename(item.filename),
114
+ url,
115
+ ...item.mime ? { mime: item.mime } : {},
116
+ ...size !== void 0 ? { size } : {}
117
+ });
118
+ }
119
+ };
120
+
121
+ // src/bar.ts
122
+ var OWNER_WAIT_MS = 3e4;
123
+ var BAR_AFTER_MS = 3e3;
124
+ var BAR_POLL_MS = 500;
125
+ async function waitForOwner(probes, limits = {}) {
126
+ const waitMs = limits.waitMs ?? OWNER_WAIT_MS;
127
+ const barAfter = limits.barAfterMs ?? BAR_AFTER_MS;
128
+ const poll = limits.pollMs ?? BAR_POLL_MS;
129
+ const start = probes.now();
130
+ let shown = false;
131
+ const maxPolls = Math.ceil(waitMs / poll);
132
+ try {
133
+ for (let polls = 0; ; polls++) {
134
+ if (!await probes.watched()) return "left";
135
+ const elapsed = Math.max(probes.now() - start, polls * poll);
136
+ if (!shown && elapsed >= barAfter) {
137
+ shown = true;
138
+ await probes.show().catch(() => void 0);
139
+ }
140
+ if (shown) {
141
+ const choice = await probes.choice().catch(() => void 0);
142
+ if (choice) return choice;
143
+ }
144
+ if (elapsed >= waitMs || polls >= maxPolls) return "timeout";
145
+ await probes.wait(poll);
146
+ }
147
+ } finally {
148
+ if (shown) await probes.hide().catch(() => void 0);
149
+ }
150
+ }
151
+ function showBar(agent) {
152
+ const holder = globalThis;
153
+ holder["__buddiBar"] = { choice: void 0 };
154
+ const doc = globalThis.document;
155
+ if (!doc || doc.getElementById("buddi-working-bar")) return;
156
+ const host = doc.createElement("div");
157
+ host.id = "buddi-working-bar";
158
+ host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
159
+ const root = host.attachShadow({ mode: "closed" });
160
+ const bar = doc.createElement("div");
161
+ bar.setAttribute("role", "status");
162
+ bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
163
+ const label = doc.createElement("span");
164
+ label.textContent = `${agent || "buddi"} is working here`;
165
+ label.setAttribute("style", "margin-right:auto;");
166
+ const button = (text2, choice, primary) => {
167
+ const b = doc.createElement("button");
168
+ b.type = "button";
169
+ b.textContent = text2;
170
+ b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
171
+ b.addEventListener("click", () => {
172
+ holder["__buddiBar"] = { choice };
173
+ });
174
+ return b;
175
+ };
176
+ bar.append(label, button("Take over", "takeover", false), button("Let it continue", "continue", true));
177
+ root.append(bar);
178
+ (doc.documentElement ?? doc.body).append(host);
179
+ }
180
+ function readBar() {
181
+ const holder = globalThis;
182
+ const choice = holder["__buddiBar"]?.choice;
183
+ if (choice) holder["__buddiBar"] = { choice: void 0 };
184
+ return choice;
185
+ }
186
+ function hideBar() {
187
+ const doc = globalThis.document;
188
+ doc?.getElementById("buddi-working-bar")?.remove();
189
+ const holder = globalThis;
190
+ delete holder["__buddiBar"];
191
+ }
192
+ var GIVE_BACK_MESSAGE = "buddi-give-back";
193
+ function showHeldBar(session) {
194
+ const doc = globalThis.document;
195
+ if (!doc || doc.getElementById("buddi-held-bar")) return;
196
+ const host = doc.createElement("div");
197
+ host.id = "buddi-held-bar";
198
+ host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
199
+ const root = host.attachShadow({ mode: "closed" });
200
+ const bar = doc.createElement("div");
201
+ bar.setAttribute("role", "status");
202
+ bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
203
+ const label = doc.createElement("span");
204
+ label.textContent = "buddi is waiting";
205
+ label.setAttribute("style", "margin-right:auto;");
206
+ const give = doc.createElement("button");
207
+ give.type = "button";
208
+ give.textContent = "Give it back";
209
+ give.setAttribute("style", "font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;background:#fff;color:#1f2a44;");
210
+ give.addEventListener("click", () => {
211
+ const runtime = globalThis.chrome?.runtime;
212
+ void runtime?.sendMessage({ type: "buddi-give-back", session })?.catch?.(() => void 0);
213
+ host.remove();
214
+ });
215
+ bar.append(label, give);
216
+ root.append(bar);
217
+ (doc.documentElement ?? doc.body).append(host);
218
+ const asking = [];
219
+ const restore = () => {
220
+ for (const button of asking.splice(0)) button.remove();
221
+ label.textContent = "buddi is waiting";
222
+ give.style.display = "";
223
+ };
224
+ globalThis["__buddiHeldAsk"] = (question, choices) => {
225
+ restore();
226
+ label.textContent = question;
227
+ give.style.display = "none";
228
+ for (const choice of choices) {
229
+ const b = doc.createElement("button");
230
+ b.type = "button";
231
+ b.textContent = choice.label;
232
+ b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${choice.primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
233
+ b.addEventListener("click", () => {
234
+ restore();
235
+ choice.pick();
236
+ });
237
+ asking.push(b);
238
+ bar.insertBefore(b, give);
239
+ }
240
+ return restore;
241
+ };
242
+ }
243
+ function hideHeldBar() {
244
+ const doc = globalThis.document;
245
+ doc?.getElementById("buddi-held-bar")?.remove();
246
+ delete globalThis["__buddiHeldAsk"];
247
+ }
248
+ var LOGIN_MESSAGE = "buddi-login";
249
+ var LOGIN_PROMPT_MS = 2 * 6e4;
250
+ function watchHeldLogins(session, facts) {
251
+ const holder = globalThis;
252
+ const known = { never: [...facts?.never ?? []], saved: [...facts?.saved ?? []] };
253
+ const existing = holder["__buddiHeldLogins"];
254
+ if (existing) {
255
+ existing.session = session;
256
+ existing.facts = known;
257
+ return;
258
+ }
259
+ const doc = globalThis.document;
260
+ if (!doc) return;
261
+ const state = { session, facts: known, last: "", lastAt: 0, typedAt: 0, cached: void 0, timer: void 0, drop: void 0 };
262
+ holder["__buddiHeldLogins"] = state;
263
+ const runtime = () => globalThis.chrome?.runtime;
264
+ const call = async (message) => {
265
+ try {
266
+ return await runtime()?.sendMessage(message);
267
+ } catch {
268
+ return void 0;
269
+ }
270
+ };
271
+ const site = () => (globalThis.location?.hostname ?? "").toLowerCase().replace(/^www\./, "");
272
+ const listed = (where) => state.facts.never.some((entry) => {
273
+ const bare = entry.toLowerCase().replace(/^www\./, "");
274
+ return where === bare || where.endsWith(`.${bare}`);
275
+ });
276
+ const bar = () => {
277
+ const question = holder["__buddiHeldAsk"];
278
+ return typeof question === "function" ? question : void 0;
279
+ };
280
+ const prompt = (where, update) => {
281
+ const question = bar();
282
+ if (!question) return;
283
+ if (state.drop) clearTimeout(state.drop);
284
+ let answered = false;
285
+ const save = () => {
286
+ if (answered) return;
287
+ answered = true;
288
+ const asking = bar();
289
+ asking?.(update ? "Updating\u2026" : "Saving\u2026", []);
290
+ void call({ type: "buddi-login", session: state.session, decision: "save" }).then((reply) => {
291
+ const answer = reply;
292
+ const now = bar();
293
+ if (!now) return;
294
+ if (answer?.saved === true) {
295
+ const restore2 = now(update ? `Updated the login for ${where}` : `Saved the login for ${where}`, []);
296
+ if (state.drop) clearTimeout(state.drop);
297
+ state.drop = setTimeout(restore2, 2e3);
298
+ return;
299
+ }
300
+ const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : "buddi did not answer. Try again.";
301
+ now(reason, [
302
+ { label: "Try again", primary: true, pick: () => {
303
+ answered = false;
304
+ save();
305
+ } },
306
+ { label: "Not now", pick: () => {
307
+ void call({ type: "buddi-login", session: state.session, decision: "later" });
308
+ } }
309
+ ]);
310
+ });
311
+ };
312
+ const restore = question(update ? `Update the login for ${where}?` : `Save this login for ${where}?`, [
313
+ { label: update ? "Update" : "Save", primary: true, pick: save },
314
+ { label: "Not now", pick: () => {
315
+ answered = true;
316
+ void call({ type: "buddi-login", session: state.session, decision: "later" });
317
+ } },
318
+ { label: "Never for this site", pick: () => {
319
+ answered = true;
320
+ state.facts.never.push(where);
321
+ void call({ type: "buddi-login", session: state.session, decision: "never" });
322
+ } }
323
+ ]);
324
+ state.drop = setTimeout(() => {
325
+ if (!answered) restore();
326
+ }, 2 * 6e4);
327
+ };
328
+ const ask = (found) => {
329
+ const where = site();
330
+ if (where === "" || listed(where) || !bar()) return;
331
+ const update = state.facts.saved.some((login) => login.site === where && login.username === found.username);
332
+ void call({ type: "buddi-login-seen", session: state.session, username: found.username, password: found.password, update }).then((reply) => {
333
+ const asked2 = reply;
334
+ if (holder["__buddiHeldLogins"] !== state || !asked2 || typeof asked2 !== "object") return;
335
+ prompt(where, asked2.update === true);
336
+ });
337
+ };
338
+ void call({ type: "buddi-login-pending", session: state.session }).then((reply) => {
339
+ const waiting = reply;
340
+ if (holder["__buddiHeldLogins"] !== state || !waiting || typeof waiting.site !== "string") return;
341
+ prompt(waiting.site, waiting.update === true);
342
+ });
343
+ const NOT_SUBMIT = /\b(show|hide|reveal|toggle|eye|forgot|reset|cancel|back|close|sign ?up|register|create)\b/i;
344
+ const USER = /user|email|login|account|ident|phone|mail/i;
345
+ const passwords = (root) => Array.from(root.querySelectorAll('input[type="password" i]'));
346
+ const usernameFor = (password) => {
347
+ const scope = password.form ?? doc;
348
+ const inputs = Array.from(scope.querySelectorAll("input")).filter((input) => ["text", "email", "tel", ""].includes((input.getAttribute("type") ?? "").toLowerCase()) && input.value.trim() !== "");
349
+ const marked = inputs.find((input) => (input.getAttribute("autocomplete") ?? "").toLowerCase().includes("username") || (input.getAttribute("type") ?? "").toLowerCase() === "email");
350
+ if (marked) return marked.value.trim();
351
+ const before = inputs.filter((input) => (input.compareDocumentPosition(password) & 4) !== 0);
352
+ const pick = before[before.length - 1] ?? inputs.find((input) => USER.test(`${input.name} ${input.id} ${input.getAttribute("autocomplete") ?? ""}`));
353
+ return pick ? pick.value.trim() : "";
354
+ };
355
+ const read = (scope) => {
356
+ const field = passwords(scope ?? doc).find((input) => input.value !== "");
357
+ return field ? { username: usernameFor(field), password: field.value } : void 0;
358
+ };
359
+ const send = (found) => {
360
+ if (holder["__buddiHeldLogins"] !== state) return;
361
+ if (!found || found.password === "") return;
362
+ const key = `${found.username}\0${found.password}`;
363
+ const now = Date.now();
364
+ if (key === state.last && now - state.lastAt < 5e3) return;
365
+ state.last = key;
366
+ state.lastAt = now;
367
+ state.cached = void 0;
368
+ ask(found);
369
+ };
370
+ doc.addEventListener("submit", (event) => {
371
+ send(read(event.target) ?? state.cached);
372
+ }, true);
373
+ doc.addEventListener("keydown", (event) => {
374
+ if (event.key !== "Enter") return;
375
+ const target = event.target;
376
+ if (!target || target.tagName !== "INPUT") return;
377
+ send(read(target.form ?? doc));
378
+ }, true);
379
+ doc.addEventListener("click", (event) => {
380
+ const target = event.target;
381
+ const button = target?.closest?.('button, input[type="submit" i], input[type="image" i], input[type="button" i], [role="button"]');
382
+ if (!button) return;
383
+ const words = `${button.textContent ?? ""} ${button.getAttribute("aria-label") ?? ""} ${button.getAttribute("value") ?? ""} ${button.getAttribute("title") ?? ""}`;
384
+ if (NOT_SUBMIT.test(words)) return;
385
+ send(read(button.form ?? doc));
386
+ }, true);
387
+ doc.addEventListener("input", (event) => {
388
+ const target = event.target;
389
+ if ((target?.getAttribute?.("type") ?? "").toLowerCase() !== "password") return;
390
+ state.typedAt = Date.now();
391
+ state.cached = read(target.form ?? doc);
392
+ }, true);
393
+ const Observer = globalThis.PerformanceObserver;
394
+ if (typeof Observer === "function") {
395
+ try {
396
+ new Observer((list) => {
397
+ if (!state.cached || Date.now() - state.typedAt > 1e4) return;
398
+ const fetched = list.getEntries().some((entry) => ["fetch", "xmlhttprequest"].includes(entry.initiatorType));
399
+ if (!fetched) return;
400
+ if (state.timer) clearTimeout(state.timer);
401
+ state.timer = setTimeout(() => {
402
+ const still = passwords(doc).some((input) => input.isConnected && input.value !== "" && input.getClientRects().length > 0);
403
+ if (!still) send(state.cached);
404
+ }, 1500);
405
+ }).observe({ type: "resource", buffered: false });
406
+ } catch {
407
+ }
408
+ }
409
+ }
410
+ var LOGIN_SEEN_MESSAGE = "buddi-login-seen";
411
+ var LOGIN_PENDING_MESSAGE = "buddi-login-pending";
412
+ function heldLoginRequest(message, sender, heldTab) {
413
+ const request = message;
414
+ if (!request || typeof request.session !== "string" || request.session === "") return void 0;
415
+ if (heldTab === void 0 || sender?.tab?.id !== heldTab) return void 0;
416
+ const session = request.session;
417
+ const url = sender.url ?? "";
418
+ if (request.type === LOGIN_SEEN_MESSAGE) {
419
+ if (typeof request.password !== "string" || request.password === "" || request.password.length > 1024) return void 0;
420
+ const username = typeof request.username === "string" ? request.username.slice(0, 200) : "";
421
+ return { kind: "seen", session, tabId: heldTab, url, username, password: request.password, update: request.update === true };
422
+ }
423
+ if (request.type === LOGIN_PENDING_MESSAGE) return { kind: "pending", session, tabId: heldTab, url };
424
+ if (request.type === LOGIN_MESSAGE && (request.decision === "save" || request.decision === "later" || request.decision === "never")) {
425
+ return { kind: "answer", session, tabId: heldTab, decision: request.decision };
426
+ }
427
+ return void 0;
428
+ }
429
+
430
+ // src/pairings.ts
431
+ var COLOURS = ["blue", "purple", "green", "orange", "pink", "cyan", "red", "yellow", "grey"];
432
+ var DEFAULT_GATEWAY = "http://127.0.0.1:4317";
433
+ var PAIRINGS_KEY = "pairings";
434
+ var LEGACY_GATEWAY = "gateway";
435
+ var LEGACY_TOKEN = "token";
436
+ var LOOPBACK = ["127.0.0.1", "localhost", "[::1]", "::1"];
437
+ function originOf2(address) {
438
+ let parsed;
439
+ try {
440
+ parsed = new URL(address.trim());
441
+ } catch {
442
+ return null;
443
+ }
444
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
445
+ if (!LOOPBACK.includes(parsed.hostname)) return null;
446
+ return parsed.origin;
447
+ }
448
+ function portOf(url) {
449
+ return url.port || (url.protocol === "https:" ? "443" : "80");
450
+ }
451
+ function sameBuddi(a, b) {
452
+ let x, y;
453
+ try {
454
+ x = new URL(a);
455
+ y = new URL(b);
456
+ } catch {
457
+ return false;
458
+ }
459
+ if (!LOOPBACK.includes(x.hostname) || !LOOPBACK.includes(y.hostname)) return false;
460
+ return x.protocol === y.protocol && portOf(x) === portOf(y);
461
+ }
462
+ function hostOf(origin) {
463
+ try {
464
+ return new URL(origin).host;
465
+ } catch {
466
+ return origin;
467
+ }
468
+ }
469
+ function displayName(entry, all) {
470
+ const own = entry.name.trim();
471
+ if (!own) return hostOf(entry.origin);
472
+ const twin = all.some((other) => other.id !== entry.id && other.name.trim().toLowerCase() === own.toLowerCase());
473
+ if (!twin) return own;
474
+ try {
475
+ return `${own} :${portOf(new URL(entry.origin))}`;
476
+ } catch {
477
+ return own;
478
+ }
479
+ }
480
+ function nextColour(all) {
481
+ const taken = new Set(all.map((entry) => entry.colour));
482
+ return COLOURS.find((colour) => !taken.has(colour)) ?? COLOURS[all.length % COLOURS.length];
483
+ }
484
+ function freshId() {
485
+ const bytes = new Uint8Array(8);
486
+ crypto.getRandomValues(bytes);
487
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
488
+ }
489
+ function readEntry(raw) {
490
+ if (!raw || typeof raw !== "object") return null;
491
+ const value = raw;
492
+ const origin = typeof value["origin"] === "string" ? originOf2(value["origin"]) : null;
493
+ if (!origin || typeof value["id"] !== "string" || value["id"] === "") return null;
494
+ const colour = COLOURS.includes(String(value["colour"])) ? value["colour"] : "grey";
495
+ const token = typeof value["token"] === "string" && value["token"] !== "" ? value["token"] : void 0;
496
+ return {
497
+ id: value["id"],
498
+ origin,
499
+ ...token ? { token } : {},
500
+ name: typeof value["name"] === "string" ? value["name"].slice(0, 60) : "",
501
+ colour,
502
+ enabled: value["enabled"] !== false
503
+ };
504
+ }
505
+ var PairingStore = class {
506
+ #storage;
507
+ #tail = Promise.resolve();
508
+ #newId;
509
+ constructor(storage, options = {}) {
510
+ this.#storage = storage;
511
+ this.#newId = options.id ?? freshId;
512
+ }
513
+ /**
514
+ * Every entry, after the one-time move from the single pairing.
515
+ *
516
+ * An extension that held `gateway` and `token` keeps exactly that buddi, as
517
+ * the first entry, switched on. One that held nothing gets the default
518
+ * address, switched on and unpaired, so a fresh install still knocks on the
519
+ * buddi at 4317 by itself and the dashboard in this browser can pair it
520
+ * without the owner typing anything.
521
+ */
522
+ list() {
523
+ return this.#queue(async () => this.#load());
524
+ }
525
+ async #load() {
526
+ const stored = await this.#storage.get([PAIRINGS_KEY, LEGACY_GATEWAY, LEGACY_TOKEN]);
527
+ const raw = stored[PAIRINGS_KEY];
528
+ if (Array.isArray(raw)) return raw.map(readEntry).filter((entry) => entry !== null);
529
+ const address = typeof stored[LEGACY_GATEWAY] === "string" ? originOf2(stored[LEGACY_GATEWAY]) : null;
530
+ const token = typeof stored[LEGACY_TOKEN] === "string" && stored[LEGACY_TOKEN] !== "" ? stored[LEGACY_TOKEN] : void 0;
531
+ const first = { id: this.#newId(), origin: address ?? DEFAULT_GATEWAY, ...token ? { token } : {}, name: "", colour: COLOURS[0], enabled: true };
532
+ await this.#storage.set({ [PAIRINGS_KEY]: [first] });
533
+ await this.#storage.remove([LEGACY_GATEWAY, LEGACY_TOKEN]);
534
+ return [first];
535
+ }
536
+ async #save(entries2) {
537
+ await this.#storage.set({ [PAIRINGS_KEY]: entries2 });
538
+ }
539
+ #queue(work) {
540
+ const next = this.#tail.then(work, work);
541
+ this.#tail = next.catch(() => void 0);
542
+ return next;
543
+ }
544
+ /**
545
+ * Add a buddi by its address, or hand back the entry that already has it
546
+ * (switched on again). Throws the sentence the popup shows for an address
547
+ * that is not on this machine.
548
+ */
549
+ add(address) {
550
+ return this.#queue(async () => {
551
+ const origin = originOf2(address);
552
+ if (!origin) throw new Error(/^\s*https?:\/\//i.test(address) ? "A buddi address has to be on this machine." : "A buddi address starts with http.");
553
+ const entries2 = await this.#load();
554
+ const existing = entries2.find((entry2) => sameBuddi(entry2.origin, origin));
555
+ if (existing) {
556
+ existing.enabled = true;
557
+ await this.#save(entries2);
558
+ return existing;
559
+ }
560
+ const entry = { id: this.#newId(), origin, name: "", colour: nextColour(entries2), enabled: true };
561
+ entries2.push(entry);
562
+ await this.#save(entries2);
563
+ return entry;
564
+ });
565
+ }
566
+ /** Change one entry in place; nothing happens to an entry that is gone. */
567
+ update(id, patch) {
568
+ return this.#queue(async () => {
569
+ const entries2 = await this.#load();
570
+ const entry = entries2.find((candidate) => candidate.id === id);
571
+ if (!entry) return void 0;
572
+ if ("token" in patch) {
573
+ if (patch.token) entry.token = patch.token;
574
+ else delete entry.token;
575
+ }
576
+ if (patch.name !== void 0) entry.name = patch.name.slice(0, 60);
577
+ if (patch.enabled !== void 0) entry.enabled = patch.enabled;
578
+ await this.#save(entries2);
579
+ return entry;
580
+ });
581
+ }
582
+ /** Forget a buddi: its entry and its token go; its tabs stay, because they are the owner's. */
583
+ remove(id) {
584
+ return this.#queue(async () => {
585
+ const entries2 = await this.#load();
586
+ await this.#save(entries2.filter((entry) => entry.id !== id));
587
+ });
588
+ }
589
+ /** The token seam `Protocol` reads and writes, for one entry. */
590
+ tokens(id) {
591
+ return {
592
+ read: async () => (await this.list()).find((entry) => entry.id === id)?.token ?? null,
593
+ write: async (token) => {
594
+ await this.update(id, { token });
595
+ },
596
+ clear: async () => {
597
+ await this.update(id, { token: void 0 });
598
+ }
599
+ };
600
+ }
601
+ };
602
+ var GroupRegistry = class {
603
+ #owners = /* @__PURE__ */ new Map();
604
+ /** Record that `instance` made (or re-joined) this group. */
605
+ claim(groupId, instance) {
606
+ if (groupId >= 0) this.#owners.set(groupId, instance);
607
+ }
608
+ /** Whose group this is, or undefined for a group no buddi made (the owner's own). */
609
+ ownerOf(groupId) {
610
+ return groupId === void 0 || groupId < 0 ? void 0 : this.#owners.get(groupId);
611
+ }
612
+ /** May `instance` act in a tab that sits in this group? Only in its own, or in no group at all. */
613
+ allows(groupId, instance) {
614
+ const owner = this.ownerOf(groupId);
615
+ return owner === void 0 || owner === instance;
616
+ }
617
+ /** A buddi was removed: its groups belong to nobody now (the tabs are the owner's). */
618
+ release(instance) {
619
+ for (const [groupId, owner] of this.#owners) if (owner === instance) this.#owners.delete(groupId);
620
+ }
621
+ groupsOf(instance) {
622
+ return [...this.#owners].filter(([, owner]) => owner === instance).map(([groupId]) => groupId);
623
+ }
624
+ };
625
+
626
+ // src/external.ts
627
+ function isLoopbackOrigin(origin) {
628
+ if (!origin) return false;
629
+ let parsed;
630
+ try {
631
+ parsed = new URL(origin);
632
+ } catch {
633
+ return false;
634
+ }
635
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
636
+ return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname);
637
+ }
638
+ function named(state) {
639
+ return state.connection === "paired" ? "paired" : state.connection === "pairing" ? "pairing" : "disconnected";
640
+ }
641
+ function describe(state, gateway, version2) {
642
+ const status = { installed: true, version: version2, state: named(state), gateway };
643
+ if (status.state === "pairing" && state.code) status.code = state.code;
644
+ return status;
645
+ }
646
+ function describeFor(origin, known, version2) {
647
+ const mine = known.find((entry) => sameBuddi(entry.origin, origin));
648
+ const status = mine ? describe(mine.state, mine.origin, version2) : describe({ connection: "offline", code: null, installation: null, error: null }, known[0]?.origin ?? origin, version2);
649
+ status.pairings = known.map((entry) => ({ origin: entry.origin, name: entry.name, state: named(entry.state), enabled: entry.enabled }));
650
+ return status;
651
+ }
652
+ function handleExternal(message, context, respond) {
653
+ if (!isLoopbackOrigin(context.origin)) return false;
654
+ const request = message;
655
+ if (!request || request.type !== "buddi.status") return false;
656
+ const origin = context.origin;
657
+ void context.known().then((known) => {
658
+ if (!known.some((entry) => sameBuddi(entry.origin, origin))) context.unknown?.(origin);
659
+ respond(describeFor(origin, known, context.version));
660
+ });
661
+ return true;
662
+ }
663
+
1
664
  // src/protocol.ts
2
665
  var COMMAND_NAMES = ["navigate", "observe", "click", "fill", "select", "press", "scroll", "tab", "close", "fieldInfo", "secretFill"];
3
- var HAND_COMMANDS = ["screencast.start", "screencast.stop", "input", "hold", "unhold"];
666
+ var HAND_COMMANDS = ["screencast.start", "screencast.stop", "input", "hold", "unhold", "capture", "copy"];
667
+ var EXTENSION_FEATURES = ["live"];
4
668
  var PreconditionError = class extends Error {
5
669
  };
6
670
  var OpenedError = class extends Error {
@@ -33,6 +697,7 @@ var Cancellation = class {
33
697
  };
34
698
  var TOKEN_KEY = "token";
35
699
  var BAD_PROOF = "That buddi could not prove it is the one this browser was paired with.";
700
+ var PAIR_AGAIN = "pair-again";
36
701
  var encoder = new TextEncoder();
37
702
  var hex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
38
703
  async function sha256Hex(text2) {
@@ -85,24 +750,48 @@ var Protocol = class {
85
750
  const stored = await this.#token();
86
751
  this.#nonce = nonce();
87
752
  this.#authenticated = false;
88
- this.#set({ connection: "connecting", code: null, error: null });
89
- this.#options.send({ type: "hello", extension: this.#options.version, nonce: this.#nonce, paired: stored !== null });
753
+ this.#set({ connection: "connecting", code: null, error: null, features: [], refused: false });
754
+ this.#options.send({ type: "hello", extension: this.#options.version, nonce: this.#nonce, paired: stored !== null, features: [...EXTENSION_FEATURES] });
90
755
  }
91
- closed(reason) {
756
+ closed(reason, options = {}) {
92
757
  this.#authenticated = false;
93
758
  for (const cancel of this.#running.values()) cancel.cancel();
94
759
  this.#running.clear();
95
760
  this.#options.onReset?.();
96
- this.#set({ connection: "offline", code: null, error: reason ?? null });
761
+ this.#set({ connection: "offline", code: null, error: reason ?? null, refused: options.refused === true });
762
+ }
763
+ /** The token this browser held is no use at that buddi: forget it, so the next hello asks for a code. */
764
+ async dropToken() {
765
+ if (this.#options.tokens) await this.#options.tokens.clear();
766
+ else await this.#options.chrome?.storage.local.remove([TOKEN_KEY]);
97
767
  }
98
768
  async #token() {
99
- const stored = await this.#options.chrome.storage.local.get([TOKEN_KEY]);
769
+ if (this.#options.tokens) return this.#options.tokens.read();
770
+ const stored = await this.#options.chrome?.storage.local.get([TOKEN_KEY]) ?? {};
100
771
  const token = stored[TOKEN_KEY];
101
772
  return typeof token === "string" && token.length > 0 ? token : null;
102
773
  }
774
+ async #keep(token) {
775
+ if (this.#options.tokens) {
776
+ await this.#options.tokens.write(token);
777
+ return;
778
+ }
779
+ await this.#options.chrome?.storage.local.set({ [TOKEN_KEY]: token });
780
+ }
781
+ /** What a handshake frame says about the buddi: its name and what it reads. */
782
+ #about(message) {
783
+ const patch = {};
784
+ const name = message["name"];
785
+ if (typeof name === "string" && name.trim() !== "") patch.name = name.trim().slice(0, 60);
786
+ const features = message["features"];
787
+ patch.features = Array.isArray(features) ? features.filter((item) => typeof item === "string" && item.length <= 40).slice(0, 20) : [];
788
+ patch.older = !patch.features.includes("frames.binary");
789
+ return patch;
790
+ }
103
791
  /** Drops the token and asks for a new socket, which starts the pairing dance again. */
104
792
  async forget() {
105
- await this.#options.chrome.storage.local.remove([TOKEN_KEY]);
793
+ if (this.#options.tokens) await this.#options.tokens.clear();
794
+ else await this.#options.chrome?.storage.local.remove([TOKEN_KEY]);
106
795
  this.#authenticated = false;
107
796
  this.#set({ connection: "offline", code: null, installation: null, error: null });
108
797
  this.#options.disconnect?.("The owner forgot this buddi.");
@@ -137,10 +826,11 @@ var Protocol = class {
137
826
  connection: "paired",
138
827
  code: null,
139
828
  error: null,
829
+ ...this.#about(message),
140
830
  installation: typeof installation === "string" ? installation : this.#state.installation
141
831
  });
142
832
  const token = message["token"];
143
- if (typeof token === "string" && token.length > 0) await this.#options.chrome.storage.local.set({ [TOKEN_KEY]: token });
833
+ if (typeof token === "string" && token.length > 0) await this.#keep(token);
144
834
  return;
145
835
  }
146
836
  case "rehello":
@@ -169,398 +859,90 @@ var Protocol = class {
169
859
  default:
170
860
  return;
171
861
  }
172
- }
173
- /**
174
- * The gateway signed our nonce. Check it before the token leaves this browser.
175
- *
176
- * The key is the token's hash, which is all the gateway keeps, so a gateway
177
- * that cannot produce this signature is not the one this browser paired with
178
- * and never gets to see the token.
179
- */
180
- async #challenge(message) {
181
- const token = await this.#token();
182
- const claimed = typeof message["proof"] === "string" ? message["proof"] : "";
183
- if (!token) {
184
- this.#options.disconnect?.("This browser has no token for that buddi.");
185
- return;
186
- }
187
- const expected = await proofFor(await sha256Hex(token), this.#nonce);
188
- if (!sameProof(expected, claimed)) {
189
- this.#authenticated = false;
190
- this.#set({ connection: "offline", code: null, error: BAD_PROOF });
191
- this.#options.disconnect?.(BAD_PROOF);
192
- return;
193
- }
194
- const installation = message["installation"];
195
- if (typeof installation === "string") this.#set({ installation });
196
- this.#options.send({ type: "auth", token });
197
- }
198
- async #command(message) {
199
- const id = typeof message["id"] === "string" ? message["id"] : null;
200
- if (!id) return;
201
- if (!this.#authenticated) {
202
- this.#options.send({ type: "result", id, ok: false, error: "This browser is not paired with you.", precondition: true });
203
- return;
204
- }
205
- const name = message["name"];
206
- const session = message["session"];
207
- if (typeof name !== "string" || !isCommandName(name)) {
208
- this.#options.send({ type: "result", id, ok: false, error: `This buddi extension does not know the command ${String(name)}.`, precondition: true });
209
- return;
210
- }
211
- if (typeof session !== "string" || session.length === 0) {
212
- this.#options.send({ type: "result", id, ok: false, error: "The command arrived without a session.", precondition: true });
213
- return;
214
- }
215
- const args = message["args"];
216
- const command = {
217
- id,
218
- name,
219
- session,
220
- args: args && typeof args === "object" ? args : {},
221
- owner: message["owner"] === true
222
- };
223
- const cancel = new Cancellation();
224
- this.#running.set(id, cancel);
225
- try {
226
- const result = await this.#options.execute(command, cancel);
227
- cancel.check();
228
- this.#options.send({ type: "result", id, ok: true, observation: result.observation ?? null, screenshot: result.screenshot ?? null });
229
- } catch (error) {
230
- if (error instanceof CancelledError) {
231
- this.#options.send({ type: "result", id, ok: false, error: "cancelled", precondition: !cancel.dispatched });
232
- } else {
233
- this.#options.send({
234
- type: "result",
235
- id,
236
- ok: false,
237
- error: sentence(error),
238
- precondition: error instanceof PreconditionError,
239
- ...error instanceof OpenedError ? { page: error.page } : {}
240
- });
241
- }
242
- } finally {
243
- this.#running.delete(id);
244
- }
245
- }
246
- };
247
- function isCommandName(name) {
248
- return name === "screenshot" || COMMAND_NAMES.includes(name) || HAND_COMMANDS.includes(name);
249
- }
250
- function sentence(error) {
251
- const text2 = error instanceof Error ? error.message : String(error);
252
- const trimmed = text2.replace(/\s+/g, " ").trim().slice(0, 300);
253
- if (!trimmed) return "The browser extension could not run that command.";
254
- return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
255
- }
256
-
257
- // src/bar.ts
258
- var OWNER_WAIT_MS = 3e4;
259
- var BAR_AFTER_MS = 3e3;
260
- var BAR_POLL_MS = 500;
261
- async function waitForOwner(probes, limits = {}) {
262
- const waitMs = limits.waitMs ?? OWNER_WAIT_MS;
263
- const barAfter = limits.barAfterMs ?? BAR_AFTER_MS;
264
- const poll = limits.pollMs ?? BAR_POLL_MS;
265
- const start = probes.now();
266
- let shown = false;
267
- const maxPolls = Math.ceil(waitMs / poll);
268
- try {
269
- for (let polls = 0; ; polls++) {
270
- if (!await probes.watched()) return "left";
271
- const elapsed = Math.max(probes.now() - start, polls * poll);
272
- if (!shown && elapsed >= barAfter) {
273
- shown = true;
274
- await probes.show().catch(() => void 0);
275
- }
276
- if (shown) {
277
- const choice = await probes.choice().catch(() => void 0);
278
- if (choice) return choice;
279
- }
280
- if (elapsed >= waitMs || polls >= maxPolls) return "timeout";
281
- await probes.wait(poll);
282
- }
283
- } finally {
284
- if (shown) await probes.hide().catch(() => void 0);
285
- }
286
- }
287
- function showBar(agent) {
288
- const holder = globalThis;
289
- holder["__buddiBar"] = { choice: void 0 };
290
- const doc = globalThis.document;
291
- if (!doc || doc.getElementById("buddi-working-bar")) return;
292
- const host = doc.createElement("div");
293
- host.id = "buddi-working-bar";
294
- host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
295
- const root = host.attachShadow({ mode: "closed" });
296
- const bar = doc.createElement("div");
297
- bar.setAttribute("role", "status");
298
- bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
299
- const label = doc.createElement("span");
300
- label.textContent = `${agent || "buddi"} is working here`;
301
- label.setAttribute("style", "margin-right:auto;");
302
- const button = (text2, choice, primary) => {
303
- const b = doc.createElement("button");
304
- b.type = "button";
305
- b.textContent = text2;
306
- b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
307
- b.addEventListener("click", () => {
308
- holder["__buddiBar"] = { choice };
309
- });
310
- return b;
311
- };
312
- bar.append(label, button("Take over", "takeover", false), button("Let it continue", "continue", true));
313
- root.append(bar);
314
- (doc.documentElement ?? doc.body).append(host);
315
- }
316
- function readBar() {
317
- const holder = globalThis;
318
- const choice = holder["__buddiBar"]?.choice;
319
- if (choice) holder["__buddiBar"] = { choice: void 0 };
320
- return choice;
321
- }
322
- function hideBar() {
323
- const doc = globalThis.document;
324
- doc?.getElementById("buddi-working-bar")?.remove();
325
- const holder = globalThis;
326
- delete holder["__buddiBar"];
327
- }
328
- var GIVE_BACK_MESSAGE = "buddi-give-back";
329
- function showHeldBar(session) {
330
- const doc = globalThis.document;
331
- if (!doc || doc.getElementById("buddi-held-bar")) return;
332
- const host = doc.createElement("div");
333
- host.id = "buddi-held-bar";
334
- host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
335
- const root = host.attachShadow({ mode: "closed" });
336
- const bar = doc.createElement("div");
337
- bar.setAttribute("role", "status");
338
- bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
339
- const label = doc.createElement("span");
340
- label.textContent = "buddi is waiting";
341
- label.setAttribute("style", "margin-right:auto;");
342
- const give = doc.createElement("button");
343
- give.type = "button";
344
- give.textContent = "Give it back";
345
- give.setAttribute("style", "font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;background:#fff;color:#1f2a44;");
346
- give.addEventListener("click", () => {
347
- const runtime = globalThis.chrome?.runtime;
348
- void runtime?.sendMessage({ type: "buddi-give-back", session })?.catch?.(() => void 0);
349
- host.remove();
350
- });
351
- bar.append(label, give);
352
- root.append(bar);
353
- (doc.documentElement ?? doc.body).append(host);
354
- const asking = [];
355
- const restore = () => {
356
- for (const button of asking.splice(0)) button.remove();
357
- label.textContent = "buddi is waiting";
358
- give.style.display = "";
359
- };
360
- globalThis["__buddiHeldAsk"] = (question, choices) => {
361
- restore();
362
- label.textContent = question;
363
- give.style.display = "none";
364
- for (const choice of choices) {
365
- const b = doc.createElement("button");
366
- b.type = "button";
367
- b.textContent = choice.label;
368
- b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${choice.primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
369
- b.addEventListener("click", () => {
370
- restore();
371
- choice.pick();
372
- });
373
- asking.push(b);
374
- bar.insertBefore(b, give);
862
+ }
863
+ /**
864
+ * The gateway signed our nonce. Check it before the token leaves this browser.
865
+ *
866
+ * The key is the token's hash, which is all the gateway keeps, so a gateway
867
+ * that cannot produce this signature is not the one this browser paired with
868
+ * and never gets to see the token.
869
+ */
870
+ async #challenge(message) {
871
+ const token = await this.#token();
872
+ const claimed = typeof message["proof"] === "string" ? message["proof"] : "";
873
+ if (!token) {
874
+ this.#options.disconnect?.("This browser has no token for that buddi.");
875
+ return;
375
876
  }
376
- return restore;
377
- };
378
- }
379
- function hideHeldBar() {
380
- const doc = globalThis.document;
381
- doc?.getElementById("buddi-held-bar")?.remove();
382
- delete globalThis["__buddiHeldAsk"];
383
- }
384
- var LOGIN_MESSAGE = "buddi-login";
385
- var LOGIN_PROMPT_MS = 2 * 6e4;
386
- function watchHeldLogins(session, facts) {
387
- const holder = globalThis;
388
- const known = { never: [...facts?.never ?? []], saved: [...facts?.saved ?? []] };
389
- const existing = holder["__buddiHeldLogins"];
390
- if (existing) {
391
- existing.session = session;
392
- existing.facts = known;
393
- return;
877
+ const expected = await proofFor(await sha256Hex(token), this.#nonce);
878
+ if (!sameProof(expected, claimed)) {
879
+ this.#authenticated = false;
880
+ await this.dropToken();
881
+ this.#set({ connection: "offline", code: null, error: BAD_PROOF });
882
+ this.#options.disconnect?.(PAIR_AGAIN);
883
+ return;
884
+ }
885
+ const installation = message["installation"];
886
+ this.#set({ ...this.#about(message), ...typeof installation === "string" ? { installation } : {} });
887
+ this.#options.send({ type: "auth", token });
394
888
  }
395
- const doc = globalThis.document;
396
- if (!doc) return;
397
- const state = { session, facts: known, last: "", lastAt: 0, typedAt: 0, cached: void 0, timer: void 0, drop: void 0 };
398
- holder["__buddiHeldLogins"] = state;
399
- const runtime = () => globalThis.chrome?.runtime;
400
- const call = async (message) => {
401
- try {
402
- return await runtime()?.sendMessage(message);
403
- } catch {
404
- return void 0;
889
+ async #command(message) {
890
+ const id = typeof message["id"] === "string" ? message["id"] : null;
891
+ if (!id) return;
892
+ if (!this.#authenticated) {
893
+ this.#options.send({ type: "result", id, ok: false, error: "This browser is not paired with you.", precondition: true });
894
+ return;
405
895
  }
406
- };
407
- const site = () => (globalThis.location?.hostname ?? "").toLowerCase().replace(/^www\./, "");
408
- const listed = (where) => state.facts.never.some((entry) => {
409
- const bare = entry.toLowerCase().replace(/^www\./, "");
410
- return where === bare || where.endsWith(`.${bare}`);
411
- });
412
- const bar = () => {
413
- const question = holder["__buddiHeldAsk"];
414
- return typeof question === "function" ? question : void 0;
415
- };
416
- const prompt = (where, update) => {
417
- const question = bar();
418
- if (!question) return;
419
- if (state.drop) clearTimeout(state.drop);
420
- let answered = false;
421
- const save = () => {
422
- if (answered) return;
423
- answered = true;
424
- const asking = bar();
425
- asking?.(update ? "Updating\u2026" : "Saving\u2026", []);
426
- void call({ type: "buddi-login", session: state.session, decision: "save" }).then((reply) => {
427
- const answer = reply;
428
- const now = bar();
429
- if (!now) return;
430
- if (answer?.saved === true) {
431
- const restore2 = now(update ? `Updated the login for ${where}` : `Saved the login for ${where}`, []);
432
- if (state.drop) clearTimeout(state.drop);
433
- state.drop = setTimeout(restore2, 2e3);
434
- return;
435
- }
436
- const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : "buddi did not answer. Try again.";
437
- now(reason, [
438
- { label: "Try again", primary: true, pick: () => {
439
- answered = false;
440
- save();
441
- } },
442
- { label: "Not now", pick: () => {
443
- void call({ type: "buddi-login", session: state.session, decision: "later" });
444
- } }
445
- ]);
446
- });
896
+ const name = message["name"];
897
+ const session = message["session"];
898
+ if (typeof name !== "string" || !isCommandName(name)) {
899
+ this.#options.send({ type: "result", id, ok: false, error: `This buddi extension does not know the command ${String(name)}.`, precondition: true });
900
+ return;
901
+ }
902
+ if (typeof session !== "string" || session.length === 0) {
903
+ this.#options.send({ type: "result", id, ok: false, error: "The command arrived without a session.", precondition: true });
904
+ return;
905
+ }
906
+ const args = message["args"];
907
+ const command = {
908
+ id,
909
+ name,
910
+ session,
911
+ args: args && typeof args === "object" ? args : {},
912
+ owner: message["owner"] === true
447
913
  };
448
- const restore = question(update ? `Update the login for ${where}?` : `Save this login for ${where}?`, [
449
- { label: update ? "Update" : "Save", primary: true, pick: save },
450
- { label: "Not now", pick: () => {
451
- answered = true;
452
- void call({ type: "buddi-login", session: state.session, decision: "later" });
453
- } },
454
- { label: "Never for this site", pick: () => {
455
- answered = true;
456
- state.facts.never.push(where);
457
- void call({ type: "buddi-login", session: state.session, decision: "never" });
458
- } }
459
- ]);
460
- state.drop = setTimeout(() => {
461
- if (!answered) restore();
462
- }, 2 * 6e4);
463
- };
464
- const ask = (found) => {
465
- const where = site();
466
- if (where === "" || listed(where) || !bar()) return;
467
- const update = state.facts.saved.some((login) => login.site === where && login.username === found.username);
468
- void call({ type: "buddi-login-seen", session: state.session, username: found.username, password: found.password, update }).then((reply) => {
469
- const asked = reply;
470
- if (holder["__buddiHeldLogins"] !== state || !asked || typeof asked !== "object") return;
471
- prompt(where, asked.update === true);
472
- });
473
- };
474
- void call({ type: "buddi-login-pending", session: state.session }).then((reply) => {
475
- const waiting = reply;
476
- if (holder["__buddiHeldLogins"] !== state || !waiting || typeof waiting.site !== "string") return;
477
- prompt(waiting.site, waiting.update === true);
478
- });
479
- const NOT_SUBMIT = /\b(show|hide|reveal|toggle|eye|forgot|reset|cancel|back|close|sign ?up|register|create)\b/i;
480
- const USER = /user|email|login|account|ident|phone|mail/i;
481
- const passwords = (root) => Array.from(root.querySelectorAll('input[type="password" i]'));
482
- const usernameFor = (password) => {
483
- const scope = password.form ?? doc;
484
- const inputs = Array.from(scope.querySelectorAll("input")).filter((input) => ["text", "email", "tel", ""].includes((input.getAttribute("type") ?? "").toLowerCase()) && input.value.trim() !== "");
485
- const marked = inputs.find((input) => (input.getAttribute("autocomplete") ?? "").toLowerCase().includes("username") || (input.getAttribute("type") ?? "").toLowerCase() === "email");
486
- if (marked) return marked.value.trim();
487
- const before = inputs.filter((input) => (input.compareDocumentPosition(password) & 4) !== 0);
488
- const pick = before[before.length - 1] ?? inputs.find((input) => USER.test(`${input.name} ${input.id} ${input.getAttribute("autocomplete") ?? ""}`));
489
- return pick ? pick.value.trim() : "";
490
- };
491
- const read = (scope) => {
492
- const field = passwords(scope ?? doc).find((input) => input.value !== "");
493
- return field ? { username: usernameFor(field), password: field.value } : void 0;
494
- };
495
- const send2 = (found) => {
496
- if (holder["__buddiHeldLogins"] !== state) return;
497
- if (!found || found.password === "") return;
498
- const key = `${found.username}\0${found.password}`;
499
- const now = Date.now();
500
- if (key === state.last && now - state.lastAt < 5e3) return;
501
- state.last = key;
502
- state.lastAt = now;
503
- state.cached = void 0;
504
- ask(found);
505
- };
506
- doc.addEventListener("submit", (event) => {
507
- send2(read(event.target) ?? state.cached);
508
- }, true);
509
- doc.addEventListener("keydown", (event) => {
510
- if (event.key !== "Enter") return;
511
- const target = event.target;
512
- if (!target || target.tagName !== "INPUT") return;
513
- send2(read(target.form ?? doc));
514
- }, true);
515
- doc.addEventListener("click", (event) => {
516
- const target = event.target;
517
- const button = target?.closest?.('button, input[type="submit" i], input[type="image" i], input[type="button" i], [role="button"]');
518
- if (!button) return;
519
- const words = `${button.textContent ?? ""} ${button.getAttribute("aria-label") ?? ""} ${button.getAttribute("value") ?? ""} ${button.getAttribute("title") ?? ""}`;
520
- if (NOT_SUBMIT.test(words)) return;
521
- send2(read(button.form ?? doc));
522
- }, true);
523
- doc.addEventListener("input", (event) => {
524
- const target = event.target;
525
- if ((target?.getAttribute?.("type") ?? "").toLowerCase() !== "password") return;
526
- state.typedAt = Date.now();
527
- state.cached = read(target.form ?? doc);
528
- }, true);
529
- const Observer = globalThis.PerformanceObserver;
530
- if (typeof Observer === "function") {
914
+ const cancel = new Cancellation();
915
+ this.#running.set(id, cancel);
531
916
  try {
532
- new Observer((list) => {
533
- if (!state.cached || Date.now() - state.typedAt > 1e4) return;
534
- const fetched = list.getEntries().some((entry) => ["fetch", "xmlhttprequest"].includes(entry.initiatorType));
535
- if (!fetched) return;
536
- if (state.timer) clearTimeout(state.timer);
537
- state.timer = setTimeout(() => {
538
- const still = passwords(doc).some((input) => input.isConnected && input.value !== "" && input.getClientRects().length > 0);
539
- if (!still) send2(state.cached);
540
- }, 1500);
541
- }).observe({ type: "resource", buffered: false });
542
- } catch {
917
+ const result = await this.#options.execute(command, cancel);
918
+ cancel.check();
919
+ this.#options.send({ type: "result", id, ok: true, observation: result.observation ?? null, screenshot: result.screenshot ?? null });
920
+ } catch (error) {
921
+ if (error instanceof CancelledError) {
922
+ this.#options.send({ type: "result", id, ok: false, error: "cancelled", precondition: !cancel.dispatched });
923
+ } else {
924
+ this.#options.send({
925
+ type: "result",
926
+ id,
927
+ ok: false,
928
+ error: sentence(error),
929
+ precondition: error instanceof PreconditionError,
930
+ ...error instanceof OpenedError ? { page: error.page } : {}
931
+ });
932
+ }
933
+ } finally {
934
+ this.#running.delete(id);
543
935
  }
544
936
  }
937
+ };
938
+ function isCommandName(name) {
939
+ return name === "screenshot" || COMMAND_NAMES.includes(name) || HAND_COMMANDS.includes(name);
545
940
  }
546
- var LOGIN_SEEN_MESSAGE = "buddi-login-seen";
547
- var LOGIN_PENDING_MESSAGE = "buddi-login-pending";
548
- function heldLoginRequest(message, sender, heldTab) {
549
- const request = message;
550
- if (!request || typeof request.session !== "string" || request.session === "") return void 0;
551
- if (heldTab === void 0 || sender?.tab?.id !== heldTab) return void 0;
552
- const session = request.session;
553
- const url = sender.url ?? "";
554
- if (request.type === LOGIN_SEEN_MESSAGE) {
555
- if (typeof request.password !== "string" || request.password === "" || request.password.length > 1024) return void 0;
556
- const username = typeof request.username === "string" ? request.username.slice(0, 200) : "";
557
- return { kind: "seen", session, tabId: heldTab, url, username, password: request.password, update: request.update === true };
558
- }
559
- if (request.type === LOGIN_PENDING_MESSAGE) return { kind: "pending", session, tabId: heldTab, url };
560
- if (request.type === LOGIN_MESSAGE && (request.decision === "save" || request.decision === "later" || request.decision === "never")) {
561
- return { kind: "answer", session, tabId: heldTab, decision: request.decision };
562
- }
563
- return void 0;
941
+ function sentence(error) {
942
+ const text2 = error instanceof Error ? error.message : String(error);
943
+ const trimmed = text2.replace(/\s+/g, " ").trim().slice(0, 300);
944
+ if (!trimmed) return "The browser extension could not run that command.";
945
+ return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
564
946
  }
565
947
 
566
948
  // src/held-logins.ts
@@ -588,8 +970,8 @@ function webOrigin(url) {
588
970
  }
589
971
  }
590
972
  var HeldLogins = class {
591
- constructor(send2, options = {}) {
592
- this.send = send2;
973
+ constructor(send, options = {}) {
974
+ this.send = send;
593
975
  this.options = options;
594
976
  }
595
977
  #byTab = /* @__PURE__ */ new Map();
@@ -602,11 +984,11 @@ var HeldLogins = class {
602
984
  #ask(tabId, frame, ms) {
603
985
  const id = this.#id();
604
986
  return new Promise((resolve) => {
605
- const timer2 = setTimeout(() => {
987
+ const timer = setTimeout(() => {
606
988
  this.#waiting.delete(id);
607
989
  resolve(void 0);
608
990
  }, ms);
609
- this.#waiting.set(id, { tabId, resolve, timer: timer2 });
991
+ this.#waiting.set(id, { tabId, resolve, timer });
610
992
  this.send({ ...frame, id });
611
993
  });
612
994
  }
@@ -733,6 +1115,9 @@ var SETTLE_MS = 600;
733
1115
  var SETTLE_STEP_MS = 50;
734
1116
  var OBSERVE_RETRY_MS = [500, 1e3, 2e3];
735
1117
  var MIN_FRAME_MS = 100;
1118
+ var WATCH_FRAME_MS = 500;
1119
+ var MAX_FRAME_MS = 2e3;
1120
+ var MAX_COPY = 4e3;
736
1121
  var MAX_CAST_SIDE = 4096;
737
1122
  var MAX_COORDINATE = 2e4;
738
1123
  var MAX_DELTA = 1e4;
@@ -757,13 +1142,13 @@ function num(value) {
757
1142
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
758
1143
  }
759
1144
  function side(value, fallback) {
760
- const asked = num(value) ?? fallback;
761
- return Math.min(MAX_CAST_SIDE, Math.max(1, Math.round(asked)));
1145
+ const asked2 = num(value) ?? fallback;
1146
+ return Math.min(MAX_CAST_SIDE, Math.max(1, Math.round(asked2)));
762
1147
  }
763
1148
  function clamp(value, limit, fallback = 0) {
764
- const asked = num(value);
765
- if (asked === void 0) return fallback;
766
- return Math.min(limit, Math.max(-limit, asked));
1149
+ const asked2 = num(value);
1150
+ if (asked2 === void 0) return fallback;
1151
+ return Math.min(limit, Math.max(-limit, asked2));
767
1152
  }
768
1153
  function metadataOf(value) {
769
1154
  const source = value ?? {};
@@ -813,8 +1198,8 @@ var VIRTUAL_KEYS = {
813
1198
  F12: 123
814
1199
  };
815
1200
  function virtualKey(key) {
816
- const named = VIRTUAL_KEYS[key];
817
- if (named !== void 0) return named;
1201
+ const named2 = VIRTUAL_KEYS[key];
1202
+ if (named2 !== void 0) return named2;
818
1203
  return key.length === 1 ? key.toUpperCase().charCodeAt(0) : 0;
819
1204
  }
820
1205
  function text(value, limit) {
@@ -935,8 +1320,17 @@ var BrowserCommands = class {
935
1320
  #casts = /* @__PURE__ */ new Map();
936
1321
  #castsByTab = /* @__PURE__ */ new Map();
937
1322
  #ownerWaitMs;
1323
+ /** Which buddi these commands belong to, and the registry that says whose each group is. */
1324
+ #instance;
1325
+ #groups;
1326
+ #groupTitle;
1327
+ #groupColour;
938
1328
  constructor(chrome2, options = {}) {
939
1329
  this.#chrome = chrome2;
1330
+ this.#instance = options.instance ?? "buddi";
1331
+ this.#groups = options.groups;
1332
+ this.#groupTitle = options.groupTitle ?? (() => GROUP_TITLE);
1333
+ this.#groupColour = options.groupColour ?? (() => void 0);
940
1334
  this.downloads = options.downloads;
941
1335
  this.logins = new HeldLogins(options.onLogin ?? (() => void 0));
942
1336
  chrome2.tabs.onRemoved?.addListener((tabId) => this.logins.clearTab(tabId));
@@ -949,11 +1343,17 @@ var BrowserCommands = class {
949
1343
  chrome2.debugger.onDetach.addListener((source) => {
950
1344
  const tabId = source.tabId;
951
1345
  if (tabId === void 0) return;
1346
+ if (!this.#attached.has(tabId) && !this.#castsByTab.has(tabId)) return;
952
1347
  this.#attached.delete(tabId);
953
1348
  const cast = this.#castsByTab.get(tabId);
954
- if (cast) this.#forgetCast(cast);
1349
+ if (cast) {
1350
+ this.#forgetCast(cast);
1351
+ this.#onEvent({ type: "event", name: "castended", session: cast.session });
1352
+ }
955
1353
  });
956
- chrome2.tabs.onUpdated?.addListener((tabId, change) => {
1354
+ chrome2.tabs.onUpdated?.addListener((tabId, change, tab) => {
1355
+ const cast = this.#castsByTab.get(tabId);
1356
+ if (cast && tab?.url) cast.url = tab.url;
957
1357
  if (change.status !== "complete" || ![...this.#held.values()].includes(tabId)) return;
958
1358
  const session = [...this.#held].find(([, held]) => held === tabId)?.[0];
959
1359
  if (session) void this.#drawHeld(tabId, session);
@@ -963,6 +1363,26 @@ var BrowserCommands = class {
963
1363
  }
964
1364
  /** Downloads an agent's command starts, noticed while it runs (and a few seconds after). */
965
1365
  downloads;
1366
+ /** How many tabs this buddi is working in right now, for the popup. */
1367
+ async tabCount() {
1368
+ let count = 0;
1369
+ for (const session of this.#sessions.values()) {
1370
+ for (const tabId of session.tabs.values()) {
1371
+ const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
1372
+ if (tab && !this.#taken(session, tab)) count += 1;
1373
+ }
1374
+ }
1375
+ return count;
1376
+ }
1377
+ /** A buddi's name changed (its handshake said one): its groups take it. */
1378
+ async retitle() {
1379
+ const title = this.#groupTitle();
1380
+ const color = this.#groupColour();
1381
+ for (const session of this.#sessions.values()) {
1382
+ if (session.groupId < 0) continue;
1383
+ await this.#chrome.tabGroups.update(session.groupId, { title, ...color ? { color } : {} }).catch(() => void 0);
1384
+ }
1385
+ }
966
1386
  /** The addresses of a session's tabs, for telling its downloads from the owner's. */
967
1387
  async #tabUrls(name) {
968
1388
  const session = this.#sessions.get(name);
@@ -1022,6 +1442,10 @@ var BrowserCommands = class {
1022
1442
  return this.#hold(command, cancel);
1023
1443
  case "unhold":
1024
1444
  return this.#unhold(command.session);
1445
+ case "capture":
1446
+ return this.#capture(command.session, cancel);
1447
+ case "copy":
1448
+ return this.#copy(command.session);
1025
1449
  case "close":
1026
1450
  return this.#close(command, cancel);
1027
1451
  default:
@@ -1090,8 +1514,8 @@ var BrowserCommands = class {
1090
1514
  const session = message?.session;
1091
1515
  const request = heldLoginRequest(message, sender, typeof session === "string" ? this.#held.get(session) : void 0);
1092
1516
  if (!request) return void 0;
1093
- if (request.kind === "seen") return this.logins.capture(request.tabId, request.session, request.url, request).then((asked) => asked ?? null);
1094
- if (request.kind === "pending") return this.logins.pending(request.tabId, request.session, request.url).then((asked) => asked ?? null);
1517
+ if (request.kind === "seen") return this.logins.capture(request.tabId, request.session, request.url, request).then((asked2) => asked2 ?? null);
1518
+ if (request.kind === "pending") return this.logins.pending(request.tabId, request.session, request.url).then((asked2) => asked2 ?? null);
1095
1519
  if (request.decision === "save") return this.logins.save(request.tabId, request.session);
1096
1520
  if (request.decision === "never") return Promise.resolve({ ok: this.logins.never(request.tabId, request.session) });
1097
1521
  this.logins.dismiss(request.tabId, request.session);
@@ -1162,6 +1586,7 @@ var BrowserCommands = class {
1162
1586
  /** The owner dragged this tab out of the session's group. A tab that never made it into the group cannot have been. */
1163
1587
  #taken(session, tab) {
1164
1588
  if (tab.id !== void 0 && session.ungrouped.has(tab.id)) return false;
1589
+ if (this.#groups && !this.#groups.allows(tab.groupId, this.#instance)) return true;
1165
1590
  return session.groupId >= 0 && tab.groupId !== session.groupId;
1166
1591
  }
1167
1592
  #activeKey(session) {
@@ -1238,8 +1663,8 @@ var BrowserCommands = class {
1238
1663
  if (normal(found)) return found.id;
1239
1664
  }
1240
1665
  }
1241
- const last2 = await windows.getLastFocused?.({ windowTypes: ["normal"] }).catch(() => void 0);
1242
- if (normal(last2)) return last2.id;
1666
+ const last = await windows.getLastFocused?.({ windowTypes: ["normal"] }).catch(() => void 0);
1667
+ if (normal(last)) return last.id;
1243
1668
  const all = await windows.getAll?.({ windowTypes: ["normal"] }).catch(() => void 0);
1244
1669
  return all?.find(normal)?.id ?? null;
1245
1670
  }
@@ -1277,9 +1702,11 @@ var BrowserCommands = class {
1277
1702
  if (groupable) {
1278
1703
  try {
1279
1704
  const groupId = await this.#chrome.tabs.group(session.groupId >= 0 ? { tabIds: [tabId], groupId: session.groupId } : { tabIds: [tabId] });
1705
+ this.#groups?.claim(groupId, this.#instance);
1280
1706
  if (session.groupId !== groupId) {
1281
1707
  session.groupId = groupId;
1282
- await this.#chrome.tabGroups.update(groupId, { title: GROUP_TITLE, collapsed: false }).catch(() => void 0);
1708
+ const color = this.#groupColour();
1709
+ await this.#chrome.tabGroups.update(groupId, { title: this.#groupTitle(), collapsed: false, ...color ? { color } : {} }).catch(() => void 0);
1283
1710
  }
1284
1711
  } catch {
1285
1712
  session.ungrouped.add(tabId);
@@ -1358,6 +1785,7 @@ var BrowserCommands = class {
1358
1785
  this.#refs.delete(command.session);
1359
1786
  cancel.check();
1360
1787
  const note = await this.#reuseOrOpen(session, url, cancel);
1788
+ await this.#followWatch(command.session);
1361
1789
  return note ? { observation: { note } } : {};
1362
1790
  }
1363
1791
  async #tab(command) {
@@ -1368,11 +1796,13 @@ var BrowserCommands = class {
1368
1796
  throw error instanceof PreconditionError ? error : new PreconditionError("No such tab in this conversation.");
1369
1797
  });
1370
1798
  session.active = wanted;
1799
+ await this.#followWatch(command.session);
1371
1800
  return {};
1372
1801
  }
1373
1802
  /** Closes this session's own tabs, and only the ones still in its group. */
1374
1803
  async #close(command, cancel) {
1375
1804
  this.#inView.delete(command.session);
1805
+ await this.#stopScreencast(command.session).catch(() => void 0);
1376
1806
  const session = this.#sessions.get(command.session);
1377
1807
  if (!session) return {};
1378
1808
  const ids = [];
@@ -1425,7 +1855,7 @@ var BrowserCommands = class {
1425
1855
  * with growing waits, before telling the model the page is not answering.
1426
1856
  */
1427
1857
  async #readFrames(tabId, cancel) {
1428
- for (let attempt2 = 0; ; attempt2 += 1) {
1858
+ for (let attempt = 0; ; attempt += 1) {
1429
1859
  await this.#inject(tabId);
1430
1860
  const frames = await this.#chrome.scripting.executeScript({
1431
1861
  target: { tabId, allFrames: true },
@@ -1433,7 +1863,7 @@ var BrowserCommands = class {
1433
1863
  }).catch(() => []);
1434
1864
  const top = frames.find((frame) => frame.frameId === 0);
1435
1865
  if (top?.result) return { frames, top: { frameId: top.frameId, result: top.result } };
1436
- const wait = OBSERVE_RETRY_MS[attempt2];
1866
+ const wait = OBSERVE_RETRY_MS[attempt];
1437
1867
  if (wait === void 0) break;
1438
1868
  await this.#wait(wait);
1439
1869
  cancel.check();
@@ -1620,36 +2050,53 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1620
2050
  await this.#releaseDebugger(tabId);
1621
2051
  }
1622
2052
  }
1623
- /* ---- the owner's own hand: a screencast out, input in ---- */
2053
+ /* ---- the picture out, and the owner's own hand in ---- */
1624
2054
  /**
1625
- * Start painting this session's tab to the dashboard.
2055
+ * Start painting this session's tab to buddi.
1626
2056
  *
1627
- * The debugger is claimed for the screencast's whole life rather than per
1628
- * frame: frames arrive as events, not as answers, and a detach between them
1629
- * would end the stream. Starting twice on the same session restarts it,
1630
- * which is what a reconnecting dashboard does.
2057
+ * Two kinds, one per session at a time. `watch: true` is the Canvas's
2058
+ * picture while the agent works: it follows the session's active tab, sends
2059
+ * at most two frames a second, takes no input, and asking for it again with
2060
+ * the same bounds on the same tab changes nothing. Without it, this is the
2061
+ * owner's take-over: pinned to the tab it starts on, ten frames a second,
2062
+ * and the only cast input reaches. Starting either replaces whatever cast
2063
+ * the session had, which is also what a re-tune and a reconnecting
2064
+ * dashboard do.
2065
+ *
2066
+ * The debugger is claimed for the cast's whole life rather than per frame:
2067
+ * frames arrive as events, not as answers, and a detach between them would
2068
+ * end the stream. And focus is emulated on the tab first: a tab in the
2069
+ * background, or in a window that is minimised or behind another, is
2070
+ * "hidden" to Chrome, and a hidden page paints nothing at all for a
2071
+ * screencast. With focus emulated the page renders as if it were in front
2072
+ * (it is told it is visible), and stays where it is.
1631
2073
  */
1632
2074
  async #startScreencast(command, cancel) {
1633
2075
  const session = await this.#session(command.session);
1634
2076
  const tab = await this.#ownTab(session, this.#activeKey(session));
1635
2077
  this.#liveUrl(tab);
1636
2078
  const tabId = tab.id;
2079
+ const mode = command.args["watch"] === true ? "watch" : "hand";
2080
+ const params = {
2081
+ // Smaller and cheaper than a screenshot on purpose: this is a picture
2082
+ // for a phone on someone else's network, not evidence for a model.
2083
+ quality: Math.min(90, Math.max(20, Math.trunc(num(command.args["quality"]) ?? 50))),
2084
+ maxWidth: side(command.args["maxWidth"], 960),
2085
+ maxHeight: side(command.args["maxHeight"], 600),
2086
+ everyNthFrame: Math.min(10, Math.max(1, Math.trunc(num(command.args["everyNthFrame"]) ?? 1)))
2087
+ };
2088
+ const interval = Math.min(MAX_FRAME_MS, Math.max(MIN_FRAME_MS, Math.trunc(num(command.args["interval"]) ?? (mode === "watch" ? WATCH_FRAME_MS : MIN_FRAME_MS))));
2089
+ const running = this.#casts.get(command.session);
2090
+ if (running && !running.stopped && mode === "watch" && running.mode === "watch" && running.tabId === tabId && running.interval === interval && JSON.stringify(running.params) === JSON.stringify(params)) return {};
1637
2091
  cancel.check();
1638
- await this.#stopScreencast(command.session);
1639
2092
  await this.#claimDebugger(tabId);
1640
- const cast = { session: command.session, tabId, lastSentAt: 0, stopped: false };
2093
+ await this.#stopScreencast(command.session);
2094
+ const cast = { session: command.session, tabId, mode, interval, params, url: tab.url, lastSentAt: 0, stopped: false };
1641
2095
  this.#casts.set(command.session, cast);
1642
2096
  this.#castsByTab.set(tabId, cast);
1643
2097
  try {
1644
- await this.#send(tabId, "Page.startScreencast", {
1645
- format: "jpeg",
1646
- // Smaller and cheaper than a screenshot on purpose: this is a picture
1647
- // for a phone on someone else's network, not evidence for a model.
1648
- quality: Math.min(90, Math.max(20, Math.trunc(num(command.args["quality"]) ?? 50))),
1649
- maxWidth: side(command.args["maxWidth"], 960),
1650
- maxHeight: side(command.args["maxHeight"], 600),
1651
- everyNthFrame: Math.min(10, Math.max(1, Math.trunc(num(command.args["everyNthFrame"]) ?? 1)))
1652
- });
2098
+ await this.#send(tabId, "Emulation.setFocusEmulationEnabled", { enabled: true }).catch(() => void 0);
2099
+ await this.#send(tabId, "Page.startScreencast", { format: "jpeg", ...params });
1653
2100
  cancel.check();
1654
2101
  } catch (error) {
1655
2102
  await this.#stopScreencast(command.session);
@@ -1657,12 +2104,27 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1657
2104
  }
1658
2105
  return {};
1659
2106
  }
2107
+ /**
2108
+ * The agent moved to another tab, or opened one: a watching picture moves
2109
+ * with it. The owner's own take-over never moves; that is their tab.
2110
+ */
2111
+ async #followWatch(sessionName) {
2112
+ const cast = this.#casts.get(sessionName);
2113
+ if (!cast || cast.mode !== "watch") return;
2114
+ const session = this.#sessions.get(sessionName);
2115
+ const key = session?.active;
2116
+ const tabId = key ? session?.tabs.get(key) : void 0;
2117
+ if (tabId === void 0 || tabId === cast.tabId) return;
2118
+ const { params, interval } = cast;
2119
+ await this.#startScreencast({ id: "follow", name: "screencast.start", session: sessionName, args: { ...params, interval, watch: true } }, new Cancellation()).catch(() => void 0);
2120
+ }
1660
2121
  /** Idempotent: a stop for a session with no screencast is an answer, not a failure. */
1661
2122
  async #stopScreencast(session) {
1662
2123
  const cast = this.#casts.get(session);
1663
2124
  if (!cast) return;
1664
2125
  this.#forgetCast(cast);
1665
2126
  await this.#send(cast.tabId, "Page.stopScreencast").catch(() => void 0);
2127
+ await this.#send(cast.tabId, "Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => void 0);
1666
2128
  await this.#releaseDebugger(cast.tabId);
1667
2129
  }
1668
2130
  /** Drop every trace of a screencast without touching the debugger, for when Chrome already has. */
@@ -1687,7 +2149,8 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1687
2149
  this.#offer(cast, { data, metadata: metadataOf(message["metadata"]), sessionId });
1688
2150
  }
1689
2151
  /**
1690
- * One painted frame, acked at once and sent at most ten times a second.
2152
+ * One painted frame, acked at once and sent at most once per interval (ten
2153
+ * a second for the owner's hand, two for the Canvas's picture).
1691
2154
  *
1692
2155
  * The ack comes first, always, and before the throttle rather than after it.
1693
2156
  * Chrome paints nothing more until the frame it sent is acknowledged, so a
@@ -1702,7 +2165,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1702
2165
  */
1703
2166
  #offer(cast, frame) {
1704
2167
  this.#ack(cast, frame.sessionId);
1705
- const wait = cast.lastSentAt + MIN_FRAME_MS - this.#now();
2168
+ const wait = cast.lastSentAt + cast.interval - this.#now();
1706
2169
  if (wait <= 0) {
1707
2170
  this.#emit(cast, frame);
1708
2171
  return;
@@ -1718,7 +2181,8 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1718
2181
  }
1719
2182
  #emit(cast, frame) {
1720
2183
  cast.lastSentAt = this.#now();
1721
- this.#onFrame({ type: "frame", session: cast.session, data: frame.data, metadata: frame.metadata, sessionId: frame.sessionId });
2184
+ const url = cast.url && /^https?:/i.test(cast.url) ? cast.url.slice(0, 2048) : void 0;
2185
+ this.#onFrame({ type: "frame", session: cast.session, data: frame.data, metadata: frame.metadata, sessionId: frame.sessionId, ...url ? { url } : {} });
1722
2186
  }
1723
2187
  #ack(cast, sessionId) {
1724
2188
  void this.#send(cast.tabId, "Page.screencastFrameAck", { sessionId }).catch(() => void 0);
@@ -1726,14 +2190,15 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1726
2190
  /**
1727
2191
  * A pointer or a key from the owner's hand.
1728
2192
  *
1729
- * Only while a screencast is running for that session: input with nothing
1730
- * painting it is an agent reaching for coordinates, which this backend does
1731
- * not do. The tab is the screencast's, not the session's active one, so a
1732
- * click cannot land somewhere the owner is not looking at.
2193
+ * Only while the owner's own screencast is running for that session: input
2194
+ * with nothing painting it is an agent reaching for coordinates, which this
2195
+ * backend does not do, and the Canvas's watching picture is not a hand. The
2196
+ * tab is the screencast's, not the session's active one, so a click cannot
2197
+ * land somewhere the owner is not looking at.
1733
2198
  */
1734
2199
  async #input(command, cancel) {
1735
2200
  const cast = this.#casts.get(command.session);
1736
- if (!cast) throw new PreconditionError(NO_CAST);
2201
+ if (!cast || cast.mode !== "hand") throw new PreconditionError(NO_CAST);
1737
2202
  const session = await this.#session(command.session);
1738
2203
  const tab = await this.#chrome.tabs.get(cast.tabId).catch(() => void 0);
1739
2204
  if (!tab) {
@@ -1745,12 +2210,89 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1745
2210
  throw new PreconditionError(TAKEN);
1746
2211
  }
1747
2212
  await this.#background(tab, command);
2213
+ if (str(command.args, "kind") === "nav") {
2214
+ cancel.check();
2215
+ cancel.dispatch();
2216
+ await this.#nav(cast.tabId, command.args);
2217
+ return {};
2218
+ }
1748
2219
  const event = inputEvent(command.args);
1749
2220
  cancel.check();
1750
2221
  cancel.dispatch();
1751
2222
  await this.#send(cast.tabId, event.method, event.params);
1752
2223
  return {};
1753
2224
  }
2225
+ /**
2226
+ * The window's own buttons while the owner holds the page from the
2227
+ * dashboard: back, forward, reload, or an address they typed (http and
2228
+ * https only; buddi checked it against the allowed sites before it came).
2229
+ */
2230
+ async #nav(tabId, args) {
2231
+ const action = str(args, "action");
2232
+ if (action === "reload") {
2233
+ await this.#send(tabId, "Page.reload", {});
2234
+ return;
2235
+ }
2236
+ if (action === "back" || action === "forward") {
2237
+ const history = await this.#send(tabId, "Page.getNavigationHistory");
2238
+ const index = (history?.currentIndex ?? 0) + (action === "back" ? -1 : 1);
2239
+ const entry = history?.entries?.[index];
2240
+ if (!entry) return;
2241
+ await this.#send(tabId, "Page.navigateToHistoryEntry", { entryId: entry.id });
2242
+ return;
2243
+ }
2244
+ if (action === "navigate") {
2245
+ const url = checkUrl(str(args, "url") ?? "");
2246
+ await this.#send(tabId, "Page.navigate", { url });
2247
+ return;
2248
+ }
2249
+ throw new PreconditionError("That is not a button this window has.");
2250
+ }
2251
+ /**
2252
+ * The owner's Cmd/Ctrl+C on the page they hold: the page's selection, or
2253
+ * the selected part of the focused field, read with `Runtime.evaluate` in
2254
+ * the page's own world. A password field is never read; its selection is
2255
+ * nothing. The text goes back to buddi as the answer and is kept by nothing.
2256
+ */
2257
+ async #copy(session) {
2258
+ const cast = this.#casts.get(session);
2259
+ if (!cast || cast.mode !== "hand") throw new PreconditionError(NO_CAST);
2260
+ const answer = await this.#send(cast.tabId, "Runtime.evaluate", { expression: SELECTION_SOURCE, returnByValue: true }).catch(() => void 0);
2261
+ const value = answer?.result?.value;
2262
+ const text2 = typeof value === "string" ? value.slice(0, MAX_COPY) : "";
2263
+ return { observation: { copied: text2 } };
2264
+ }
2265
+ /**
2266
+ * Capture, for the owner's Files: the tab's viewport as a PNG, at its own
2267
+ * size, with every password field painted over first and uncovered after.
2268
+ * Whoever holds the page; not one of the agent's steps, and nothing about it
2269
+ * is evidence.
2270
+ */
2271
+ async #capture(sessionName, cancel) {
2272
+ const session = await this.#session(sessionName);
2273
+ const cast = this.#casts.get(sessionName);
2274
+ const tabId = cast?.tabId ?? (await this.#ownTab(session, this.#activeKey(session))).id;
2275
+ const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
2276
+ if (!tab) throw new PreconditionError("That tab is gone.");
2277
+ this.#liveUrl(tab);
2278
+ cancel.check();
2279
+ await this.#chrome.scripting.executeScript({ target: { tabId, allFrames: true }, func: maskPasswords, args: [true] }).catch(() => void 0);
2280
+ try {
2281
+ const png = await this.#withDebugger(tabId, async (send) => {
2282
+ await send("Emulation.setFocusEmulationEnabled", { enabled: true }).catch(() => void 0);
2283
+ try {
2284
+ const shot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
2285
+ return shot?.data ?? null;
2286
+ } finally {
2287
+ if (!this.#castsByTab.has(tabId)) await send("Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => void 0);
2288
+ }
2289
+ });
2290
+ if (!png) throw new Error("Chrome did not hand over a picture of that tab.");
2291
+ return { screenshot: png, observation: { url: tab.url ?? "", title: tab.title ?? "" } };
2292
+ } finally {
2293
+ await this.#chrome.scripting.executeScript({ target: { tabId, allFrames: true }, func: maskPasswords, args: [false] }).catch(() => void 0);
2294
+ }
2295
+ }
1754
2296
  async #click(command, cancel) {
1755
2297
  const { ref, tab } = await this.#aim(command, cancel);
1756
2298
  const { point } = await this.#locate(ref);
@@ -1760,11 +2302,11 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1760
2302
  await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: clickRef, args: [ref.local] });
1761
2303
  return {};
1762
2304
  }
1763
- await this.#withDebugger(ref.tabId, async (send2) => {
2305
+ await this.#withDebugger(ref.tabId, async (send) => {
1764
2306
  const base = { x: point.x, y: point.y, button: "left", clickCount: 1, buttons: 1 };
1765
- await send2("Input.dispatchMouseEvent", { ...base, type: "mouseMoved", buttons: 0 });
1766
- await send2("Input.dispatchMouseEvent", { ...base, type: "mousePressed" });
1767
- await send2("Input.dispatchMouseEvent", { ...base, type: "mouseReleased" });
2307
+ await send("Input.dispatchMouseEvent", { ...base, type: "mouseMoved", buttons: 0 });
2308
+ await send("Input.dispatchMouseEvent", { ...base, type: "mousePressed" });
2309
+ await send("Input.dispatchMouseEvent", { ...base, type: "mouseReleased" });
1768
2310
  });
1769
2311
  await this.#settle(ref.tabId, tab);
1770
2312
  return {};
@@ -1779,7 +2321,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1779
2321
  await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: typeRef, args: [ref.local, value] });
1780
2322
  return {};
1781
2323
  }
1782
- await this.#withDebugger(ref.tabId, (send2) => send2("Input.insertText", { text: value }));
2324
+ await this.#withDebugger(ref.tabId, (send) => send("Input.insertText", { text: value }));
1783
2325
  return {};
1784
2326
  }
1785
2327
  /* ---- the owner's secret: one read to aim it, one write that delivers it ---- */
@@ -1841,7 +2383,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1841
2383
  await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: typeRef, args: [ref.local, value] });
1842
2384
  return {};
1843
2385
  }
1844
- await this.#withDebugger(ref.tabId, (send2) => send2("Input.insertText", { text: value }));
2386
+ await this.#withDebugger(ref.tabId, (send) => send("Input.insertText", { text: value }));
1845
2387
  return {};
1846
2388
  }
1847
2389
  async #press(command, cancel) {
@@ -1856,10 +2398,10 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1856
2398
  await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: pressRef, args: [ref.local, descriptor.key, descriptor.code, descriptor.keyCode] });
1857
2399
  return {};
1858
2400
  }
1859
- await this.#withDebugger(ref.tabId, async (send2) => {
2401
+ await this.#withDebugger(ref.tabId, async (send) => {
1860
2402
  const common = { key: descriptor.key, code: descriptor.code, windowsVirtualKeyCode: descriptor.keyCode, nativeVirtualKeyCode: descriptor.keyCode };
1861
- await send2("Input.dispatchKeyEvent", { ...common, type: descriptor.text ? "keyDown" : "rawKeyDown", ...descriptor.text ? { text: descriptor.text } : {} });
1862
- await send2("Input.dispatchKeyEvent", { ...common, type: "keyUp" });
2403
+ await send("Input.dispatchKeyEvent", { ...common, type: descriptor.text ? "keyDown" : "rawKeyDown", ...descriptor.text ? { text: descriptor.text } : {} });
2404
+ await send("Input.dispatchKeyEvent", { ...common, type: "keyUp" });
1863
2405
  });
1864
2406
  await this.#settle(ref.tabId, tab);
1865
2407
  return {};
@@ -1873,264 +2415,173 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1873
2415
  func: chooseRef,
1874
2416
  args: [ref.local, value]
1875
2417
  });
1876
- if (!frame?.result?.ok) throw new PreconditionError(frame?.result?.reason ?? "That option is not in the list.");
1877
- return {};
1878
- }
1879
- /**
1880
- * Scroll names no element, so what it checks instead is the observation: the
1881
- * page the model is reading has to be the page still in the tab.
1882
- */
1883
- async #scroll(command, cancel) {
1884
- const session = await this.#session(command.session);
1885
- const observed = session.observed;
1886
- if (!observed || observed.generation !== session.generation) throw new PreconditionError("Stale page observation. Observe again before scrolling.");
1887
- const tab = await this.#ownTab(session, observed.tabKey);
1888
- if (this.#liveUrl(tab) !== observed.url) throw new PreconditionError(MOVED);
1889
- await this.#background(tab, command);
1890
- const tabId = tab.id;
1891
- const direction = str(command.args, "direction") === "up" ? "up" : "down";
1892
- cancel.check();
1893
- await this.#inject(tabId);
1894
- cancel.check();
1895
- cancel.dispatch();
1896
- await this.#chrome.scripting.executeScript({ target: { tabId }, func: scrollPage, args: [direction] });
1897
- return {};
1898
- }
1899
- async #screenshot(sessionId, cancel) {
1900
- const session = await this.#session(sessionId);
1901
- const tab = await this.#ownTab(session, this.#activeKey(session));
1902
- this.#liveUrl(tab);
1903
- const tabId = tab.id;
1904
- cancel.check();
1905
- return this.#withDebugger(tabId, async (send2) => {
1906
- const shot = await send2("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
1907
- return shot?.data ?? null;
1908
- }).catch(() => null);
1909
- }
1910
- };
1911
- function readReadyState() {
1912
- return document.readyState;
1913
- }
1914
- function readObservation() {
1915
- const api = globalThis["__buddiBrowser"];
1916
- return api ? api.observe() : null;
1917
- }
1918
- function locateRef(ref, options) {
1919
- const api = globalThis["__buddiBrowser"];
1920
- return api ? api.locate(ref, options) : { ok: false, reason: "The page was reloaded. Observe again." };
1921
- }
1922
- function chooseRef(ref, value) {
1923
- const api = globalThis["__buddiBrowser"];
1924
- return api ? api.choose(ref, value) : { ok: false, reason: "The page was reloaded. Observe again." };
1925
- }
1926
- function scrollPage(direction) {
1927
- const api = globalThis["__buddiBrowser"];
1928
- api?.scroll(direction);
1929
- }
1930
- function clickRef(ref) {
1931
- const api = globalThis["__buddiBrowser"];
1932
- api?.locate(ref, {});
1933
- const holder = globalThis["__buddiBrowserRegistry"];
1934
- const element = holder?.refs.get(ref)?.deref();
1935
- element?.click();
1936
- }
1937
- function typeRef(ref, value) {
1938
- const holder = globalThis["__buddiBrowserRegistry"];
1939
- const element = holder?.refs.get(ref)?.deref();
1940
- if (!element) return;
1941
- element.value = value;
1942
- element.dispatchEvent(new Event("input", { bubbles: true }));
1943
- element.dispatchEvent(new Event("change", { bubbles: true }));
1944
- }
1945
- function readFieldRef(ref) {
1946
- const api = globalThis["__buddiBrowser"];
1947
- const located = api?.locate(ref, {});
1948
- if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
1949
- return { ok: true, origin: location.origin, password: located.password === true, name: located.name ?? "" };
1950
- }
1951
- function prepareSecretRef(ref, expectedOrigin) {
1952
- const api = globalThis["__buddiBrowser"];
1953
- const located = api?.locate(ref, {});
1954
- if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
1955
- const origin = location.origin;
1956
- if (origin !== expectedOrigin) return { ok: false, reason: "The page moved since the owner approved this fill. Nothing was entered." };
1957
- const holder = globalThis["__buddiBrowserRegistry"];
1958
- const element = holder?.refs.get(ref)?.deref();
1959
- if (!element || !element.isConnected) return { ok: false, reason: "The referenced element changed or disappeared." };
1960
- if ("focus" in element) element.focus();
1961
- const input = element;
1962
- if ("value" in input) {
1963
- input.value = "";
1964
- input.dispatchEvent(new Event("input", { bubbles: true }));
1965
- }
1966
- return { ok: true, origin, password: located.password === true };
1967
- }
1968
- function pressRef(ref, key, code, keyCode) {
1969
- const holder = globalThis["__buddiBrowserRegistry"];
1970
- const element = holder?.refs.get(ref)?.deref();
1971
- if (!element) return;
1972
- for (const type of ["keydown", "keypress", "keyup"]) {
1973
- element.dispatchEvent(new KeyboardEvent(type, { key, code, keyCode, bubbles: true, cancelable: true }));
1974
- }
1975
- }
1976
- function heldLoginFacts(raw) {
1977
- if (!raw || typeof raw !== "object") return void 0;
1978
- const { never, saved } = raw;
1979
- const sites = Array.isArray(never) ? never.filter((site) => typeof site === "string" && site.length <= 253).slice(0, 500) : [];
1980
- const kept = Array.isArray(saved) ? saved.filter((login) => typeof login?.site === "string" && typeof login?.username === "string").slice(0, 500).map(({ site, username }) => ({ site, username })) : [];
1981
- return { never: sites, saved: kept };
1982
- }
1983
-
1984
- // src/downloads.ts
1985
- var DOWNLOAD_GRACE_MS = 15e3;
1986
- var DOWNLOADS_PERMISSION = "downloads";
1987
- function originOf(url) {
1988
- if (!url) return void 0;
1989
- try {
1990
- const parsed = new URL(url);
1991
- if (parsed.protocol === "blob:") return new URL(parsed.pathname).origin;
1992
- return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.origin : void 0;
1993
- } catch {
1994
- return void 0;
1995
- }
1996
- }
1997
- function basename(file) {
1998
- return file.split(/[\\/]/).pop() || "download";
1999
- }
2000
- var AgentDownloads = class {
2001
- #actions = /* @__PURE__ */ new Map();
2002
- #next = 1;
2003
- #claimed = /* @__PURE__ */ new Map();
2004
- #api;
2005
- #send;
2006
- #now;
2007
- constructor(options) {
2008
- this.#send = options.send;
2009
- this.#now = options.now ?? (() => Date.now());
2010
- }
2011
- /** Chrome's downloads API, once the owner granted it. Listening starts here, once. */
2012
- attach(api) {
2013
- if (!api || this.#api) return;
2014
- this.#api = api;
2015
- api.onCreated.addListener((item) => this.created(item));
2016
- api.onChanged.addListener((delta) => {
2017
- void this.changed(delta).catch(() => void 0);
2018
- });
2019
- }
2020
- get attached() {
2021
- return this.#api !== void 0;
2022
- }
2023
- /** An agent command starts in this session, whose tabs are on these addresses. Returns the command's handle for `end`. */
2024
- begin(session, urls) {
2025
- this.#expire();
2026
- const id = this.#next++;
2027
- const origins = /* @__PURE__ */ new Set();
2028
- for (const url of urls) {
2029
- const origin = originOf(url);
2030
- if (origin) origins.add(origin);
2031
- }
2032
- this.#actions.set(id, { session, origins, running: true, until: 0, at: this.#now() });
2033
- return id;
2034
- }
2035
- /** That command ended; its tabs may have moved on to these. Its downloads still count for a few seconds, its own. */
2036
- end(id, urls = []) {
2037
- const action = this.#actions.get(id);
2038
- if (!action) return;
2039
- for (const url of urls) {
2040
- const origin = originOf(url);
2041
- if (origin) action.origins.add(origin);
2042
- }
2043
- action.running = false;
2044
- action.until = this.#now() + DOWNLOAD_GRACE_MS;
2045
- }
2046
- /** Commands whose few seconds are over count for nothing any more. */
2047
- #expire() {
2048
- const now = this.#now();
2049
- for (const [id, action] of this.#actions) if (!action.running && now > action.until) this.#actions.delete(id);
2050
- }
2051
- /** The session ended or the socket went: nothing more is its. */
2052
- forget(session) {
2053
- if (session === void 0) {
2054
- this.#actions.clear();
2055
- this.#claimed.clear();
2056
- return;
2057
- }
2058
- for (const [id, action] of this.#actions) if (action.session === session) this.#actions.delete(id);
2059
- for (const [id, owner] of this.#claimed) if (owner === session) this.#claimed.delete(id);
2060
- }
2061
- /** Which session's command this download belongs to, if any. */
2062
- #owner(item) {
2063
- if (item.byExtensionId) return void 0;
2064
- const origin = originOf(item.referrer) ?? originOf(item.finalUrl ?? item.url);
2065
- if (!origin) return void 0;
2066
- this.#expire();
2067
- let best;
2068
- for (const action of this.#actions.values()) {
2069
- if (!action.origins.has(origin)) continue;
2070
- if (!best || action.at > best.at) best = action;
2071
- }
2072
- return best?.session;
2073
- }
2074
- created(item) {
2075
- const session = this.#owner(item);
2076
- if (session) this.#claimed.set(item.id, session);
2077
- }
2078
- async changed(delta) {
2079
- const session = this.#claimed.get(delta.id);
2080
- const state = delta.state?.current;
2081
- if (!session || !state) return;
2082
- if (state === "interrupted") {
2083
- this.#claimed.delete(delta.id);
2084
- return;
2085
- }
2086
- if (state !== "complete") return;
2087
- this.#claimed.delete(delta.id);
2088
- const [item] = await (this.#api?.search({ id: delta.id }) ?? Promise.resolve([]));
2089
- if (!item?.filename) return;
2090
- const url = item.finalUrl || item.url || "";
2091
- const size = item.fileSize !== void 0 && item.fileSize >= 0 ? item.fileSize : item.totalBytes !== void 0 && item.totalBytes >= 0 ? item.totalBytes : void 0;
2092
- this.#send({
2093
- type: "download",
2094
- session,
2095
- path: item.filename,
2096
- filename: basename(item.filename),
2097
- url,
2098
- ...item.mime ? { mime: item.mime } : {},
2099
- ...size !== void 0 ? { size } : {}
2100
- });
2418
+ if (!frame?.result?.ok) throw new PreconditionError(frame?.result?.reason ?? "That option is not in the list.");
2419
+ return {};
2420
+ }
2421
+ /**
2422
+ * Scroll names no element, so what it checks instead is the observation: the
2423
+ * page the model is reading has to be the page still in the tab.
2424
+ */
2425
+ async #scroll(command, cancel) {
2426
+ const session = await this.#session(command.session);
2427
+ const observed = session.observed;
2428
+ if (!observed || observed.generation !== session.generation) throw new PreconditionError("Stale page observation. Observe again before scrolling.");
2429
+ const tab = await this.#ownTab(session, observed.tabKey);
2430
+ if (this.#liveUrl(tab) !== observed.url) throw new PreconditionError(MOVED);
2431
+ await this.#background(tab, command);
2432
+ const tabId = tab.id;
2433
+ const direction = str(command.args, "direction") === "up" ? "up" : "down";
2434
+ cancel.check();
2435
+ await this.#inject(tabId);
2436
+ cancel.check();
2437
+ cancel.dispatch();
2438
+ await this.#chrome.scripting.executeScript({ target: { tabId }, func: scrollPage, args: [direction] });
2439
+ return {};
2440
+ }
2441
+ async #screenshot(sessionId, cancel) {
2442
+ const session = await this.#session(sessionId);
2443
+ const tab = await this.#ownTab(session, this.#activeKey(session));
2444
+ this.#liveUrl(tab);
2445
+ const tabId = tab.id;
2446
+ cancel.check();
2447
+ return this.#withDebugger(tabId, async (send) => {
2448
+ const shot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
2449
+ return shot?.data ?? null;
2450
+ }).catch(() => null);
2101
2451
  }
2102
2452
  };
2103
-
2104
- // src/external.ts
2105
- function isLoopbackOrigin(origin) {
2106
- if (!origin) return false;
2107
- let parsed;
2108
- try {
2109
- parsed = new URL(origin);
2110
- } catch {
2111
- return false;
2453
+ var SELECTION_SOURCE = `(() => {
2454
+ const active = document.activeElement;
2455
+ if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) {
2456
+ if (active.tagName === 'INPUT' && String(active.type).toLowerCase() === 'password') return '';
2457
+ const start = active.selectionStart, end = active.selectionEnd;
2458
+ if (typeof start === 'number' && typeof end === 'number' && end > start) return String(active.value).slice(start, end).slice(0, ${MAX_COPY});
2459
+ }
2460
+ const selection = window.getSelection();
2461
+ return selection ? String(selection).slice(0, ${MAX_COPY}) : '';
2462
+ })()`;
2463
+ function maskPasswords(on) {
2464
+ const id = "buddi-capture-mask";
2465
+ document.getElementById(id)?.remove();
2466
+ if (!on) return;
2467
+ const style = document.createElement("style");
2468
+ style.id = id;
2469
+ style.textContent = "input[type=password]{background:#c3d0e3 !important;color:transparent !important;-webkit-text-fill-color:transparent !important;text-shadow:none !important;caret-color:transparent !important;}";
2470
+ (document.head ?? document.documentElement).appendChild(style);
2471
+ }
2472
+ function readReadyState() {
2473
+ return document.readyState;
2474
+ }
2475
+ function readObservation() {
2476
+ const api = globalThis["__buddiBrowser"];
2477
+ return api ? api.observe() : null;
2478
+ }
2479
+ function locateRef(ref, options) {
2480
+ const api = globalThis["__buddiBrowser"];
2481
+ return api ? api.locate(ref, options) : { ok: false, reason: "The page was reloaded. Observe again." };
2482
+ }
2483
+ function chooseRef(ref, value) {
2484
+ const api = globalThis["__buddiBrowser"];
2485
+ return api ? api.choose(ref, value) : { ok: false, reason: "The page was reloaded. Observe again." };
2486
+ }
2487
+ function scrollPage(direction) {
2488
+ const api = globalThis["__buddiBrowser"];
2489
+ api?.scroll(direction);
2490
+ }
2491
+ function clickRef(ref) {
2492
+ const api = globalThis["__buddiBrowser"];
2493
+ api?.locate(ref, {});
2494
+ const holder = globalThis["__buddiBrowserRegistry"];
2495
+ const element = holder?.refs.get(ref)?.deref();
2496
+ element?.click();
2497
+ }
2498
+ function typeRef(ref, value) {
2499
+ const holder = globalThis["__buddiBrowserRegistry"];
2500
+ const element = holder?.refs.get(ref)?.deref();
2501
+ if (!element) return;
2502
+ element.value = value;
2503
+ element.dispatchEvent(new Event("input", { bubbles: true }));
2504
+ element.dispatchEvent(new Event("change", { bubbles: true }));
2505
+ }
2506
+ function readFieldRef(ref) {
2507
+ const api = globalThis["__buddiBrowser"];
2508
+ const located = api?.locate(ref, {});
2509
+ if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
2510
+ return { ok: true, origin: location.origin, password: located.password === true, name: located.name ?? "" };
2511
+ }
2512
+ function prepareSecretRef(ref, expectedOrigin) {
2513
+ const api = globalThis["__buddiBrowser"];
2514
+ const located = api?.locate(ref, {});
2515
+ if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
2516
+ const origin = location.origin;
2517
+ if (origin !== expectedOrigin) return { ok: false, reason: "The page moved since the owner approved this fill. Nothing was entered." };
2518
+ const holder = globalThis["__buddiBrowserRegistry"];
2519
+ const element = holder?.refs.get(ref)?.deref();
2520
+ if (!element || !element.isConnected) return { ok: false, reason: "The referenced element changed or disappeared." };
2521
+ if ("focus" in element) element.focus();
2522
+ const input = element;
2523
+ if ("value" in input) {
2524
+ input.value = "";
2525
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2112
2526
  }
2113
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
2114
- return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname);
2527
+ return { ok: true, origin, password: located.password === true };
2115
2528
  }
2116
- function describe(state, gateway2, version) {
2117
- const named = state.connection === "paired" ? "paired" : state.connection === "pairing" ? "pairing" : "disconnected";
2118
- const status = { installed: true, version, state: named, gateway: gateway2 };
2119
- if (named === "pairing" && state.code) status.code = state.code;
2120
- return status;
2529
+ function pressRef(ref, key, code, keyCode) {
2530
+ const holder = globalThis["__buddiBrowserRegistry"];
2531
+ const element = holder?.refs.get(ref)?.deref();
2532
+ if (!element) return;
2533
+ for (const type of ["keydown", "keypress", "keyup"]) {
2534
+ element.dispatchEvent(new KeyboardEvent(type, { key, code, keyCode, bubbles: true, cancelable: true }));
2535
+ }
2121
2536
  }
2122
- function handleExternal(message, context, respond) {
2123
- if (!isLoopbackOrigin(context.origin)) return false;
2124
- const request = message;
2125
- if (!request || request.type !== "buddi.status") return false;
2126
- void context.gateway().then((address) => respond(describe(context.state(), address, context.version)));
2127
- return true;
2537
+ function heldLoginFacts(raw) {
2538
+ if (!raw || typeof raw !== "object") return void 0;
2539
+ const { never, saved } = raw;
2540
+ const sites = Array.isArray(never) ? never.filter((site) => typeof site === "string" && site.length <= 253).slice(0, 500) : [];
2541
+ const kept = Array.isArray(saved) ? saved.filter((login) => typeof login?.site === "string" && typeof login?.username === "string").slice(0, 500).map(({ site, username }) => ({ site, username })) : [];
2542
+ return { never: sites, saved: kept };
2128
2543
  }
2129
2544
 
2130
- // src/background.ts
2131
- var DEFAULT_GATEWAY = "http://127.0.0.1:4317";
2132
- var GATEWAY_KEY = "gateway";
2133
- var MAX_BACKOFF = 3e4;
2545
+ // src/frames.ts
2546
+ var BINARY_FRAMES = "frames.binary";
2547
+ var VERSION = 1;
2548
+ var MAX_HEADER = 8 * 1024;
2549
+ var encoder2 = new TextEncoder();
2550
+ var decoder = new TextDecoder();
2551
+ function base64Bytes(data) {
2552
+ const binary = atob(data);
2553
+ const bytes = new Uint8Array(binary.length);
2554
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2555
+ return bytes;
2556
+ }
2557
+ function packFrame(header, jpeg) {
2558
+ const head = encoder2.encode(JSON.stringify(header));
2559
+ if (head.length > MAX_HEADER) throw new Error("A frame header that long is not a frame header.");
2560
+ const out = new Uint8Array(3 + head.length + jpeg.length);
2561
+ out[0] = VERSION;
2562
+ out[1] = head.length >> 8 & 255;
2563
+ out[2] = head.length & 255;
2564
+ out.set(head, 3);
2565
+ out.set(jpeg, 3 + head.length);
2566
+ return out;
2567
+ }
2568
+
2569
+ // src/link.ts
2570
+ var BACKOFF_MS = [1e3, 2e3, 5e3, 1e4, 2e4, 3e4];
2571
+ var REPAIR_GAP_MS = 3e4;
2572
+ var NO_ANSWER2 = "buddi did not answer at this address. Is it running?";
2573
+ var OTHER_BROWSER = "This buddi is paired with another copy of the extension (the Chrome Web Store one, or another unpacked one). In its Settings \u2192 Browser & apps choose Pair again, then press Try again here.";
2574
+ var TURNED_AWAY = "buddi is running at this address but turned this browser away. Update buddi, then press Try again.";
2575
+ function readClose(code, reason) {
2576
+ const said = reason.trim();
2577
+ if (/paired with another browser/i.test(said)) return { kind: "refused", message: OTHER_BROWSER };
2578
+ if (/token is not this buddi/i.test(said)) return { kind: "repair", message: "This buddi no longer knows this browser. Asking it for a new code." };
2579
+ if (/forgot its browser/i.test(said)) return { kind: "repair", message: "This buddi forgot this browser. Asking it for a new code." };
2580
+ if (code === 1008 && !/too many pairing requests|one hello per connection/i.test(said)) {
2581
+ return { kind: "refused", message: said ? `This buddi turned this browser away: ${said}. Press Try again.` : TURNED_AWAY };
2582
+ }
2583
+ return { kind: "retry", message: said ? `buddi closed the connection (${said}). Trying again shortly.` : "buddi closed the connection. Trying again shortly." };
2584
+ }
2134
2585
  var KNOCK_MS = 3e3;
2135
2586
  var SILENCE = 7e4;
2136
2587
  var KEEPALIVE_MS = 2e4;
@@ -2148,136 +2599,321 @@ function safely(work) {
2148
2599
  console.warn(LOST);
2149
2600
  }
2150
2601
  }
2151
- var socket;
2152
2602
  var DURABLE = /* @__PURE__ */ new Set(["hello", "auth", "result"]);
2153
- var pending = [];
2154
2603
  var frameType = (frame) => frame && typeof frame === "object" ? String(frame.type ?? "") : "";
2155
- var send = (frame) => {
2156
- const live = socket;
2157
- const text2 = JSON.stringify(frame);
2158
- if (live && live.readyState === WebSocket.OPEN) {
2604
+ var Link = class {
2605
+ id;
2606
+ origin;
2607
+ commands;
2608
+ protocol;
2609
+ downloads;
2610
+ #options;
2611
+ #socket;
2612
+ /** Text frames waiting for the socket that is still connecting; discarded if it never opens. */
2613
+ #pending = [];
2614
+ #attempt = 0;
2615
+ #timer;
2616
+ #silence;
2617
+ #keepalive;
2618
+ #last = { connection: "offline", code: null, installation: null, error: null };
2619
+ /** The tail of the frame queue: every frame waits for the one before it. */
2620
+ #incoming = Promise.resolve();
2621
+ /** Switched off or removed: no reconnects, nothing sent. */
2622
+ #disposed = false;
2623
+ /** Turned away by the buddi: no reconnects until the owner presses Try again (or pairs again). */
2624
+ #halted = false;
2625
+ /** Why this side closed the socket, when it did. */
2626
+ #closing;
2627
+ #lastRepair = -Infinity;
2628
+ constructor(options) {
2629
+ this.#options = options;
2630
+ this.id = options.id;
2631
+ this.origin = options.origin;
2632
+ this.downloads = new AgentDownloads({ send: (frame) => this.send(frame) });
2633
+ this.commands = new BrowserCommands(options.chrome, {
2634
+ // Screencast frames are not answers to anything: they arrive while the
2635
+ // agent or the owner is driving and go straight out, outside the
2636
+ // command/result pairing.
2637
+ onFrame: (frame) => this.sendFrame(frame),
2638
+ onEvent: (event) => this.send(event),
2639
+ onLogin: (frame) => this.send(frame),
2640
+ downloads: this.downloads,
2641
+ instance: options.id,
2642
+ groups: options.groups,
2643
+ groupTitle: options.title,
2644
+ groupColour: options.colour
2645
+ });
2646
+ this.protocol = new Protocol({
2647
+ tokens: options.store.tokens(options.id),
2648
+ version: options.version,
2649
+ send: (frame) => this.send(frame),
2650
+ execute: (command, cancel) => this.commands.run(command, cancel),
2651
+ // The socket ended: sessions and refs go with it. The tabs do not.
2652
+ onReset: () => this.commands.reset(),
2653
+ onLoginAck: (id, answer) => this.commands.logins.ack(id, answer),
2654
+ onState: (state) => {
2655
+ if (state.connection === "paired") this.#attempt = 0;
2656
+ const named2 = state.name && state.name !== this.#last.name ? state.name : null;
2657
+ this.#last = state;
2658
+ if (named2) {
2659
+ this.#options.onName?.(this.id, named2);
2660
+ safely(() => this.commands.retitle());
2661
+ }
2662
+ this.#options.onState?.(this.id, state);
2663
+ },
2664
+ disconnect: (reason) => {
2665
+ this.#closing = reason;
2666
+ this.#socket?.close();
2667
+ }
2668
+ });
2669
+ }
2670
+ state() {
2671
+ return { ...this.#last };
2672
+ }
2673
+ attachDownloads(api) {
2674
+ if (api) this.downloads.attach(api);
2675
+ }
2676
+ get #WebSocket() {
2677
+ return this.#options.WebSocket ?? WebSocket;
2678
+ }
2679
+ /**
2680
+ * One way out for everything this link says, so a frame and a result travel
2681
+ * the same socket.
2682
+ *
2683
+ * A socket that is still CONNECTING throws on `send`, and the worker reconnects
2684
+ * often enough — an alarm, a pong, a screencast that outlived the last socket —
2685
+ * that something always arrives early. So this is the only place that decides:
2686
+ * send it, queue it, or let it go.
2687
+ */
2688
+ send(frame) {
2689
+ const live = this.#socket;
2690
+ const text2 = JSON.stringify(frame);
2691
+ const Socket = this.#WebSocket;
2692
+ if (live && live.readyState === Socket.OPEN) {
2693
+ try {
2694
+ live.send(text2);
2695
+ } catch {
2696
+ }
2697
+ return;
2698
+ }
2699
+ if (live && live.readyState === Socket.CONNECTING && DURABLE.has(frameType(frame))) this.#pending.push(text2);
2700
+ }
2701
+ /**
2702
+ * A screencast frame: as bytes to a gateway that said it reads them (the
2703
+ * picture, without base64 inside JSON), as the old JSON frame otherwise.
2704
+ * Never queued: a frame that missed its socket is stale.
2705
+ */
2706
+ sendFrame(frame) {
2707
+ const live = this.#socket;
2708
+ if (!live || live.readyState !== this.#WebSocket.OPEN) return;
2709
+ if (!(this.#last.features ?? []).includes(BINARY_FRAMES)) {
2710
+ this.send(frame);
2711
+ return;
2712
+ }
2159
2713
  try {
2160
- live.send(text2);
2714
+ const bytes = packFrame({ session: frame.session, ...frame.metadata, ...frame.url ? { url: frame.url } : {} }, base64Bytes(frame.data));
2715
+ live.send(bytes);
2161
2716
  } catch {
2162
2717
  }
2163
- return;
2164
2718
  }
2165
- if (live && live.readyState === WebSocket.CONNECTING && DURABLE.has(frameType(frame))) pending.push(text2);
2166
- };
2167
- function flush(open) {
2168
- const queued = pending;
2169
- pending = [];
2170
- for (const text2 of queued) {
2171
- if (open.readyState !== WebSocket.OPEN) return;
2719
+ /** Everything that waited for this socket, in the order it was said. */
2720
+ #flush(open) {
2721
+ const queued = this.#pending;
2722
+ this.#pending = [];
2723
+ for (const text2 of queued) {
2724
+ if (open.readyState !== this.#WebSocket.OPEN) return;
2725
+ try {
2726
+ open.send(text2);
2727
+ } catch {
2728
+ return;
2729
+ }
2730
+ }
2731
+ }
2732
+ #quiet() {
2733
+ if (this.#silence) clearTimeout(this.#silence);
2734
+ this.#silence = setTimeout(() => this.#socket?.close(), SILENCE);
2735
+ }
2736
+ /** Connect now, unless a socket is already open or opening. */
2737
+ async connect() {
2738
+ if (this.#disposed || this.#halted) return;
2739
+ const Socket = this.#WebSocket;
2740
+ const socket = this.#socket;
2741
+ if (socket && (socket.readyState === Socket.OPEN || socket.readyState === Socket.CONNECTING)) return;
2742
+ if (this.#timer) {
2743
+ clearTimeout(this.#timer);
2744
+ this.#timer = void 0;
2745
+ }
2746
+ this.#socket = void 0;
2747
+ this.#pending = [];
2748
+ this.commands.reset();
2749
+ let url;
2172
2750
  try {
2173
- open.send(text2);
2174
- } catch {
2751
+ url = socketUrl(this.origin);
2752
+ } catch (error) {
2753
+ this.protocol.closed(error instanceof Error ? error.message : String(error));
2754
+ return;
2755
+ }
2756
+ if (this.#attempt > 0 && !await answering(url)) {
2757
+ if (this.#disposed) return;
2758
+ this.protocol.closed(NO_ANSWER2);
2759
+ this.#schedule();
2175
2760
  return;
2176
2761
  }
2762
+ if (this.#disposed) return;
2763
+ let opened = false;
2764
+ this.#closing = void 0;
2765
+ const opening = new Socket(url);
2766
+ opening.binaryType = "arraybuffer";
2767
+ this.#socket = opening;
2768
+ opening.addEventListener("open", () => {
2769
+ opened = true;
2770
+ this.#quiet();
2771
+ this.#flush(opening);
2772
+ if (this.#keepalive) clearInterval(this.#keepalive);
2773
+ this.#keepalive = setInterval(() => {
2774
+ if (this.#socket === opening) this.send({ type: "keepalive" });
2775
+ }, KEEPALIVE_MS);
2776
+ safely(() => this.protocol.open());
2777
+ });
2778
+ opening.addEventListener("message", (event) => {
2779
+ this.#quiet();
2780
+ const text2 = String(event.data);
2781
+ this.#incoming = this.#incoming.then(() => this.protocol.receive(text2)).catch(() => {
2782
+ console.warn(LOST);
2783
+ });
2784
+ });
2785
+ opening.addEventListener("error", () => {
2786
+ if (this.#socket === opening) this.#pending = [];
2787
+ });
2788
+ opening.addEventListener("close", (event) => {
2789
+ if (this.#socket !== opening) return;
2790
+ this.#socket = void 0;
2791
+ this.#pending = [];
2792
+ if (this.#keepalive) {
2793
+ clearInterval(this.#keepalive);
2794
+ this.#keepalive = void 0;
2795
+ }
2796
+ if (this.#silence) {
2797
+ clearTimeout(this.#silence);
2798
+ this.#silence = void 0;
2799
+ }
2800
+ if (this.#disposed) return;
2801
+ const closing = this.#closing;
2802
+ this.#closing = void 0;
2803
+ const { code = 1005, reason = "" } = event ?? {};
2804
+ safely(() => this.#closed(opened, closing, code, reason));
2805
+ });
2177
2806
  }
2178
- }
2179
- var downloads = new AgentDownloads({ send });
2180
- var attachDownloads = () => {
2181
- if (chrome.downloads) downloads.attach(chrome.downloads);
2182
- };
2183
- attachDownloads();
2184
- chrome.permissions?.onAdded?.addListener((added) => {
2185
- if (added.permissions?.includes(DOWNLOADS_PERMISSION)) attachDownloads();
2186
- });
2187
- var commands = new BrowserCommands(chrome, { onFrame: send, onEvent: send, onLogin: send, downloads });
2188
- var attempt = 0;
2189
- var timer;
2190
- var silence;
2191
- var keepalive;
2192
- var last = { connection: "offline", code: null, installation: null, error: null };
2193
- var incoming = Promise.resolve();
2194
- var protocol = new Protocol({
2195
- chrome,
2196
- version: chrome.runtime.getManifest().version,
2197
- send,
2198
- execute: (command, cancel) => commands.run(command, cancel),
2199
- // The socket ended: sessions and refs go with it. The tabs do not.
2200
- onReset: () => commands.reset(),
2201
- onLoginAck: (id, answer) => commands.logins.ack(id, answer),
2202
- onState: (state) => {
2203
- last = state;
2204
- safely(() => chrome.runtime.sendMessage({ type: "buddi-state", state }).catch(() => void 0));
2205
- },
2206
- disconnect: () => socket?.close()
2207
- });
2208
- async function gateway() {
2209
- const stored = await chrome.storage.local.get([GATEWAY_KEY]);
2210
- const address = stored[GATEWAY_KEY];
2211
- return typeof address === "string" && address ? address : DEFAULT_GATEWAY;
2212
- }
2213
- function quiet() {
2214
- if (silence) clearTimeout(silence);
2215
- silence = setTimeout(() => socket?.close(), SILENCE);
2216
- }
2217
- async function connect() {
2218
- if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
2219
- if (timer) {
2220
- clearTimeout(timer);
2221
- timer = void 0;
2222
- }
2223
- socket = void 0;
2224
- pending = [];
2225
- commands.reset();
2226
- let url;
2227
- try {
2228
- url = socketUrl(await gateway());
2229
- } catch (error) {
2230
- protocol.closed(error instanceof Error ? error.message : String(error));
2231
- return;
2807
+ /**
2808
+ * What a closed socket means, said once, and what happens next. A socket
2809
+ * that never opened is a buddi that did not answer, unless something on
2810
+ * that port did answer the knock and turned the socket away. One that
2811
+ * opened was answered: the buddi's own close reason decides.
2812
+ */
2813
+ async #closed(opened, closing, code, reason) {
2814
+ if (this.#socket) return;
2815
+ if (closing === PAIR_AGAIN) {
2816
+ await this.#repair("That buddi does not know this browser any more. Asking it for a new code.");
2817
+ return;
2818
+ }
2819
+ if (!opened) {
2820
+ const knocked = await answering(socketUrl(this.origin)).catch(() => false);
2821
+ if (this.#disposed || this.#socket) return;
2822
+ if (knocked) {
2823
+ this.#halt(TURNED_AWAY);
2824
+ return;
2825
+ }
2826
+ this.protocol.closed(NO_ANSWER2);
2827
+ this.#schedule();
2828
+ return;
2829
+ }
2830
+ const read = readClose(code, reason);
2831
+ if (read.kind === "refused") {
2832
+ this.#halt(read.message);
2833
+ return;
2834
+ }
2835
+ if (read.kind === "repair") {
2836
+ await this.protocol.dropToken().catch(() => void 0);
2837
+ await this.#repair(read.message);
2838
+ return;
2839
+ }
2840
+ this.protocol.closed(read.message);
2841
+ this.#schedule();
2842
+ }
2843
+ /** Stop knocking and say why, until the owner presses Try again. */
2844
+ #halt(message) {
2845
+ this.#halted = true;
2846
+ if (this.#timer) {
2847
+ clearTimeout(this.#timer);
2848
+ this.#timer = void 0;
2849
+ }
2850
+ this.protocol.closed(message, { refused: true });
2232
2851
  }
2233
- if (attempt > 0 && !await answering(url)) {
2234
- protocol.closed("buddi did not answer at this address. Is it running?");
2235
- schedule();
2236
- return;
2852
+ /** Start over at once with no token, which asks the buddi for a code; at most once in a while. */
2853
+ async #repair(message) {
2854
+ const now = Date.now();
2855
+ this.protocol.closed(message);
2856
+ if (now - this.#lastRepair < REPAIR_GAP_MS) {
2857
+ this.#schedule();
2858
+ return;
2859
+ }
2860
+ this.#lastRepair = now;
2861
+ this.#attempt = 0;
2862
+ await this.connect();
2237
2863
  }
2238
- const opening = new WebSocket(url);
2239
- socket = opening;
2240
- opening.addEventListener("open", () => {
2241
- attempt = 0;
2242
- quiet();
2243
- flush(opening);
2244
- if (keepalive) clearInterval(keepalive);
2245
- keepalive = setInterval(() => {
2246
- if (socket === opening) send({ type: "keepalive" });
2247
- }, KEEPALIVE_MS);
2248
- safely(() => protocol.open());
2249
- });
2250
- opening.addEventListener("message", (event) => {
2251
- quiet();
2252
- const text2 = String(event.data);
2253
- incoming = incoming.then(() => protocol.receive(text2)).catch(() => {
2254
- console.warn(LOST);
2255
- });
2256
- });
2257
- opening.addEventListener("error", () => {
2258
- if (socket === opening) pending = [];
2259
- });
2260
- opening.addEventListener("close", () => {
2261
- if (socket === opening) {
2262
- socket = void 0;
2263
- pending = [];
2264
- if (keepalive) {
2265
- clearInterval(keepalive);
2266
- keepalive = void 0;
2267
- }
2864
+ /** The owner pressed Connect or Try again: start over at once. */
2865
+ async retry() {
2866
+ this.#halted = false;
2867
+ this.#attempt = 0;
2868
+ this.#socket?.close();
2869
+ await this.connect();
2870
+ }
2871
+ /** Drop the token and pair again from a fresh code. */
2872
+ forget() {
2873
+ return this.protocol.forget();
2874
+ }
2875
+ /**
2876
+ * Switched off or removed in the popup: the socket closes and stays closed,
2877
+ * the sessions and their refs go. The tabs stay; they are the owner's.
2878
+ */
2879
+ dispose() {
2880
+ this.#disposed = true;
2881
+ if (this.#timer) {
2882
+ clearTimeout(this.#timer);
2883
+ this.#timer = void 0;
2268
2884
  }
2269
- if (silence) {
2270
- clearTimeout(silence);
2271
- silence = void 0;
2885
+ if (this.#keepalive) {
2886
+ clearInterval(this.#keepalive);
2887
+ this.#keepalive = void 0;
2272
2888
  }
2273
- protocol.closed("buddi did not answer at this address. Is it running?");
2274
- schedule();
2275
- });
2276
- }
2277
- async function answering(socket2) {
2889
+ if (this.#silence) {
2890
+ clearTimeout(this.#silence);
2891
+ this.#silence = void 0;
2892
+ }
2893
+ const socket = this.#socket;
2894
+ this.#socket = void 0;
2895
+ this.#pending = [];
2896
+ try {
2897
+ socket?.close();
2898
+ } catch {
2899
+ }
2900
+ this.protocol.closed();
2901
+ this.commands.reset();
2902
+ }
2903
+ #schedule() {
2904
+ if (this.#timer || this.#disposed || this.#halted) return;
2905
+ const wait = BACKOFF_MS[Math.min(this.#attempt, BACKOFF_MS.length - 1)];
2906
+ this.#attempt += 1;
2907
+ this.#timer = setTimeout(() => {
2908
+ this.#timer = void 0;
2909
+ safely(() => this.connect());
2910
+ }, wait);
2911
+ }
2912
+ };
2913
+ async function answering(socket) {
2278
2914
  let http;
2279
2915
  try {
2280
- http = new URL(socket2);
2916
+ http = new URL(socket);
2281
2917
  http.protocol = http.protocol === "wss:" ? "https:" : "http:";
2282
2918
  http.pathname = "/api/version";
2283
2919
  http.search = "";
@@ -2291,25 +2927,109 @@ async function answering(socket2) {
2291
2927
  return false;
2292
2928
  }
2293
2929
  }
2294
- function schedule() {
2295
- if (timer) return;
2296
- const wait = Math.min(MAX_BACKOFF, 1e3 * 2 ** Math.min(attempt, 5));
2297
- attempt += 1;
2298
- timer = setTimeout(() => {
2299
- timer = void 0;
2300
- safely(() => connect());
2301
- }, wait);
2930
+
2931
+ // src/background.ts
2932
+ var store = new PairingStore(chrome.storage.local);
2933
+ var groups = new GroupRegistry();
2934
+ var links = /* @__PURE__ */ new Map();
2935
+ var entries = [];
2936
+ var asked = null;
2937
+ var version = chrome.runtime.getManifest().version;
2938
+ function entryOf(id) {
2939
+ return entries.find((entry) => entry.id === id);
2940
+ }
2941
+ function pushState() {
2942
+ safely(() => chrome.runtime.sendMessage({ type: "buddi-state" }).catch(() => void 0));
2943
+ }
2944
+ function linkFor(entry) {
2945
+ const link = new Link({
2946
+ chrome,
2947
+ id: entry.id,
2948
+ origin: entry.origin,
2949
+ store,
2950
+ groups,
2951
+ version,
2952
+ title: () => {
2953
+ const current = entryOf(entry.id);
2954
+ return current ? displayName(current, entries) : "buddi";
2955
+ },
2956
+ colour: () => entryOf(entry.id)?.colour,
2957
+ onState: () => pushState(),
2958
+ onName: (id, name) => safely(async () => {
2959
+ await store.update(id, { name });
2960
+ entries = await store.list();
2961
+ pushState();
2962
+ })
2963
+ });
2964
+ if (chrome.downloads) link.attachDownloads(chrome.downloads);
2965
+ return link;
2966
+ }
2967
+ async function sync() {
2968
+ entries = await store.list();
2969
+ for (const [id, link] of links) {
2970
+ const entry = entryOf(id);
2971
+ if (!entry || !entry.enabled || !sameBuddi(entry.origin, link.origin)) {
2972
+ link.dispose();
2973
+ links.delete(id);
2974
+ if (!entry) groups.release(id);
2975
+ }
2976
+ }
2977
+ for (const entry of entries) {
2978
+ if (!entry.enabled) continue;
2979
+ let link = links.get(entry.id);
2980
+ if (!link) {
2981
+ link = linkFor(entry);
2982
+ links.set(entry.id, link);
2983
+ }
2984
+ await link.connect();
2985
+ }
2986
+ }
2987
+ async function views() {
2988
+ entries = await store.list();
2989
+ const out = [];
2990
+ for (const entry of entries) {
2991
+ const link = links.get(entry.id);
2992
+ const state = link?.state() ?? { connection: "offline", code: null, installation: null, error: null };
2993
+ out.push({
2994
+ id: entry.id,
2995
+ origin: entry.origin,
2996
+ name: displayName(entry, entries),
2997
+ colour: entry.colour,
2998
+ enabled: entry.enabled,
2999
+ state,
3000
+ tabs: link && state.connection === "paired" ? await link.commands.tabCount().catch(() => null) : null
3001
+ });
3002
+ }
3003
+ return out;
3004
+ }
3005
+ function firstLink() {
3006
+ return links.values().next().value;
3007
+ }
3008
+ chrome.permissions?.onAdded?.addListener((added) => {
3009
+ if (added.permissions?.includes(DOWNLOADS_PERMISSION) && chrome.downloads) for (const link of links.values()) link.attachDownloads(chrome.downloads);
3010
+ });
3011
+ function holders(session) {
3012
+ const holding = [...links.values()].filter((link) => link.commands.heldTab(session) !== void 0);
3013
+ return holding.length > 0 ? holding : [...links.values()];
2302
3014
  }
2303
3015
  chrome.runtime.onMessage.addListener((message, sender, respond) => {
2304
3016
  const request = message;
2305
3017
  if (!request || typeof request.type !== "string") return;
2306
3018
  if (request.type === GIVE_BACK_MESSAGE) {
2307
3019
  const tabId = sender?.tab?.id;
2308
- if (typeof request.session === "string") safely(() => commands.giveBack(request.session, tabId).then(() => void 0));
3020
+ const session = request.session;
3021
+ if (typeof session === "string") safely(async () => {
3022
+ for (const link of holders(session)) await link.commands.giveBack(session, tabId);
3023
+ });
2309
3024
  return;
2310
3025
  }
2311
3026
  if (request.type === LOGIN_MESSAGE || request.type === LOGIN_SEEN_MESSAGE || request.type === LOGIN_PENDING_MESSAGE) {
2312
- const reply = commands.loginMessage(message, sender);
3027
+ const from = sender;
3028
+ let reply;
3029
+ for (const link of links.values()) {
3030
+ reply = link.commands.loginMessage(message, from);
3031
+ if (reply) break;
3032
+ }
2313
3033
  if (!reply) {
2314
3034
  respond(null);
2315
3035
  return;
@@ -2318,50 +3038,87 @@ chrome.runtime.onMessage.addListener((message, sender, respond) => {
2318
3038
  return true;
2319
3039
  }
2320
3040
  if (request.type === "buddi-get-state") {
2321
- respond({ state: last });
2322
- return;
3041
+ safely(async () => respond({ pairings: await views(), asked }));
3042
+ return true;
2323
3043
  }
2324
- if (request.type === "buddi-connect") {
3044
+ if (request.type === "buddi-add" || request.type === "buddi-connect") {
2325
3045
  safely(async () => {
2326
- if (request.gateway) {
2327
- try {
2328
- socketUrl(request.gateway);
2329
- } catch (error) {
2330
- respond({ error: error instanceof Error ? error.message : String(error) });
2331
- return;
2332
- }
2333
- await chrome.storage.local.set({ [GATEWAY_KEY]: request.gateway });
3046
+ let entry;
3047
+ const before = new Set(links.keys());
3048
+ try {
3049
+ entry = await store.add(request.gateway?.trim() || DEFAULT_GATEWAY);
3050
+ } catch (error) {
3051
+ respond({ error: error instanceof Error ? error.message : String(error) });
3052
+ return;
2334
3053
  }
2335
- attempt = 0;
2336
- socket?.close();
2337
- await connect();
2338
- respond({ state: last });
3054
+ if (asked && sameBuddi(asked, entry.origin)) asked = null;
3055
+ await sync();
3056
+ if (before.has(entry.id)) await links.get(entry.id)?.retry();
3057
+ respond({ pairings: await views(), added: entry.id });
3058
+ });
3059
+ return true;
3060
+ }
3061
+ if (request.type === "buddi-enable" && typeof request.id === "string") {
3062
+ safely(async () => {
3063
+ await store.update(request.id, { enabled: request.enabled !== false });
3064
+ await sync();
3065
+ respond({ pairings: await views() });
3066
+ });
3067
+ return true;
3068
+ }
3069
+ if (request.type === "buddi-retry" && typeof request.id === "string") {
3070
+ safely(async () => {
3071
+ await links.get(request.id)?.retry();
3072
+ respond({ pairings: await views() });
2339
3073
  });
2340
3074
  return true;
2341
3075
  }
2342
3076
  if (request.type === "buddi-forget") {
2343
- safely(() => protocol.forget().then(() => respond({ state: last })));
3077
+ safely(async () => {
3078
+ const link = typeof request.id === "string" ? links.get(request.id) : firstLink();
3079
+ await link?.forget();
3080
+ respond({ pairings: await views() });
3081
+ });
2344
3082
  return true;
2345
3083
  }
2346
- if (request.type === "buddi-gateway") {
2347
- safely(() => gateway().then((address) => respond({ gateway: address })));
3084
+ if (request.type === "buddi-remove" && typeof request.id === "string") {
3085
+ safely(async () => {
3086
+ await store.remove(request.id);
3087
+ await sync();
3088
+ respond({ pairings: await views() });
3089
+ });
2348
3090
  return true;
2349
3091
  }
2350
3092
  return;
2351
3093
  });
2352
- chrome.runtime.onMessageExternal.addListener((message, sender, respond) => handleExternal(message, { origin: sender.origin, state: () => last, gateway, version: chrome.runtime.getManifest().version }, respond));
2353
- chrome.runtime.onInstalled.addListener(() => safely(() => connect()));
2354
- chrome.runtime.onStartup.addListener(() => safely(() => connect()));
3094
+ chrome.runtime.onMessageExternal.addListener((message, sender, respond) => handleExternal(message, {
3095
+ origin: sender.origin,
3096
+ version,
3097
+ known: async () => {
3098
+ entries = await store.list().catch(() => entries);
3099
+ return entries.map((entry) => ({
3100
+ origin: entry.origin,
3101
+ name: displayName(entry, entries),
3102
+ enabled: entry.enabled,
3103
+ state: links.get(entry.id)?.state() ?? { connection: "offline", code: null, installation: null, error: null }
3104
+ }));
3105
+ },
3106
+ unknown: (origin) => {
3107
+ asked = origin;
3108
+ }
3109
+ }, respond));
3110
+ chrome.runtime.onInstalled.addListener(() => safely(() => sync()));
3111
+ chrome.runtime.onStartup.addListener(() => safely(() => sync()));
2355
3112
  var RECONNECT_ALARM = "buddi-reconnect";
2356
3113
  chrome.alarms.create(RECONNECT_ALARM, { periodInMinutes: 1 });
2357
3114
  chrome.alarms.onAlarm.addListener((alarm) => {
2358
- if (alarm.name === RECONNECT_ALARM) safely(() => connect());
3115
+ if (alarm.name === RECONNECT_ALARM) safely(() => sync());
2359
3116
  });
2360
- safely(() => connect());
3117
+ safely(() => sync());
2361
3118
  export {
2362
3119
  DEFAULT_GATEWAY,
2363
3120
  KEEPALIVE_MS,
2364
- connect,
2365
- send,
2366
- socketUrl
3121
+ socketUrl,
3122
+ sync,
3123
+ views
2367
3124
  };