@hybridaione/hybridclaw 0.2.3 → 0.2.7

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 (285) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +95 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +57 -19
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +72 -7
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1405 -0
  12. package/container/src/browser-tools.ts +672 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +65 -16
  15. package/container/src/index.ts +463 -110
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +107 -18
  18. package/container/src/tools.ts +600 -225
  19. package/container/src/types.ts +49 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +15 -4
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +24 -4
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +31 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +156 -6
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +7 -4
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +755 -150
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +19 -12
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +77 -23
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +24 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +113 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +59 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +624 -87
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +303 -26
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +62 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/hybridai-models.d.ts +8 -0
  132. package/dist/hybridai-models.d.ts.map +1 -0
  133. package/dist/hybridai-models.js +94 -0
  134. package/dist/hybridai-models.js.map +1 -0
  135. package/dist/instruction-approval-audit.d.ts.map +1 -1
  136. package/dist/instruction-approval-audit.js.map +1 -1
  137. package/dist/instruction-integrity.d.ts.map +1 -1
  138. package/dist/instruction-integrity.js +8 -2
  139. package/dist/instruction-integrity.js.map +1 -1
  140. package/dist/ipc.d.ts.map +1 -1
  141. package/dist/ipc.js +6 -1
  142. package/dist/ipc.js.map +1 -1
  143. package/dist/logger.js.map +1 -1
  144. package/dist/memory-consolidation.d.ts +17 -0
  145. package/dist/memory-consolidation.d.ts.map +1 -0
  146. package/dist/memory-consolidation.js +25 -0
  147. package/dist/memory-consolidation.js.map +1 -0
  148. package/dist/memory-service.d.ts +200 -0
  149. package/dist/memory-service.d.ts.map +1 -0
  150. package/dist/memory-service.js +294 -0
  151. package/dist/memory-service.js.map +1 -0
  152. package/dist/mount-security.d.ts.map +1 -1
  153. package/dist/mount-security.js +31 -7
  154. package/dist/mount-security.js.map +1 -1
  155. package/dist/observability-ingest.d.ts.map +1 -1
  156. package/dist/observability-ingest.js +32 -11
  157. package/dist/observability-ingest.js.map +1 -1
  158. package/dist/onboarding.d.ts.map +1 -1
  159. package/dist/onboarding.js +32 -9
  160. package/dist/onboarding.js.map +1 -1
  161. package/dist/proactive-policy.d.ts.map +1 -1
  162. package/dist/proactive-policy.js +2 -1
  163. package/dist/proactive-policy.js.map +1 -1
  164. package/dist/prompt-hooks.d.ts.map +1 -1
  165. package/dist/prompt-hooks.js +11 -7
  166. package/dist/prompt-hooks.js.map +1 -1
  167. package/dist/runtime-config.d.ts +104 -1
  168. package/dist/runtime-config.d.ts.map +1 -1
  169. package/dist/runtime-config.js +503 -24
  170. package/dist/runtime-config.js.map +1 -1
  171. package/dist/scheduled-task-runner.d.ts +1 -0
  172. package/dist/scheduled-task-runner.d.ts.map +1 -1
  173. package/dist/scheduled-task-runner.js +43 -10
  174. package/dist/scheduled-task-runner.js.map +1 -1
  175. package/dist/scheduler.d.ts +43 -4
  176. package/dist/scheduler.d.ts.map +1 -1
  177. package/dist/scheduler.js +530 -56
  178. package/dist/scheduler.js.map +1 -1
  179. package/dist/session-export.d.ts +26 -0
  180. package/dist/session-export.d.ts.map +1 -0
  181. package/dist/session-export.js +149 -0
  182. package/dist/session-export.js.map +1 -0
  183. package/dist/session-maintenance.d.ts.map +1 -1
  184. package/dist/session-maintenance.js +75 -13
  185. package/dist/session-maintenance.js.map +1 -1
  186. package/dist/session-transcripts.d.ts.map +1 -1
  187. package/dist/session-transcripts.js.map +1 -1
  188. package/dist/side-effects.d.ts.map +1 -1
  189. package/dist/side-effects.js +14 -2
  190. package/dist/side-effects.js.map +1 -1
  191. package/dist/skills-guard.d.ts.map +1 -1
  192. package/dist/skills-guard.js +893 -130
  193. package/dist/skills-guard.js.map +1 -1
  194. package/dist/skills.d.ts +5 -0
  195. package/dist/skills.d.ts.map +1 -1
  196. package/dist/skills.js +29 -15
  197. package/dist/skills.js.map +1 -1
  198. package/dist/token-efficiency.d.ts.map +1 -1
  199. package/dist/token-efficiency.js.map +1 -1
  200. package/dist/tui.js +92 -11
  201. package/dist/tui.js.map +1 -1
  202. package/dist/types.d.ts +150 -0
  203. package/dist/types.d.ts.map +1 -1
  204. package/dist/types.js +24 -1
  205. package/dist/types.js.map +1 -1
  206. package/dist/update.d.ts.map +1 -1
  207. package/dist/update.js +42 -14
  208. package/dist/update.js.map +1 -1
  209. package/dist/workspace.d.ts.map +1 -1
  210. package/dist/workspace.js +49 -9
  211. package/dist/workspace.js.map +1 -1
  212. package/docs/chat.html +9 -3
  213. package/docs/index.html +72 -13
  214. package/package.json +8 -2
  215. package/src/agent.ts +31 -4
  216. package/src/audit-cli.ts +44 -16
  217. package/src/audit-events.ts +69 -5
  218. package/src/audit-trail.ts +41 -15
  219. package/src/channels/discord/attachments.ts +81 -27
  220. package/src/channels/discord/debounce.ts +25 -0
  221. package/src/channels/discord/delivery.ts +65 -14
  222. package/src/channels/discord/human-delay.ts +48 -0
  223. package/src/channels/discord/inbound.ts +190 -8
  224. package/src/channels/discord/mentions.ts +42 -18
  225. package/src/channels/discord/presence.ts +148 -0
  226. package/src/channels/discord/rate-limiter.ts +58 -0
  227. package/src/channels/discord/reactions.ts +211 -0
  228. package/src/channels/discord/runtime.ts +1161 -205
  229. package/src/channels/discord/stream.ts +85 -31
  230. package/src/channels/discord/tool-actions.ts +78 -37
  231. package/src/channels/discord/typing.ts +140 -0
  232. package/src/chunk.ts +12 -4
  233. package/src/cli.ts +174 -57
  234. package/src/config.ts +208 -36
  235. package/src/container-runner.ts +134 -42
  236. package/src/container-setup.ts +57 -22
  237. package/src/conversation.ts +9 -7
  238. package/src/db.ts +2217 -84
  239. package/src/delegation-manager.ts +6 -2
  240. package/src/gateway-client.ts +41 -17
  241. package/src/gateway-service.ts +996 -220
  242. package/src/gateway-types.ts +33 -0
  243. package/src/gateway.ts +487 -52
  244. package/src/health.ts +66 -26
  245. package/src/heartbeat.ts +99 -22
  246. package/src/hybridai-bots.ts +14 -5
  247. package/src/hybridai-models.ts +158 -0
  248. package/src/instruction-approval-audit.ts +4 -1
  249. package/src/instruction-integrity.ts +30 -9
  250. package/src/ipc.ts +23 -5
  251. package/src/logger.ts +4 -1
  252. package/src/memory-consolidation.ts +41 -0
  253. package/src/memory-service.ts +606 -0
  254. package/src/mount-security.ts +58 -13
  255. package/src/observability-ingest.ts +134 -35
  256. package/src/onboarding.ts +126 -35
  257. package/src/proactive-policy.ts +3 -1
  258. package/src/prompt-hooks.ts +42 -17
  259. package/src/runtime-config.ts +1112 -122
  260. package/src/scheduled-task-runner.ts +77 -11
  261. package/src/scheduler.ts +683 -60
  262. package/src/session-export.ts +196 -0
  263. package/src/session-maintenance.ts +125 -22
  264. package/src/session-transcripts.ts +12 -3
  265. package/src/side-effects.ts +28 -5
  266. package/src/skills-guard.ts +1067 -219
  267. package/src/skills.ts +163 -65
  268. package/src/token-efficiency.ts +31 -9
  269. package/src/tui.ts +166 -25
  270. package/src/types.ts +199 -2
  271. package/src/update.ts +79 -23
  272. package/src/workspace.ts +63 -11
  273. package/tests/approval-policy.test.ts +335 -0
  274. package/tests/discord.basic.test.ts +213 -4
  275. package/tests/discord.human-presence.test.ts +85 -0
  276. package/tests/gateway-service.media-routing.test.ts +8 -2
  277. package/tests/hybridai-client.test.ts +112 -0
  278. package/tests/hybridai-models.test.ts +46 -0
  279. package/tests/memory-service.test.ts +1114 -0
  280. package/tests/token-efficiency.basic.test.ts +8 -2
  281. package/tests/token-usage.cache.test.ts +128 -0
  282. package/vitest.e2e.config.ts +3 -1
  283. package/vitest.integration.config.ts +3 -1
  284. package/vitest.live.config.ts +3 -1
  285. package/vitest.unit.config.ts +9 -0
