@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -9,81 +9,57 @@ import { spawnSync } from 'child_process'
9
9
  import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
10
10
  import { getSearchProvider } from './search-providers'
11
11
  import { dedupeScreenshotMarkdownLines } from './web-output'
12
+ import { withRetry } from '../tool-retry'
13
+ import type { Plugin, PluginHooks } from '@/types'
14
+ import { getPluginManager } from '../plugins'
15
+ import { normalizeToolInputArgs } from './normalize-tool-args'
12
16
 
13
- // ---------------------------------------------------------------------------
14
- // Search result compression summarize verbose results before injecting into context
15
- // ---------------------------------------------------------------------------
16
-
17
- async function compressSearchResults(
18
- results: Array<{ title?: string; url?: string; snippet?: string }>,
19
- query: string,
20
- bctx: ToolBuildContext,
21
- ): Promise<string | null> {
17
+ // --- Search result compression logic ---
18
+ async function compressSearchResults(results: any[], query: string, bctx: any): Promise<string | null> {
22
19
  const session = bctx.resolveCurrentSession?.()
23
20
  if (!session?.provider || !session?.model) return null
24
-
25
21
  const { getProvider } = await import('@/lib/providers')
26
22
  const { loadCredentials, decryptKey } = await import('../storage')
27
23
  const providerEntry = getProvider(session.provider)
28
24
  if (!providerEntry?.handler?.streamChat) return null
29
-
30
- // Resolve API key
31
25
  let apiKey: string | undefined
32
26
  if (session.credentialId) {
33
27
  const creds = loadCredentials()
34
28
  const cred = creds[session.credentialId]
35
- if (cred) apiKey = decryptKey(cred)
29
+ if (cred) apiKey = decryptKey(cred.encryptedKey)
36
30
  }
37
-
38
31
  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.'
39
32
  const message = `Query: "${query}"\n\nResults:\n${JSON.stringify(results, null, 1)}\n\nSummarize these results concisely.`
40
-
41
33
  let compressed = ''
42
34
  await providerEntry.handler.streamChat({
43
- session: { ...session, messages: [] },
44
- message,
45
- apiKey,
46
- systemPrompt,
35
+ session: { ...session, messages: [] }, message, apiKey, systemPrompt,
47
36
  write: (raw: string) => {
48
- // Extract text data from SSE lines
49
37
  const lines = raw.split('\n').filter(Boolean)
50
38
  for (const line of lines) {
51
39
  if (!line.startsWith('data: ')) continue
52
40
  try {
53
41
  const ev = JSON.parse(line.slice(6))
54
42
  if (ev.t === 'd' && ev.text) compressed += ev.text
55
- } catch { /* skip */ }
43
+ } catch { /* ignore */ }
56
44
  }
57
45
  },
58
- active: new Map(),
59
- loadHistory: () => [],
46
+ active: new Map(), loadHistory: () => [],
60
47
  })
61
-
62
48
  return compressed.trim() || null
63
49
  }
64
50
 
65
- // ---------------------------------------------------------------------------
66
- // Global registry of active browser instances for cleanup sweeps
67
- // ---------------------------------------------------------------------------
68
-
69
51
  export const activeBrowsers = new Map<string, { client: any; server: any; createdAt: number }>()
70
-
71
- /** Kill all browser instances that have been alive longer than maxAge (default 30 min) */
72
52
  export function sweepOrphanedBrowsers(maxAgeMs = 30 * 60 * 1000): number {
73
- const now = Date.now()
74
- let cleaned = 0
53
+ const now = Date.now(); let cleaned = 0
75
54
  for (const [key, entry] of activeBrowsers) {
76
55
  if (now - entry.createdAt > maxAgeMs) {
77
56
  try { entry.client?.close?.() } catch { /* ignore */ }
78
57
  try { entry.server?.close?.() } catch { /* ignore */ }
79
- activeBrowsers.delete(key)
80
- cleaned++
58
+ activeBrowsers.delete(key); cleaned++
81
59
  }
82
60
  }
83
61
  return cleaned
84
62
  }
