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