@@ -1,10 +1,10 @@
1
- import { execFile, spawnSync } from 'child_process';
2
- import { createHash } from 'crypto';
3
- import { lookup } from 'dns/promises';
4
- import fs from 'fs';
5
- import net from 'net';
6
- import path from 'path';
7
- import { promisify } from 'util';
1
+ import { execFile, spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { lookup } from 'node:dns/promises';
4
+ import fs from 'node:fs';
5
+ import net from 'node:net';
6
+ import path from 'node:path';
7
+ import { promisify } from 'node:util';
8
8
 
9
9
  import type { ToolDefinition } from './types.js';
10
10
 
@@ -13,14 +13,21 @@ const execFileAsync = promisify(execFile);
13
13
  const WORKSPACE_ROOT = '/workspace';
14
14
  const BROWSER_SOCKET_ROOT = '/tmp/hybridclaw-browser';
15
15
  const BROWSER_ARTIFACT_ROOT = path.join(WORKSPACE_ROOT, '.browser-artifacts');
16
+ const DISCORD_MEDIA_CACHE_ROOT = '/discord-media-cache';
16
17
  const BROWSER_DEFAULT_TIMEOUT_MS = 45_000;
17
18
  const BROWSER_MAX_SNAPSHOT_CHARS = 12_000;
18
19
  const BROWSER_RUNTIME_ROOT = path.join(WORKSPACE_ROOT, '.hybridclaw-runtime');
19
20
  const BROWSER_TMP_HOME = path.join(BROWSER_RUNTIME_ROOT, 'home');
20
21
  const BROWSER_NPM_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'npm-cache');
21
22
  const BROWSER_XDG_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'cache');
22
- const BROWSER_PLAYWRIGHT_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'ms-playwright');
23
- const BROWSER_PROFILE_ROOT = path.join(BROWSER_RUNTIME_ROOT, 'browser-profiles');
23
+ const BROWSER_PLAYWRIGHT_CACHE = path.join(
24
+ BROWSER_RUNTIME_ROOT,
25
+ 'ms-playwright',
26
+ );
27
+ const BROWSER_PROFILE_ROOT = path.join(
28
+ BROWSER_RUNTIME_ROOT,
29
+ 'browser-profiles',
30
+ );
24
31
  const ENV_FALSEY = new Set(['0', 'false', 'no', 'off']);
