@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -2,12 +2,13 @@ import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import fs from 'fs'
4
4
  import path from 'path'
5
+ import { fileURLToPath, pathToFileURL } from 'url'
5
6
  import * as cheerio from 'cheerio'
6
7
  import { UPLOAD_DIR } from '../storage'
7
8
  import type { ToolBuildContext } from './context'
8
9
  import { spawnSync } from 'child_process'
9
10
  import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
10
- import { getSearchProvider } from './search-providers'
11
+ import { getSearchProvider, type SearchResult } from './search-providers'
11
12
  import { dedupeScreenshotMarkdownLines } from './web-output'
12
13
  import { withRetry } from '../tool-retry'
13
14
  import type { Plugin, PluginHooks } from '@/types'
@@ -16,44 +17,55 @@ import { normalizeToolInputArgs } from './normalize-tool-args'
16
17
  import {
17
18
  ensureSessionBrowserProfileId,
18
19
  getBrowserProfileDir,
20
+ loadBrowserSessionRecord,
19
21
  markBrowserSessionClosed,
20
22
  recordBrowserObservation,
21
23
  removeBrowserSessionRecord,
22
24
  upsertBrowserSessionRecord,
23
25
  } from '../browser-state'
24
26
 
25
- // --- Search result compression logic ---
26
- async function compressSearchResults(results: any[], query: string, bctx: any): Promise<string | null> {
27
- const session = bctx.resolveCurrentSession?.()
28
- if (!session?.provider || !session?.model) return null
29
- const { getProvider } = await import('@/lib/providers')
30
- const { loadCredentials, decryptKey } = await import('../storage')
31
- const providerEntry = getProvider(session.provider)
32
- if (!providerEntry?.handler?.streamChat) return null
33
- let apiKey: string | undefined
34
- if (session.credentialId) {
35
- const creds = loadCredentials()
36
- const cred = creds[session.credentialId]
37
- if (cred) apiKey = decryptKey(cred.encryptedKey)
38
- }
39
- const systemPrompt = 'You are a search result summarizer. Condense search results into a concise reference. Keep key facts, URLs, and data points. Remove filler and redundancy. Output plain text, not JSON.'
40
- const message = `Query: "${query}"\n\nResults:\n${JSON.stringify(results, null, 1)}\n\nSummarize these results concisely.`
41
- let compressed = ''
42
- await providerEntry.handler.streamChat({
43
- session: { ...session, messages: [] }, message, apiKey, systemPrompt,
44
- write: (raw: string) => {
45
- const lines = raw.split('\n').filter(Boolean)
46
- for (const line of lines) {
47
- if (!line.startsWith('data: ')) continue
48
- try {
49
- const ev = JSON.parse(line.slice(6))
50
- if (ev.t === 'd' && ev.text) compressed += ev.text
51
- } catch { /* ignore */ }
27
+ function cleanSearchField(value: string | undefined): string {
28
+ return (value || '').replace(/\s+/g, ' ').trim()
29
+ }
30
+
31
+ export function formatWebSearchResults(query: string, results: SearchResult[], maxChars = MAX_OUTPUT): string {
32
+ const cleanedQuery = cleanSearchField(query)
33
+ const header = cleanedQuery ? `Search results for: ${cleanedQuery}` : 'Search results'
34
+ const sections: string[] = [header]
35
+ const joinSections = (items: string[]) => items.filter(Boolean).join('\n\n')
36
+
37
+ for (let index = 0; index < results.length; index++) {
38
+ const result = results[index]
39
+ const title = cleanSearchField(result?.title) || cleanSearchField(result?.url) || `Result ${index + 1}`
40
+ const url = cleanSearchField(result?.url)
41
+ const snippet = cleanSearchField(result?.snippet)
42
+ const lines = [`${index + 1}. ${title}`]
43
+ if (url) lines.push(`URL: ${url}`)
44
+ if (snippet) lines.push(`Snippet: ${snippet}`)
45
+ const candidate = joinSections([...sections, lines.join('\n')])
46
+ if (candidate.length <= maxChars) {
47
+ sections.push(lines.join('\n'))
48
+ continue
49
+ }
50
+
51
+ if (url) {
52
+ const minimalLines = [`${index + 1}. ${title}`, `URL: ${url}`]
53
+ const minimalCandidate = joinSections([...sections, minimalLines.join('\n')])
54
+ if (minimalCandidate.length <= maxChars) {
55
+ sections.push(minimalLines.join('\n'))
52
56
  }
53
- },
54
- active: new Map(), loadHistory: () => [],
55
- })
56
- return compressed.trim() || null
57
+ }
58
+
59
+ const omitted = results.length - index
60
+ if (omitted > 0) {
61
+ const remainingNotice = `(${omitted} additional result${omitted === 1 ? '' : 's'} omitted for brevity)`
62
+ const withNotice = joinSections([...sections, remainingNotice])
63
+ if (withNotice.length <= maxChars) sections.push(remainingNotice)
64
+ }
65
+ return truncate(joinSections(sections), maxChars)
66
+ }
67
+
68
+ return truncate(joinSections(sections), maxChars)
57
69
  }
58
70
 
59
71
  type BrowserRuntimeEntry = {
@@ -159,13 +171,221 @@ export function cleanupSessionBrowser(sessionId: string): void {
159
171
  export function getActiveBrowserCount(): number { return activeBrowsers.size }
160
172
  export function hasActiveBrowser(sessionId: string): boolean { return activeBrowsers.has(sessionId) }
161
173
 
174
+ export function inferWebActionFromArgs(params: {
175
+ action?: string
176
+ query?: string
177
+ url?: string
178
+ }): 'search' | 'fetch' | undefined {
179
+ if (params.action === 'search' || params.action === 'fetch') return params.action
180
+ if (typeof params.url === 'string' && /^https?:\/\//i.test(params.url.trim())) return 'fetch'
181
+ if (typeof params.query === 'string' && params.query.trim()) return 'search'
182
+ if (typeof params.url === 'string' && params.url.trim()) return 'search'
183
+ return undefined
184
+ }
185
+
186
+ function parseStructuredJsonValue(value: unknown): unknown {
187
+ if (typeof value !== 'string') return value
188
+ const trimmed = value.trim()
189
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return value
190
+ try {
191
+ return JSON.parse(trimmed)
192
+ } catch {
193
+ return value
194
+ }
195
+ }
196
+
197
+ function parseJsonObjectValue(value: unknown): Record<string, unknown> | null {
198
+ const parsed = parseStructuredJsonValue(value)
199
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
200
+ ? parsed as Record<string, unknown>
201
+ : null
202
+ }
203
+
204
+ function parseJsonArrayValue(value: unknown): unknown[] | null {
205
+ const parsed = parseStructuredJsonValue(value)
206
+ return Array.isArray(parsed) ? parsed : null
207
+ }
208
+
209
+ function pickNonEmptyBrowserString(...values: unknown[]): string | undefined {
210
+ for (const value of values) {
211
+ if (typeof value !== 'string') continue
212
+ const trimmed = value.trim()
213
+ if (trimmed) return trimmed
214
+ }
215
+ return undefined
216
+ }
217
+
218
+ function wrapBrowserEvaluateFunction(code: string): string {
219
+ const trimmed = code.trim()
220
+ if (!trimmed) return trimmed
221
+ if (/^(?:async\s+)?function\b/.test(trimmed)) return trimmed
222
+ if (/^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed)) return trimmed
223
+ return /[;{}]/.test(trimmed)
224
+ ? `() => { ${trimmed} }`
225
+ : `() => (${trimmed})`
226
+ }
227
+
228
+ function wrapBrowserRunCodeFunction(code: string): string {
229
+ const trimmed = code.trim()
230
+ if (!trimmed) return trimmed
231
+ if (/^(?:async\s+)?function\b/.test(trimmed)) return trimmed
232
+ if (/^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed)) return trimmed
233
+ return /[;{}]/.test(trimmed)
234
+ ? `async (page) => { ${trimmed} }`
235
+ : `async (page) => (${trimmed})`
236
+ }
237
+
238
+ export function normalizeBrowserActionParams(rawParams: Record<string, unknown>): Record<string, unknown> {
239
+ const normalized = normalizeToolInputArgs(rawParams)
240
+ const action = String(normalized.action || '').trim().toLowerCase()
241
+ const params: Record<string, unknown> = { ...normalized }
242
+
243
+ const parsedFields = parseJsonArrayValue(params.fields)
244
+ if (parsedFields) params.fields = parsedFields
245
+
246
+ const parsedForm = parseJsonObjectValue(params.form)
247
+ if (parsedForm) params.form = parsedForm
248
+
249
+ if (typeof params.selector === 'string' && !pickNonEmptyBrowserString(params.element)) {
250
+ params.element = params.selector
251
+ }
252
+
253
+ if (action === 'submit_form' && typeof params.selector === 'string' && !pickNonEmptyBrowserString(params.submitElement)) {
254
+ params.submitElement = params.selector
255
+ }
256
+
257
+ if (action === 'select') {
258
+ const parsedValues = parseJsonArrayValue(params.values ?? params.option ?? params.value)
259
+ if (parsedValues) params.values = parsedValues
260
+ else if (params.values === undefined) {
261
+ const scalar = pickNonEmptyBrowserString(params.option, params.value, params.text)
262
+ if (scalar) params.values = [scalar]
263
+ }
264
+ }
265
+
266
+ if (action === 'evaluate' && !pickNonEmptyBrowserString(params.function)) {
267
+ const code = pickNonEmptyBrowserString(params.code, params.script, params.javascript, params.js)
268
+ if (code) params.function = wrapBrowserEvaluateFunction(code)
269
+ }
270
+
271
+ if (action === 'run_code') {
272
+ const code = pickNonEmptyBrowserString(params.code, params.function, params.script, params.javascript, params.js)
273
+ if (code) params.code = wrapBrowserRunCodeFunction(code)
274
+ }
275
+
276
+ return params
277
+ }
278
+
279
+ function pickBrowserTargetFromParams(params: Record<string, unknown>): string | null {
280
+ for (const value of [
281
+ params.url,
282
+ params.filePath,
283
+ params.path,
284
+ params.href,
285
+ params.link,
286
+ params.target,
287
+ params.page,
288
+ ]) {
289
+ if (typeof value !== 'string') continue
290
+ const trimmed = value.trim()
291
+ if (trimmed) return trimmed
292
+ }
293
+ return null
294
+ }
295
+
296
+ function resolveUploadFilePath(target: string): string | null {
297
+ const normalized = target.replace(/^sandbox:/, '')
298
+ const match = normalized.match(/^\/api\/uploads\/([^?#]+)/)
299
+ if (!match) return null
300
+ let decoded = match[1]
301
+ try {
302
+ decoded = decodeURIComponent(decoded)
303
+ } catch {
304
+ // keep raw segment
305
+ }
306
+ const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
307
+ const resolved = path.join(UPLOAD_DIR, safeName)
308
+ return fs.existsSync(resolved) ? resolved : null
309
+ }
310
+
311
+ function resolveBrowserFileUrlPath(target: string): string | null {
312
+ if (!/^file:/i.test(target)) return null
313
+ try {
314
+ const resolved = fileURLToPath(target)
315
+ return fs.existsSync(resolved) ? resolved : null
316
+ } catch {
317
+ return null
318
+ }
319
+ }
320
+
321
+ function tryResolveBrowserLocalPath(cwd: string, target: string): string | null {
322
+ const uploadPath = resolveUploadFilePath(target)
323
+ if (uploadPath) return uploadPath
324
+
325
+ const fileUrlPath = resolveBrowserFileUrlPath(target)
326
+ if (fileUrlPath) return fileUrlPath
327
+
328
+ if (/^(?:https?:|about:|data:)/i.test(target)) return null
329
+
330
+ const normalized = target.replace(/^sandbox:/, '')
331
+ const looksLikePath = normalized.startsWith('/')
332
+ || normalized.startsWith('./')
333
+ || normalized.startsWith('../')
334
+ || normalized.includes('/')
335
+ || /\.(?:html?|xhtml|txt|md|json|ya?ml|csv|ts|tsx|js|jsx|mjs|cjs|css|png|jpe?g|gif|webp|svg|pdf)$/i.test(normalized)
336
+ if (!looksLikePath) return null
337
+
338
+ const candidates = new Set<string>()
339
+ if (path.isAbsolute(normalized)) candidates.add(normalized)
340
+ try { candidates.add(safePath(cwd, normalized)) } catch { /* ignore */ }
341
+ try { candidates.add(path.resolve(cwd, normalized)) } catch { /* ignore */ }
342
+
343
+ for (const candidate of candidates) {
344
+ if (!candidate || !fs.existsSync(candidate)) continue
345
+ const stat = fs.statSync(candidate)
346
+ if (stat.isDirectory()) {
347
+ const indexPath = path.join(candidate, 'index.html')
348
+ if (fs.existsSync(indexPath)) return indexPath
349
+ return null
350
+ }
351
+ return candidate
352
+ }
353
+ return null
354
+ }
355
+
356
+ function localHtmlFileToDataUrl(filePath: string): string | null {
357
+ const ext = path.extname(filePath).toLowerCase()
358
+ if (ext !== '.html' && ext !== '.htm') return null
359
+ try {
360
+ const html = fs.readFileSync(filePath, 'utf8')
361
+ const hasRelativeAssetReferences = /<(?:script|img|source|video|audio)\b[^>]+\b(?:src|poster)\s*=\s*["'](?![a-z]+:|\/\/|#|\/)([^"']+)["']|<link\b[^>]+\bhref\s*=\s*["'](?![a-z]+:|\/\/|#|\/)([^"']+)["']/i.test(html)
362
+ if (hasRelativeAssetReferences) return null
363
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
364
+ } catch {
365
+ return null
366
+ }
367
+ }
368
+
369
+ export function resolveBrowserNavigationTarget(cwd: string, target: string): string {
370
+ const trimmed = target.trim()
371
+ if (!trimmed) return trimmed
372
+ const localPath = tryResolveBrowserLocalPath(cwd, trimmed)
373
+ if (localPath) return localHtmlFileToDataUrl(localPath) || pathToFileURL(localPath).toString()
374
+ return trimmed.replace(/^sandbox:/, '')
375
+ }
376
+
162
377
  /**
163
378
  * Unified Web Execution Logic
164
379
  */
165
380
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
- async function executeWebAction(args: Record<string, unknown>, bctx: any) {
381
+ async function executeWebAction(args: Record<string, unknown>) {
167
382
  const normalized = normalizeToolInputArgs(args)
168
- const { action, query, url, maxResults } = normalized as { action: string; query?: string; url?: string; maxResults?: number }
383
+ const { query, url, maxResults } = normalized as { query?: string; url?: string; maxResults?: number }
384
+ const action = inferWebActionFromArgs({
385
+ action: (normalized as { action?: string }).action,
386
+ query,
387
+ url,
388
+ })
169
389
  try {
170
390
  if (action === 'search') {
171
391
  const searchQuery = query || url
@@ -176,12 +396,7 @@ async function executeWebAction(args: Record<string, unknown>, bctx: any) {
176
396
  const provider = await getSearchProvider(settings)
177
397
  const results = await provider.search(searchQuery, limit)
178
398
  if (results.length === 0) return 'No results found.'
179
- const raw = JSON.stringify(results, null, 2)
180
- if (raw.length > 2000) {
181
- const compressed = await compressSearchResults(results, searchQuery, bctx)
182
- if (compressed) return compressed
183
- }
184
- return raw
399
+ return formatWebSearchResults(searchQuery, results)
185
400
  } else if (action === 'fetch') {
186
401
  const fetchUrl = url || query
187
402
  if (!fetchUrl) return 'Error: "url" is required for fetch action.'
@@ -238,7 +453,7 @@ const WebPlugin: Plugin = {
238
453
  },
239
454
  required: ['action']
240
455
  },
241
- execute: async (args, context) => executeWebAction(args, { ...context.session, resolveCurrentSession: () => context.session })
456
+ execute: async (args) => executeWebAction(args)
242
457
  }
243
458
  ]
244
459
  }
@@ -255,7 +470,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
255
470
  if (bctx.hasPlugin('web')) {
256
471
  tools.push(
257
472
  tool(
258
- async (args) => executeWebAction(args, bctx),
473
+ async (args) => executeWebAction(args),
259
474
  {
260
475
  name: 'web',
261
476
  description: WebPlugin.tools![0].description,
@@ -329,13 +544,30 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
329
544
  pendingBrowserInitializations.set(sessionKey, connectPromise)
330
545
  const entry = await connectPromise
331
546
  acquireExistingEntry(entry)
547
+ const lastState = loadBrowserSessionRecord(sessionKey)
548
+ const restoreUrl = typeof lastState?.currentUrl === 'string' ? lastState.currentUrl.trim() : ''
549
+ if (restoreUrl && restoreUrl !== 'about:blank') {
550
+ try {
551
+ await entry.client.callTool({ name: 'browser_navigate', arguments: { url: restoreUrl } })
552
+ } catch (err: unknown) {
553
+ upsertBrowserSessionRecord({
554
+ sessionId: sessionKey,
555
+ profileId: profileInfo.profileId,
556
+ profileDir,
557
+ inheritedFromSessionId: profileInfo.inheritedFromSessionId,
558
+ status: 'error',
559
+ lastAction: 'browser_restore',
560
+ lastError: err instanceof Error ? err.message : String(err),
561
+ })
562
+ }
563
+ }
332
564
  upsertBrowserSessionRecord({
333
565
  sessionId: sessionKey,
334
566
  profileId: profileInfo.profileId,
335
567
  profileDir,
336
568
  inheritedFromSessionId: profileInfo.inheritedFromSessionId,
337
569
  status: 'active',
338
- lastAction: 'browser_open',
570
+ lastAction: restoreUrl && restoreUrl !== 'about:blank' ? 'browser_restore' : 'browser_open',
339
571
  })
340
572
  } finally {
341
573
  if (pendingBrowserInitializations.get(sessionKey)) {
@@ -404,6 +636,105 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
404
636
  function: fn,
405
637
  })
406
638
 
639
+ const performSelectorDomAction = async (
640
+ action: 'click' | 'type' | 'select' | 'hover',
641
+ params: Record<string, unknown>,
642
+ ): Promise<{ ok: true; output: string } | { ok: false; error: string } | null> => {
643
+ const selector = pickNonEmptyBrowserString(params.element, params.selector)
644
+ if (!selector) return null
645
+ if (typeof params.ref === 'string' && params.ref.trim()) return null
646
+
647
+ const payload =
648
+ action === 'click'
649
+ ? `() => {
650
+ const selector = ${JSON.stringify(selector)};
651
+ try {
652
+ const element = document.querySelector(selector);
653
+ if (!element) return { ok: false, error: 'No element matched selector.' };
654
+ element.click?.();
655
+ return { ok: true, action: 'click', selector };
656
+ } catch (error) {
657
+ return { ok: false, error: String(error) };
658
+ }
659
+ }`
660
+ : action === 'hover'
661
+ ? `() => {
662
+ const selector = ${JSON.stringify(selector)};
663
+ try {
664
+ const element = document.querySelector(selector);
665
+ if (!element) return { ok: false, error: 'No element matched selector.' };
666
+ element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
667
+ element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
668
+ return { ok: true, action: 'hover', selector };
669
+ } catch (error) {
670
+ return { ok: false, error: String(error) };
671
+ }
672
+ }`
673
+ : action === 'type'
674
+ ? `() => {
675
+ const selector = ${JSON.stringify(selector)};
676
+ const value = ${JSON.stringify(String(params.text ?? params.value ?? ''))};
677
+ const submit = ${params.submit === true ? 'true' : 'false'};
678
+ try {
679
+ const element = document.querySelector(selector);
680
+ if (!element) return { ok: false, error: 'No element matched selector.' };
681
+ element.focus?.();
682
+ if ('value' in element) element.value = value;
683
+ else if (element.isContentEditable) element.textContent = value;
684
+ else return { ok: false, error: 'Matched element is not editable.' };
685
+ element.dispatchEvent(new Event('input', { bubbles: true }));
686
+ element.dispatchEvent(new Event('change', { bubbles: true }));
687
+ if (submit) {
688
+ if (element.form?.requestSubmit) element.form.requestSubmit();
689
+ else {
690
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
691
+ element.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
692
+ }
693
+ }
694
+ return { ok: true, action: 'type', selector, valueLength: value.length };
695
+ } catch (error) {
696
+ return { ok: false, error: String(error) };
697
+ }
698
+ }`
699
+ : `() => {
700
+ const selector = ${JSON.stringify(selector)};
701
+ const desired = ${JSON.stringify(
702
+ Array.isArray(params.values)
703
+ ? params.values.map((value) => String(value ?? ''))
704
+ : [String(params.value ?? params.option ?? '')],
705
+ )};
706
+ try {
707
+ const element = document.querySelector(selector);
708
+ if (!element) return { ok: false, error: 'No element matched selector.' };
709
+ if (!(element instanceof HTMLSelectElement)) return { ok: false, error: 'Matched element is not a <select>.' };
710
+ const selected = [];
711
+ for (const option of Array.from(element.options)) {
712
+ const match = desired.includes(option.value) || desired.includes(option.text);
713
+ option.selected = match;
714
+ if (match) selected.push(option.value || option.text || '');
715
+ }
716
+ element.dispatchEvent(new Event('input', { bubbles: true }));
717
+ element.dispatchEvent(new Event('change', { bubbles: true }));
718
+ return { ok: true, action: 'select', selector, selected };
719
+ } catch (error) {
720
+ return { ok: false, error: String(error) };
721
+ }
722
+ }`
723
+
724
+ const raw = await callBrowserEvaluate(payload)
725
+ const parsed = extractJsonPayload(raw)
726
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
727
+ return { ok: false, error: cleanPlaywrightOutput(raw) || `DOM ${action} fallback failed.` }
728
+ }
729
+ if ((parsed as Record<string, unknown>).ok !== true) {
730
+ return {
731
+ ok: false,
732
+ error: String((parsed as Record<string, unknown>).error || `DOM ${action} fallback failed.`),
733
+ }
734
+ }
735
+ return { ok: true, output: stringifyStructured(parsed) }
736
+ }
737
+
407
738
  const captureStructuredObservation = async () => {
408
739
  const expression = `() => {
409
740
  const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
@@ -650,12 +981,141 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
650
981
  }
651
982
 
652
983
  const dismissCookieBanners = async (mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>) => {
653
- await new Promise((r) => setTimeout(r, 1500))
984
+ await new Promise((r) => setTimeout(r, 1200))
654
985
  const js = `() => {
655
- const sel = ['button[id*="reject" i]', 'button[class*="reject" i]', 'a[id*="reject" i]', 'a[class*="reject" i]', '#onetrust-reject-all-handler', '#CybotCookiebotDialogBodyButtonDecline', '#didomi-notice-disagree-button', '.qc-cmp2-summary-buttons button:first-child', 'button.sp_choice_type_12'];
656
- for (const s of sel) { const el = document.querySelector(s); if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; } }
657
- const btns = [...document.querySelectorAll('button, a[role="button"]')]; const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
658
- for (const b of btns) { const txt = (b.textContent || '').trim(); if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; } }
986
+ const docs = [document];
987
+ const roots = [document];
988
+ const seenRoots = new Set([document]);
989
+ const pushRoot = (root) => {
990
+ if (!root || seenRoots.has(root)) return;
991
+ seenRoots.add(root);
992
+ roots.push(root);
993
+ };
994
+ const collectFrames = (doc) => {
995
+ try {
996
+ const frames = doc.querySelectorAll('iframe');
997
+ for (const frame of frames) {
998
+ try {
999
+ const child = frame.contentDocument || frame.contentWindow?.document;
1000
+ if (child && !docs.includes(child)) {
1001
+ docs.push(child);
1002
+ pushRoot(child);
1003
+ }
1004
+ } catch {}
1005
+ }
1006
+ } catch {}
1007
+ };
1008
+ const collectShadowRoots = () => {
1009
+ for (const root of [...roots]) {
1010
+ try {
1011
+ const all = root.querySelectorAll('*');
1012
+ for (const el of all) {
1013
+ if (el.shadowRoot) pushRoot(el.shadowRoot);
1014
+ }
1015
+ } catch {}
1016
+ }
1017
+ };
1018
+ collectFrames(document);
1019
+ collectShadowRoots();
1020
+ const allRoots = [...new Set([...docs, ...roots])];
1021
+ const visible = (el) => {
1022
+ if (!el) return false;
1023
+ const style = window.getComputedStyle(el);
1024
+ if (!style || style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
1025
+ const rect = el.getBoundingClientRect();
1026
+ return rect.width > 0 && rect.height > 0;
1027
+ };
1028
+ const normalizedText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
1029
+ const candidateSelectors = [
1030
+ '#onetrust-reject-all-handler',
1031
+ '#CybotCookiebotDialogBodyButtonDecline',
1032
+ '#didomi-notice-disagree-button',
1033
+ '.qc-cmp2-summary-buttons button:first-child',
1034
+ 'button.sp_choice_type_12',
1035
+ 'button[id*="reject" i]',
1036
+ 'button[class*="reject" i]',
1037
+ 'button[id*="decline" i]',
1038
+ 'button[class*="decline" i]',
1039
+ 'button[id*="consent" i]',
1040
+ 'button[class*="consent" i]',
1041
+ 'a[id*="reject" i]',
1042
+ 'a[class*="reject" i]',
1043
+ 'a[id*="decline" i]',
1044
+ 'a[class*="decline" i]'
1045
+ ];
1046
+ for (const root of allRoots) {
1047
+ for (const selector of candidateSelectors) {
1048
+ try {
1049
+ const el = root.querySelector(selector);
1050
+ if (el && visible(el)) {
1051
+ el.click();
1052
+ return 'clicked:' + selector;
1053
+ }
1054
+ } catch {}
1055
+ }
1056
+ }
1057
+ const buttonSelector = 'button, a[role="button"], [role="button"], input[type="button"], input[type="submit"]';
1058
+ const rejectRe = /^(reject|reject all|decline|decline all|deny|deny all|refuse|no,? thanks|only necessary|necessary only|use necessary cookies only)$/i;
1059
+ const acceptRe = /^(accept|accept all|allow all|agree|i agree|okay|ok|got it|continue|consent)$/i;
1060
+ const closeRe = /^(close|dismiss|skip|not now|x|×)$/i;
1061
+ const clickMatching = (matcher, label) => {
1062
+ for (const root of allRoots) {
1063
+ let buttons = [];
1064
+ try { buttons = [...root.querySelectorAll(buttonSelector)]; } catch {}
1065
+ for (const button of buttons) {
1066
+ const txt = normalizedText(button.textContent || button.getAttribute?.('aria-label') || button.getAttribute?.('value'));
1067
+ if (!txt || !matcher.test(txt) || !visible(button)) continue;
1068
+ try {
1069
+ button.click();
1070
+ return label + ':' + txt.slice(0, 80);
1071
+ } catch {}
1072
+ }
1073
+ }
1074
+ return null;
1075
+ };
1076
+ const clicked = clickMatching(rejectRe, 'reject') || clickMatching(acceptRe, 'accept') || clickMatching(closeRe, 'close');
1077
+ if (clicked) return clicked;
1078
+ const overlaySelectors = [
1079
+ '#onetrust-banner-sdk',
1080
+ '#onetrust-consent-sdk',
1081
+ '#CybotCookiebotDialog',
1082
+ '.didomi-popup-container',
1083
+ '.fc-consent-root',
1084
+ '[id*="cookie" i]',
1085
+ '[class*="cookie" i]',
1086
+ '[id*="consent" i]',
1087
+ '[class*="consent" i]',
1088
+ '[id*="privacy" i]',
1089
+ '[class*="privacy" i]',
1090
+ '[id*="sp_message" i]',
1091
+ '[class*="sp_message" i]'
1092
+ ];
1093
+ const hidden = [];
1094
+ for (const root of allRoots) {
1095
+ for (const selector of overlaySelectors) {
1096
+ let nodes = [];
1097
+ try { nodes = [...root.querySelectorAll(selector)]; } catch {}
1098
+ for (const node of nodes) {
1099
+ if (!visible(node)) continue;
1100
+ const text = normalizedText(node.textContent).toLowerCase();
1101
+ const attrs = normalizedText(node.id + ' ' + node.className).toLowerCase();
1102
+ if (!text.includes('cookie') && !text.includes('consent') && !text.includes('privacy') && !attrs.includes('cookie') && !attrs.includes('consent') && !attrs.includes('privacy') && !attrs.includes('onetrust') && !attrs.includes('didomi') && !attrs.includes('sp_message')) continue;
1103
+ try {
1104
+ node.style.setProperty('display', 'none', 'important');
1105
+ node.style.setProperty('visibility', 'hidden', 'important');
1106
+ node.style.setProperty('pointer-events', 'none', 'important');
1107
+ hidden.push(selector);
1108
+ } catch {}
1109
+ }
1110
+ }
1111
+ }
1112
+ if (hidden.length) {
1113
+ try {
1114
+ document.documentElement.style.removeProperty('overflow');
1115
+ document.body.style.removeProperty('overflow');
1116
+ } catch {}
1117
+ return 'hidden:' + hidden[0];
1118
+ }
659
1119
  return 'none';
660
1120
  }`
661
1121
  await mcpCall('browser_evaluate', { function: js })
@@ -665,9 +1125,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
665
1125
  const fields = Array.isArray(params.fields)
666
1126
  ? params.fields
667
1127
  : (() => {
668
- const form = params.form
669
- if (!form || typeof form !== 'object' || Array.isArray(form)) return []
670
- return Object.entries(form as Record<string, unknown>).map(([key, value]) => {
1128
+ const form = params.form
1129
+ if (!form || typeof form !== 'object' || Array.isArray(form)) return []
1130
+ return Object.entries(form as Record<string, unknown>).map(([key, value]) => {
671
1131
  const escapedId = String(key).replace(/[^a-zA-Z0-9_-]/g, '')
672
1132
  const escapedAttr = String(key).replace(/["\\]/g, '\\$&')
673
1133
  const inferredType = typeof value === 'boolean'
@@ -692,11 +1152,43 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
692
1152
  const ref = typeof entry.ref === 'string' ? entry.ref : undefined
693
1153
  const element = typeof entry.element === 'string' ? entry.element : undefined
694
1154
  const fieldType = String(entry.type || 'text').toLowerCase()
695
- const value = entry.value
1155
+ const value = entry.value ?? entry.text
696
1156
  if (!ref && !element) continue
1157
+ const selectValues = Array.isArray(entry.values)
1158
+ ? entry.values.map((item) => String(item ?? ''))
1159
+ : Array.isArray(parseJsonArrayValue(entry.values ?? entry.option ?? value))
1160
+ ? (parseJsonArrayValue(entry.values ?? entry.option ?? value) as unknown[]).map((item) => String(item ?? ''))
1161
+ : [String(entry.option ?? value ?? '')]
1162
+ if (!ref && element) {
1163
+ if (fieldType === 'select') {
1164
+ const result = await performSelectorDomAction('select', { element, values: selectValues })
1165
+ if (!result) return { ok: false, error: 'Selector fallback failed for select field.' }
1166
+ if (!result.ok) return result
1167
+ } else if (fieldType === 'checkbox' || fieldType === 'radio') {
1168
+ if (value === true || value === 'true' || value === 'on' || value === 'checked') {
1169
+ const result = await performSelectorDomAction('click', { element })
1170
+ if (!result) return { ok: false, error: 'Selector fallback failed for checkbox field.' }
1171
+ if (!result.ok) return result
1172
+ }
1173
+ } else {
1174
+ const result = await performSelectorDomAction('type', {
1175
+ element,
1176
+ text: String(value ?? ''),
1177
+ submit: params.submit === true,
1178
+ })
1179
+ if (!result) return { ok: false, error: 'Selector fallback failed for input field.' }
1180
+ if (!result.ok) return result
1181
+ }
1182
+ filled.push({
1183
+ ref: null,
1184
+ element,
1185
+ type: fieldType,
1186
+ value: value ?? null,
1187
+ })
1188
+ continue
1189
+ }
697
1190
  if (fieldType === 'select') {
698
- const values = Array.isArray(value) ? value.map(String) : [String(value ?? '')]
699
- await callMcpTool('browser_select_option', { ref, element, values })
1191
+ await callMcpTool('browser_select_option', { ref, element, values: selectValues })
700
1192
  } else if (fieldType === 'checkbox' || fieldType === 'radio') {
701
1193
  if (value === true || value === 'true' || value === 'on' || value === 'checked') {
702
1194
  await callMcpTool('browser_click', { ref, element })
@@ -720,11 +1212,19 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
720
1212
  }
721
1213
 
722
1214
  const submitForm = async (params: Record<string, unknown>) => {
723
- if (typeof params.submitRef === 'string' || typeof params.submitElement === 'string') {
724
- await callMcpTool('browser_click', {
725
- ref: typeof params.submitRef === 'string' ? params.submitRef : undefined,
726
- element: typeof params.submitElement === 'string' ? params.submitElement : undefined,
727
- })
1215
+ const submitRef = pickNonEmptyBrowserString(params.submitRef)
1216
+ const submitElement = pickNonEmptyBrowserString(params.submitElement, params.selector, params.element)
1217
+ if (submitRef || submitElement) {
1218
+ if (submitRef) {
1219
+ await callMcpTool('browser_click', {
1220
+ ref: submitRef,
1221
+ element: submitElement,
1222
+ })
1223
+ } else if (submitElement) {
1224
+ const result = await performSelectorDomAction('click', { element: submitElement })
1225
+ if (!result) return { ok: false, error: 'submitElement is required for selector-based submit.' }
1226
+ if (!result.ok) return result
1227
+ }
728
1228
  } else {
729
1229
  await callBrowserEvaluate(`() => {
730
1230
  const form = document.forms[0];
@@ -911,8 +1411,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
911
1411
  const completeWebTask = async (params: Record<string, unknown>) => {
912
1412
  const steps: string[] = []
913
1413
  if (typeof params.url === 'string' && params.url.trim()) {
914
- await callMcpTool('browser_navigate', { url: params.url.trim() })
915
- steps.push(`navigate:${params.url.trim()}`)
1414
+ const navigationTarget = resolveBrowserNavigationTarget(cwd, params.url.trim())
1415
+ await callMcpTool('browser_navigate', { url: navigationTarget })
1416
+ steps.push(`navigate:${navigationTarget}`)
916
1417
  try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
917
1418
  }
918
1419
 
@@ -927,7 +1428,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
927
1428
  if (scroll.ok) initialPage = scroll.page
928
1429
  }
929
1430
 
930
- if (Array.isArray(params.fields) && params.fields.length > 0) {
1431
+ if ((Array.isArray(params.fields) && params.fields.length > 0) || (params.form && typeof params.form === 'object' && !Array.isArray(params.form))) {
931
1432
  const filled = await performFillForm(params)
932
1433
  if (!filled.ok) return filled
933
1434
  steps.push('fill_form')
@@ -991,7 +1492,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
991
1492
  tools.push(
992
1493
  tool(
993
1494
  async (rawParams) => {
994
- const params = normalizeToolInputArgs((rawParams ?? {}) as Record<string, unknown>)
1495
+ const params = normalizeBrowserActionParams((rawParams ?? {}) as Record<string, unknown>)
995
1496
  try {
996
1497
  const action = String(params.action || '').trim()
997
1498
 
@@ -1029,9 +1530,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1029
1530
  }
1030
1531
 
1031
1532
  if (action === 'read_page') {
1032
- const url = typeof params.url === 'string' ? params.url : ''
1033
- if (url) {
1034
- await callMcpTool('browser_navigate', { url })
1533
+ const target = pickBrowserTargetFromParams(params)
1534
+ if (target) {
1535
+ await callMcpTool('browser_navigate', { url: resolveBrowserNavigationTarget(cwd, target) })
1035
1536
  try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
1036
1537
  }
1037
1538
  return stringifyStructured(await captureStructuredObservation())
@@ -1106,6 +1607,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1106
1607
  if (v !== undefined && v !== null && v !== '') args[k] = v
1107
1608
  }
1108
1609
 
1610
+ if (action === 'navigate') {
1611
+ const target = pickBrowserTargetFromParams(params)
1612
+ if (!target) return 'Error: url or filePath is required for navigate.'
1613
+ args.url = resolveBrowserNavigationTarget(cwd, target)
1614
+ delete args.filePath
1615
+ delete args.path
1616
+ delete args.href
1617
+ delete args.link
1618
+ delete args.target
1619
+ delete args.page
1620
+ }
1621
+
1109
1622
  if (action === 'tabs') {
1110
1623
  args.action = typeof params.tabAction === 'string' ? params.tabAction : 'list'
1111
1624
  delete args.tabAction
@@ -1118,9 +1631,13 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1118
1631
  args.values = Array.isArray(args.option) ? args.option : [String(args.option)]
1119
1632
  delete args.option
1120
1633
  }
1634
+ if (action === 'select' && args.values === undefined && args.value !== undefined) {
1635
+ args.values = Array.isArray(args.value) ? args.value : [String(args.value)]
1636
+ delete args.value
1637
+ }
1121
1638
 
1122
1639
  if ((action === 'screenshot' || action === 'snapshot') && args.url) {
1123
- const navUrl = args.url
1640
+ const navUrl = resolveBrowserNavigationTarget(cwd, String(args.url))
1124
1641
  delete args.url
1125
1642
  await callMcpTool('browser_navigate', { url: navUrl })
1126
1643
  try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
@@ -1139,6 +1656,15 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1139
1656
  } catch {
1140
1657
  await new Promise((r) => setTimeout(r, 1200))
1141
1658
  }
1659
+ try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
1660
+ }
1661
+
1662
+ if (['click', 'hover', 'type', 'select'].includes(action) && !args.ref && typeof args.element === 'string') {
1663
+ const selectorResult = await performSelectorDomAction(action as 'click' | 'hover' | 'type' | 'select', args)
1664
+ if (!selectorResult) return `Error: ${action} requires a target element.`
1665
+ if (!selectorResult.ok) return `Error: ${selectorResult.error}`
1666
+ try { await captureStructuredObservation() } catch { /* ignore */ }
1667
+ return selectorResult.output
1142
1668
  }
1143
1669
 
1144
1670
  let result = await callMcpTool(mcpTool, args, { saveTo: typeof params.saveTo === 'string' ? params.saveTo : undefined })
@@ -1219,9 +1745,34 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1219
1745
  const cmdArgs = (normalized.args ?? normalized.arguments) as string | undefined
1220
1746
  try {
1221
1747
  if (!command) return 'Error: command is required.'
1222
- const spawnArgs = ['browser', command, '--json']
1223
- if (cmdArgs) spawnArgs.push(...cmdArgs.split(/\s+/).filter(Boolean))
1224
- const result = spawnSync(openclawPath, spawnArgs, { encoding: 'utf-8', timeout: 60_000, maxBuffer: MAX_OUTPUT })
1748
+ const parsedArgs = cmdArgs ? cmdArgs.split(/\s+/).filter(Boolean) : []
1749
+ const runBrowserCommand = (browserCommand: string, browserArgs: string[]) => {
1750
+ const spawnArgs = ['browser', '--json', browserCommand, ...browserArgs]
1751
+ return spawnSync(openclawPath, spawnArgs, {
1752
+ encoding: 'utf-8',
1753
+ timeout: 60_000,
1754
+ maxBuffer: MAX_OUTPUT,
1755
+ })
1756
+ }
1757
+
1758
+ if (command === 'capture') {
1759
+ const outputs: string[] = []
1760
+ if (parsedArgs.length > 0) {
1761
+ const openResult = runBrowserCommand('open', parsedArgs)
1762
+ if (openResult.status !== 0) {
1763
+ return `Error (exit ${openResult.status}): ${openResult.stderr || openResult.stdout || 'unknown'}`
1764
+ }
1765
+ if (openResult.stdout?.trim()) outputs.push(openResult.stdout.trim())
1766
+ }
1767
+ const screenshotResult = runBrowserCommand('screenshot', [])
1768
+ if (screenshotResult.status !== 0) {
1769
+ return `Error (exit ${screenshotResult.status}): ${screenshotResult.stderr || screenshotResult.stdout || 'unknown'}`
1770
+ }
1771
+ if (screenshotResult.stdout?.trim()) outputs.push(screenshotResult.stdout.trim())
1772
+ return truncate(outputs.join('\n').trim() || '(no output)', MAX_OUTPUT)
1773
+ }
1774
+
1775
+ const result = runBrowserCommand(command, parsedArgs)
1225
1776
  if (result.status !== 0) return `Error (exit ${result.status}): ${result.stderr || result.stdout || 'unknown'}`
1226
1777
  return truncate(result.stdout || '(no output)', MAX_OUTPUT)
1227
1778
  } catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }