@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.
- package/README.md +82 -39
- 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 +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- 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/graph/route.ts +46 -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/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -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 +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- 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/checkpoint-timeline.tsx +112 -0
- 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 +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- 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 +37 -7
- package/src/components/home/home-view.tsx +54 -24
- 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 +87 -19
- 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-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- 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 +28 -9
- 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/hint-tip.tsx +31 -0
- 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 +149 -4
- 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 +224 -0
- 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 +72 -48
- 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 +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -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 +115 -16
- 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 +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -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 +32 -2
- 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 +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- 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 +78 -0
- 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 +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- 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 +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- 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
|
-
|
|
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 { /*
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
if (bctx.hasTool('web_search')) {
|
|
165
|
+
if (bctx.hasTool('web')) {
|
|
117
166
|
tools.push(
|
|
118
167
|
tool(
|
|
119
|
-
async (
|
|
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: '
|
|
144
|
-
description:
|
|
145
|
-
schema: z.object({
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
fs.writeFileSync(filepath, imageBuffer)
|
|
290
|
-
saveArtifact(imageBuffer, 'png')
|
|
291
|
-
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(``)
|
|
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
|
|
296
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
parts.push(``)
|
|
327
|
-
} else {
|
|
328
|
-
parts.push(`[Download ${filename}](/api/uploads/${filename})`)
|
|
329
|
-
}
|
|
265
|
+
parts.push(IMAGE_EXTS.includes(ext) ? `` : `[Download ${filename}](/api/uploads/${filename})`)
|
|
330
266
|
}
|
|
331
|
-
} else
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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}"
|
|
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
|
-
|
|
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'])
|
|
447
|
-
url: z.string().optional().
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
//
|
|
464
|
-
|
|
465
|
-
if (bctx.hasTool('browser') || bctx.hasTool('openclaw_browser')) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|