85
-
86
- /** Kill a specific session's browser instance */
87
63
  export function cleanupSessionBrowser(sessionId: string): void {
88
64
  const entry = activeBrowsers.get(sessionId)
89
65
  if (entry) {
@@ -92,106 +68,115 @@ export function cleanupSessionBrowser(sessionId: string): void {
92
68
  activeBrowsers.delete(sessionId)
93
69
  }
94
70
  }
95
-
96
- /** Get count of active browser instances */
97
- export function getActiveBrowserCount(): number {
98
- return activeBrowsers.size
71
+ export function getActiveBrowserCount(): number { return activeBrowsers.size }
72
+ export function hasActiveBrowser(sessionId: string): boolean { return activeBrowsers.has(sessionId) }
73
+
74
+ /**
75
+ * Unified Web Execution Logic
76
+ */
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ async function executeWebAction(args: Record<string, unknown>, bctx: any) {
79
+ const normalized = normalizeToolInputArgs(args)
80
+ const { action, query, url, maxResults } = normalized as { action: string; query?: string; url?: string; maxResults?: number }
81
+ try {
82
+ if (action === 'search') {
83
+ const searchQuery = query || url
84
+ if (!searchQuery) return 'Error: "query" is required for search action.'
85
+ const limit = Math.min(maxResults || 5, 10)
86
+ const { loadSettings } = await import('../storage')
87
+ const settings = loadSettings()
88
+ const provider = await getSearchProvider(settings)
89
+ const results = await provider.search(searchQuery, limit)
90
+ if (results.length === 0) return 'No results found.'
91
+ const raw = JSON.stringify(results, null, 2)
92
+ if (raw.length > 2000) {
93
+ const compressed = await compressSearchResults(results, searchQuery, bctx)
94
+ if (compressed) return compressed
95
+ }
96
+ return raw
97
+ } else if (action === 'fetch') {
98
+ const fetchUrl = url || query
99
+ if (!fetchUrl) return 'Error: "url" is required for fetch action.'
100
+ const res = await fetch(fetchUrl, {
101
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
102
+ signal: AbortSignal.timeout(15000),
103
+ })
104
+ if (!res.ok) return `HTTP ${res.status}: ${res.statusText}`
105
+ const contentType = res.headers.get('content-type') || ''
106
+ if (contentType.includes('application/pdf')) {
107
+ try {
108
+ const pdfMod = await import(/* webpackIgnore: true */ 'pdf-parse')
109
+ const pdfParse = ((pdfMod as Record<string, unknown>).default ?? pdfMod) as (buf: Buffer) => Promise<{ text: string }>
110
+ const arrayBuffer = await res.arrayBuffer()
111
+ const result = await pdfParse(Buffer.from(arrayBuffer))
112
+ return truncate(result.text, MAX_OUTPUT)
113
+ } catch (err: unknown) {
114
+ return `Error parsing PDF: ${err instanceof Error ? err.message : String(err)}`
115
+ }
116
+ }
117
+ const html = await res.text()
118
+ const $ = cheerio.load(html)
119
+ $('script, style, noscript, nav, footer, header').remove()
120
+ const main = $('article, main, [role="main"]').first()
121
+ const text = (main.length ? main.text() : $('body').text()).replace(/\s+/g, ' ').trim()
122
+ return truncate(text, MAX_OUTPUT)
123
+ }
124
+ return `Error: Unknown action "${action}"`
125
+ } catch (err: unknown) {
126
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
127
+ }
99
128
  }
100
129
 
101
- /** Check if a specific session has an active browser */
102
- export function hasActiveBrowser(sessionId: string): boolean {
103
- return activeBrowsers.has(sessionId)
130
+ /**
131
+ * Register as a Built-in Plugin
132
+ */
133
+ const WebPlugin: Plugin = {
134
+ name: 'Core Web',
135
+ description: 'Search the web and fetch content from URLs.',
136
+ hooks: {} as PluginHooks,
137
+ tools: [
138
+ {
139
+ name: 'web',
140
+ description: 'Unified web access tool. Actions: search, fetch.',
141
+ parameters: {
142
+ type: 'object',
143
+ properties: {
144
+ action: { type: 'string', enum: ['search', 'fetch'] },
145
+ query: { type: 'string' },
146
+ url: { type: 'string' },
147
+ maxResults: { type: 'number' }
148
+ },
149
+ required: ['action']
150
+ },
151
+ execute: async (args, context) => executeWebAction(args, { ...context.session, resolveCurrentSession: () => context.session })
152
+ }
153
+ ]
104
154
  }
105
155
 
106
- // ---------------------------------------------------------------------------
107
- // buildWebTools
108
- // ---------------------------------------------------------------------------
156
+ getPluginManager().registerBuiltin('web', WebPlugin)
109
157
 
158
+ /**
159
+ * Legacy Bridge
160
+ */
110
161
  export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[] {
111
162
  const tools: StructuredToolInterface[] = []
112
163
  const { cwd, ctx, cleanupFns } = bctx
113
164
 
114
- // ---- web_search --------------------------------------------------------
115
-
116
- if (bctx.hasTool('web_search')) {
165
+ if (bctx.hasTool('web')) {
117
166
  tools.push(
118
167
  tool(
119
- async ({ query, maxResults }) => {
120
- try {
121
- const limit = Math.min(maxResults || 5, 10)
122
- const { loadSettings } = await import('../storage')
123
- const settings = loadSettings()
124
- const provider = await getSearchProvider(settings)
125
- const results = await provider.search(query, limit)
126
- if (results.length === 0) return 'No results found.'
127
- const raw = JSON.stringify(results, null, 2)
128
- // Compress search results if they exceed 2000 chars
129
- if (raw.length > 2000) {
130
- try {
131
- const compressed = await compressSearchResults(results, query, bctx)
132
- if (compressed) return compressed
133
- } catch {
134
- // Compression failed — fall through to raw results
135
- }
136
- }
137
- return raw
138
- } catch (err: unknown) {
139
- return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
140
- }
141
- },
168
+ async (args) => executeWebAction(args, bctx),
142
169
  {
143
- name: 'web_search',
144
- description: 'Search the web for information. Returns results with title, url, and snippet.',
145
- schema: z.object({
146
- query: z.string().describe('Search query'),
147
- maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
148
- }),
149
- },
150
- ),
151
- )
152
- }
153
-
154
- // ---- web_fetch ---------------------------------------------------------
155
-
156
- if (bctx.hasTool('web_fetch')) {
157
- tools.push(
158
- tool(
159
- async ({ url }) => {
160
- try {
161
- const res = await fetch(url, {
162
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
163
- signal: AbortSignal.timeout(15000),
164
- })
165
- if (!res.ok) return `HTTP ${res.status}: ${res.statusText}`
166
- const html = await res.text()
167
- // Use cheerio for robust HTML text extraction
168
- const $ = cheerio.load(html)
169
- $('script, style, noscript, nav, footer, header').remove()
170
- // Prefer article/main content if available
171
- const main = $('article, main, [role="main"]').first()
172
- let text = (main.length ? main.text() : $('body').text())
173
- .replace(/\s+/g, ' ')
174
- .trim()
175
- return truncate(text, MAX_OUTPUT)
176
- } catch (err: unknown) {
177
- return `Error fetching URL: ${err instanceof Error ? err.message : String(err)}`
178
- }
179
- },
180
- {
181
- name: 'web_fetch',
182
- description: 'Fetch a URL and read its content (HTML stripped to text). How I read web pages and pull in external information.',
183
- schema: z.object({
184
- url: z.string().describe('The URL to fetch'),
185
- }),
186
- },
187
- ),
170
+ name: 'web',
171
+ description: WebPlugin.tools![0].description,
172
+ schema: z.object({}).passthrough()
173
+ }
174
+ )
188
175
  )
189
176
  }
190
177
 
191
- // ---- browser -----------------------------------------------------------
192
-
178
+ // Browser tool (kept as direct injection for now due to complexity)
193
179
  if (bctx.hasTool('browser')) {
194
- // In-process Playwright MCP client via @playwright/mcp programmatic API
195
180
  const sessionKey = ctx?.sessionId || `anon-${Date.now()}`
196
181
  let mcpClient: any = null
197
182
  let mcpServer: any = null
@@ -204,206 +189,111 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
204
189
  const { createConnection } = await import('@playwright/mcp')
205
190
  const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
206
191
  const { InMemoryTransport } = await import('@modelcontextprotocol/sdk/inMemory.js')
207
-
208
192
  const server = await createConnection({
209
- browser: {
210
- launchOptions: { headless: true },
211
- isolated: true,
212
- },
213
- imageResponses: 'allow',
214
- capabilities: ['core', 'pdf', 'vision', 'network'],
193
+ browser: { launchOptions: { headless: true }, isolated: true },
194
+ imageResponses: 'allow', capabilities: ['core', 'pdf', 'vision', 'network'],
215
195
  })
216
196
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
217
197
  const client = new Client({ name: 'swarmclaw', version: '1.0' })
218
- await Promise.all([
219
- client.connect(clientTransport),
220
- server.connect(serverTransport),
221
- ])
222
- mcpClient = client
223
- mcpServer = server
224
- // Register in global tracker
198
+ await Promise.all([client.connect(clientTransport), server.connect(serverTransport)])
199
+ mcpClient = client; mcpServer = server
225
200
  activeBrowsers.set(sessionKey, { client, server, createdAt: Date.now() })
226
201
  })()
227
202
  return mcpInitializing
228
203
  }
229
204
 
230
- // Register cleanup for this session's browser
231
205
  cleanupFns.push(async () => {
232
206
  try { mcpClient?.close?.() } catch { /* ignore */ }
233
207
  try { mcpServer?.close?.() } catch { /* ignore */ }
234
208
  activeBrowsers.delete(sessionKey)
235
- mcpClient = null
236
- mcpServer = null
209
+ mcpClient = null; mcpServer = null
237
210
  })
238
211
 
239
- /** Strip Playwright debug noise — keep page context for the LLM */
240
212
  const cleanPlaywrightOutput = (text: string): string => {
241
- // Remove "### Ran Playwright code" blocks (internal debug)
242
213
  text = text.replace(/### Ran Playwright code[\s\S]*?(?=###|$)/g, '')
243
- // Truncate snapshot to first 40 lines so LLM has page context without flooding
244
214
  text = text.replace(/### Snapshot\n([\s\S]*?)(?=###|$)/g, (_match, snapshot) => {
245
215
  const lines = (snapshot as string).split('\n')
246
- if (lines.length > 40) {
247
- return 'Page elements:\n' + lines.slice(0, 40).join('\n') + '\n... (truncated)\n'
248
- }
216
+ if (lines.length > 40) return 'Page elements:\n' + lines.slice(0, 40).join('\n') + '\n... (truncated)\n'
249
217
  return 'Page elements:\n' + snapshot
250
218
  })
251
- // Clean headers
252
- text = text.replace(/^### Result\n/gm, '')
253
- text = text.replace(/^### Page\n/gm, '')
219
+ text = text.replace(/^### Result\n/gm, ''); text = text.replace(/^### Page\n/gm, '')
254
220
  return text.replace(/\n{3,}/g, '\n').trim()
255
221
  }
256
222
 
257
- const callMcpTool = async (
258
- toolName: string,
259
- args: Record<string, any>,
260
- options?: { saveTo?: string },
261
- ): Promise<string> => {
223
+ const callMcpTool = async (toolName: string, args: Record<string, any>, options?: { saveTo?: string }): Promise<string> => {
262
224
  await ensureMcp()
263
225
  const result = await mcpClient.callTool({ name: toolName, arguments: args })
264
- const isError = result?.isError === true
265
- const content = result?.content
266
- const savedPaths: string[] = []
267
-
226
+ const isError = result?.isError === true; const content = result?.content; const savedPaths: string[] = []
268
227
  const saveArtifact = (buffer: Buffer, suggestedExt: string): void => {
269
228
  const rawSaveTo = options?.saveTo?.trim()
270
229
  if (!rawSaveTo) return
271
230
  let resolved = safePath(cwd, rawSaveTo)
272
- if (!path.extname(resolved) && suggestedExt) {
273
- resolved = `${resolved}.${suggestedExt}`
274
- }
275
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
276
- fs.writeFileSync(resolved, buffer)
231
+ if (!path.extname(resolved) && suggestedExt) resolved = `${resolved}.${suggestedExt}`
232
+ fs.mkdirSync(path.dirname(resolved), { recursive: true }); fs.writeFileSync(resolved, buffer)
277
233
  savedPaths.push(resolved)
278
234
  }
279
-
280
235
  if (Array.isArray(content)) {
281
236
  let parts: string[] = []
282
237
  const isScreenshotTool = toolName === 'browser_take_screenshot'
283
238
  const contentHasBinaryImage = content.some((c) => c.type === 'image' && !!c.data)
284
239
  for (const c of content) {
285
240
  if (c.type === 'image' && c.data) {
286
- const imageBuffer = Buffer.from(c.data, 'base64')
287
- const filename = `screenshot-${Date.now()}.png`
288
- const filepath = path.join(UPLOAD_DIR, filename)
289
- fs.writeFileSync(filepath, imageBuffer)
290
- saveArtifact(imageBuffer, 'png')
291
- parts.push(`![Screenshot](/api/uploads/${filename})`)
241
+ const imageBuffer = Buffer.from(c.data, 'base64'); const filename = `screenshot-${Date.now()}.png`
242
+ const filepath = path.join(UPLOAD_DIR, filename); fs.writeFileSync(filepath, imageBuffer)
243
+ saveArtifact(imageBuffer, 'png'); parts.push(`![Screenshot](/api/uploads/${filename})`)
292
244
  } else if (c.type === 'resource' && c.resource?.blob) {
293
245
  const ext = c.resource.mimeType?.includes('pdf') ? 'pdf' : 'bin'
294
- const resourceBuffer = Buffer.from(c.resource.blob, 'base64')
295
- const filename = `browser-${Date.now()}.${ext}`
296
- const filepath = path.join(UPLOAD_DIR, filename)
297
- fs.writeFileSync(filepath, resourceBuffer)
298
- saveArtifact(resourceBuffer, ext)
299
- parts.push(`[Download ${filename}](/api/uploads/${filename})`)
246
+ const resourceBuffer = Buffer.from(c.resource.blob, 'base64'); const filename = `browser-${Date.now()}.${ext}`
247
+ const filepath = path.join(UPLOAD_DIR, filename); fs.writeFileSync(filepath, resourceBuffer)
248
+ saveArtifact(resourceBuffer, ext); parts.push(`[Download ${filename}](/api/uploads/${filename})`)
300
249
  } else {
301
250
  let text = c.text || ''
302
- // Detect file paths in output (e.g. PDF save returns a local path)
303
251
  const fileMatch = text.match(/\]\((\.\.\/[^\s)]+|\/[^\s)]+\.(pdf|png|jpg|jpeg|gif|webp|html|mp4|webm))\)/)
304
252
  if (fileMatch) {
305
- const rawPath = fileMatch[1]
306
- const srcPath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath)
253
+ const rawPath = fileMatch[1]; const srcPath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath)
307
254
  if (fs.existsSync(srcPath)) {
308
- const ext = path.extname(srcPath).slice(1).toLowerCase()
309
- const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
310
- // Skip file-path images whenever MCP already returned image binary payloads.
311
- if (IMAGE_EXTS.includes(ext) && contentHasBinaryImage) {
312
- parts.push(isError ? text : cleanPlaywrightOutput(text))
313
- } else {
314
- const filename = `browser-${Date.now()}.${ext}`
315
- const destPath = path.join(UPLOAD_DIR, filename)
316
- fs.copyFileSync(srcPath, destPath)
255
+ const ext = path.extname(srcPath).slice(1).toLowerCase(); const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
256
+ if (IMAGE_EXTS.includes(ext) && contentHasBinaryImage) parts.push(isError ? text : cleanPlaywrightOutput(text))
257
+ else {
258
+ const filename = `browser-${Date.now()}.${ext}`; const destPath = path.join(UPLOAD_DIR, filename); fs.copyFileSync(srcPath, destPath)
317
259
  if (options?.saveTo?.trim()) {
318
- const raw = options.saveTo.trim()
319
- let targetPath = safePath(cwd, raw)
260
+ let targetPath = safePath(cwd, options.saveTo.trim())
320
261
  if (!path.extname(targetPath)) targetPath = `${targetPath}.${ext}`
321
- fs.mkdirSync(path.dirname(targetPath), { recursive: true })
322
- fs.copyFileSync(srcPath, targetPath)
262
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.copyFileSync(srcPath, targetPath)
323
263
  savedPaths.push(targetPath)
324
264
  }
325
- if (IMAGE_EXTS.includes(ext)) {
326
- parts.push(`![Screenshot](/api/uploads/${filename})`)
327
- } else {
328
- parts.push(`[Download ${filename}](/api/uploads/${filename})`)
329
- }
265
+ parts.push(IMAGE_EXTS.includes(ext) ? `![Screenshot](/api/uploads/${filename})` : `[Download ${filename}](/api/uploads/${filename})`)
330
266
  }
331
- } else {
332
- parts.push(isError ? text : cleanPlaywrightOutput(text))
333
- }
334
- } else {
335
- parts.push(isError ? text : cleanPlaywrightOutput(text))
336
- }
267
+ } else parts.push(isError ? text : cleanPlaywrightOutput(text))
268
+ } else parts.push(isError ? text : cleanPlaywrightOutput(text))
337
269
  }
338
270
  }
339
271
  if (isScreenshotTool) parts = dedupeScreenshotMarkdownLines(parts)
340
-
341
272
  if (savedPaths.length > 0) {
342
273
  const unique = Array.from(new Set(savedPaths))
343
- const rendered = unique.map((p) => path.relative(cwd, p) || '.').join(', ')
344
- parts.push(`Saved to: ${rendered}`)
274
+ parts.push(`Saved to: ${unique.map((p) => path.relative(cwd, p) || '.').join(', ')}`)
345
275
  }
346
276
  return parts.join('\n')
347
277
  }
