@swarmclawai/swarmclaw 0.2.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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import {
|
|
4
|
+
clearManagedProcess,
|
|
5
|
+
getManagedProcess,
|
|
6
|
+
killManagedProcess,
|
|
7
|
+
listManagedProcesses,
|
|
8
|
+
pollManagedProcess,
|
|
9
|
+
readManagedProcessLog,
|
|
10
|
+
removeManagedProcess,
|
|
11
|
+
startManagedProcess,
|
|
12
|
+
writeManagedProcessStdin,
|
|
13
|
+
} from '../process-manager'
|
|
14
|
+
import type { ToolBuildContext } from './context'
|
|
15
|
+
import { safePath, truncate, coerceEnvMap, MAX_OUTPUT } from './context'
|
|
16
|
+
|
|
17
|
+
export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
18
|
+
const tools: StructuredToolInterface[] = []
|
|
19
|
+
const { cwd, ctx, hasTool, commandTimeoutMs } = bctx
|
|
20
|
+
|
|
21
|
+
if (hasTool('shell')) {
|
|
22
|
+
tools.push(
|
|
23
|
+
tool(
|
|
24
|
+
async ({ command, background, yieldMs, timeoutSec, env, workdir }) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await startManagedProcess({
|
|
27
|
+
command,
|
|
28
|
+
cwd: workdir ? safePath(cwd, workdir) : cwd,
|
|
29
|
+
env: coerceEnvMap(env),
|
|
30
|
+
agentId: ctx?.agentId || null,
|
|
31
|
+
sessionId: ctx?.sessionId || null,
|
|
32
|
+
background: !!background,
|
|
33
|
+
yieldMs: typeof yieldMs === 'number' ? yieldMs : undefined,
|
|
34
|
+
timeoutMs: typeof timeoutSec === 'number'
|
|
35
|
+
? Math.max(1, Math.trunc(timeoutSec)) * 1000
|
|
36
|
+
: commandTimeoutMs,
|
|
37
|
+
})
|
|
38
|
+
if (result.status === 'completed') {
|
|
39
|
+
return truncate(result.output || '(no output)', MAX_OUTPUT)
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
status: 'running',
|
|
43
|
+
processId: result.processId,
|
|
44
|
+
tail: result.tail || '',
|
|
45
|
+
}, null, 2)
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
return truncate(`Error: ${err.message || String(err)}`, MAX_OUTPUT)
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'execute_command',
|
|
52
|
+
description: 'Execute a shell command in the session working directory. Supports background mode and timeout/yield controls.',
|
|
53
|
+
schema: z.object({
|
|
54
|
+
command: z.string().describe('The shell command to execute'),
|
|
55
|
+
background: z.boolean().optional().describe('If true, start command in background immediately'),
|
|
56
|
+
yieldMs: z.number().optional().describe('If command runs longer than this, return a running process id instead of blocking'),
|
|
57
|
+
timeoutSec: z.number().optional().describe('Per-command timeout in seconds'),
|
|
58
|
+
workdir: z.string().optional().describe('Relative working directory override'),
|
|
59
|
+
env: z.record(z.string(), z.string()).optional().describe('Environment variable overrides'),
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hasTool('process')) {
|
|
67
|
+
tools.push(
|
|
68
|
+
tool(
|
|
69
|
+
async ({ action, processId, offset, limit, data, eof, signal }) => {
|
|
70
|
+
try {
|
|
71
|
+
if (action === 'list') {
|
|
72
|
+
return JSON.stringify(listManagedProcesses(ctx?.agentId || null).map((p) => ({
|
|
73
|
+
id: p.id,
|
|
74
|
+
command: p.command,
|
|
75
|
+
status: p.status,
|
|
76
|
+
pid: p.pid,
|
|
77
|
+
startedAt: p.startedAt,
|
|
78
|
+
endedAt: p.endedAt,
|
|
79
|
+
exitCode: p.exitCode,
|
|
80
|
+
signal: p.signal,
|
|
81
|
+
})), null, 2)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!processId) return 'Error: processId is required for this action.'
|
|
85
|
+
|
|
86
|
+
const ownerCheck = getManagedProcess(processId)
|
|
87
|
+
if (ownerCheck && ctx?.sessionId && ownerCheck.sessionId && ownerCheck.sessionId !== ctx.sessionId) {
|
|
88
|
+
return `Error: process ${processId} belongs to a different session.`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action === 'poll') {
|
|
92
|
+
const res = pollManagedProcess(processId)
|
|
93
|
+
if (!res) return `Process not found: ${processId}`
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
id: res.process.id,
|
|
96
|
+
status: res.process.status,
|
|
97
|
+
exitCode: res.process.exitCode,
|
|
98
|
+
signal: res.process.signal,
|
|
99
|
+
chunk: res.chunk,
|
|
100
|
+
}, null, 2)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (action === 'log') {
|
|
104
|
+
const res = readManagedProcessLog(processId, offset, limit)
|
|
105
|
+
if (!res) return `Process not found: ${processId}`
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
id: res.process.id,
|
|
108
|
+
status: res.process.status,
|
|
109
|
+
totalLines: res.totalLines,
|
|
110
|
+
text: res.text,
|
|
111
|
+
}, null, 2)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (action === 'write') {
|
|
115
|
+
const out = writeManagedProcessStdin(processId, data || '', !!eof)
|
|
116
|
+
return out.ok ? `Wrote to process ${processId}` : `Error: ${out.error}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (action === 'kill') {
|
|
120
|
+
const out = killManagedProcess(processId, (signal as NodeJS.Signals) || 'SIGTERM')
|
|
121
|
+
return out.ok ? `Killed process ${processId}` : `Error: ${out.error}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (action === 'clear') {
|
|
125
|
+
const out = clearManagedProcess(processId)
|
|
126
|
+
return out.ok ? `Cleared process ${processId}` : `Error: ${out.error}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (action === 'remove') {
|
|
130
|
+
const out = removeManagedProcess(processId)
|
|
131
|
+
return out.ok ? `Removed process ${processId}` : `Error: ${out.error}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (action === 'status') {
|
|
135
|
+
const p = getManagedProcess(processId)
|
|
136
|
+
if (!p) return `Process not found: ${processId}`
|
|
137
|
+
return JSON.stringify({
|
|
138
|
+
id: p.id,
|
|
139
|
+
status: p.status,
|
|
140
|
+
pid: p.pid,
|
|
141
|
+
startedAt: p.startedAt,
|
|
142
|
+
endedAt: p.endedAt,
|
|
143
|
+
exitCode: p.exitCode,
|
|
144
|
+
signal: p.signal,
|
|
145
|
+
}, null, 2)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return `Unknown action "${action}".`
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
return `Error: ${err.message || String(err)}`
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'process_tool',
|
|
155
|
+
description: 'Manage long-running shell processes started by execute_command. Supports list, status, poll, log, write, kill, clear, and remove.',
|
|
156
|
+
schema: z.object({
|
|
157
|
+
action: z.enum(['list', 'status', 'poll', 'log', 'write', 'kill', 'clear', 'remove']),
|
|
158
|
+
processId: z.string().optional(),
|
|
159
|
+
offset: z.number().optional(),
|
|
160
|
+
limit: z.number().optional(),
|
|
161
|
+
data: z.string().optional(),
|
|
162
|
+
eof: z.boolean().optional(),
|
|
163
|
+
signal: z.string().optional().describe('Signal for kill action, e.g. SIGTERM or SIGKILL'),
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return tools
|
|
171
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import * as cheerio from 'cheerio'
|
|
6
|
+
import { UPLOAD_DIR } from '../storage'
|
|
7
|
+
import type { ToolBuildContext } from './context'
|
|
8
|
+
import { safePath, truncate, MAX_OUTPUT } from './context'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// DuckDuckGo redirect-URL decoder
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function decodeDuckDuckGoUrl(rawUrl: string): string {
|
|
15
|
+
if (!rawUrl) return rawUrl
|
|
16
|
+
try {
|
|
17
|
+
const url = rawUrl.startsWith('http')
|
|
18
|
+
? new URL(rawUrl)
|
|
19
|
+
: new URL(rawUrl, 'https://duckduckgo.com')
|
|
20
|
+
const uddg = url.searchParams.get('uddg')
|
|
21
|
+
if (uddg) return decodeURIComponent(uddg)
|
|
22
|
+
return url.toString()
|
|
23
|
+
} catch {
|
|
24
|
+
const fromQuery = rawUrl.match(/[?&]uddg=([^&]+)/)?.[1]
|
|
25
|
+
if (fromQuery) {
|
|
26
|
+
try { return decodeURIComponent(fromQuery) } catch { /* noop */ }
|
|
27
|
+
}
|
|
28
|
+
return rawUrl
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Global registry of active browser instances for cleanup sweeps
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export const activeBrowsers = new Map<string, { client: any; server: any; createdAt: number }>()
|
|
37
|
+
|
|
38
|
+
/** Kill all browser instances that have been alive longer than maxAge (default 30 min) */
|
|
39
|
+
export function sweepOrphanedBrowsers(maxAgeMs = 30 * 60 * 1000): number {
|
|
40
|
+
const now = Date.now()
|
|
41
|
+
let cleaned = 0
|
|
42
|
+
for (const [key, entry] of activeBrowsers) {
|
|
43
|
+
if (now - entry.createdAt > maxAgeMs) {
|
|
44
|
+
try { entry.client?.close?.() } catch { /* ignore */ }
|
|
45
|
+
try { entry.server?.close?.() } catch { /* ignore */ }
|
|
46
|
+
activeBrowsers.delete(key)
|
|
47
|
+
cleaned++
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return cleaned
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Kill a specific session's browser instance */
|
|
54
|
+
export function cleanupSessionBrowser(sessionId: string): void {
|
|
55
|
+
const entry = activeBrowsers.get(sessionId)
|
|
56
|
+
if (entry) {
|
|
57
|
+
try { entry.client?.close?.() } catch { /* ignore */ }
|
|
58
|
+
try { entry.server?.close?.() } catch { /* ignore */ }
|
|
59
|
+
activeBrowsers.delete(sessionId)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get count of active browser instances */
|
|
64
|
+
export function getActiveBrowserCount(): number {
|
|
65
|
+
return activeBrowsers.size
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Check if a specific session has an active browser */
|
|
69
|
+
export function hasActiveBrowser(sessionId: string): boolean {
|
|
70
|
+
return activeBrowsers.has(sessionId)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// buildWebTools
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
78
|
+
const tools: StructuredToolInterface[] = []
|
|
79
|
+
const { cwd, ctx, cleanupFns } = bctx
|
|
80
|
+
|
|
81
|
+
// ---- web_search --------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
if (bctx.hasTool('web_search')) {
|
|
84
|
+
tools.push(
|
|
85
|
+
tool(
|
|
86
|
+
async ({ query, maxResults }) => {
|
|
87
|
+
try {
|
|
88
|
+
const limit = Math.min(maxResults || 5, 10)
|
|
89
|
+
const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`
|
|
90
|
+
const res = await fetch(url, {
|
|
91
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
92
|
+
signal: AbortSignal.timeout(15000),
|
|
93
|
+
})
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
return `Error searching web: HTTP ${res.status} ${res.statusText}`
|
|
96
|
+
}
|
|
97
|
+
const html = await res.text()
|
|
98
|
+
const $ = cheerio.load(html)
|
|
99
|
+
const results: { title: string; url: string; snippet: string }[] = []
|
|
100
|
+
|
|
101
|
+
// Primary parser: DuckDuckGo result cards
|
|
102
|
+
$('.result').each((_i, el) => {
|
|
103
|
+
if (results.length >= limit) return false
|
|
104
|
+
const link = $(el).find('a.result__a').first()
|
|
105
|
+
const rawHref = link.attr('href') || ''
|
|
106
|
+
const title = link.text().replace(/\s+/g, ' ').trim()
|
|
107
|
+
if (!rawHref || !title) return
|
|
108
|
+
const snippet = $(el).find('.result__snippet').first().text().replace(/\s+/g, ' ').trim()
|
|
109
|
+
results.push({
|
|
110
|
+
title,
|
|
111
|
+
url: decodeDuckDuckGoUrl(rawHref),
|
|
112
|
+
snippet,
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Fallback parser: any result__a anchors
|
|
117
|
+
if (results.length === 0) {
|
|
118
|
+
$('a.result__a').each((_i, el) => {
|
|
119
|
+
if (results.length >= limit) return false
|
|
120
|
+
const rawHref = $(el).attr('href') || ''
|
|
121
|
+
const title = $(el).text().replace(/\s+/g, ' ').trim()
|
|
122
|
+
if (!rawHref || !title) return
|
|
123
|
+
results.push({
|
|
124
|
+
title,
|
|
125
|
+
url: decodeDuckDuckGoUrl(rawHref),
|
|
126
|
+
snippet: '',
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return results.length > 0
|
|
132
|
+
? JSON.stringify(results, null, 2)
|
|
133
|
+
: 'No results found.'
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
return `Error searching web: ${err.message}`
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'web_search',
|
|
140
|
+
description: 'Search the web using DuckDuckGo. Returns an array of results with title, url, and snippet.',
|
|
141
|
+
schema: z.object({
|
|
142
|
+
query: z.string().describe('Search query'),
|
|
143
|
+
maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- web_fetch ---------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
if (bctx.hasTool('web_fetch')) {
|
|
153
|
+
tools.push(
|
|
154
|
+
tool(
|
|
155
|
+
async ({ url }) => {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
159
|
+
signal: AbortSignal.timeout(15000),
|
|
160
|
+
})
|
|
161
|
+
if (!res.ok) return `HTTP ${res.status}: ${res.statusText}`
|
|
162
|
+
const html = await res.text()
|
|
163
|
+
// Use cheerio for robust HTML text extraction
|
|
164
|
+
const $ = cheerio.load(html)
|
|
165
|
+
$('script, style, noscript, nav, footer, header').remove()
|
|
166
|
+
// Prefer article/main content if available
|
|
167
|
+
const main = $('article, main, [role="main"]').first()
|
|
168
|
+
let text = (main.length ? main.text() : $('body').text())
|
|
169
|
+
.replace(/\s+/g, ' ')
|
|
170
|
+
.trim()
|
|
171
|
+
return truncate(text, MAX_OUTPUT)
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
return `Error fetching URL: ${err.message}`
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'web_fetch',
|
|
178
|
+
description: 'Fetch a URL and return its text content (HTML stripped). Useful for reading web pages.',
|
|
179
|
+
schema: z.object({
|
|
180
|
+
url: z.string().describe('The URL to fetch'),
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- browser -----------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
if (bctx.hasTool('browser')) {
|
|
190
|
+
// In-process Playwright MCP client via @playwright/mcp programmatic API
|
|
191
|
+
const sessionKey = ctx?.sessionId || `anon-${Date.now()}`
|
|
192
|
+
let mcpClient: any = null
|
|
193
|
+
let mcpServer: any = null
|
|
194
|
+
let mcpInitializing: Promise<void> | null = null
|
|
195
|
+
|
|
196
|
+
const ensureMcp = (): Promise<void> => {
|
|
197
|
+
if (mcpClient) return Promise.resolve()
|
|
198
|
+
if (mcpInitializing) return mcpInitializing
|
|
199
|
+
mcpInitializing = (async () => {
|
|
200
|
+
const { createConnection } = await import('@playwright/mcp')
|
|
201
|
+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
|
|
202
|
+
const { InMemoryTransport } = await import('@modelcontextprotocol/sdk/inMemory.js')
|
|
203
|
+
|
|
204
|
+
const server = await createConnection({
|
|
205
|
+
browser: {
|
|
206
|
+
launchOptions: { headless: true },
|
|
207
|
+
isolated: true,
|
|
208
|
+
},
|
|
209
|
+
imageResponses: 'allow',
|
|
210
|
+
capabilities: ['core', 'pdf', 'vision', 'network'],
|
|
211
|
+
})
|
|
212
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
|
213
|
+
const client = new Client({ name: 'swarmclaw', version: '1.0' })
|
|
214
|
+
await Promise.all([
|
|
215
|
+
client.connect(clientTransport),
|
|
216
|
+
server.connect(serverTransport),
|
|
217
|
+
])
|
|
218
|
+
mcpClient = client
|
|
219
|
+
mcpServer = server
|
|
220
|
+
// Register in global tracker
|
|
221
|
+
activeBrowsers.set(sessionKey, { client, server, createdAt: Date.now() })
|
|
222
|
+
})()
|
|
223
|
+
return mcpInitializing
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Register cleanup for this session's browser
|
|
227
|
+
cleanupFns.push(async () => {
|
|
228
|
+
try { mcpClient?.close?.() } catch { /* ignore */ }
|
|
229
|
+
try { mcpServer?.close?.() } catch { /* ignore */ }
|
|
230
|
+
activeBrowsers.delete(sessionKey)
|
|
231
|
+
mcpClient = null
|
|
232
|
+
mcpServer = null
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
/** Strip Playwright debug noise — keep page context for the LLM */
|
|
236
|
+
const cleanPlaywrightOutput = (text: string): string => {
|
|
237
|
+
// Remove "### Ran Playwright code" blocks (internal debug)
|
|
238
|
+
text = text.replace(/### Ran Playwright code[\s\S]*?(?=###|$)/g, '')
|
|
239
|
+
// Truncate snapshot to first 40 lines so LLM has page context without flooding
|
|
240
|
+
text = text.replace(/### Snapshot\n([\s\S]*?)(?=###|$)/g, (_match, snapshot) => {
|
|
241
|
+
const lines = (snapshot as string).split('\n')
|
|
242
|
+
if (lines.length > 40) {
|
|
243
|
+
return 'Page elements:\n' + lines.slice(0, 40).join('\n') + '\n... (truncated)\n'
|
|
244
|
+
}
|
|
245
|
+
return 'Page elements:\n' + snapshot
|
|
246
|
+
})
|
|
247
|
+
// Clean headers
|
|
248
|
+
text = text.replace(/^### Result\n/gm, '')
|
|
249
|
+
text = text.replace(/^### Page\n/gm, '')
|
|
250
|
+
return text.replace(/\n{3,}/g, '\n').trim()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const callMcpTool = async (
|
|
254
|
+
toolName: string,
|
|
255
|
+
args: Record<string, any>,
|
|
256
|
+
options?: { saveTo?: string },
|
|
257
|
+
): Promise<string> => {
|
|
258
|
+
await ensureMcp()
|
|
259
|
+
const result = await mcpClient.callTool({ name: toolName, arguments: args })
|
|
260
|
+
const isError = result?.isError === true
|
|
261
|
+
const content = result?.content
|
|
262
|
+
const savedPaths: string[] = []
|
|
263
|
+
|
|
264
|
+
const saveArtifact = (buffer: Buffer, suggestedExt: string): void => {
|
|
265
|
+
const rawSaveTo = options?.saveTo?.trim()
|
|
266
|
+
if (!rawSaveTo) return
|
|
267
|
+
let resolved = safePath(cwd, rawSaveTo)
|
|
268
|
+
if (!path.extname(resolved) && suggestedExt) {
|
|
269
|
+
resolved = `${resolved}.${suggestedExt}`
|
|
270
|
+
}
|
|
271
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
272
|
+
fs.writeFileSync(resolved, buffer)
|
|
273
|
+
savedPaths.push(resolved)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (Array.isArray(content)) {
|
|
277
|
+
const parts: string[] = []
|
|
278
|
+
let hasBinaryImage = false
|
|
279
|
+
for (const c of content) {
|
|
280
|
+
if (c.type === 'image' && c.data) {
|
|
281
|
+
hasBinaryImage = true
|
|
282
|
+
const imageBuffer = Buffer.from(c.data, 'base64')
|
|
283
|
+
const filename = `screenshot-${Date.now()}.png`
|
|
284
|
+
const filepath = path.join(UPLOAD_DIR, filename)
|
|
285
|
+
fs.writeFileSync(filepath, imageBuffer)
|
|
286
|
+
saveArtifact(imageBuffer, 'png')
|
|
287
|
+
parts.push(``)
|
|
288
|
+
} else if (c.type === 'resource' && c.resource?.blob) {
|
|
289
|
+
const ext = c.resource.mimeType?.includes('pdf') ? 'pdf' : 'bin'
|
|
290
|
+
const resourceBuffer = Buffer.from(c.resource.blob, 'base64')
|
|
291
|
+
const filename = `browser-${Date.now()}.${ext}`
|
|
292
|
+
const filepath = path.join(UPLOAD_DIR, filename)
|
|
293
|
+
fs.writeFileSync(filepath, resourceBuffer)
|
|
294
|
+
saveArtifact(resourceBuffer, ext)
|
|
295
|
+
parts.push(`[Download ${filename}](/api/uploads/${filename})`)
|
|
296
|
+
} else {
|
|
297
|
+
let text = c.text || ''
|
|
298
|
+
// Detect file paths in output (e.g. PDF save returns a local path)
|
|
299
|
+
const fileMatch = text.match(/\]\((\.\.\/[^\s)]+|\/[^\s)]+\.(pdf|png|jpg|jpeg|gif|webp|html|mp4|webm))\)/)
|
|
300
|
+
if (fileMatch) {
|
|
301
|
+
const rawPath = fileMatch[1]
|
|
302
|
+
const srcPath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath)
|
|
303
|
+
if (fs.existsSync(srcPath)) {
|
|
304
|
+
const ext = path.extname(srcPath).slice(1).toLowerCase()
|
|
305
|
+
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
|
|
306
|
+
// Skip file-path images if we already have a binary image (avoids duplicates)
|
|
307
|
+
if (IMAGE_EXTS.includes(ext) && hasBinaryImage) {
|
|
308
|
+
parts.push(isError ? text : cleanPlaywrightOutput(text))
|
|
309
|
+
} else {
|
|
310
|
+
const filename = `browser-${Date.now()}.${ext}`
|
|
311
|
+
const destPath = path.join(UPLOAD_DIR, filename)
|
|
312
|
+
fs.copyFileSync(srcPath, destPath)
|
|
313
|
+
if (options?.saveTo?.trim()) {
|
|
314
|
+
const raw = options.saveTo.trim()
|
|
315
|
+
let targetPath = safePath(cwd, raw)
|
|
316
|
+
if (!path.extname(targetPath)) targetPath = `${targetPath}.${ext}`
|
|
317
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
318
|
+
fs.copyFileSync(srcPath, targetPath)
|
|
319
|
+
savedPaths.push(targetPath)
|
|
320
|
+
}
|
|
321
|
+
if (IMAGE_EXTS.includes(ext)) {
|
|
322
|
+
parts.push(``)
|
|
323
|
+
} else {
|
|
324
|
+
parts.push(`[Download ${filename}](/api/uploads/${filename})`)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
parts.push(isError ? text : cleanPlaywrightOutput(text))
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
parts.push(isError ? text : cleanPlaywrightOutput(text))
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (savedPaths.length > 0) {
|
|
336
|
+
const unique = Array.from(new Set(savedPaths))
|
|
337
|
+
const rendered = unique.map((p) => path.relative(cwd, p) || '.').join(', ')
|
|
338
|
+
parts.push(`Saved to: ${rendered}`)
|
|
339
|
+
}
|
|
340
|
+
return parts.join('\n')
|
|
341
|
+
}
|
|
342
|
+
return JSON.stringify(result)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Action-to-MCP tool mapping
|
|
346
|
+
const MCP_TOOL_MAP: Record<string, string> = {
|
|
347
|
+
navigate: 'browser_navigate',
|
|
348
|
+
screenshot: 'browser_take_screenshot',
|
|
349
|
+
snapshot: 'browser_snapshot',
|
|
350
|
+
click: 'browser_click',
|
|
351
|
+
type: 'browser_type',
|
|
352
|
+
press_key: 'browser_press_key',
|
|
353
|
+
select: 'browser_select_option',
|
|
354
|
+
evaluate: 'browser_evaluate',
|
|
355
|
+
pdf: 'browser_pdf_save',
|
|
356
|
+
upload: 'browser_file_upload',
|
|
357
|
+
wait: 'browser_wait_for',
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
tools.push(
|
|
361
|
+
tool(
|
|
362
|
+
async (params) => {
|
|
363
|
+
try {
|
|
364
|
+
const { action, ...rest } = params
|
|
365
|
+
// Build MCP args based on action
|
|
366
|
+
const mcpTool = MCP_TOOL_MAP[action]
|
|
367
|
+
if (!mcpTool) return `Unknown browser action: "${action}". Valid: ${Object.keys(MCP_TOOL_MAP).join(', ')}`
|
|
368
|
+
// Pass only defined (non-undefined) params to MCP
|
|
369
|
+
const args: Record<string, any> = {}
|
|
370
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
371
|
+
if (v !== undefined && v !== null && v !== '') args[k] = v
|
|
372
|
+
}
|
|
373
|
+
const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
|
|
374
|
+
? params.saveTo.trim()
|
|
375
|
+
: undefined
|
|
376
|
+
return await callMcpTool(mcpTool, args, { saveTo })
|
|
377
|
+
} catch (err: any) {
|
|
378
|
+
return `Error: ${err.message}`
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'browser',
|
|
383
|
+
description: [
|
|
384
|
+
'Control the browser. Use action to specify what to do.',
|
|
385
|
+
'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).',
|
|
386
|
+
'Workflow: use snapshot to see the page and get element refs, then use click/type/select with those refs.',
|
|
387
|
+
'Screenshots are returned as images visible to the user. Use saveTo to persist screenshot/PDF artifacts to disk.',
|
|
388
|
+
].join(' '),
|
|
389
|
+
schema: z.object({
|
|
390
|
+
action: z.enum(['navigate', 'screenshot', 'snapshot', 'click', 'type', 'press_key', 'select', 'evaluate', 'pdf', 'upload', 'wait']).describe('The browser action to perform'),
|
|
391
|
+
url: z.string().optional().describe('URL to navigate to (for navigate action)'),
|
|
392
|
+
element: z.string().optional().describe('CSS selector or description of an element (for click/type/select)'),
|
|
393
|
+
ref: z.string().optional().describe('Element reference from a previous snapshot (for click/type/select/upload)'),
|
|
394
|
+
text: z.string().optional().describe('Text to type (for type action) or text to wait for (for wait action)'),
|
|
395
|
+
key: z.string().optional().describe('Key to press, e.g. Enter, Tab, Escape (for press_key action)'),
|
|
396
|
+
option: z.string().optional().describe('Option value or label to select (for select action)'),
|
|
397
|
+
expression: z.string().optional().describe('JavaScript expression to evaluate (for evaluate action)'),
|
|
398
|
+
paths: z.array(z.string()).optional().describe('File paths to upload (for upload action)'),
|
|
399
|
+
timeout: z.number().optional().describe('Timeout in milliseconds (for wait action, default 30000)'),
|
|
400
|
+
saveTo: z.string().optional().describe('Optional output path for screenshot/pdf artifacts (relative to working directory).'),
|
|
401
|
+
}),
|
|
402
|
+
},
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return tools
|
|
408
|
+
}
|