25
32
  const BOT_DETECTION_PATTERNS = [
26
33
  'access denied',
@@ -92,11 +99,55 @@ const CLEAR_NETWORK_TIMINGS_SCRIPT = `(() => {
92
99
  return true;
93
100
  })()`;
94
101
 
102
+ const FIND_FILE_INPUT_SELECTORS_SCRIPT = `(() => {
103
+ const selectors = [];
104
+ const seen = new Set();
105
+ const esc = (value) => {
106
+ const text = String(value || '');
107
+ if (typeof CSS !== 'undefined' && CSS && typeof CSS.escape === 'function') {
108
+ return CSS.escape(text);
109
+ }
110
+ return text.replace(/["\\\\]/g, '\\\\$&');
111
+ };
112
+ const push = (selector) => {
113
+ const normalized = String(selector || '').trim();
114
+ if (!normalized || seen.has(normalized)) return;
115
+ seen.add(normalized);
116
+ selectors.push(normalized);
117
+ };
118
+ const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
119
+ for (const input of inputs) {
120
+ const id = input.getAttribute('id');
121
+ if (id) push(\`#\${esc(id)}\`);
122
+
123
+ const name = input.getAttribute('name');
124
+ if (name) push(\`input[type="file"][name="\${esc(name)}"]\`);
125
+
126
+ const accept = input.getAttribute('accept');
127
+ if (accept) push(\`input[type="file"][accept="\${esc(accept)}"]\`);
128
+
129
+ const form = input.closest('form');
130
+ const formId = form ? form.getAttribute('id') : null;
131
+ if (formId) {
132
+ if (name) {
133
+ push(\`#\${esc(formId)} input[type="file"][name="\${esc(name)}"]\`);
134
+ }
135
+ push(\`#\${esc(formId)} input[type="file"]\`);
136
+ }
137
+ }
138
+ push('input[type="file"]');
139
+ return selectors.slice(0, 10);
140
+ })()`;
141
+
95
142
  type SnapshotMode = 'default' | 'interactive' | 'full';
96
143
  type FrameTarget = {
97
144
  raw: string;
98
145
  isMain: boolean;
99
146
  };
147
+ type UploadTarget = {
148
+ raw: string;
149
+ source: 'ref' | 'selector';
150
+ };
100
151
  type BrowserModelContext = {
101
152
  baseUrl: string;
102
153
  apiKey: string;
@@ -134,7 +185,9 @@ export function setBrowserModelContext(
134
185
  chatbotId: string,
135
186
  ): void {
136
187
  currentBrowserModelContext = {
137
- baseUrl: String(baseUrl || '').trim().replace(/\/+$/, ''),
188
+ baseUrl: String(baseUrl || '')
189
+ .trim()
190
+ .replace(/\/+$/, ''),
138
191
  apiKey: String(apiKey || '').trim(),
139
192
  model: String(model || '').trim(),
140
193
  chatbotId: String(chatbotId || '').trim(),
@@ -178,7 +231,9 @@ function shouldPersistSessionState(): boolean {
178
231
  function resolveProfileRoot(): string {
179
232
  const configured = String(process.env.BROWSER_PROFILE_ROOT || '').trim();
180
233
  if (!configured) return ensureWritableDir(BROWSER_PROFILE_ROOT);
181
- const resolved = path.isAbsolute(configured) ? configured : path.resolve(WORKSPACE_ROOT, configured);
234
+ const resolved = path.isAbsolute(configured)
235
+ ? configured
236
+ : path.resolve(WORKSPACE_ROOT, configured);
182
237
  return ensureWritableDir(resolved);
183
238
  }
184
239
 
@@ -206,7 +261,9 @@ function resolveRunner(): BrowserRunner | null {
206
261
  return cachedRunner;
207
262
  }
208
263
 
209
- const whichAgentBrowser = spawnSync('which', ['agent-browser'], { encoding: 'utf-8' });
264
+ const whichAgentBrowser = spawnSync('which', ['agent-browser'], {
265
+ encoding: 'utf-8',
266
+ });
210
267
  if (whichAgentBrowser.status === 0 && whichAgentBrowser.stdout.trim()) {
211
268
  cachedRunner = { cmd: whichAgentBrowser.stdout.trim(), prefixArgs: [] };
212
269
  return cachedRunner;
@@ -214,7 +271,10 @@ function resolveRunner(): BrowserRunner | null {
214
271
 
215
272
  const whichNpx = spawnSync('which', ['npx'], { encoding: 'utf-8' });
216
273
  if (whichNpx.status === 0 && whichNpx.stdout.trim()) {
217
- cachedRunner = { cmd: whichNpx.stdout.trim(), prefixArgs: ['--yes', 'agent-browser'] };
274
+ cachedRunner = {
275
+ cmd: whichNpx.stdout.trim(),
276
+ prefixArgs: ['--yes', 'agent-browser'],
277
+ };
218
278
  return cachedRunner;
219
279
  }
220
280
 
@@ -238,14 +298,18 @@ function getSession(sessionId: string): BrowserSession {
238
298
  let profileDir: string | undefined;
239
299
  if (shouldPersistProfiles()) {
240
300
  try {
241
- profileDir = ensureWritableDir(path.join(resolveProfileRoot(), runtimeKey));
301
+ profileDir = ensureWritableDir(
302
+ path.join(resolveProfileRoot(), runtimeKey),
303
+ );
242
304
  } catch {
243
305
  // Fallback to ephemeral browser context if profile dir cannot be created.
244
306
  profileDir = undefined;
245
307
  }
246
308
  }
247
309
 
248
- const stateName = shouldPersistSessionState() ? deriveStableId(sessionKey, 48) : undefined;
310
+ const stateName = shouldPersistSessionState()
311
+ ? deriveStableId(sessionKey, 48)
312
+ : undefined;
249
313
 
250
314
  const session: BrowserSession = {
251
315
  sessionKey,
@@ -304,7 +368,10 @@ function removeSession(sessionId: string): void {
304
368
 
305
369
  function isPrivateIpv4(ip: string): boolean {
306
370
  const parts = ip.split('.').map((part) => Number.parseInt(part, 10));
307
- if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) {
371
+ if (
372
+ parts.length !== 4 ||
373
+ parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)
374
+ ) {
308
375
  return false;
309
376
  }
310
377
  const [a, b] = parts;
@@ -340,7 +407,12 @@ function isPrivateIp(ip: string): boolean {
340
407
  async function isPrivateHost(hostname: string): Promise<boolean> {
341
408
  const host = hostname.trim().toLowerCase();
342
409
  if (!host) return true;
343
- if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) return true;
410
+ if (
411
+ host === 'localhost' ||
412
+ host.endsWith('.localhost') ||
413
+ host.endsWith('.local')
414
+ )
415
+ return true;
344
416
  if (net.isIP(host) > 0) return isPrivateIp(host);
345
417
  try {
346
418
  const resolved = await lookup(host, { all: true, verbatim: true });
@@ -371,7 +443,9 @@ async function assertNavigationUrl(raw: unknown): Promise<URL> {
371
443
  throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
372
444
  }
373
445
 
374
- const allowPrivate = String(process.env.BROWSER_ALLOW_PRIVATE_NETWORK || '').toLowerCase() === 'true';
446
+ const allowPrivate =
447
+ String(process.env.BROWSER_ALLOW_PRIVATE_NETWORK || '').toLowerCase() ===
448
+ 'true';
375
449
  if (!allowPrivate && (await isPrivateHost(parsed.hostname))) {
376
450
  throw new Error(
377
451
  `Navigation blocked by SSRF guard: private or loopback host (${parsed.hostname}). ` +
@@ -399,6 +473,85 @@ function ensureRef(raw: unknown): string {
399
473
  return ref.startsWith('@') ? ref : `@${ref}`;
400
474
  }
401
475
 
476
+ function resolveUploadTarget(args: Record<string, unknown>): UploadTarget {
477
+ const selector = String(args.selector || args.target || '').trim();
478
+ if (selector) return { raw: selector, source: 'selector' };
479
+
480
+ const ref = String(args.ref || '').trim();
481
+ if (!ref) {
482
+ throw new Error('ref is required (or provide selector)');
483
+ }
484
+ return {
485
+ raw: ref.startsWith('@') ? ref : `@${ref}`,
486
+ source: 'ref',
487
+ };
488
+ }
489
+
490
+ function normalizeUploadPath(rawPath: string): string | null {
491
+ const trimmed = rawPath.trim();
492
+ if (!trimmed) return null;
493
+ const normalizedInput = trimmed.replace(/\\/g, '/');
494
+ const candidate = normalizedInput.startsWith('/')
495
+ ? path.posix.normalize(normalizedInput)
496
+ : path.posix.normalize(path.posix.join(WORKSPACE_ROOT, normalizedInput));
497
+ if (
498
+ !(
499
+ candidate === WORKSPACE_ROOT || candidate.startsWith(`${WORKSPACE_ROOT}/`)
500
+ ) &&
501
+ !(
502
+ candidate === DISCORD_MEDIA_CACHE_ROOT ||
503
+ candidate.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`)
504
+ )
505
+ ) {
506
+ return null;
507
+ }
508
+ return candidate;
509
+ }
510
+
511
+ function resolveUploadPaths(args: Record<string, unknown>): string[] {
512
+ const candidates: string[] = [];
513
+ const addPath = (value: unknown): void => {
514
+ if (typeof value === 'string') {
515
+ const trimmed = value.trim();
516
+ if (trimmed) candidates.push(trimmed);
517
+ return;
518
+ }
519
+ if (Array.isArray(value)) {
520
+ for (const item of value) {
521
+ if (typeof item !== 'string') continue;
522
+ const trimmed = item.trim();
523
+ if (trimmed) candidates.push(trimmed);
524
+ }
525
+ }
526
+ };
527
+
528
+ addPath(args.path);
529
+ addPath(args.file);
530
+ addPath(args.files);
531
+ addPath(args.paths);
532
+
533
+ const deduped: string[] = [];
534
+ const seen = new Set<string>();
535
+ for (const raw of candidates) {
536
+ const normalized = normalizeUploadPath(raw);
537
+ if (!normalized) {
538
+ throw new Error(
539
+ `invalid upload path "${raw}" (must stay within /workspace or /discord-media-cache)`,
540
+ );
541
+ }
542
+ if (!fs.existsSync(normalized)) {
543
+ throw new Error(`upload file not found: ${normalized}`);
544
+ }
545
+ if (seen.has(normalized)) continue;
546
+ seen.add(normalized);
547
+ deduped.push(normalized);
548
+ }
549
+ if (deduped.length === 0) {
550
+ throw new Error('path is required (or provide files/paths)');
551
+ }
552
+ return deduped;
553
+ }
554
+
402
555
  function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
403
556
  fs.mkdirSync(BROWSER_ARTIFACT_ROOT, { recursive: true });
404
557
 
@@ -409,7 +562,9 @@ function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
409
562
  }
410
563
 
411
564
  if (path.isAbsolute(requested)) {
412
- throw new Error('Absolute output paths are not allowed. Use a relative path.');
565
+ throw new Error(
566
+ 'Absolute output paths are not allowed. Use a relative path.',
567
+ );
413
568
  }
414
569
  const normalized = requested.replace(/\\/g, '/');
415
570
  const clean = path.posix.normalize(normalized);
@@ -417,7 +572,9 @@ function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
417
572
  throw new Error('Output path escapes browser artifacts directory.');
418
573
  }
419
574
 
420
- const withExt = clean.endsWith(`.${extension}`) ? clean : `${clean}.${extension}`;
575
+ const withExt = clean.endsWith(`.${extension}`)
576
+ ? clean
577
+ : `${clean}.${extension}`;
421
578
  const resolved = path.resolve(BROWSER_ARTIFACT_ROOT, withExt);
422
579
  const root = path.resolve(BROWSER_ARTIFACT_ROOT);
423
580
  if (resolved !== root && !resolved.startsWith(root + path.sep)) {
@@ -430,7 +587,10 @@ function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
430
587
  function createTempScreenshotPath(prefix: string): string {
431
588
  fs.mkdirSync(BROWSER_ARTIFACT_ROOT, { recursive: true });
432
589
  const nonce = Math.random().toString(36).slice(2, 10);
433
- return path.join(BROWSER_ARTIFACT_ROOT, `${prefix}-${Date.now()}-${nonce}.png`);
590
+ return path.join(
591
+ BROWSER_ARTIFACT_ROOT,
592
+ `${prefix}-${Date.now()}-${nonce}.png`,
593
+ );
434
594
  }
435
595
 
436
596
  function asRecord(value: unknown): Record<string, unknown> | null {
@@ -441,7 +601,8 @@ function asRecord(value: unknown): Record<string, unknown> | null {
441
601
  function normalizeSnapshotMode(rawMode: unknown): SnapshotMode {
442
602
  if (rawMode == null || String(rawMode).trim() === '') return 'default';
443
603
  const mode = String(rawMode).trim().toLowerCase();
444
- if (mode === 'default' || mode === 'interactive' || mode === 'full') return mode;
604
+ if (mode === 'default' || mode === 'interactive' || mode === 'full')
605
+ return mode;
445
606
  throw new Error('mode must be one of "default", "interactive", or "full"');
446
607
  }
447
608
 
@@ -455,12 +616,17 @@ function parseOptionalFrame(raw: unknown): FrameTarget | null {
455
616
  };
456
617
  }
457
618
 
458
- async function applyFrameTarget(sessionId: string, target: FrameTarget | null): Promise<void> {
619
+ async function applyFrameTarget(
620
+ sessionId: string,
621
+ target: FrameTarget | null,
622
+ ): Promise<void> {
459
623
  if (!target) return;
460
624
  const commandArgs = target.isMain ? ['main'] : [target.raw];
461
625
  const frameResult = await runAgentBrowser(sessionId, 'frame', commandArgs);
462
626
  if (!frameResult.success) {
463
- throw new Error(frameResult.error || `failed to switch to frame "${target.raw}"`);
627
+ throw new Error(
628
+ frameResult.error || `failed to switch to frame "${target.raw}"`,
629
+ );
464
630
  }
465
631
  }
466
632
 
@@ -469,7 +635,9 @@ async function runBrowserEval(
469
635
  script: string,
470
636
  timeoutMs = 30_000,
471
637
  ): Promise<{ success: boolean; result?: unknown; error?: string }> {
472
- const response = await runAgentBrowser(sessionId, 'eval', [script], { timeoutMs });
638
+ const response = await runAgentBrowser(sessionId, 'eval', [script], {
639
+ timeoutMs,
640
+ });
473
641
  if (!response.success) {
474
642
  return { success: false, error: response.error || 'browser eval failed' };
475
643
  }
@@ -483,7 +651,10 @@ function normalizeFrameMetadata(raw: unknown): Record<string, unknown>[] {
483
651
  for (const item of raw) {
484
652
  const entry = asRecord(item);
485
653
  if (!entry) continue;
486
- const index = typeof entry.index === 'number' && Number.isFinite(entry.index) ? entry.index : null;
654
+ const index =
655
+ typeof entry.index === 'number' && Number.isFinite(entry.index)
656
+ ? entry.index
657
+ : null;
487
658
  const id = typeof entry.id === 'string' ? entry.id : null;
488
659
  const name = typeof entry.name === 'string' ? entry.name : null;
489
660
  const title = typeof entry.title === 'string' ? entry.title : null;
@@ -503,13 +674,43 @@ function normalizeImageList(raw: unknown): Record<string, unknown>[] {
503
674
  const src = typeof entry.src === 'string' ? entry.src : '';
504
675
  if (!src || src.startsWith('data:')) continue;
505
676
  const alt = typeof entry.alt === 'string' ? entry.alt : '';
506
- const width = typeof entry.width === 'number' && Number.isFinite(entry.width) ? entry.width : null;
507
- const height = typeof entry.height === 'number' && Number.isFinite(entry.height) ? entry.height : null;
677
+ const width =
678
+ typeof entry.width === 'number' && Number.isFinite(entry.width)
679
+ ? entry.width
680
+ : null;
681
+ const height =
682
+ typeof entry.height === 'number' && Number.isFinite(entry.height)
683
+ ? entry.height
684
+ : null;
508
685
  images.push({ src, alt, width, height });
509
686
  }
510
687
  return images;
511
688
  }
512
689
 
690
+ function normalizeStringList(raw: unknown, max = 10): string[] {
691
+ if (!Array.isArray(raw)) return [];
692
+ const values: string[] = [];
693
+ const seen = new Set<string>();
694
+ for (const item of raw) {
695
+ if (typeof item !== 'string') continue;
696
+ const normalized = item.trim();
697
+ if (!normalized || seen.has(normalized)) continue;
698
+ seen.add(normalized);
699
+ values.push(normalized);
700
+ if (values.length >= max) break;
701
+ }
702
+ return values;
703
+ }
704
+
705
+ function isUploadTypeMismatchError(message: string): boolean {
706
+ const normalized = String(message || '').toLowerCase();
707
+ if (!normalized) return false;
708
+ return (
709
+ normalized.includes('setinputfiles') ||
710
+ normalized.includes('not an htmlinputelement')
711
+ );
712
+ }
713
+
513
714
  function normalizeTrackedRequests(raw: unknown): Record<string, unknown>[] {
514
715
  if (!Array.isArray(raw)) return [];
515
716
  const requests: Record<string, unknown>[] = [];
@@ -519,8 +720,12 @@ function normalizeTrackedRequests(raw: unknown): Record<string, unknown>[] {
519
720
  const url = typeof entry.url === 'string' ? entry.url : '';
520
721
  if (!url) continue;
521
722
  const method = typeof entry.method === 'string' ? entry.method : null;
522
- const type = typeof entry.resourceType === 'string' ? entry.resourceType : null;
523
- const timestamp = typeof entry.timestamp === 'number' && Number.isFinite(entry.timestamp) ? entry.timestamp : null;
723
+ const type =
724
+ typeof entry.resourceType === 'string' ? entry.resourceType : null;
725
+ const timestamp =
726
+ typeof entry.timestamp === 'number' && Number.isFinite(entry.timestamp)
727
+ ? entry.timestamp
728
+ : null;
524
729
  requests.push({
525
730
  url,
526
731
  method,
@@ -534,7 +739,10 @@ function normalizeTrackedRequests(raw: unknown): Record<string, unknown>[] {
534
739
  return requests;
535
740
  }
536
741
 
537
- function normalizePerformanceRequests(raw: unknown, filter?: string): Record<string, unknown>[] {
742
+ function normalizePerformanceRequests(
743
+ raw: unknown,
744
+ filter?: string,
745
+ ): Record<string, unknown>[] {
538
746
  if (!Array.isArray(raw)) return [];
539
747
  const loweredFilter = (filter || '').toLowerCase();
540
748
  const requests: Record<string, unknown>[] = [];
@@ -545,10 +753,19 @@ function normalizePerformanceRequests(raw: unknown, filter?: string): Record<str
545
753
  if (!url) continue;
546
754
  if (loweredFilter && !url.toLowerCase().includes(loweredFilter)) continue;
547
755
  const type = typeof entry.type === 'string' ? entry.type : null;
548
- const duration = typeof entry.duration === 'number' && Number.isFinite(entry.duration) ? entry.duration : null;
756
+ const duration =
757
+ typeof entry.duration === 'number' && Number.isFinite(entry.duration)
758
+ ? entry.duration
759
+ : null;
549
760
  const transferSize =
550
- typeof entry.transfer_size === 'number' && Number.isFinite(entry.transfer_size) ? entry.transfer_size : null;
551
- const startTime = typeof entry.start_time === 'number' && Number.isFinite(entry.start_time) ? entry.start_time : null;
761
+ typeof entry.transfer_size === 'number' &&
762
+ Number.isFinite(entry.transfer_size)
763
+ ? entry.transfer_size
764
+ : null;
765
+ const startTime =
766
+ typeof entry.start_time === 'number' && Number.isFinite(entry.start_time)
767
+ ? entry.start_time
768
+ : null;
552
769
  requests.push({
553
770
  url,
554
771
  method: 'GET',
@@ -563,11 +780,15 @@ function normalizePerformanceRequests(raw: unknown, filter?: string): Record<str
563
780
  return requests;
564
781
  }
565
782
 
566
- function buildBotDetectionWarning(titleValue: unknown): Record<string, unknown> | null {
783
+ function buildBotDetectionWarning(
784
+ titleValue: unknown,
785
+ ): Record<string, unknown> | null {
567
786
  const title = String(titleValue || '').trim();
568
787
  if (!title) return null;
569
788
  const lower = title.toLowerCase();
570
- const matched = BOT_DETECTION_PATTERNS.find((pattern) => lower.includes(pattern));
789
+ const matched = BOT_DETECTION_PATTERNS.find((pattern) =>
790
+ lower.includes(pattern),
791
+ );
571
792
  if (!matched) return null;
572
793
  const hintOverride = String(process.env.BROWSER_STEALTH_HINT || '').trim();
573
794
  const hint =
@@ -611,22 +832,33 @@ function extractVisionTextContent(content: unknown): string {
611
832
  return chunks.join('\n').trim();
612
833
  }
613
834
 
614
- async function callVisionModel(question: string, imageBase64: string): Promise<{ model: string; analysis: string }> {
835
+ async function callVisionModel(
836
+ question: string,
837
+ imageBase64: string,
838
+ ): Promise<{ model: string; analysis: string }> {
615
839
  const apiKey = currentBrowserModelContext.apiKey;
616
840
  const baseUrl = currentBrowserModelContext.baseUrl;
617
841
  const model = currentBrowserModelContext.model;
618
842
  const chatbotId = currentBrowserModelContext.chatbotId;
619
843
  if (!apiKey) {
620
- throw new Error('browser_vision is not configured: missing active request API key context.');
844
+ throw new Error(
845
+ 'browser_vision is not configured: missing active request API key context.',
846
+ );
621
847
  }
622
848
  if (!baseUrl) {
623
- throw new Error('browser_vision is not configured: missing active request base URL context.');
849
+ throw new Error(
850
+ 'browser_vision is not configured: missing active request base URL context.',
851
+ );
624
852
  }
625
853
  if (!model) {
626
- throw new Error('browser_vision is not configured: missing active request model context.');
854
+ throw new Error(
855
+ 'browser_vision is not configured: missing active request model context.',
856
+ );
627
857
  }
628
858
  if (!chatbotId) {
629
- throw new Error('browser_vision is not configured: missing active request chatbot_id context.');
859
+ throw new Error(
860
+ 'browser_vision is not configured: missing active request chatbot_id context.',
861
+ );
630
862
  }
631
863
  const endpoint = `${baseUrl}/v1/chat/completions`;
632
864
  const payload = {
@@ -638,7 +870,10 @@ async function callVisionModel(question: string, imageBase64: string): Promise<{
638
870
  role: 'user',
639
871
  content: [
640
872
  { type: 'text', text: question },
641
- { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
873
+ {
874
+ type: 'image_url',
875
+ image_url: { url: `data:image/png;base64,${imageBase64}` },
876
+ },
642
877
  ],
643
878
  },
644
879
  ],
@@ -655,8 +890,11 @@ async function callVisionModel(question: string, imageBase64: string): Promise<{
655
890
 
656
891
  const bodyText = await response.text();
657
892
  if (!response.ok) {
658
- const details = bodyText.length > 600 ? `${bodyText.slice(0, 600)}...` : bodyText;
659
- throw new Error(`vision API request failed (${response.status}): ${details}`);
893
+ const details =
894
+ bodyText.length > 600 ? `${bodyText.slice(0, 600)}...` : bodyText;
895
+ throw new Error(
896
+ `vision API request failed (${response.status}): ${details}`,
897
+ );
660
898
  }
661
899
 
662
900
  let parsed: unknown;
@@ -697,7 +935,10 @@ async function runAgentBrowser(
697
935
  };
698
936
  }
699
937
 
700
- const timeoutMs = Math.max(1_000, Math.min(options.timeoutMs ?? BROWSER_DEFAULT_TIMEOUT_MS, 180_000));
938
+ const timeoutMs = Math.max(
939
+ 1_000,
940
+ Math.min(options.timeoutMs ?? BROWSER_DEFAULT_TIMEOUT_MS, 180_000),
941
+ );
701
942
  const session = getSession(sessionId);
702
943
  const homeDir = resolveWritableHome();
703
944
  const npmCacheDir = ensureWritableDir(BROWSER_NPM_CACHE);
@@ -742,27 +983,47 @@ async function runAgentBrowser(
742
983
  return { success: true, data: {} };
743
984
  }
744
985
 
745
- let parsed: any;
986
+ let parsed: unknown;
746
987
  try {
747
988
  parsed = JSON.parse(output);
748
989
  } catch {
749
990
  return { success: true, data: { raw: output } };
750
991
  }
751
992
 
752
- if (parsed && typeof parsed === 'object' && parsed.success === false) {
753
- return { success: false, error: String(parsed.error || 'browser command failed') };
754
- }
755
- if (parsed && typeof parsed === 'object' && 'data' in parsed) {
756
- return { success: true, data: parsed.data };
993
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
994
+ const parsedRecord = parsed as Record<string, unknown>;
995
+ if (parsedRecord.success === false) {
996
+ return {
997
+ success: false,
998
+ error: String(parsedRecord.error || 'browser command failed'),
999
+ };
1000
+ }
1001
+ if ('data' in parsedRecord) {
1002
+ return { success: true, data: parsedRecord.data };
1003
+ }
757
1004
  }
758
1005
  return { success: true, data: parsed };
759
- } catch (err: any) {
760
- const stderr = typeof err?.stderr === 'string' ? err.stderr.trim() : '';
761
- const stdout = typeof err?.stdout === 'string' ? err.stdout.trim() : '';
1006
+ } catch (err: unknown) {
1007
+ const errorRecord = err as {
1008
+ stderr?: unknown;
1009
+ stdout?: unknown;
1010
+ code?: unknown;
1011
+ message?: unknown;
1012
+ };
1013
+ const stderr =
1014
+ typeof errorRecord.stderr === 'string' ? errorRecord.stderr.trim() : '';
1015
+ const stdout =
1016
+ typeof errorRecord.stdout === 'string' ? errorRecord.stdout.trim() : '';
762
1017
  const timeoutHint =
763
- err?.code === 'ETIMEDOUT' || /timed out/i.test(String(err?.message || '')) ? ` (timeout ${timeoutMs}ms)` : '';
764
- const msg = stderr || stdout || String(err?.message || err);
765
- return { success: false, error: `browser command failed${timeoutHint}: ${msg}` };
1018
+ errorRecord.code === 'ETIMEDOUT' ||
1019
+ /timed out/i.test(String(errorRecord.message || ''))
1020
+ ? ` (timeout ${timeoutMs}ms)`
1021
+ : '';
1022
+ const msg = stderr || stdout || String(errorRecord.message || err);
1023
+ return {
1024
+ success: false,
1025
+ error: `browser command failed${timeoutHint}: ${msg}`,
1026
+ };
766
1027
  }
767
1028
  }
768
1029
 
@@ -774,38 +1035,63 @@ function failure(message: string): string {
774
1035
  return JSON.stringify({ success: false, error: message }, null, 2);
775
1036
  }
776
1037
 
777
- export async function executeBrowserTool(name: string, args: Record<string, unknown>, sessionId: string): Promise<string> {
1038
+ export async function executeBrowserTool(
1039
+ name: string,
1040
+ args: Record<string, unknown>,
1041
+ sessionId: string,
1042
+ ): Promise<string> {
778
1043
  try {
779
1044
  const effectiveSessionId = normalizeSessionKey(sessionId || 'default');
780
1045
  switch (name) {
781
1046
  case 'browser_navigate': {
782
1047
  const parsed = await assertNavigationUrl(args.url);
783
- const result = await runAgentBrowser(effectiveSessionId, 'open', [parsed.toString()], { timeoutMs: 60_000 });
784
- if (!result.success) return failure(result.error || 'navigation failed');
1048
+ const result = await runAgentBrowser(
1049
+ effectiveSessionId,
1050
+ 'open',
1051
+ [parsed.toString()],
1052
+ { timeoutMs: 60_000 },
1053
+ );
1054
+ if (!result.success)
1055
+ return failure(result.error || 'navigation failed');
785
1056
  const data = (result.data || {}) as Record<string, unknown>;
786
1057
  const title = String(data.title || '');
787
1058
  const botWarning = buildBotDetectionWarning(title);
788
- const textEval = await runBrowserEval(effectiveSessionId, EXTRACT_TEXT_PREVIEW_SCRIPT, 20_000);
1059
+ const textEval = await runBrowserEval(
1060
+ effectiveSessionId,
1061
+ EXTRACT_TEXT_PREVIEW_SCRIPT,
1062
+ 20_000,
1063
+ );
789
1064
  const textData = textEval.success ? asRecord(textEval.result) : null;
790
- const contentPreview = typeof textData?.preview === 'string' ? textData.preview : '';
1065
+ const contentPreview =
1066
+ typeof textData?.preview === 'string' ? textData.preview : '';
791
1067
  const contentLength =
792
- typeof textData?.text_length === 'number' && Number.isFinite(textData.text_length)
1068
+ typeof textData?.text_length === 'number' &&
1069
+ Number.isFinite(textData.text_length)
793
1070
  ? Math.max(0, Math.floor(textData.text_length))
794
1071
  : 0;
795
1072
  const contentPreviewTruncated = textData?.preview_truncated === true;
796
1073
  const hasNoscript = textData?.has_noscript === true;
797
1074
  const rootShell = textData?.root_shell === true;
798
- const readyState = typeof textData?.ready_state === 'string' ? textData.ready_state : '';
799
- const extractionHint = buildReadExtractionHint({ contentLength, hasNoscript, rootShell });
1075
+ const readyState =
1076
+ typeof textData?.ready_state === 'string' ? textData.ready_state : '';
1077
+ const extractionHint = buildReadExtractionHint({
1078
+ contentLength,
1079
+ hasNoscript,
1080
+ rootShell,
1081
+ });
800
1082
  // Best-effort priming so browser_network has request listeners active quickly.
801
- await runAgentBrowser(effectiveSessionId, 'network', ['requests']).catch(() => undefined);
1083
+ await runAgentBrowser(effectiveSessionId, 'network', [
1084
+ 'requests',
1085
+ ]).catch(() => undefined);
802
1086
  return success({
803
1087
  url: data.url || parsed.toString(),
804
1088
  title,
805
1089
  session_id: effectiveSessionId,
806
1090
  content_text_length: contentLength,
807
1091
  ...(contentPreview ? { content_preview: contentPreview } : {}),
808
- ...(contentPreview ? { content_preview_truncated: contentPreviewTruncated } : {}),
1092
+ ...(contentPreview
1093
+ ? { content_preview_truncated: contentPreviewTruncated }
1094
+ : {}),
809
1095
  ...(readyState ? { ready_state: readyState } : {}),
810
1096
  ...(hasNoscript ? { has_noscript: true } : {}),
811
1097
  ...(rootShell ? { root_shell: true } : {}),
@@ -822,18 +1108,31 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
822
1108
  else if (mode === 'full') commandArgs = [];
823
1109
  else commandArgs = full ? [] : ['-i', '-c'];
824
1110
 
825
- const result = await runAgentBrowser(effectiveSessionId, 'snapshot', commandArgs, { timeoutMs: 45_000 });
1111
+ const result = await runAgentBrowser(
1112
+ effectiveSessionId,
1113
+ 'snapshot',
1114
+ commandArgs,
1115
+ { timeoutMs: 45_000 },
1116
+ );
826
1117
  if (!result.success) return failure(result.error || 'snapshot failed');
827
1118
  const data = (result.data || {}) as Record<string, unknown>;
828
1119
  const rawSnapshot = String(data.snapshot || '');
829
1120
  const truncated = truncateSnapshot(rawSnapshot);
830
- const frameEval = await runBrowserEval(effectiveSessionId, EXTRACT_IFRAMES_SCRIPT, 15_000);
831
- const frames = frameEval.success ? normalizeFrameMetadata(frameEval.result) : [];
1121
+ const frameEval = await runBrowserEval(
1122
+ effectiveSessionId,
1123
+ EXTRACT_IFRAMES_SCRIPT,
1124
+ 15_000,
1125
+ );
1126
+ const frames = frameEval.success
1127
+ ? normalizeFrameMetadata(frameEval.result)
1128
+ : [];
832
1129
  return success({
833
1130
  snapshot: truncated.text,
834
1131
  truncated: truncated.truncated,
835
1132
  element_count:
836
- data.refs && typeof data.refs === 'object' ? Object.keys(data.refs as Record<string, unknown>).length : 0,
1133
+ data.refs && typeof data.refs === 'object'
1134
+ ? Object.keys(data.refs as Record<string, unknown>).length
1135
+ : 0,
837
1136
  url: data.url || data.origin || '',
838
1137
  mode,
839
1138
  ...(frames.length > 0 ? { frames, frame_count: frames.length } : {}),
@@ -844,8 +1143,11 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
844
1143
  const ref = ensureRef(args.ref);
845
1144
  const frame = parseOptionalFrame(args.frame);
846
1145
  await applyFrameTarget(effectiveSessionId, frame);
847
- const result = await runAgentBrowser(effectiveSessionId, 'click', [ref]);
848
- if (!result.success) return failure(result.error || `failed to click ${ref}`);
1146
+ const result = await runAgentBrowser(effectiveSessionId, 'click', [
1147
+ ref,
1148
+ ]);
1149
+ if (!result.success)
1150
+ return failure(result.error || `failed to click ${ref}`);
849
1151
  return success({
850
1152
  clicked: ref,
851
1153
  ...(frame ? { frame: frame.raw } : {}),
@@ -858,8 +1160,12 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
858
1160
  if (!text) return failure('text is required');
859
1161
  const frame = parseOptionalFrame(args.frame);
860
1162
  await applyFrameTarget(effectiveSessionId, frame);
861
- const result = await runAgentBrowser(effectiveSessionId, 'fill', [ref, text]);
862
- if (!result.success) return failure(result.error || `failed to fill ${ref}`);
1163
+ const result = await runAgentBrowser(effectiveSessionId, 'fill', [
1164
+ ref,
1165
+ text,
1166
+ ]);
1167
+ if (!result.success)
1168
+ return failure(result.error || `failed to fill ${ref}`);
863
1169
  return success({
864
1170
  element: ref,
865
1171
  typed_chars: text.length,
@@ -867,13 +1173,69 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
867
1173
  });
868
1174
  }
869
1175
 
1176
+ case 'browser_upload': {
1177
+ const target = resolveUploadTarget(args);
1178
+ const filePaths = resolveUploadPaths(args);
1179
+ const frame = parseOptionalFrame(args.frame);
1180
+ await applyFrameTarget(effectiveSessionId, frame);
1181
+ const result = await runAgentBrowser(effectiveSessionId, 'upload', [
1182
+ target.raw,
1183
+ ...filePaths,
1184
+ ]);
1185
+ if (
1186
+ !result.success &&
1187
+ target.source === 'ref' &&
1188
+ isUploadTypeMismatchError(result.error || '')
1189
+ ) {
1190
+ const selectorEval = await runBrowserEval(
1191
+ effectiveSessionId,
1192
+ FIND_FILE_INPUT_SELECTORS_SCRIPT,
1193
+ 15_000,
1194
+ );
1195
+ const selectors = selectorEval.success
1196
+ ? normalizeStringList(selectorEval.result, 10)
1197
+ : [];
1198
+ for (const selector of selectors) {
1199
+ const retry = await runAgentBrowser(effectiveSessionId, 'upload', [
1200
+ selector,
1201
+ ...filePaths,
1202
+ ]);
1203
+ if (!retry.success) continue;
1204
+ return success({
1205
+ element: target.raw,
1206
+ selector,
1207
+ target: selector,
1208
+ uploaded_count: filePaths.length,
1209
+ files: filePaths,
1210
+ fallback_from_ref: true,
1211
+ ...(frame ? { frame: frame.raw } : {}),
1212
+ });
1213
+ }
1214
+ }
1215
+ if (!result.success) {
1216
+ return failure(result.error || `failed to upload via ${target.raw}`);
1217
+ }
1218
+ return success({
1219
+ target: target.raw,
1220
+ ...(target.source === 'ref'
1221
+ ? { element: target.raw }
1222
+ : { selector: target.raw }),
1223
+ uploaded_count: filePaths.length,
1224
+ files: filePaths,
1225
+ ...(frame ? { frame: frame.raw } : {}),
1226
+ });
1227
+ }
1228
+
870
1229
  case 'browser_press': {
871
1230
  const key = String(args.key || '').trim();
872
1231
  if (!key) return failure('key is required');
873
1232
  const frame = parseOptionalFrame(args.frame);
874
1233
  await applyFrameTarget(effectiveSessionId, frame);
875
- const result = await runAgentBrowser(effectiveSessionId, 'press', [key]);
876
- if (!result.success) return failure(result.error || `failed to press ${key}`);
1234
+ const result = await runAgentBrowser(effectiveSessionId, 'press', [
1235
+ key,
1236
+ ]);
1237
+ if (!result.success)
1238
+ return failure(result.error || `failed to press ${key}`);
877
1239
  return success({
878
1240
  key,
879
1241
  ...(frame ? { frame: frame.raw } : {}),
@@ -881,20 +1243,30 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
881
1243
  }
882
1244
 
883
1245
  case 'browser_scroll': {
884
- const direction = String(args.direction || '').trim().toLowerCase();
1246
+ const direction = String(args.direction || '')
1247
+ .trim()
1248
+ .toLowerCase();
885
1249
  if (direction !== 'up' && direction !== 'down') {
886
1250
  return failure('direction must be "up" or "down"');
887
1251
  }
888
1252
  const pixelsRaw = Number(args.pixels);
889
- const pixels = Number.isFinite(pixelsRaw) && pixelsRaw > 0 ? Math.floor(pixelsRaw) : 800;
890
- const result = await runAgentBrowser(effectiveSessionId, 'scroll', [direction, String(pixels)]);
891
- if (!result.success) return failure(result.error || `failed to scroll ${direction}`);
1253
+ const pixels =
1254
+ Number.isFinite(pixelsRaw) && pixelsRaw > 0
1255
+ ? Math.floor(pixelsRaw)
1256
+ : 800;
1257
+ const result = await runAgentBrowser(effectiveSessionId, 'scroll', [
1258
+ direction,
1259
+ String(pixels),
1260
+ ]);
1261
+ if (!result.success)
1262
+ return failure(result.error || `failed to scroll ${direction}`);
892
1263
  return success({ direction, pixels });
893
1264
  }
894
1265
 
895
1266
  case 'browser_back': {
896
1267
  const result = await runAgentBrowser(effectiveSessionId, 'back', []);
897
- if (!result.success) return failure(result.error || 'failed to navigate back');
1268
+ if (!result.success)
1269
+ return failure(result.error || 'failed to navigate back');
898
1270
  const data = (result.data || {}) as Record<string, unknown>;
899
1271
  return success({ url: data.url || '' });
900
1272
  }
@@ -903,8 +1275,14 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
903
1275
  const outPath = resolveOutputPath(args.path, 'png');
904
1276
  const fullPage = args.fullPage === true;
905
1277
  const commandArgs = fullPage ? ['--full', outPath] : [outPath];
906
- const result = await runAgentBrowser(effectiveSessionId, 'screenshot', commandArgs, { timeoutMs: 60_000 });
907
- if (!result.success) return failure(result.error || 'failed to capture screenshot');
1278
+ const result = await runAgentBrowser(
1279
+ effectiveSessionId,
1280
+ 'screenshot',
1281
+ commandArgs,
1282
+ { timeoutMs: 60_000 },
1283
+ );
1284
+ if (!result.success)
1285
+ return failure(result.error || 'failed to capture screenshot');
908
1286
  return success({
909
1287
  path: path.relative(WORKSPACE_ROOT, outPath),
910
1288
  full_page: fullPage,
@@ -913,8 +1291,14 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
913
1291
 
914
1292
  case 'browser_pdf': {
915
1293
  const outPath = resolveOutputPath(args.path, 'pdf');
916
- const result = await runAgentBrowser(effectiveSessionId, 'pdf', [outPath], { timeoutMs: 60_000 });
917
- if (!result.success) return failure(result.error || 'failed to generate pdf');
1294
+ const result = await runAgentBrowser(
1295
+ effectiveSessionId,
1296
+ 'pdf',
1297
+ [outPath],
1298
+ { timeoutMs: 60_000 },
1299
+ );
1300
+ if (!result.success)
1301
+ return failure(result.error || 'failed to generate pdf');
918
1302
  return success({ path: path.relative(WORKSPACE_ROOT, outPath) });
919
1303
  }
920
1304
 
@@ -924,11 +1308,19 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
924
1308
 
925
1309
  const tempPath = createTempScreenshotPath('browser-vision');
926
1310
  try {
927
- const screenshotResult = await runAgentBrowser(effectiveSessionId, 'screenshot', [tempPath], {
928
- timeoutMs: 60_000,
929
- });
1311
+ const screenshotResult = await runAgentBrowser(
1312
+ effectiveSessionId,
1313
+ 'screenshot',
1314
+ [tempPath],
1315
+ {
1316
+ timeoutMs: 60_000,
1317
+ },
1318
+ );
930
1319
  if (!screenshotResult.success) {
931
- return failure(screenshotResult.error || 'failed to capture screenshot for vision analysis');
1320
+ return failure(
1321
+ screenshotResult.error ||
1322
+ 'failed to capture screenshot for vision analysis',
1323
+ );
932
1324
  }
933
1325
 
934
1326
  const imageBuffer = await fs.promises.readFile(tempPath);
@@ -944,8 +1336,13 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
944
1336
  }
945
1337
 
946
1338
  case 'browser_get_images': {
947
- const evalResult = await runBrowserEval(effectiveSessionId, EXTRACT_IMAGES_SCRIPT, 20_000);
948
- if (!evalResult.success) return failure(evalResult.error || 'failed to extract images');
1339
+ const evalResult = await runBrowserEval(
1340
+ effectiveSessionId,
1341
+ EXTRACT_IMAGES_SCRIPT,
1342
+ 20_000,
1343
+ );
1344
+ if (!evalResult.success)
1345
+ return failure(evalResult.error || 'failed to extract images');
949
1346
  const images = normalizeImageList(evalResult.result);
950
1347
  return success({ count: images.length, images });
951
1348
  }
@@ -953,8 +1350,14 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
953
1350
  case 'browser_console': {
954
1351
  const clear = args.clear === true;
955
1352
  const commandArgs = clear ? ['--clear'] : [];
956
- const result = await runAgentBrowser(effectiveSessionId, 'console', commandArgs, { timeoutMs: 20_000 });
957
- if (!result.success) return failure(result.error || 'failed to read console logs');
1353
+ const result = await runAgentBrowser(
1354
+ effectiveSessionId,
1355
+ 'console',
1356
+ commandArgs,
1357
+ { timeoutMs: 20_000 },
1358
+ );
1359
+ if (!result.success)
1360
+ return failure(result.error || 'failed to read console logs');
958
1361
  const data = asRecord(result.data) || {};
959
1362
  if (clear) {
960
1363
  return success({ cleared: true, count: 0, messages: [] });
@@ -967,11 +1370,22 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
967
1370
  const text = typeof entry.text === 'string' ? entry.text : '';
968
1371
  const level = typeof entry.type === 'string' ? entry.type : 'log';
969
1372
  const timestamp =
970
- typeof entry.timestamp === 'number' && Number.isFinite(entry.timestamp) ? entry.timestamp : null;
1373
+ typeof entry.timestamp === 'number' &&
1374
+ Number.isFinite(entry.timestamp)
1375
+ ? entry.timestamp
1376
+ : null;
971
1377
  if (!text) return null;
972
1378
  return { level, text, timestamp };
973
1379
  })
974
- .filter((item): item is { level: string; text: string; timestamp: number | null } => item !== null);
1380
+ .filter(
1381
+ (
1382
+ item,
1383
+ ): item is {
1384
+ level: string;
1385
+ text: string;
1386
+ timestamp: number | null;
1387
+ } => item !== null,
1388
+ );
975
1389
  return success({
976
1390
  messages,
977
1391
  count: messages.length,
@@ -983,39 +1397,70 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
983
1397
  const clear = args.clear === true;
984
1398
  const filter = String(args.filter || '').trim();
985
1399
  if (clear) {
986
- const clearRequestsResult = await runAgentBrowser(effectiveSessionId, 'network', ['requests', '--clear'], {
987
- timeoutMs: 20_000,
988
- });
1400
+ const clearRequestsResult = await runAgentBrowser(
1401
+ effectiveSessionId,
1402
+ 'network',
1403
+ ['requests', '--clear'],
1404
+ {
1405
+ timeoutMs: 20_000,
1406
+ },
1407
+ );
989
1408
  if (!clearRequestsResult.success) {
990
- return failure(clearRequestsResult.error || 'failed to clear network request history');
1409
+ return failure(
1410
+ clearRequestsResult.error ||
1411
+ 'failed to clear network request history',
1412
+ );
991
1413
  }
992
- await runBrowserEval(effectiveSessionId, CLEAR_NETWORK_TIMINGS_SCRIPT, 10_000).catch(() => undefined);
1414
+ await runBrowserEval(
1415
+ effectiveSessionId,
1416
+ CLEAR_NETWORK_TIMINGS_SCRIPT,
1417
+ 10_000,
1418
+ ).catch(() => undefined);
993
1419
  return success({ cleared: true, count: 0, requests: [] });
994
1420
  }
995
1421
 
996
1422
  const networkArgs = ['requests'];
997
1423
  if (filter) networkArgs.push('--filter', filter);
998
- const trackedResult = await runAgentBrowser(effectiveSessionId, 'network', networkArgs, { timeoutMs: 20_000 });
1424
+ const trackedResult = await runAgentBrowser(
1425
+ effectiveSessionId,
1426
+ 'network',
1427
+ networkArgs,
1428
+ { timeoutMs: 20_000 },
1429
+ );
999
1430
  const trackedData = asRecord(trackedResult.data);
1000
- const trackedRequests = trackedResult.success ? normalizeTrackedRequests(trackedData?.requests) : [];
1001
-
1002
- const timingsEval = await runBrowserEval(effectiveSessionId, NETWORK_TIMINGS_SCRIPT, 20_000);
1003
- const perfRequests = timingsEval.success ? normalizePerformanceRequests(timingsEval.result, filter) : [];
1431
+ const trackedRequests = trackedResult.success
1432
+ ? normalizeTrackedRequests(trackedData?.requests)
1433
+ : [];
1434
+
1435
+ const timingsEval = await runBrowserEval(
1436
+ effectiveSessionId,
1437
+ NETWORK_TIMINGS_SCRIPT,
1438
+ 20_000,
1439
+ );
1440
+ const perfRequests = timingsEval.success
1441
+ ? normalizePerformanceRequests(timingsEval.result, filter)
1442
+ : [];
1004
1443
 
1005
1444
  if (!trackedResult.success && !timingsEval.success) {
1006
- return failure(trackedResult.error || timingsEval.error || 'failed to read network requests');
1445
+ return failure(
1446
+ trackedResult.error ||
1447
+ timingsEval.error ||
1448
+ 'failed to read network requests',
1449
+ );
1007
1450
  }
1008
1451
 
1009
1452
  const dedupe = new Set<string>();
1010
- const requests = [...trackedRequests, ...perfRequests].filter((entry) => {
1011
- const url = typeof entry.url === 'string' ? entry.url : '';
1012
- const method = typeof entry.method === 'string' ? entry.method : '';
1013
- const type = typeof entry.type === 'string' ? entry.type : '';
1014
- const key = `${method}|${type}|${url}`;
1015
- if (!url || dedupe.has(key)) return false;
1016
- dedupe.add(key);
1017
- return true;
1018
- });
1453
+ const requests = [...trackedRequests, ...perfRequests].filter(
1454
+ (entry) => {
1455
+ const url = typeof entry.url === 'string' ? entry.url : '';
1456
+ const method = typeof entry.method === 'string' ? entry.method : '';
1457
+ const type = typeof entry.type === 'string' ? entry.type : '';
1458
+ const key = `${method}|${type}|${url}`;
1459
+ if (!url || dedupe.has(key)) return false;
1460
+ dedupe.add(key);
1461
+ return true;
1462
+ },
1463
+ );
1019
1464
 
1020
1465
  return success({
1021
1466
  count: requests.length,
@@ -1054,7 +1499,10 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1054
1499
  parameters: {
1055
1500
  type: 'object',
1056
1501
  properties: {
1057
- url: { type: 'string', description: 'URL to open (http:// or https://)' },
1502
+ url: {
1503
+ type: 'string',
1504
+ description: 'URL to open (http:// or https://)',
1505
+ },
1058
1506
  },
1059
1507
  required: ['url'],
1060
1508
  },
@@ -1069,7 +1517,11 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1069
1517
  parameters: {
1070
1518
  type: 'object',
1071
1519
  properties: {
1072
- full: { type: 'boolean', description: 'If true, request fuller snapshot output (default: false).' },
1520
+ full: {
1521
+ type: 'boolean',
1522
+ description:
1523
+ 'If true, request fuller snapshot output (default: false).',
1524
+ },
1073
1525
  mode: {
1074
1526
  type: 'string',
1075
1527
  enum: ['default', 'interactive', 'full'],
@@ -1089,10 +1541,14 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1089
1541
  parameters: {
1090
1542
  type: 'object',
1091
1543
  properties: {
1092
- ref: { type: 'string', description: 'Element reference from browser_snapshot.' },
1544
+ ref: {
1545
+ type: 'string',
1546
+ description: 'Element reference from browser_snapshot.',
1547
+ },
1093
1548
  frame: {
1094
1549
  type: 'string',
1095
- description: 'Optional frame selector. Use "main" to target the main document again.',
1550
+ description:
1551
+ 'Optional frame selector. Use "main" to target the main document again.',
1096
1552
  },
1097
1553
  },
1098
1554
  required: ['ref'],
@@ -1103,33 +1559,80 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1103
1559
  type: 'function',
1104
1560
  function: {
1105
1561
  name: 'browser_type',
1106
- description: 'Type text into an input element by snapshot ref (clears then fills).',
1562
+ description:
1563
+ 'Type text into an input element by snapshot ref (clears then fills).',
1107
1564
  parameters: {
1108
1565
  type: 'object',
1109
1566
  properties: {
1110
- ref: { type: 'string', description: 'Element reference from browser_snapshot.' },
1567
+ ref: {
1568
+ type: 'string',
1569
+ description: 'Element reference from browser_snapshot.',
1570
+ },
1111
1571
  text: { type: 'string', description: 'Text to type.' },
1112
1572
  frame: {
1113
1573
  type: 'string',
1114
- description: 'Optional frame selector. Use "main" to target the main document again.',
1574
+ description:
1575
+ 'Optional frame selector. Use "main" to target the main document again.',
1115
1576
  },
1116
1577
  },
1117
1578
  required: ['ref', 'text'],
1118
1579
  },
1119
1580
  },
1120
1581
  },
1582
+ {
1583
+ type: 'function',
1584
+ function: {
1585
+ name: 'browser_upload',
1586
+ description:
1587
+ 'Upload one or more local files to a file input. Prefer a snapshot ref (for example "@e12"); if that ref points to a wrapper (like a span/button), provide selector for the underlying input[type=file].',
1588
+ parameters: {
1589
+ type: 'object',
1590
+ properties: {
1591
+ ref: {
1592
+ type: 'string',
1593
+ description:
1594
+ 'Optional element reference from browser_snapshot (for example "@e12").',
1595
+ },
1596
+ selector: {
1597
+ type: 'string',
1598
+ description:
1599
+ 'Optional CSS selector for the actual file input (for example input[type="file"]).',
1600
+ },
1601
+ path: {
1602
+ type: 'string',
1603
+ description:
1604
+ 'Primary local file path to upload (relative to /workspace or absolute /discord-media-cache path).',
1605
+ },
1606
+ files: {
1607
+ type: 'array',
1608
+ items: { type: 'string' },
1609
+ description:
1610
+ 'Optional additional local file paths for multi-file inputs.',
1611
+ },
1612
+ frame: {
1613
+ type: 'string',
1614
+ description:
1615
+ 'Optional frame selector. Use "main" to target the main document again.',
1616
+ },
1617
+ },
1618
+ required: ['path'],
1619
+ },
1620
+ },
1621
+ },
1121
1622
  {
1122
1623
  type: 'function',
1123
1624
  function: {
1124
1625
  name: 'browser_press',
1125
- description: 'Press a keyboard key in the active page (Enter, Tab, Escape, etc.).',
1626
+ description:
1627
+ 'Press a keyboard key in the active page (Enter, Tab, Escape, etc.).',
1126
1628
  parameters: {
1127
1629
  type: 'object',
1128
1630
  properties: {
1129
1631
  key: { type: 'string', description: 'Keyboard key name.' },
1130
1632
  frame: {
1131
1633
  type: 'string',
1132
- description: 'Optional frame selector. Use "main" to target the main document again.',
1634
+ description:
1635
+ 'Optional frame selector. Use "main" to target the main document again.',
1133
1636
  },
1134
1637
  },
1135
1638
  required: ['key'],
@@ -1144,8 +1647,14 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1144
1647
  parameters: {
1145
1648
  type: 'object',
1146
1649
  properties: {
1147
- direction: { type: 'string', description: 'Scroll direction: "up" or "down".' },
1148
- pixels: { type: 'number', description: 'Optional pixel amount (default: 800).' },
1650
+ direction: {
1651
+ type: 'string',
1652
+ description: 'Scroll direction: "up" or "down".',
1653
+ },
1654
+ pixels: {
1655
+ type: 'number',
1656
+ description: 'Optional pixel amount (default: 800).',
1657
+ },
1149
1658
  },
1150
1659
  required: ['direction'],
1151
1660
  },
@@ -1172,8 +1681,15 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1172
1681
  parameters: {
1173
1682
  type: 'object',
1174
1683
  properties: {
1175
- path: { type: 'string', description: 'Optional relative output path under .browser-artifacts.' },
1176
- fullPage: { type: 'boolean', description: 'Capture full page when true.' },
1684
+ path: {
1685
+ type: 'string',
1686
+ description:
1687
+ 'Optional relative output path under .browser-artifacts.',
1688
+ },
1689
+ fullPage: {
1690
+ type: 'boolean',
1691
+ description: 'Capture full page when true.',
1692
+ },
1177
1693
  },
1178
1694
  required: [],
1179
1695
  },
@@ -1188,7 +1704,11 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1188
1704
  parameters: {
1189
1705
  type: 'object',
1190
1706
  properties: {
1191
- path: { type: 'string', description: 'Optional relative output path under .browser-artifacts.' },
1707
+ path: {
1708
+ type: 'string',
1709
+ description:
1710
+ 'Optional relative output path under .browser-artifacts.',
1711
+ },
1192
1712
  },
1193
1713
  required: [],
1194
1714
  },
@@ -1203,7 +1723,10 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1203
1723
  parameters: {
1204
1724
  type: 'object',
1205
1725
  properties: {
1206
- question: { type: 'string', description: 'Question to ask about the current page screenshot.' },
1726
+ question: {
1727
+ type: 'string',
1728
+ description: 'Question to ask about the current page screenshot.',
1729
+ },
1207
1730
  },
1208
1731
  required: ['question'],
1209
1732
  },
@@ -1225,11 +1748,16 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1225
1748
  type: 'function',
1226
1749
  function: {
1227
1750
  name: 'browser_console',
1228
- description: 'Return console messages captured from the current page; optionally clear them.',
1751
+ description:
1752
+ 'Return console messages captured from the current page; optionally clear them.',
1229
1753
  parameters: {
1230
1754
  type: 'object',
1231
1755
  properties: {
1232
- clear: { type: 'boolean', description: 'When true, clear stored console messages before returning.' },
1756
+ clear: {
1757
+ type: 'boolean',
1758
+ description:
1759
+ 'When true, clear stored console messages before returning.',
1760
+ },
1233
1761
  },
1234
1762
  required: [],
1235
1763
  },
@@ -1244,8 +1772,15 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1244
1772
  parameters: {
1245
1773
  type: 'object',
1246
1774
  properties: {
1247
- filter: { type: 'string', description: 'Optional URL substring filter.' },
1248
- clear: { type: 'boolean', description: 'When true, clear recorded network request history first.' },
1775
+ filter: {
1776
+ type: 'string',
1777
+ description: 'Optional URL substring filter.',
1778
+ },
1779
+ clear: {
1780
+ type: 'boolean',
1781
+ description:
1782
+ 'When true, clear recorded network request history first.',
1783
+ },
1249
1784
  },
1250
1785
  required: [],
1251
1786
  },
@@ -1255,7 +1790,8 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
1255
1790
  type: 'function',
1256
1791
  function: {
1257
1792
  name: 'browser_close',
1258
- description: 'Close the current browser session and release associated resources.',
1793
+ description:
1794
+ 'Close the current browser session and release associated resources.',
1259
1795
  parameters: {
1260
1796
  type: 'object',
1261
1797
  properties: {},