348
278
  return JSON.stringify(result)
349
279
  }
350
280
 
351
- // Best-effort cookie/consent banner dismissal after navigation
352
- const dismissCookieBanners = async (
353
- mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>,
354
- ) => {
355
- // Wait briefly for consent overlays to appear
281
+ const dismissCookieBanners = async (mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>) => {
356
282
  await new Promise((r) => setTimeout(r, 1500))
357
- const js = `
358
- (() => {
359
- const sel = [
360
- // Common "Reject" / "Reject all" / "Decline" buttons
361
- 'button[id*="reject" i]', 'button[class*="reject" i]',
362
- 'a[id*="reject" i]', 'a[class*="reject" i]',
363
- '[data-testid*="reject" i]', '[data-action="reject"]',
364
- // OneTrust
365
- '#onetrust-reject-all-handler',
366
- // Cookiebot
367
- '#CybotCookiebotDialogBodyButtonDecline',
368
- // Didomi
369
- '#didomi-notice-disagree-button',
370
- // Quantcast / IAB TCF
371
- '.qc-cmp2-summary-buttons button:first-child',
372
- 'button.sp_choice_type_12',
373
- // Generic patterns
374
- 'button[aria-label*="reject" i]', 'button[aria-label*="decline" i]',
375
- 'button[aria-label*="deny" i]', 'button[aria-label*="refuse" i]',
376
- ];
377
- for (const s of sel) {
378
- const el = document.querySelector(s);
379
- if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; }
380
- }
381
- // Fallback: find buttons by visible text
382
- const btns = [...document.querySelectorAll('button, a[role="button"], [class*="cookie"] button, [class*="consent"] button, [id*="cookie"] button')];
383
- const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
384
- for (const b of btns) {
385
- const txt = (b.textContent || '').trim();
386
- if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; }
387
- }
388
- return 'none';
389
- })()
390
- `
283
+ const js = `(() => {
284
+ 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'];
285
+ for (const s of sel) { const el = document.querySelector(s); if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; } }
286
+ const btns = [...document.querySelectorAll('button, a[role="button"]')]; const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
287
+ for (const b of btns) { const txt = (b.textContent || '').trim(); if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; } }
288
+ return 'none';
289
+ })()`
391
290
  await mcpCall('browser_evaluate', { expression: js })
392
291
  }
393
292
 
394
- // Action-to-MCP tool mapping
395
293
  const MCP_TOOL_MAP: Record<string, string> = {
396
- navigate: 'browser_navigate',
397
- screenshot: 'browser_take_screenshot',
398
- snapshot: 'browser_snapshot',
399
- click: 'browser_click',
400
- type: 'browser_type',
401
- press_key: 'browser_press_key',
402
- select: 'browser_select_option',
403
- evaluate: 'browser_evaluate',
404
- pdf: 'browser_pdf_save',
405
- upload: 'browser_file_upload',
406
- wait: 'browser_wait_for',
294
+ navigate: 'browser_navigate', screenshot: 'browser_take_screenshot', snapshot: 'browser_snapshot', click: 'browser_click',
295
+ type: 'browser_type', press_key: 'browser_press_key', select: 'browser_select_option', evaluate: 'browser_evaluate',
296
+ pdf: 'browser_pdf_save', upload: 'browser_file_upload', wait: 'browser_wait_for',
407
297
  }
408
298
 
409
299
  tools.push(
@@ -411,92 +301,56 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
411
301
  async (params) => {
412
302
  try {
413
303
  const { action, ...rest } = params
414
- // Build MCP args based on action
415
304
  const mcpTool = MCP_TOOL_MAP[action]
416
- if (!mcpTool) return `Unknown browser action: "${action}". Valid: ${Object.keys(MCP_TOOL_MAP).join(', ')}`
417
- // Pass only defined (non-undefined) params to MCP
305
+ if (!mcpTool) return `Unknown browser action: "${action}"`
418
306
  const args: Record<string, any> = {}
419
- for (const [k, v] of Object.entries(rest)) {
420
- if (v !== undefined && v !== null && v !== '') args[k] = v
421
- }
422
- const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
423
- ? params.saveTo.trim()
424
- : undefined
425
- const result = await callMcpTool(mcpTool, args, { saveTo })
426
-
427
- // After navigation, attempt to dismiss cookie consent banners
428
- if (action === 'navigate') {
429
- try { await dismissCookieBanners(callMcpTool) } catch { /* best-effort */ }
430
- }
431
-
307
+ for (const [k, v] of Object.entries(rest)) { if (v !== undefined && v !== null && v !== '') args[k] = v }
308
+ const result = await callMcpTool(mcpTool, args, { saveTo: params.saveTo })
309
+ if (action === 'navigate') { try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ } }
432
310
  return result
433
- } catch (err: unknown) {
434
- return `Error: ${err instanceof Error ? err.message : String(err)}`
435
- }
311
+ } catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }
436
312
  },
