@poolzin/pool-bot 2026.2.0 → 2026.2.2

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { ensurePageState, getPageForTargetId, refLocator, restoreRoleRefsForTarget, } from "./pw-session.js";
1
+ import { ensurePageState, forceDisconnectPlaywrightForTarget, getPageForTargetId, refLocator, restoreRoleRefsForTarget, } from "./pw-session.js";
2
2
  import { normalizeTimeoutMs, requireRef, toAIFriendlyError } from "./pw-tools-core.shared.js";
3
3
  export async function highlightViaPlaywright(opts) {
4
4
  const page = await getPageForTargetId(opts);
@@ -160,40 +160,138 @@ export async function fillFormViaPlaywright(opts) {
160
160
  }
161
161
  export async function evaluateViaPlaywright(opts) {
162
162
  const fnText = String(opts.fn ?? "").trim();
163
- if (!fnText)
163
+ if (!fnText) {
164
164
  throw new Error("function is required");
165
+ }
165
166
  const page = await getPageForTargetId(opts);
166
167
  ensurePageState(page);
167
168
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
168
- if (opts.ref) {
169
- const locator = refLocator(page, opts.ref);
170
- // Use Function constructor at runtime to avoid esbuild adding __name helper
171
- // which doesn't exist in the browser context
169
+ // Clamp evaluate timeout to prevent permanently blocking Playwright's command queue.
170
+ // Without this, a long-running async evaluate blocks all subsequent page operations
171
+ // because Playwright serializes CDP commands per page.
172
+ //
173
+ // NOTE: Playwright's { timeout } on evaluate only applies to installing the function,
174
+ // NOT to its execution time. We must inject a Promise.race timeout into the browser
175
+ // context itself so async functions are bounded.
176
+ const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 20_000);
177
+ // Leave headroom for routing/serialization overhead so the outer request timeout
178
+ // doesn't fire first and strand a long-running evaluate.
179
+ let evaluateTimeout = Math.max(1000, Math.min(120_000, outerTimeout - 500));
180
+ evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
181
+ const signal = opts.signal;
182
+ let abortListener;
183
+ let abortReject;
184
+ let abortPromise;
185
+ if (signal) {
186
+ abortPromise = new Promise((_, reject) => {
187
+ abortReject = reject;
188
+ });
189
+ // Ensure the abort promise never becomes an unhandled rejection if we throw early.
190
+ void abortPromise.catch(() => { });
191
+ }
192
+ if (signal) {
193
+ const disconnect = () => {
194
+ void forceDisconnectPlaywrightForTarget({
195
+ cdpUrl: opts.cdpUrl,
196
+ targetId: opts.targetId,
197
+ reason: "evaluate aborted",
198
+ }).catch(() => { });
199
+ };
200
+ if (signal.aborted) {
201
+ disconnect();
202
+ throw signal.reason ?? new Error("aborted");
203
+ }
204
+ abortListener = () => {
205
+ disconnect();
206
+ abortReject?.(signal.reason ?? new Error("aborted"));
207
+ };
208
+ signal.addEventListener("abort", abortListener, { once: true });
209
+ // If the signal aborted between the initial check and listener registration, handle it.
210
+ if (signal.aborted) {
211
+ abortListener();
212
+ throw signal.reason ?? new Error("aborted");
213
+ }
214
+ }
215
+ try {
216
+ if (opts.ref) {
217
+ const locator = refLocator(page, opts.ref);
218
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval -- required for browser-context eval
219
+ const elementEvaluator = new Function("el", "args", `
220
+ "use strict";
221
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
222
+ try {
223
+ var candidate = eval("(" + fnBody + ")");
224
+ var result = typeof candidate === "function" ? candidate(el) : candidate;
225
+ if (result && typeof result.then === "function") {
226
+ return Promise.race([
227
+ result,
228
+ new Promise(function(_, reject) {
229
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
230
+ })
231
+ ]);
232
+ }
233
+ return result;
234
+ } catch (err) {
235
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
236
+ }
237
+ `);
238
+ const evalPromise = locator.evaluate(elementEvaluator, {
239
+ fnBody: fnText,
240
+ timeoutMs: evaluateTimeout,
241
+ });
242
+ if (!abortPromise) {
243
+ return await evalPromise;
244
+ }
245
+ try {
246
+ return await Promise.race([evalPromise, abortPromise]);
247
+ }
248
+ catch (err) {
249
+ // If abort wins the race, the underlying evaluate may reject later; ensure we don't
250
+ // surface it as an unhandled rejection.
251
+ void evalPromise.catch(() => { });
252
+ throw err;
253
+ }
254
+ }
172
255
  // eslint-disable-next-line @typescript-eslint/no-implied-eval -- required for browser-context eval
173
- const elementEvaluator = new Function("el", "fnBody", `
174
- "use strict";
175
- try {
176
- var candidate = eval("(" + fnBody + ")");
177
- return typeof candidate === "function" ? candidate(el) : candidate;
178
- } catch (err) {
179
- throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
180
- }
256
+ const browserEvaluator = new Function("args", `
257
+ "use strict";
258
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
259
+ try {
260
+ var candidate = eval("(" + fnBody + ")");
261
+ var result = typeof candidate === "function" ? candidate() : candidate;
262
+ if (result && typeof result.then === "function") {
263
+ return Promise.race([
264
+ result,
265
+ new Promise(function(_, reject) {
266
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
267
+ })
268
+ ]);
269
+ }
270
+ return result;
271
+ } catch (err) {
272
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
273
+ }
181
274
  `);
182
- return await locator.evaluate(elementEvaluator, fnText);
275
+ const evalPromise = page.evaluate(browserEvaluator, {
276
+ fnBody: fnText,
277
+ timeoutMs: evaluateTimeout,
278
+ });
279
+ if (!abortPromise) {
280
+ return await evalPromise;
281
+ }
282
+ try {
283
+ return await Promise.race([evalPromise, abortPromise]);
284
+ }
285
+ catch (err) {
286
+ void evalPromise.catch(() => { });
287
+ throw err;
288
+ }
183
289
  }
184
- // Use Function constructor at runtime to avoid esbuild adding __name helper
185
- // which doesn't exist in the browser context
186
- // eslint-disable-next-line @typescript-eslint/no-implied-eval -- required for browser-context eval
187
- const browserEvaluator = new Function("fnBody", `
188
- "use strict";
189
- try {
190
- var candidate = eval("(" + fnBody + ")");
191
- return typeof candidate === "function" ? candidate() : candidate;
192
- } catch (err) {
193
- throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
290
+ finally {
291
+ if (signal && abortListener) {
292
+ signal.removeEventListener("abort", abortListener);
293
+ }
194
294
  }
195
- `);
196
- return await page.evaluate(browserEvaluator, fnText);
197
295
  }
198
296
  export async function scrollIntoViewViaPlaywright(opts) {
199
297
  const page = await getPageForTargetId(opts);
@@ -70,7 +70,7 @@ export async function responseBodyViaPlaywright(opts) {
70
70
  }
71
71
  }
72
72
  catch (err) {
73
- throw new Error(`Failed to read response body for "${url}": ${String(err)}`);
73
+ throw new Error(`Failed to read response body for "${url}": ${String(err)}`, { cause: err });
74
74
  }
75
75
  const trimmed = bodyText.length > maxChars ? bodyText.slice(0, maxChars) : bodyText;
76
76
  return {
@@ -100,7 +100,7 @@ export async function setTimezoneViaPlaywright(opts) {
100
100
  if (msg.includes("Timezone override is already in effect"))
101
101
  return;
102
102
  if (msg.includes("Invalid timezone"))
103
- throw new Error(`Invalid timezone ID: ${timezoneId}`);
103
+ throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
104
104
  throw err;
105
105
  }
106
106
  });
@@ -4,8 +4,9 @@ import { jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty } from "
4
4
  export function registerBrowserAgentActRoutes(app, ctx) {
5
5
  app.post("/act", async (req, res) => {
6
6
  const profileCtx = resolveProfileContext(req, res, ctx);
7
- if (!profileCtx)
7
+ if (!profileCtx) {
8
8
  return;
9
+ }
9
10
  const body = readBody(req);
10
11
  const kindRaw = toStringOrEmpty(body.kind);
11
12
  if (!isActKind(kindRaw)) {
@@ -20,20 +21,23 @@ export function registerBrowserAgentActRoutes(app, ctx) {
20
21
  const tab = await profileCtx.ensureTabAvailable(targetId);
21
22
  const cdpUrl = profileCtx.profile.cdpUrl;
22
23
  const pw = await requirePwAi(res, `act:${kind}`);
23
- if (!pw)
24
+ if (!pw) {
24
25
  return;
26
+ }
25
27
  const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
26
28
  switch (kind) {
27
29
  case "click": {
28
30
  const ref = toStringOrEmpty(body.ref);
29
- if (!ref)
31
+ if (!ref) {
30
32
  return jsonError(res, 400, "ref is required");
33
+ }
31
34
  const doubleClick = toBoolean(body.doubleClick) ?? false;
32
35
  const timeoutMs = toNumber(body.timeoutMs);
33
36
  const buttonRaw = toStringOrEmpty(body.button) || "";
34
37
  const button = buttonRaw ? parseClickButton(buttonRaw) : undefined;
35
- if (buttonRaw && !button)
38
+ if (buttonRaw && !button) {
36
39
  return jsonError(res, 400, "button must be left|right|middle");
40
+ }
37
41
  const modifiersRaw = toStringArray(body.modifiers) ?? [];
38
42
  const parsedModifiers = parseClickModifiers(modifiersRaw);
39
43
  if (parsedModifiers.error) {
@@ -46,21 +50,26 @@ export function registerBrowserAgentActRoutes(app, ctx) {
46
50
  ref,
47
51
  doubleClick,
48
52
  };
49
- if (button)
53
+ if (button) {
50
54
  clickRequest.button = button;
51
- if (modifiers)
55
+ }
56
+ if (modifiers) {
52
57
  clickRequest.modifiers = modifiers;
53
- if (timeoutMs)
58
+ }
59
+ if (timeoutMs) {
54
60
  clickRequest.timeoutMs = timeoutMs;
61
+ }
55
62
  await pw.clickViaPlaywright(clickRequest);
56
63
  return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
57
64
  }
58
65
  case "type": {
59
66
  const ref = toStringOrEmpty(body.ref);
60
- if (!ref)
67
+ if (!ref) {
61
68
  return jsonError(res, 400, "ref is required");
62
- if (typeof body.text !== "string")
69
+ }
70
+ if (typeof body.text !== "string") {
63
71
  return jsonError(res, 400, "text is required");
72
+ }
64
73
  const text = body.text;
65
74
  const submit = toBoolean(body.submit) ?? false;
66
75
  const slowly = toBoolean(body.slowly) ?? false;
@@ -73,15 +82,17 @@ export function registerBrowserAgentActRoutes(app, ctx) {
73
82
  submit,
74
83
  slowly,
75
84
  };
76
- if (timeoutMs)
85
+ if (timeoutMs) {
77
86
  typeRequest.timeoutMs = timeoutMs;
87
+ }
78
88
  await pw.typeViaPlaywright(typeRequest);
79
89
  return res.json({ ok: true, targetId: tab.targetId });
80
90
  }
81
91
  case "press": {
82
92
  const key = toStringOrEmpty(body.key);
83
- if (!key)
93
+ if (!key) {
84
94
  return jsonError(res, 400, "key is required");
95
+ }
85
96
  const delayMs = toNumber(body.delayMs);
86
97
  await pw.pressKeyViaPlaywright({
87
98
  cdpUrl,
@@ -93,8 +104,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
93
104
  }
94
105
  case "hover": {
95
106
  const ref = toStringOrEmpty(body.ref);
96
- if (!ref)
107
+ if (!ref) {
97
108
  return jsonError(res, 400, "ref is required");
109
+ }
98
110
  const timeoutMs = toNumber(body.timeoutMs);
99
111
  await pw.hoverViaPlaywright({
100
112
  cdpUrl,
@@ -106,24 +118,27 @@ export function registerBrowserAgentActRoutes(app, ctx) {
106
118
  }
107
119
  case "scrollIntoView": {
108
120
  const ref = toStringOrEmpty(body.ref);
109
- if (!ref)
121
+ if (!ref) {
110
122
  return jsonError(res, 400, "ref is required");
123
+ }
111
124
  const timeoutMs = toNumber(body.timeoutMs);
112
125
  const scrollRequest = {
113
126
  cdpUrl,
114
127
  targetId: tab.targetId,
115
128
  ref,
116
129
  };
117
- if (timeoutMs)
130
+ if (timeoutMs) {
118
131
  scrollRequest.timeoutMs = timeoutMs;
132
+ }
119
133
  await pw.scrollIntoViewViaPlaywright(scrollRequest);
120
134
  return res.json({ ok: true, targetId: tab.targetId });
121
135
  }
122
136
  case "drag": {
123
137
  const startRef = toStringOrEmpty(body.startRef);
124
138
  const endRef = toStringOrEmpty(body.endRef);
125
- if (!startRef || !endRef)
139
+ if (!startRef || !endRef) {
126
140
  return jsonError(res, 400, "startRef and endRef are required");
141
+ }
127
142
  const timeoutMs = toNumber(body.timeoutMs);
128
143
  await pw.dragViaPlaywright({
129
144
  cdpUrl,
@@ -137,8 +152,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
137
152
  case "select": {
138
153
  const ref = toStringOrEmpty(body.ref);
139
154
  const values = toStringArray(body.values);
140
- if (!ref || !values?.length)
155
+ if (!ref || !values?.length) {
141
156
  return jsonError(res, 400, "ref and values are required");
157
+ }
142
158
  const timeoutMs = toNumber(body.timeoutMs);
143
159
  await pw.selectOptionViaPlaywright({
144
160
  cdpUrl,
@@ -153,13 +169,15 @@ export function registerBrowserAgentActRoutes(app, ctx) {
153
169
  const rawFields = Array.isArray(body.fields) ? body.fields : [];
154
170
  const fields = rawFields
155
171
  .map((field) => {
156
- if (!field || typeof field !== "object")
172
+ if (!field || typeof field !== "object") {
157
173
  return null;
174
+ }
158
175
  const rec = field;
159
176
  const ref = toStringOrEmpty(rec.ref);
160
177
  const type = toStringOrEmpty(rec.type);
161
- if (!ref || !type)
178
+ if (!ref || !type) {
162
179
  return null;
180
+ }
163
181
  const value = typeof rec.value === "string" ||
164
182
  typeof rec.value === "number" ||
165
183
  typeof rec.value === "boolean"
@@ -169,8 +187,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
169
187
  return parsed;
170
188
  })
171
189
  .filter((field) => field !== null);
172
- if (!fields.length)
190
+ if (!fields.length) {
173
191
  return jsonError(res, 400, "fields are required");
192
+ }
174
193
  const timeoutMs = toNumber(body.timeoutMs);
175
194
  await pw.fillFormViaPlaywright({
176
195
  cdpUrl,
@@ -183,8 +202,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
183
202
  case "resize": {
184
203
  const width = toNumber(body.width);
185
204
  const height = toNumber(body.height);
186
- if (!width || !height)
205
+ if (!width || !height) {
187
206
  return jsonError(res, 400, "width and height are required");
207
+ }
188
208
  await pw.resizeViewportViaPlaywright({
189
209
  cdpUrl,
190
210
  targetId: tab.targetId,
@@ -244,15 +264,22 @@ export function registerBrowserAgentActRoutes(app, ctx) {
244
264
  ].join("\n"));
245
265
  }
246
266
  const fn = toStringOrEmpty(body.fn);
247
- if (!fn)
267
+ if (!fn) {
248
268
  return jsonError(res, 400, "fn is required");
269
+ }
249
270
  const ref = toStringOrEmpty(body.ref) || undefined;
250
- const result = await pw.evaluateViaPlaywright({
271
+ const evalTimeoutMs = toNumber(body.timeoutMs);
272
+ const evalRequest = {
251
273
  cdpUrl,
252
274
  targetId: tab.targetId,
253
275
  fn,
254
276
  ref,
255
- });
277
+ signal: req.signal,
278
+ };
279
+ if (evalTimeoutMs !== undefined) {
280
+ evalRequest.timeoutMs = evalTimeoutMs;
281
+ }
282
+ const result = await pw.evaluateViaPlaywright(evalRequest);
256
283
  return res.json({
257
284
  ok: true,
258
285
  targetId: tab.targetId,
@@ -275,8 +302,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
275
302
  });
276
303
  app.post("/hooks/file-chooser", async (req, res) => {
277
304
  const profileCtx = resolveProfileContext(req, res, ctx);
278
- if (!profileCtx)
305
+ if (!profileCtx) {
279
306
  return;
307
+ }
280
308
  const body = readBody(req);
281
309
  const targetId = toStringOrEmpty(body.targetId) || undefined;
282
310
  const ref = toStringOrEmpty(body.ref) || undefined;
@@ -284,13 +312,15 @@ export function registerBrowserAgentActRoutes(app, ctx) {
284
312
  const element = toStringOrEmpty(body.element) || undefined;
285
313
  const paths = toStringArray(body.paths) ?? [];
286
314
  const timeoutMs = toNumber(body.timeoutMs);
287
- if (!paths.length)
315
+ if (!paths.length) {
288
316
  return jsonError(res, 400, "paths are required");
317
+ }
289
318
  try {
290
319
  const tab = await profileCtx.ensureTabAvailable(targetId);
291
320
  const pw = await requirePwAi(res, "file chooser hook");
292
- if (!pw)
321
+ if (!pw) {
293
322
  return;
323
+ }
294
324
  if (inputRef || element) {
295
325
  if (ref) {
296
326
  return jsonError(res, 400, "ref cannot be combined with inputRef/element");
@@ -326,20 +356,23 @@ export function registerBrowserAgentActRoutes(app, ctx) {
326
356
  });
327
357
  app.post("/hooks/dialog", async (req, res) => {
328
358
  const profileCtx = resolveProfileContext(req, res, ctx);
329
- if (!profileCtx)
359
+ if (!profileCtx) {
330
360
  return;
361
+ }
331
362
  const body = readBody(req);
332
363
  const targetId = toStringOrEmpty(body.targetId) || undefined;
333
364
  const accept = toBoolean(body.accept);
334
365
  const promptText = toStringOrEmpty(body.promptText) || undefined;
335
366
  const timeoutMs = toNumber(body.timeoutMs);
336
- if (accept === undefined)
367
+ if (accept === undefined) {
337
368
  return jsonError(res, 400, "accept is required");
369
+ }
338
370
  try {
339
371
  const tab = await profileCtx.ensureTabAvailable(targetId);
340
372
  const pw = await requirePwAi(res, "dialog hook");
341
- if (!pw)
373
+ if (!pw) {
342
374
  return;
375
+ }
343
376
  await pw.armDialogViaPlaywright({
344
377
  cdpUrl: profileCtx.profile.cdpUrl,
345
378
  targetId: tab.targetId,
@@ -355,8 +388,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
355
388
  });
356
389
  app.post("/wait/download", async (req, res) => {
357
390
  const profileCtx = resolveProfileContext(req, res, ctx);
358
- if (!profileCtx)
391
+ if (!profileCtx) {
359
392
  return;
393
+ }
360
394
  const body = readBody(req);
361
395
  const targetId = toStringOrEmpty(body.targetId) || undefined;
362
396
  const out = toStringOrEmpty(body.path) || undefined;
@@ -364,8 +398,9 @@ export function registerBrowserAgentActRoutes(app, ctx) {
364
398
  try {
365
399
  const tab = await profileCtx.ensureTabAvailable(targetId);
366
400
  const pw = await requirePwAi(res, "wait for download");
367
- if (!pw)
401
+ if (!pw) {
368
402
  return;
403
+ }
369
404
  const result = await pw.waitForDownloadViaPlaywright({
370
405
  cdpUrl: profileCtx.profile.cdpUrl,
371
406
  targetId: tab.targetId,
@@ -380,22 +415,26 @@ export function registerBrowserAgentActRoutes(app, ctx) {
380
415
  });
381
416
  app.post("/download", async (req, res) => {
382
417
  const profileCtx = resolveProfileContext(req, res, ctx);
383
- if (!profileCtx)
418
+ if (!profileCtx) {
384
419
  return;
420
+ }
385
421
  const body = readBody(req);
386
422
  const targetId = toStringOrEmpty(body.targetId) || undefined;
387
423
  const ref = toStringOrEmpty(body.ref);
388
424
  const out = toStringOrEmpty(body.path);
389
425
  const timeoutMs = toNumber(body.timeoutMs);
390
- if (!ref)
426
+ if (!ref) {
391
427
  return jsonError(res, 400, "ref is required");
392
- if (!out)
428
+ }
429
+ if (!out) {
393
430
  return jsonError(res, 400, "path is required");
431
+ }
394
432
  try {
395
433
  const tab = await profileCtx.ensureTabAvailable(targetId);
396
434
  const pw = await requirePwAi(res, "download");
397
- if (!pw)
435
+ if (!pw) {
398
436
  return;
437
+ }
399
438
  const result = await pw.downloadViaPlaywright({
400
439
  cdpUrl: profileCtx.profile.cdpUrl,
401
440
  targetId: tab.targetId,
@@ -411,20 +450,23 @@ export function registerBrowserAgentActRoutes(app, ctx) {
411
450
  });
412
451
  app.post("/response/body", async (req, res) => {
413
452
  const profileCtx = resolveProfileContext(req, res, ctx);
414
- if (!profileCtx)
453
+ if (!profileCtx) {
415
454
  return;
455
+ }
416
456
  const body = readBody(req);
417
457
  const targetId = toStringOrEmpty(body.targetId) || undefined;
418
458
  const url = toStringOrEmpty(body.url);
419
459
  const timeoutMs = toNumber(body.timeoutMs);
420
460
  const maxChars = toNumber(body.maxChars);
421
- if (!url)
461
+ if (!url) {
422
462
  return jsonError(res, 400, "url is required");
463
+ }
423
464
  try {
424
465
  const tab = await profileCtx.ensureTabAvailable(targetId);
425
466
  const pw = await requirePwAi(res, "response body");
426
- if (!pw)
467
+ if (!pw) {
427
468
  return;
469
+ }
428
470
  const result = await pw.responseBodyViaPlaywright({
429
471
  cdpUrl: profileCtx.profile.cdpUrl,
430
472
  targetId: tab.targetId,
@@ -440,18 +482,21 @@ export function registerBrowserAgentActRoutes(app, ctx) {
440
482
  });
441
483
  app.post("/highlight", async (req, res) => {
442
484
  const profileCtx = resolveProfileContext(req, res, ctx);
443
- if (!profileCtx)
485
+ if (!profileCtx) {
444
486
  return;
487
+ }
445
488
  const body = readBody(req);
446
489
  const targetId = toStringOrEmpty(body.targetId) || undefined;
447
490
  const ref = toStringOrEmpty(body.ref);
448
- if (!ref)
491
+ if (!ref) {
449
492
  return jsonError(res, 400, "ref is required");
493
+ }
450
494
  try {
451
495
  const tab = await profileCtx.ensureTabAvailable(targetId);
452
496
  const pw = await requirePwAi(res, "highlight");
453
- if (!pw)
497
+ if (!pw) {
454
498
  return;
499
+ }
455
500
  await pw.highlightViaPlaywright({
456
501
  cdpUrl: profileCtx.profile.cdpUrl,
457
502
  targetId: tab.targetId,
@@ -1,7 +1,5 @@
1
1
  import { registerBrowserRoutes } from "./index.js";
2
- function escapeRegex(value) {
3
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
- }
2
+ import { escapeRegExp } from "../../utils.js";
5
3
  function compileRoute(path) {
6
4
  const paramNames = [];
7
5
  const parts = path.split("/").map((part) => {
@@ -10,7 +8,7 @@ function compileRoute(path) {
10
8
  paramNames.push(name);
11
9
  return "([^/]+)";
12
10
  }
13
- return escapeRegex(part);
11
+ return escapeRegExp(part);
14
12
  });
15
13
  return { regex: new RegExp(`^${parts.join("/")}$`), paramNames };
16
14
  }
@@ -41,6 +39,7 @@ export function createBrowserRouteDispatcher(ctx) {
41
39
  const path = normalizePath(req.path);
42
40
  const query = req.query ?? {};
43
41
  const body = req.body;
42
+ const signal = req.signal;
44
43
  const match = registry.routes.find((route) => {
45
44
  if (route.method !== method)
46
45
  return false;
@@ -75,6 +74,7 @@ export function createBrowserRouteDispatcher(ctx) {
75
74
  params,
76
75
  query,
77
76
  body,
77
+ signal,
78
78
  }, res);
79
79
  }
80
80
  catch (err) {
@@ -16,7 +16,7 @@ export async function normalizeBrowserScreenshot(buffer, opts) {
16
16
  const sideGrid = [sideStart, 1800, 1600, 1400, 1200, 1000, 800]
17
17
  .map((v) => Math.min(maxSide, v))
18
18
  .filter((v, i, arr) => v > 0 && arr.indexOf(v) === i)
19
- .sort((a, b) => b - a);
19
+ .toSorted((a, b) => b - a);
20
20
  let smallest = null;
21
21
  for (const side of sideGrid) {
22
22
  for (const quality of qualities) {
@@ -17,6 +17,19 @@ export async function startBrowserControlServerFromConfig() {
17
17
  return null;
18
18
  const app = express();
19
19
  app.use(express.json({ limit: "1mb" }));
20
+ app.use((req, res, next) => {
21
+ const ctrl = new AbortController();
22
+ const abort = () => ctrl.abort(new Error("request aborted"));
23
+ req.once("aborted", abort);
24
+ res.once("close", () => {
25
+ if (!res.writableEnded) {
26
+ abort();
27
+ }
28
+ });
29
+ // Make the signal available to browser route handlers (best-effort).
30
+ req.signal = ctrl.signal;
31
+ next();
32
+ });
20
33
  const ctx = createBrowserRouteContext({
21
34
  getState: () => state,
22
35
  });
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.41",
3
- "commit": "b5ac4e052499a0af9a58c0e535c5f8a21da0c270",
4
- "builtAt": "2026-02-12T00:03:57.467Z"
2
+ "version": "2026.2.2",
3
+ "commit": "84e7160aa161f8c42b9f4a276ea99d44597d7f19",
4
+ "builtAt": "2026-02-12T23:46:09.256Z"
5
5
  }