@poolzin/pool-bot 2026.2.23 → 2026.2.25

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 (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,312 +1,325 @@
1
1
  import { isActKind, parseClickButton, parseClickModifiers, } from "./agent.act.shared.js";
2
- import { handleRouteError, readBody, requirePwAi, resolveProfileContext, SELECTOR_UNSUPPORTED_MESSAGE, } from "./agent.shared.js";
2
+ import { readBody, resolveTargetIdFromBody, withPlaywrightRouteContext, SELECTOR_UNSUPPORTED_MESSAGE, } from "./agent.shared.js";
3
+ import { DEFAULT_DOWNLOAD_DIR, DEFAULT_UPLOAD_DIR, resolvePathWithinRoot, resolvePathsWithinRoot, } from "./path-output.js";
3
4
  import { jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty } from "./utils.js";
5
+ function resolveDownloadPathOrRespond(res, requestedPath) {
6
+ const downloadPathResult = resolvePathWithinRoot({
7
+ rootDir: DEFAULT_DOWNLOAD_DIR,
8
+ requestedPath,
9
+ scopeLabel: "downloads directory",
10
+ });
11
+ if (!downloadPathResult.ok) {
12
+ res.status(400).json({ error: downloadPathResult.error });
13
+ return null;
14
+ }
15
+ return downloadPathResult.path;
16
+ }
17
+ function buildDownloadRequestBase(cdpUrl, targetId, timeoutMs) {
18
+ return {
19
+ cdpUrl,
20
+ targetId,
21
+ timeoutMs: timeoutMs ?? undefined,
22
+ };
23
+ }
24
+ function respondWithDownloadResult(res, targetId, result) {
25
+ res.json({ ok: true, targetId, download: result });
26
+ }
4
27
  export function registerBrowserAgentActRoutes(app, ctx) {
5
28
  app.post("/act", async (req, res) => {
6
- const profileCtx = resolveProfileContext(req, res, ctx);
7
- if (!profileCtx) {
8
- return;
9
- }
10
29
  const body = readBody(req);
11
30
  const kindRaw = toStringOrEmpty(body.kind);
12
31
  if (!isActKind(kindRaw)) {
13
32
  return jsonError(res, 400, "kind is required");
14
33
  }
15
34
  const kind = kindRaw;
16
- const targetId = toStringOrEmpty(body.targetId) || undefined;
35
+ const targetId = resolveTargetIdFromBody(body);
17
36
  if (Object.hasOwn(body, "selector") && kind !== "wait") {
18
37
  return jsonError(res, 400, SELECTOR_UNSUPPORTED_MESSAGE);
19
38
  }
20
- try {
21
- const tab = await profileCtx.ensureTabAvailable(targetId);
22
- const cdpUrl = profileCtx.profile.cdpUrl;
23
- const pw = await requirePwAi(res, `act:${kind}`);
24
- if (!pw) {
25
- return;
26
- }
27
- const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
28
- switch (kind) {
29
- case "click": {
30
- const ref = toStringOrEmpty(body.ref);
31
- if (!ref) {
32
- return jsonError(res, 400, "ref is required");
33
- }
34
- const doubleClick = toBoolean(body.doubleClick) ?? false;
35
- const timeoutMs = toNumber(body.timeoutMs);
36
- const buttonRaw = toStringOrEmpty(body.button) || "";
37
- const button = buttonRaw ? parseClickButton(buttonRaw) : undefined;
38
- if (buttonRaw && !button) {
39
- return jsonError(res, 400, "button must be left|right|middle");
40
- }
41
- const modifiersRaw = toStringArray(body.modifiers) ?? [];
42
- const parsedModifiers = parseClickModifiers(modifiersRaw);
43
- if (parsedModifiers.error) {
44
- return jsonError(res, 400, parsedModifiers.error);
45
- }
46
- const modifiers = parsedModifiers.modifiers;
47
- const clickRequest = {
48
- cdpUrl,
49
- targetId: tab.targetId,
50
- ref,
51
- doubleClick,
52
- };
53
- if (button) {
54
- clickRequest.button = button;
55
- }
56
- if (modifiers) {
57
- clickRequest.modifiers = modifiers;
58
- }
59
- if (timeoutMs) {
60
- clickRequest.timeoutMs = timeoutMs;
61
- }
62
- await pw.clickViaPlaywright(clickRequest);
63
- return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
64
- }
65
- case "type": {
66
- const ref = toStringOrEmpty(body.ref);
67
- if (!ref) {
68
- return jsonError(res, 400, "ref is required");
39
+ await withPlaywrightRouteContext({
40
+ req,
41
+ res,
42
+ ctx,
43
+ targetId,
44
+ feature: `act:${kind}`,
45
+ run: async ({ cdpUrl, tab, pw }) => {
46
+ const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
47
+ switch (kind) {
48
+ case "click": {
49
+ const ref = toStringOrEmpty(body.ref);
50
+ if (!ref) {
51
+ return jsonError(res, 400, "ref is required");
52
+ }
53
+ const doubleClick = toBoolean(body.doubleClick) ?? false;
54
+ const timeoutMs = toNumber(body.timeoutMs);
55
+ const buttonRaw = toStringOrEmpty(body.button) || "";
56
+ const button = buttonRaw ? parseClickButton(buttonRaw) : undefined;
57
+ if (buttonRaw && !button) {
58
+ return jsonError(res, 400, "button must be left|right|middle");
59
+ }
60
+ const modifiersRaw = toStringArray(body.modifiers) ?? [];
61
+ const parsedModifiers = parseClickModifiers(modifiersRaw);
62
+ if (parsedModifiers.error) {
63
+ return jsonError(res, 400, parsedModifiers.error);
64
+ }
65
+ const modifiers = parsedModifiers.modifiers;
66
+ const clickRequest = {
67
+ cdpUrl,
68
+ targetId: tab.targetId,
69
+ ref,
70
+ doubleClick,
71
+ };
72
+ if (button) {
73
+ clickRequest.button = button;
74
+ }
75
+ if (modifiers) {
76
+ clickRequest.modifiers = modifiers;
77
+ }
78
+ if (timeoutMs) {
79
+ clickRequest.timeoutMs = timeoutMs;
80
+ }
81
+ await pw.clickViaPlaywright(clickRequest);
82
+ return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
69
83
  }
70
- if (typeof body.text !== "string") {
71
- return jsonError(res, 400, "text is required");
84
+ case "type": {
85
+ const ref = toStringOrEmpty(body.ref);
86
+ if (!ref) {
87
+ return jsonError(res, 400, "ref is required");
88
+ }
89
+ if (typeof body.text !== "string") {
90
+ return jsonError(res, 400, "text is required");
91
+ }
92
+ const text = body.text;
93
+ const submit = toBoolean(body.submit) ?? false;
94
+ const slowly = toBoolean(body.slowly) ?? false;
95
+ const timeoutMs = toNumber(body.timeoutMs);
96
+ const typeRequest = {
97
+ cdpUrl,
98
+ targetId: tab.targetId,
99
+ ref,
100
+ text,
101
+ submit,
102
+ slowly,
103
+ };
104
+ if (timeoutMs) {
105
+ typeRequest.timeoutMs = timeoutMs;
106
+ }
107
+ await pw.typeViaPlaywright(typeRequest);
108
+ return res.json({ ok: true, targetId: tab.targetId });
72
109
  }
73
- const text = body.text;
74
- const submit = toBoolean(body.submit) ?? false;
75
- const slowly = toBoolean(body.slowly) ?? false;
76
- const timeoutMs = toNumber(body.timeoutMs);
77
- const typeRequest = {
78
- cdpUrl,
79
- targetId: tab.targetId,
80
- ref,
81
- text,
82
- submit,
83
- slowly,
84
- };
85
- if (timeoutMs) {
86
- typeRequest.timeoutMs = timeoutMs;
110
+ case "press": {
111
+ const key = toStringOrEmpty(body.key);
112
+ if (!key) {
113
+ return jsonError(res, 400, "key is required");
114
+ }
115
+ const delayMs = toNumber(body.delayMs);
116
+ await pw.pressKeyViaPlaywright({
117
+ cdpUrl,
118
+ targetId: tab.targetId,
119
+ key,
120
+ delayMs: delayMs ?? undefined,
121
+ });
122
+ return res.json({ ok: true, targetId: tab.targetId });
87
123
  }
88
- await pw.typeViaPlaywright(typeRequest);
89
- return res.json({ ok: true, targetId: tab.targetId });
90
- }
91
- case "press": {
92
- const key = toStringOrEmpty(body.key);
93
- if (!key) {
94
- return jsonError(res, 400, "key is required");
124
+ case "hover": {
125
+ const ref = toStringOrEmpty(body.ref);
126
+ if (!ref) {
127
+ return jsonError(res, 400, "ref is required");
128
+ }
129
+ const timeoutMs = toNumber(body.timeoutMs);
130
+ await pw.hoverViaPlaywright({
131
+ cdpUrl,
132
+ targetId: tab.targetId,
133
+ ref,
134
+ timeoutMs: timeoutMs ?? undefined,
135
+ });
136
+ return res.json({ ok: true, targetId: tab.targetId });
95
137
  }
96
- const delayMs = toNumber(body.delayMs);
97
- await pw.pressKeyViaPlaywright({
98
- cdpUrl,
99
- targetId: tab.targetId,
100
- key,
101
- delayMs: delayMs ?? undefined,
102
- });
103
- return res.json({ ok: true, targetId: tab.targetId });
104
- }
105
- case "hover": {
106
- const ref = toStringOrEmpty(body.ref);
107
- if (!ref) {
108
- return jsonError(res, 400, "ref is required");
138
+ case "scrollIntoView": {
139
+ const ref = toStringOrEmpty(body.ref);
140
+ if (!ref) {
141
+ return jsonError(res, 400, "ref is required");
142
+ }
143
+ const timeoutMs = toNumber(body.timeoutMs);
144
+ const scrollRequest = {
145
+ cdpUrl,
146
+ targetId: tab.targetId,
147
+ ref,
148
+ };
149
+ if (timeoutMs) {
150
+ scrollRequest.timeoutMs = timeoutMs;
151
+ }
152
+ await pw.scrollIntoViewViaPlaywright(scrollRequest);
153
+ return res.json({ ok: true, targetId: tab.targetId });
109
154
  }
110
- const timeoutMs = toNumber(body.timeoutMs);
111
- await pw.hoverViaPlaywright({
112
- cdpUrl,
113
- targetId: tab.targetId,
114
- ref,
115
- timeoutMs: timeoutMs ?? undefined,
116
- });
117
- return res.json({ ok: true, targetId: tab.targetId });
118
- }
119
- case "scrollIntoView": {
120
- const ref = toStringOrEmpty(body.ref);
121
- if (!ref) {
122
- return jsonError(res, 400, "ref is required");
155
+ case "drag": {
156
+ const startRef = toStringOrEmpty(body.startRef);
157
+ const endRef = toStringOrEmpty(body.endRef);
158
+ if (!startRef || !endRef) {
159
+ return jsonError(res, 400, "startRef and endRef are required");
160
+ }
161
+ const timeoutMs = toNumber(body.timeoutMs);
162
+ await pw.dragViaPlaywright({
163
+ cdpUrl,
164
+ targetId: tab.targetId,
165
+ startRef,
166
+ endRef,
167
+ timeoutMs: timeoutMs ?? undefined,
168
+ });
169
+ return res.json({ ok: true, targetId: tab.targetId });
123
170
  }
124
- const timeoutMs = toNumber(body.timeoutMs);
125
- const scrollRequest = {
126
- cdpUrl,
127
- targetId: tab.targetId,
128
- ref,
129
- };
130
- if (timeoutMs) {
131
- scrollRequest.timeoutMs = timeoutMs;
171
+ case "select": {
172
+ const ref = toStringOrEmpty(body.ref);
173
+ const values = toStringArray(body.values);
174
+ if (!ref || !values?.length) {
175
+ return jsonError(res, 400, "ref and values are required");
176
+ }
177
+ const timeoutMs = toNumber(body.timeoutMs);
178
+ await pw.selectOptionViaPlaywright({
179
+ cdpUrl,
180
+ targetId: tab.targetId,
181
+ ref,
182
+ values,
183
+ timeoutMs: timeoutMs ?? undefined,
184
+ });
185
+ return res.json({ ok: true, targetId: tab.targetId });
132
186
  }
133
- await pw.scrollIntoViewViaPlaywright(scrollRequest);
134
- return res.json({ ok: true, targetId: tab.targetId });
135
- }
136
- case "drag": {
137
- const startRef = toStringOrEmpty(body.startRef);
138
- const endRef = toStringOrEmpty(body.endRef);
139
- if (!startRef || !endRef) {
140
- return jsonError(res, 400, "startRef and endRef are required");
187
+ case "fill": {
188
+ const rawFields = Array.isArray(body.fields) ? body.fields : [];
189
+ const fields = rawFields
190
+ .map((field) => {
191
+ if (!field || typeof field !== "object") {
192
+ return null;
193
+ }
194
+ const rec = field;
195
+ const ref = toStringOrEmpty(rec.ref);
196
+ const type = toStringOrEmpty(rec.type);
197
+ if (!ref || !type) {
198
+ return null;
199
+ }
200
+ const value = typeof rec.value === "string" ||
201
+ typeof rec.value === "number" ||
202
+ typeof rec.value === "boolean"
203
+ ? rec.value
204
+ : undefined;
205
+ const parsed = value === undefined ? { ref, type } : { ref, type, value };
206
+ return parsed;
207
+ })
208
+ .filter((field) => field !== null);
209
+ if (!fields.length) {
210
+ return jsonError(res, 400, "fields are required");
211
+ }
212
+ const timeoutMs = toNumber(body.timeoutMs);
213
+ await pw.fillFormViaPlaywright({
214
+ cdpUrl,
215
+ targetId: tab.targetId,
216
+ fields,
217
+ timeoutMs: timeoutMs ?? undefined,
218
+ });
219
+ return res.json({ ok: true, targetId: tab.targetId });
141
220
  }
142
- const timeoutMs = toNumber(body.timeoutMs);
143
- await pw.dragViaPlaywright({
144
- cdpUrl,
145
- targetId: tab.targetId,
146
- startRef,
147
- endRef,
148
- timeoutMs: timeoutMs ?? undefined,
149
- });
150
- return res.json({ ok: true, targetId: tab.targetId });
151
- }
152
- case "select": {
153
- const ref = toStringOrEmpty(body.ref);
154
- const values = toStringArray(body.values);
155
- if (!ref || !values?.length) {
156
- return jsonError(res, 400, "ref and values are required");
221
+ case "resize": {
222
+ const width = toNumber(body.width);
223
+ const height = toNumber(body.height);
224
+ if (!width || !height) {
225
+ return jsonError(res, 400, "width and height are required");
226
+ }
227
+ await pw.resizeViewportViaPlaywright({
228
+ cdpUrl,
229
+ targetId: tab.targetId,
230
+ width,
231
+ height,
232
+ });
233
+ return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
157
234
  }
158
- const timeoutMs = toNumber(body.timeoutMs);
159
- await pw.selectOptionViaPlaywright({
160
- cdpUrl,
161
- targetId: tab.targetId,
162
- ref,
163
- values,
164
- timeoutMs: timeoutMs ?? undefined,
165
- });
166
- return res.json({ ok: true, targetId: tab.targetId });
167
- }
168
- case "fill": {
169
- const rawFields = Array.isArray(body.fields) ? body.fields : [];
170
- const fields = rawFields
171
- .map((field) => {
172
- if (!field || typeof field !== "object") {
173
- return null;
235
+ case "wait": {
236
+ const timeMs = toNumber(body.timeMs);
237
+ const text = toStringOrEmpty(body.text) || undefined;
238
+ const textGone = toStringOrEmpty(body.textGone) || undefined;
239
+ const selector = toStringOrEmpty(body.selector) || undefined;
240
+ const url = toStringOrEmpty(body.url) || undefined;
241
+ const loadStateRaw = toStringOrEmpty(body.loadState);
242
+ const loadState = loadStateRaw === "load" ||
243
+ loadStateRaw === "domcontentloaded" ||
244
+ loadStateRaw === "networkidle"
245
+ ? loadStateRaw
246
+ : undefined;
247
+ const fn = toStringOrEmpty(body.fn) || undefined;
248
+ const timeoutMs = toNumber(body.timeoutMs) ?? undefined;
249
+ if (fn && !evaluateEnabled) {
250
+ return jsonError(res, 403, [
251
+ "wait --fn is disabled by config (browser.evaluateEnabled=false).",
252
+ "Docs: /gateway/configuration#browser-clawd-managed-browser",
253
+ ].join("\n"));
174
254
  }
175
- const rec = field;
176
- const ref = toStringOrEmpty(rec.ref);
177
- const type = toStringOrEmpty(rec.type);
178
- if (!ref || !type) {
179
- return null;
255
+ if (timeMs === undefined &&
256
+ !text &&
257
+ !textGone &&
258
+ !selector &&
259
+ !url &&
260
+ !loadState &&
261
+ !fn) {
262
+ return jsonError(res, 400, "wait requires at least one of: timeMs, text, textGone, selector, url, loadState, fn");
180
263
  }
181
- const value = typeof rec.value === "string" ||
182
- typeof rec.value === "number" ||
183
- typeof rec.value === "boolean"
184
- ? rec.value
185
- : undefined;
186
- const parsed = value === undefined ? { ref, type } : { ref, type, value };
187
- return parsed;
188
- })
189
- .filter((field) => field !== null);
190
- if (!fields.length) {
191
- return jsonError(res, 400, "fields are required");
192
- }
193
- const timeoutMs = toNumber(body.timeoutMs);
194
- await pw.fillFormViaPlaywright({
195
- cdpUrl,
196
- targetId: tab.targetId,
197
- fields,
198
- timeoutMs: timeoutMs ?? undefined,
199
- });
200
- return res.json({ ok: true, targetId: tab.targetId });
201
- }
202
- case "resize": {
203
- const width = toNumber(body.width);
204
- const height = toNumber(body.height);
205
- if (!width || !height) {
206
- return jsonError(res, 400, "width and height are required");
207
- }
208
- await pw.resizeViewportViaPlaywright({
209
- cdpUrl,
210
- targetId: tab.targetId,
211
- width,
212
- height,
213
- });
214
- return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
215
- }
216
- case "wait": {
217
- const timeMs = toNumber(body.timeMs);
218
- const text = toStringOrEmpty(body.text) || undefined;
219
- const textGone = toStringOrEmpty(body.textGone) || undefined;
220
- const selector = toStringOrEmpty(body.selector) || undefined;
221
- const url = toStringOrEmpty(body.url) || undefined;
222
- const loadStateRaw = toStringOrEmpty(body.loadState);
223
- const loadState = loadStateRaw === "load" ||
224
- loadStateRaw === "domcontentloaded" ||
225
- loadStateRaw === "networkidle"
226
- ? loadStateRaw
227
- : undefined;
228
- const fn = toStringOrEmpty(body.fn) || undefined;
229
- const timeoutMs = toNumber(body.timeoutMs) ?? undefined;
230
- if (fn && !evaluateEnabled) {
231
- return jsonError(res, 403, [
232
- "wait --fn is disabled by config (browser.evaluateEnabled=false).",
233
- "Docs: /gateway/configuration#browser-clawd-managed-browser",
234
- ].join("\n"));
235
- }
236
- if (timeMs === undefined &&
237
- !text &&
238
- !textGone &&
239
- !selector &&
240
- !url &&
241
- !loadState &&
242
- !fn) {
243
- return jsonError(res, 400, "wait requires at least one of: timeMs, text, textGone, selector, url, loadState, fn");
264
+ await pw.waitForViaPlaywright({
265
+ cdpUrl,
266
+ targetId: tab.targetId,
267
+ timeMs,
268
+ text,
269
+ textGone,
270
+ selector,
271
+ url,
272
+ loadState,
273
+ fn,
274
+ timeoutMs,
275
+ });
276
+ return res.json({ ok: true, targetId: tab.targetId });
244
277
  }
245
- await pw.waitForViaPlaywright({
246
- cdpUrl,
247
- targetId: tab.targetId,
248
- timeMs,
249
- text,
250
- textGone,
251
- selector,
252
- url,
253
- loadState,
254
- fn,
255
- timeoutMs,
256
- });
257
- return res.json({ ok: true, targetId: tab.targetId });
258
- }
259
- case "evaluate": {
260
- if (!evaluateEnabled) {
261
- return jsonError(res, 403, [
262
- "act:evaluate is disabled by config (browser.evaluateEnabled=false).",
263
- "Docs: /gateway/configuration#browser-clawd-managed-browser",
264
- ].join("\n"));
278
+ case "evaluate": {
279
+ if (!evaluateEnabled) {
280
+ return jsonError(res, 403, [
281
+ "act:evaluate is disabled by config (browser.evaluateEnabled=false).",
282
+ "Docs: /gateway/configuration#browser-clawd-managed-browser",
283
+ ].join("\n"));
284
+ }
285
+ const fn = toStringOrEmpty(body.fn);
286
+ if (!fn) {
287
+ return jsonError(res, 400, "fn is required");
288
+ }
289
+ const ref = toStringOrEmpty(body.ref) || undefined;
290
+ const evalTimeoutMs = toNumber(body.timeoutMs);
291
+ const evalRequest = {
292
+ cdpUrl,
293
+ targetId: tab.targetId,
294
+ fn,
295
+ ref,
296
+ signal: req.signal,
297
+ };
298
+ if (evalTimeoutMs !== undefined) {
299
+ evalRequest.timeoutMs = evalTimeoutMs;
300
+ }
301
+ const result = await pw.evaluateViaPlaywright(evalRequest);
302
+ return res.json({
303
+ ok: true,
304
+ targetId: tab.targetId,
305
+ url: tab.url,
306
+ result,
307
+ });
265
308
  }
266
- const fn = toStringOrEmpty(body.fn);
267
- if (!fn) {
268
- return jsonError(res, 400, "fn is required");
309
+ case "close": {
310
+ await pw.closePageViaPlaywright({ cdpUrl, targetId: tab.targetId });
311
+ return res.json({ ok: true, targetId: tab.targetId });
269
312
  }
270
- const ref = toStringOrEmpty(body.ref) || undefined;
271
- const evalTimeoutMs = toNumber(body.timeoutMs);
272
- const evalRequest = {
273
- cdpUrl,
274
- targetId: tab.targetId,
275
- fn,
276
- ref,
277
- signal: req.signal,
278
- };
279
- if (evalTimeoutMs !== undefined) {
280
- evalRequest.timeoutMs = evalTimeoutMs;
313
+ default: {
314
+ return jsonError(res, 400, "unsupported kind");
281
315
  }
282
- const result = await pw.evaluateViaPlaywright(evalRequest);
283
- return res.json({
284
- ok: true,
285
- targetId: tab.targetId,
286
- url: tab.url,
287
- result,
288
- });
289
- }
290
- case "close": {
291
- await pw.closePageViaPlaywright({ cdpUrl, targetId: tab.targetId });
292
- return res.json({ ok: true, targetId: tab.targetId });
293
316
  }
294
- default: {
295
- return jsonError(res, 400, "unsupported kind");
296
- }
297
- }
298
- }
299
- catch (err) {
300
- handleRouteError(ctx, res, err);
301
- }
317
+ },
318
+ });
302
319
  });
303
320
  app.post("/hooks/file-chooser", async (req, res) => {
304
- const profileCtx = resolveProfileContext(req, res, ctx);
305
- if (!profileCtx) {
306
- return;
307
- }
308
321
  const body = readBody(req);
309
- const targetId = toStringOrEmpty(body.targetId) || undefined;
322
+ const targetId = resolveTargetIdFromBody(body);
310
323
  const ref = toStringOrEmpty(body.ref) || undefined;
311
324
  const inputRef = toStringOrEmpty(body.inputRef) || undefined;
312
325
  const element = toStringOrEmpty(body.element) || undefined;
@@ -315,111 +328,113 @@ export function registerBrowserAgentActRoutes(app, ctx) {
315
328
  if (!paths.length) {
316
329
  return jsonError(res, 400, "paths are required");
317
330
  }
318
- try {
319
- const tab = await profileCtx.ensureTabAvailable(targetId);
320
- const pw = await requirePwAi(res, "file chooser hook");
321
- if (!pw) {
322
- return;
323
- }
324
- if (inputRef || element) {
325
- if (ref) {
326
- return jsonError(res, 400, "ref cannot be combined with inputRef/element");
327
- }
328
- await pw.setInputFilesViaPlaywright({
329
- cdpUrl: profileCtx.profile.cdpUrl,
330
- targetId: tab.targetId,
331
- inputRef,
332
- element,
333
- paths,
334
- });
335
- }
336
- else {
337
- await pw.armFileUploadViaPlaywright({
338
- cdpUrl: profileCtx.profile.cdpUrl,
339
- targetId: tab.targetId,
340
- paths,
341
- timeoutMs: timeoutMs ?? undefined,
331
+ await withPlaywrightRouteContext({
332
+ req,
333
+ res,
334
+ ctx,
335
+ targetId,
336
+ feature: "file chooser hook",
337
+ run: async ({ cdpUrl, tab, pw }) => {
338
+ const uploadPathsResult = resolvePathsWithinRoot({
339
+ rootDir: DEFAULT_UPLOAD_DIR,
340
+ requestedPaths: paths,
341
+ scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`,
342
342
  });
343
- if (ref) {
344
- await pw.clickViaPlaywright({
345
- cdpUrl: profileCtx.profile.cdpUrl,
343
+ if (!uploadPathsResult.ok) {
344
+ res.status(400).json({ error: uploadPathsResult.error });
345
+ return;
346
+ }
347
+ const resolvedPaths = uploadPathsResult.paths;
348
+ if (inputRef || element) {
349
+ if (ref) {
350
+ return jsonError(res, 400, "ref cannot be combined with inputRef/element");
351
+ }
352
+ await pw.setInputFilesViaPlaywright({
353
+ cdpUrl,
346
354
  targetId: tab.targetId,
347
- ref,
355
+ inputRef,
356
+ element,
357
+ paths: resolvedPaths,
348
358
  });
349
359
  }
350
- }
351
- res.json({ ok: true });
352
- }
353
- catch (err) {
354
- handleRouteError(ctx, res, err);
355
- }
360
+ else {
361
+ await pw.armFileUploadViaPlaywright({
362
+ cdpUrl,
363
+ targetId: tab.targetId,
364
+ paths: resolvedPaths,
365
+ timeoutMs: timeoutMs ?? undefined,
366
+ });
367
+ if (ref) {
368
+ await pw.clickViaPlaywright({
369
+ cdpUrl,
370
+ targetId: tab.targetId,
371
+ ref,
372
+ });
373
+ }
374
+ }
375
+ res.json({ ok: true });
376
+ },
377
+ });
356
378
  });
357
379
  app.post("/hooks/dialog", async (req, res) => {
358
- const profileCtx = resolveProfileContext(req, res, ctx);
359
- if (!profileCtx) {
360
- return;
361
- }
362
380
  const body = readBody(req);
363
- const targetId = toStringOrEmpty(body.targetId) || undefined;
381
+ const targetId = resolveTargetIdFromBody(body);
364
382
  const accept = toBoolean(body.accept);
365
383
  const promptText = toStringOrEmpty(body.promptText) || undefined;
366
384
  const timeoutMs = toNumber(body.timeoutMs);
367
385
  if (accept === undefined) {
368
386
  return jsonError(res, 400, "accept is required");
369
387
  }
370
- try {
371
- const tab = await profileCtx.ensureTabAvailable(targetId);
372
- const pw = await requirePwAi(res, "dialog hook");
373
- if (!pw) {
374
- return;
375
- }
376
- await pw.armDialogViaPlaywright({
377
- cdpUrl: profileCtx.profile.cdpUrl,
378
- targetId: tab.targetId,
379
- accept,
380
- promptText,
381
- timeoutMs: timeoutMs ?? undefined,
382
- });
383
- res.json({ ok: true });
384
- }
385
- catch (err) {
386
- handleRouteError(ctx, res, err);
387
- }
388
+ await withPlaywrightRouteContext({
389
+ req,
390
+ res,
391
+ ctx,
392
+ targetId,
393
+ feature: "dialog hook",
394
+ run: async ({ cdpUrl, tab, pw }) => {
395
+ await pw.armDialogViaPlaywright({
396
+ cdpUrl,
397
+ targetId: tab.targetId,
398
+ accept,
399
+ promptText,
400
+ timeoutMs: timeoutMs ?? undefined,
401
+ });
402
+ res.json({ ok: true });
403
+ },
404
+ });
388
405
  });
389
406
  app.post("/wait/download", async (req, res) => {
390
- const profileCtx = resolveProfileContext(req, res, ctx);
391
- if (!profileCtx) {
392
- return;
393
- }
394
407
  const body = readBody(req);
395
- const targetId = toStringOrEmpty(body.targetId) || undefined;
396
- const out = toStringOrEmpty(body.path) || undefined;
408
+ const targetId = resolveTargetIdFromBody(body);
409
+ const out = toStringOrEmpty(body.path) || "";
397
410
  const timeoutMs = toNumber(body.timeoutMs);
398
- try {
399
- const tab = await profileCtx.ensureTabAvailable(targetId);
400
- const pw = await requirePwAi(res, "wait for download");
401
- if (!pw) {
402
- return;
403
- }
404
- const result = await pw.waitForDownloadViaPlaywright({
405
- cdpUrl: profileCtx.profile.cdpUrl,
406
- targetId: tab.targetId,
407
- path: out,
408
- timeoutMs: timeoutMs ?? undefined,
409
- });
410
- res.json({ ok: true, targetId: tab.targetId, download: result });
411
- }
412
- catch (err) {
413
- handleRouteError(ctx, res, err);
414
- }
411
+ await withPlaywrightRouteContext({
412
+ req,
413
+ res,
414
+ ctx,
415
+ targetId,
416
+ feature: "wait for download",
417
+ run: async ({ cdpUrl, tab, pw }) => {
418
+ let downloadPath;
419
+ if (out.trim()) {
420
+ const resolvedDownloadPath = resolveDownloadPathOrRespond(res, out);
421
+ if (!resolvedDownloadPath) {
422
+ return;
423
+ }
424
+ downloadPath = resolvedDownloadPath;
425
+ }
426
+ const requestBase = buildDownloadRequestBase(cdpUrl, tab.targetId, timeoutMs);
427
+ const result = await pw.waitForDownloadViaPlaywright({
428
+ ...requestBase,
429
+ path: downloadPath,
430
+ });
431
+ respondWithDownloadResult(res, tab.targetId, result);
432
+ },
433
+ });
415
434
  });
416
435
  app.post("/download", async (req, res) => {
417
- const profileCtx = resolveProfileContext(req, res, ctx);
418
- if (!profileCtx) {
419
- return;
420
- }
421
436
  const body = readBody(req);
422
- const targetId = toStringOrEmpty(body.targetId) || undefined;
437
+ const targetId = resolveTargetIdFromBody(body);
423
438
  const ref = toStringOrEmpty(body.ref);
424
439
  const out = toStringOrEmpty(body.path);
425
440
  const timeoutMs = toNumber(body.timeoutMs);
@@ -429,83 +444,75 @@ export function registerBrowserAgentActRoutes(app, ctx) {
429
444
  if (!out) {
430
445
  return jsonError(res, 400, "path is required");
431
446
  }
432
- try {
433
- const tab = await profileCtx.ensureTabAvailable(targetId);
434
- const pw = await requirePwAi(res, "download");
435
- if (!pw) {
436
- return;
437
- }
438
- const result = await pw.downloadViaPlaywright({
439
- cdpUrl: profileCtx.profile.cdpUrl,
440
- targetId: tab.targetId,
441
- ref,
442
- path: out,
443
- timeoutMs: timeoutMs ?? undefined,
444
- });
445
- res.json({ ok: true, targetId: tab.targetId, download: result });
446
- }
447
- catch (err) {
448
- handleRouteError(ctx, res, err);
449
- }
447
+ await withPlaywrightRouteContext({
448
+ req,
449
+ res,
450
+ ctx,
451
+ targetId,
452
+ feature: "download",
453
+ run: async ({ cdpUrl, tab, pw }) => {
454
+ const downloadPath = resolveDownloadPathOrRespond(res, out);
455
+ if (!downloadPath) {
456
+ return;
457
+ }
458
+ const requestBase = buildDownloadRequestBase(cdpUrl, tab.targetId, timeoutMs);
459
+ const result = await pw.downloadViaPlaywright({
460
+ ...requestBase,
461
+ ref,
462
+ path: downloadPath,
463
+ });
464
+ respondWithDownloadResult(res, tab.targetId, result);
465
+ },
466
+ });
450
467
  });
451
468
  app.post("/response/body", async (req, res) => {
452
- const profileCtx = resolveProfileContext(req, res, ctx);
453
- if (!profileCtx) {
454
- return;
455
- }
456
469
  const body = readBody(req);
457
- const targetId = toStringOrEmpty(body.targetId) || undefined;
470
+ const targetId = resolveTargetIdFromBody(body);
458
471
  const url = toStringOrEmpty(body.url);
459
472
  const timeoutMs = toNumber(body.timeoutMs);
460
473
  const maxChars = toNumber(body.maxChars);
461
474
  if (!url) {
462
475
  return jsonError(res, 400, "url is required");
463
476
  }
464
- try {
465
- const tab = await profileCtx.ensureTabAvailable(targetId);
466
- const pw = await requirePwAi(res, "response body");
467
- if (!pw) {
468
- return;
469
- }
470
- const result = await pw.responseBodyViaPlaywright({
471
- cdpUrl: profileCtx.profile.cdpUrl,
472
- targetId: tab.targetId,
473
- url,
474
- timeoutMs: timeoutMs ?? undefined,
475
- maxChars: maxChars ?? undefined,
476
- });
477
- res.json({ ok: true, targetId: tab.targetId, response: result });
478
- }
479
- catch (err) {
480
- handleRouteError(ctx, res, err);
481
- }
477
+ await withPlaywrightRouteContext({
478
+ req,
479
+ res,
480
+ ctx,
481
+ targetId,
482
+ feature: "response body",
483
+ run: async ({ cdpUrl, tab, pw }) => {
484
+ const result = await pw.responseBodyViaPlaywright({
485
+ cdpUrl,
486
+ targetId: tab.targetId,
487
+ url,
488
+ timeoutMs: timeoutMs ?? undefined,
489
+ maxChars: maxChars ?? undefined,
490
+ });
491
+ res.json({ ok: true, targetId: tab.targetId, response: result });
492
+ },
493
+ });
482
494
  });
483
495
  app.post("/highlight", async (req, res) => {
484
- const profileCtx = resolveProfileContext(req, res, ctx);
485
- if (!profileCtx) {
486
- return;
487
- }
488
496
  const body = readBody(req);
489
- const targetId = toStringOrEmpty(body.targetId) || undefined;
497
+ const targetId = resolveTargetIdFromBody(body);
490
498
  const ref = toStringOrEmpty(body.ref);
491
499
  if (!ref) {
492
500
  return jsonError(res, 400, "ref is required");
493
501
  }
494
- try {
495
- const tab = await profileCtx.ensureTabAvailable(targetId);
496
- const pw = await requirePwAi(res, "highlight");
497
- if (!pw) {
498
- return;
499
- }
500
- await pw.highlightViaPlaywright({
501
- cdpUrl: profileCtx.profile.cdpUrl,
502
- targetId: tab.targetId,
503
- ref,
504
- });
505
- res.json({ ok: true, targetId: tab.targetId });
506
- }
507
- catch (err) {
508
- handleRouteError(ctx, res, err);
509
- }
502
+ await withPlaywrightRouteContext({
503
+ req,
504
+ res,
505
+ ctx,
506
+ targetId,
507
+ feature: "highlight",
508
+ run: async ({ cdpUrl, tab, pw }) => {
509
+ await pw.highlightViaPlaywright({
510
+ cdpUrl,
511
+ targetId: tab.targetId,
512
+ ref,
513
+ });
514
+ res.json({ ok: true, targetId: tab.targetId });
515
+ },
516
+ });
510
517
  });
511
518
  }