437
313
  {
438
314
  name: 'browser',
439
- description: [
440
- 'Control the browser. Use action to specify what to do.',
441
- 'Actions: navigate (url), screenshot, snapshot (get page elements), click (element/ref), type (element/ref, text), press_key (key), select (element/ref, option), evaluate (expression), pdf, upload (paths, ref), wait (text/timeout).',
442
- 'Workflow: use snapshot to see the page and get element refs, then use click/type/select with those refs.',
443
- 'Screenshots are returned as images visible to the user. Use saveTo to persist screenshot/PDF artifacts to disk.',
444
- ].join(' '),
315
+ description: 'Control the browser. Actions: navigate, screenshot, snapshot, click, type, press_key, select, evaluate, pdf, upload, wait.',
445
316
  schema: z.object({
446
- action: z.enum(['navigate', 'screenshot', 'snapshot', 'click', 'type', 'press_key', 'select', 'evaluate', 'pdf', 'upload', 'wait']).describe('The browser action to perform'),
447
- url: z.string().optional().describe('URL to navigate to (for navigate action)'),
448
- element: z.string().optional().describe('CSS selector or description of an element (for click/type/select)'),
449
- ref: z.string().optional().describe('Element reference from a previous snapshot (for click/type/select/upload)'),
450
- text: z.string().optional().describe('Text to type (for type action) or text to wait for (for wait action)'),
451
- key: z.string().optional().describe('Key to press, e.g. Enter, Tab, Escape (for press_key action)'),
452
- option: z.string().optional().describe('Option value or label to select (for select action)'),
453
- expression: z.string().optional().describe('JavaScript expression to evaluate (for evaluate action)'),
454
- paths: z.array(z.string()).optional().describe('File paths to upload (for upload action)'),
455
- timeout: z.number().optional().describe('Timeout in milliseconds (for wait action, default 30000)'),
456
- saveTo: z.string().optional().describe('Optional output path for screenshot/pdf artifacts (relative to working directory).'),
317
+ action: z.enum(['navigate', 'screenshot', 'snapshot', 'click', 'type', 'press_key', 'select', 'evaluate', 'pdf', 'upload', 'wait']),
318
+ url: z.string().optional(), element: z.string().optional(), ref: z.string().optional(), text: z.string().optional(),
319
+ key: z.string().optional(), option: z.string().optional(), expression: z.string().optional(),
320
+ paths: z.array(z.string()).optional(), timeout: z.number().optional(), saveTo: z.string().optional(),
457
321
  }),
458
322
  },
459
323
  ),
460
324
  )
461
325
  }
462
326
 
463
- // ---- openclaw_browser (CLI passthrough) -----------------------------------
464
-
465
- if (bctx.hasTool('browser') || bctx.hasTool('openclaw_browser')) {
466
- const openclawPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
467
- if (openclawPath) {
468
- tools.push(
469
- tool(
470
- async ({ command, args: cmdArgs }) => {
471
- try {
472
- const spawnArgs = ['browser', command, '--json']
473
- if (cmdArgs) spawnArgs.push(...cmdArgs.split(/\s+/).filter(Boolean))
474
- const result = spawnSync(openclawPath, spawnArgs, {
475
- encoding: 'utf-8',
476
- timeout: 60_000,
477
- maxBuffer: MAX_OUTPUT,
478
- })
479
- const stdout = (result.stdout || '').trim()
480
- const stderr = (result.stderr || '').trim()
481
- if (result.status !== 0) {
482
- return `Error (exit ${result.status}): ${stderr || stdout || 'unknown error'}`
483
- }
484
- return truncate(stdout || '(no output)', MAX_OUTPUT)
485
- } catch (err: unknown) {
486
- return `Error: ${err instanceof Error ? err.message : String(err)}`
487
- }
488
- },
489
- {
490
- name: 'openclaw_browser',
491
- description: 'Control a browser through the OpenClaw CLI. Requires openclaw/clawdbot CLI on PATH. Passes through to `openclaw browser <command> --json`.',
492
- schema: z.object({
493
- command: z.string().describe('Browser command (navigate, screenshot, click, type, evaluate, etc.)'),
494
- args: z.string().optional().describe('Additional arguments as a space-separated string'),
495
- }),
496
- },
497
- ),
498
- )
499
- }
327
+ // openclaw_browser CLI passthrough
328
+ const openclawPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
329
+ if (openclawPath && (bctx.hasTool('browser') || bctx.hasTool('openclaw_browser'))) {
330
+ tools.push(
331
+ tool(
332
+ async (rawArgs) => {
333
+ const normalized = normalizeToolInputArgs((rawArgs ?? {}) as Record<string, unknown>)
334
+ const command = normalized.command as string | undefined
335
+ const cmdArgs = (normalized.args ?? normalized.arguments) as string | undefined
336
+ try {
337
+ if (!command) return 'Error: command is required.'
338
+ const spawnArgs = ['browser', command, '--json']
339
+ if (cmdArgs) spawnArgs.push(...cmdArgs.split(/\s+/).filter(Boolean))
340
+ const result = spawnSync(openclawPath, spawnArgs, { encoding: 'utf-8', timeout: 60_000, maxBuffer: MAX_OUTPUT })
341
+ if (result.status !== 0) return `Error (exit ${result.status}): ${result.stderr || result.stdout || 'unknown'}`
342
+ return truncate(result.stdout || '(no output)', MAX_OUTPUT)
343
+ } catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }
344
+ },
345
+ {
346
+ name: 'openclaw_browser',
347
+ description: 'Control a browser through the OpenClaw CLI.',
348
+ schema: z.object({
349
+ command: z.string(), args: z.string().optional(),
350
+ }),
351
+ },
352
+ ),
353
+ )
500
354
  }
501
355
 
502
356
  return tools