@swarmclawai/swarmclaw 0.3.1 → 0.4.5
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 +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { ToolBuildContext } from './context'
|
|
4
|
+
|
|
5
|
+
export function buildOpenClawNodeTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
6
|
+
if (!bctx.hasTool('openclaw_nodes')) return []
|
|
7
|
+
|
|
8
|
+
const tools: StructuredToolInterface[] = []
|
|
9
|
+
|
|
10
|
+
tools.push(
|
|
11
|
+
tool(
|
|
12
|
+
async () => {
|
|
13
|
+
try {
|
|
14
|
+
const { listRunningConnectors } = await import('../connectors/manager')
|
|
15
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
16
|
+
if (!openclawConnectors.length) {
|
|
17
|
+
return JSON.stringify({ error: 'No running OpenClaw connector found.' })
|
|
18
|
+
}
|
|
19
|
+
const { getRunningInstance } = await import('../connectors/manager')
|
|
20
|
+
const inst = getRunningInstance(openclawConnectors[0].id)
|
|
21
|
+
if (!inst) return JSON.stringify({ error: 'OpenClaw connector instance not accessible.' })
|
|
22
|
+
|
|
23
|
+
// Proxy through RPC — use sendMessage as a workaround to invoke RPC
|
|
24
|
+
// We need direct RPC access, so check if the instance exposes it
|
|
25
|
+
// For now, return a helpful message about the integration
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
status: 'openclaw_nodes_list requires nodes.list RPC support on the gateway',
|
|
28
|
+
connectorId: openclawConnectors[0].id,
|
|
29
|
+
note: 'This feature requires the OpenClaw gateway to support nodes.* RPCs.',
|
|
30
|
+
})
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
return JSON.stringify({ error: err.message })
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'openclaw_nodes_list',
|
|
37
|
+
description: 'List connected nodes/IoT devices through the OpenClaw gateway. Requires a running OpenClaw connector with nodes.* RPC support.',
|
|
38
|
+
schema: z.object({}),
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
tools.push(
|
|
44
|
+
tool(
|
|
45
|
+
async ({ nodeId, action, params }) => {
|
|
46
|
+
try {
|
|
47
|
+
const { listRunningConnectors, getRunningInstance } = await import('../connectors/manager')
|
|
48
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
49
|
+
if (!openclawConnectors.length) {
|
|
50
|
+
return JSON.stringify({ error: 'No running OpenClaw connector found.' })
|
|
51
|
+
}
|
|
52
|
+
const inst = getRunningInstance(openclawConnectors[0].id)
|
|
53
|
+
if (!inst) return JSON.stringify({ error: 'OpenClaw connector instance not accessible.' })
|
|
54
|
+
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
status: 'openclaw_node_invoke requires nodes.invoke RPC support on the gateway',
|
|
57
|
+
nodeId,
|
|
58
|
+
action,
|
|
59
|
+
params: params || null,
|
|
60
|
+
connectorId: openclawConnectors[0].id,
|
|
61
|
+
})
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
return JSON.stringify({ error: err.message })
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'openclaw_node_invoke',
|
|
68
|
+
description: 'Invoke an action on a connected node/IoT device through the OpenClaw gateway.',
|
|
69
|
+
schema: z.object({
|
|
70
|
+
nodeId: z.string().describe('Target node ID'),
|
|
71
|
+
action: z.string().describe('Action to invoke on the node'),
|
|
72
|
+
params: z.record(z.string(), z.unknown()).optional().describe('Optional parameters for the action'),
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
tools.push(
|
|
79
|
+
tool(
|
|
80
|
+
async ({ nodeId, message }) => {
|
|
81
|
+
try {
|
|
82
|
+
const { listRunningConnectors, getRunningInstance } = await import('../connectors/manager')
|
|
83
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
84
|
+
if (!openclawConnectors.length) {
|
|
85
|
+
return JSON.stringify({ error: 'No running OpenClaw connector found.' })
|
|
86
|
+
}
|
|
87
|
+
const inst = getRunningInstance(openclawConnectors[0].id)
|
|
88
|
+
if (!inst) return JSON.stringify({ error: 'OpenClaw connector instance not accessible.' })
|
|
89
|
+
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
status: 'openclaw_node_notify requires nodes.notify RPC support on the gateway',
|
|
92
|
+
nodeId,
|
|
93
|
+
message,
|
|
94
|
+
connectorId: openclawConnectors[0].id,
|
|
95
|
+
})
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
return JSON.stringify({ error: err.message })
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'openclaw_node_notify',
|
|
102
|
+
description: 'Send a notification to a connected node/IoT device through the OpenClaw gateway.',
|
|
103
|
+
schema: z.object({
|
|
104
|
+
nodeId: z.string().describe('Target node ID'),
|
|
105
|
+
message: z.string().describe('Notification message'),
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return tools
|
|
112
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
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 { spawnSync } from 'child_process'
|
|
6
|
+
import { UPLOAD_DIR } from '../storage'
|
|
7
|
+
import { findBinaryOnPath, truncate, MAX_OUTPUT } from './context'
|
|
8
|
+
import type { ToolBuildContext } from './context'
|
|
9
|
+
|
|
10
|
+
function getDenoPath(): string | null {
|
|
11
|
+
return findBinaryOnPath('deno')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getPythonPath(): string | null {
|
|
15
|
+
return findBinaryOnPath('python3') ?? findBinaryOnPath('python')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const EXT_MAP: Record<string, string> = {
|
|
19
|
+
javascript: 'js',
|
|
20
|
+
typescript: 'ts',
|
|
21
|
+
python: 'py',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
25
|
+
if (!bctx.hasTool('sandbox')) return []
|
|
26
|
+
|
|
27
|
+
const tools: StructuredToolInterface[] = []
|
|
28
|
+
|
|
29
|
+
tools.push(
|
|
30
|
+
tool(
|
|
31
|
+
async ({ language, code, timeoutSec }) => {
|
|
32
|
+
const timeout = Math.min(Math.max(timeoutSec ?? 60, 5), 300) * 1000
|
|
33
|
+
const ext = EXT_MAP[language]
|
|
34
|
+
const sessionId = bctx.ctx?.sessionId ?? 'unknown'
|
|
35
|
+
const sandboxDir = path.join('/tmp', `swarmclaw-sandbox-${sessionId}-${Date.now()}`)
|
|
36
|
+
|
|
37
|
+
// Check runtime availability
|
|
38
|
+
if ((language === 'javascript' || language === 'typescript') && !getDenoPath()) {
|
|
39
|
+
return JSON.stringify({ error: 'Deno is not installed. Install it with: curl -fsSL https://deno.land/install.sh | sh' })
|
|
40
|
+
}
|
|
41
|
+
if (language === 'python' && !getPythonPath()) {
|
|
42
|
+
return JSON.stringify({ error: 'Python is not installed. Install python3 to use Python sandbox.' })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
fs.mkdirSync(sandboxDir, { recursive: true })
|
|
47
|
+
const scriptFile = `script.${ext}`
|
|
48
|
+
const scriptPath = path.join(sandboxDir, scriptFile)
|
|
49
|
+
fs.writeFileSync(scriptPath, code, 'utf-8')
|
|
50
|
+
|
|
51
|
+
let result: ReturnType<typeof spawnSync>
|
|
52
|
+
|
|
53
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
54
|
+
const denoPath = getDenoPath()!
|
|
55
|
+
result = spawnSync(denoPath, [
|
|
56
|
+
'run',
|
|
57
|
+
'--allow-read=.',
|
|
58
|
+
'--allow-write=.',
|
|
59
|
+
'--allow-net',
|
|
60
|
+
'--deny-env',
|
|
61
|
+
'--no-prompt',
|
|
62
|
+
scriptFile,
|
|
63
|
+
], {
|
|
64
|
+
cwd: sandboxDir,
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
timeout,
|
|
67
|
+
maxBuffer: MAX_OUTPUT,
|
|
68
|
+
})
|
|
69
|
+
} else {
|
|
70
|
+
const pythonPath = getPythonPath()!
|
|
71
|
+
result = spawnSync(pythonPath, [scriptPath], {
|
|
72
|
+
cwd: sandboxDir,
|
|
73
|
+
encoding: 'utf-8',
|
|
74
|
+
timeout,
|
|
75
|
+
maxBuffer: MAX_OUTPUT,
|
|
76
|
+
env: { PATH: process.env.PATH || '/usr/bin:/bin' } as unknown as NodeJS.ProcessEnv,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stdout = truncate((result.stdout || '').toString(), MAX_OUTPUT)
|
|
81
|
+
const stderr = truncate((result.stderr || '').toString(), MAX_OUTPUT)
|
|
82
|
+
const exitCode = result.status ?? (result.error ? 1 : 0)
|
|
83
|
+
const timedOut = result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM'
|
|
84
|
+
|
|
85
|
+
// Scan for created files (exclude the script itself)
|
|
86
|
+
const artifacts: { name: string; url: string }[] = []
|
|
87
|
+
try {
|
|
88
|
+
const files = fs.readdirSync(sandboxDir)
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
if (file === scriptFile) continue
|
|
91
|
+
const src = path.join(sandboxDir, file)
|
|
92
|
+
const stat = fs.statSync(src)
|
|
93
|
+
if (!stat.isFile()) continue
|
|
94
|
+
// Copy to upload dir
|
|
95
|
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
96
|
+
const destName = `sandbox-${Date.now()}-${file}`
|
|
97
|
+
const dest = path.join(UPLOAD_DIR, destName)
|
|
98
|
+
fs.copyFileSync(src, dest)
|
|
99
|
+
artifacts.push({
|
|
100
|
+
name: file,
|
|
101
|
+
url: `/api/uploads/${encodeURIComponent(destName)}`,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// ignore scan errors
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
exitCode,
|
|
110
|
+
timedOut,
|
|
111
|
+
stdout,
|
|
112
|
+
stderr,
|
|
113
|
+
artifacts,
|
|
114
|
+
})
|
|
115
|
+
} catch (err: unknown) {
|
|
116
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
117
|
+
} finally {
|
|
118
|
+
try { fs.rmSync(sandboxDir, { recursive: true, force: true }) } catch { /* ignore */ }
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'sandbox_exec',
|
|
123
|
+
description:
|
|
124
|
+
'Execute code in an isolated sandbox. JS/TS runs via Deno with network access but no env vars. Python runs with a stripped environment. ' +
|
|
125
|
+
'Files created in the sandbox directory are returned as downloadable artifact URLs. Use this for data processing, API calls, calculations, and file generation.',
|
|
126
|
+
schema: z.object({
|
|
127
|
+
language: z.enum(['javascript', 'typescript', 'python']).describe('Programming language to execute'),
|
|
128
|
+
code: z.string().describe('Source code to run'),
|
|
129
|
+
timeoutSec: z.number().optional().describe('Execution timeout in seconds (default 60, max 300)'),
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
tools.push(
|
|
136
|
+
tool(
|
|
137
|
+
async () => {
|
|
138
|
+
const denoPath = getDenoPath()
|
|
139
|
+
const pythonPath = getPythonPath()
|
|
140
|
+
|
|
141
|
+
const runtimes: Record<string, { available: boolean; path: string | null; version: string | null }> = {}
|
|
142
|
+
|
|
143
|
+
for (const [name, bin] of [['deno', denoPath], ['python', pythonPath]] as const) {
|
|
144
|
+
if (bin) {
|
|
145
|
+
const ver = spawnSync(bin, ['--version'], { encoding: 'utf-8', timeout: 3000 })
|
|
146
|
+
const version = (ver.stdout || '').split('\n')[0]?.trim() || null
|
|
147
|
+
runtimes[name] = { available: true, path: bin, version }
|
|
148
|
+
} else {
|
|
149
|
+
runtimes[name] = { available: false, path: null, version: null }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return JSON.stringify(runtimes)
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'sandbox_list_runtimes',
|
|
157
|
+
description: 'List available sandbox runtimes (Deno for JS/TS, Python) and their versions. Use this to check what languages are available before running code.',
|
|
158
|
+
schema: z.object({}),
|
|
159
|
+
},
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// ---- openclaw_sandbox (CLI passthrough) -----------------------------------
|
|
164
|
+
|
|
165
|
+
const openclawSandboxPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
|
|
166
|
+
if (openclawSandboxPath) {
|
|
167
|
+
tools.push(
|
|
168
|
+
tool(
|
|
169
|
+
async ({ code, explain }) => {
|
|
170
|
+
try {
|
|
171
|
+
const args = explain ? ['sandbox', 'explain', code] : ['sandbox', 'run', code]
|
|
172
|
+
const result = spawnSync(openclawSandboxPath, args, {
|
|
173
|
+
encoding: 'utf-8',
|
|
174
|
+
timeout: 60_000,
|
|
175
|
+
maxBuffer: MAX_OUTPUT,
|
|
176
|
+
})
|
|
177
|
+
const stdout = truncate((result.stdout || '').trim(), MAX_OUTPUT)
|
|
178
|
+
const stderr = truncate((result.stderr || '').trim(), MAX_OUTPUT)
|
|
179
|
+
return JSON.stringify({ exitCode: result.status ?? 0, stdout, stderr })
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
return JSON.stringify({ error: err.message })
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'openclaw_sandbox',
|
|
186
|
+
description: 'Execute or explain code through the OpenClaw CLI sandbox. CLI passthrough to `openclaw sandbox run|explain <code>`. Requires openclaw/clawdbot CLI on PATH.',
|
|
187
|
+
schema: z.object({
|
|
188
|
+
code: z.string().describe('Code to run or explain'),
|
|
189
|
+
explain: z.boolean().optional().describe('If true, explain the code instead of running it'),
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return tools
|
|
197
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio'
|
|
2
|
+
import type { AppSettings } from '@/types'
|
|
3
|
+
|
|
4
|
+
export interface SearchResult {
|
|
5
|
+
title: string
|
|
6
|
+
url: string
|
|
7
|
+
snippet: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WebSearchProvider {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
search(query: string, maxResults: number): Promise<SearchResult[]>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const UA = 'Mozilla/5.0 (compatible; SwarmClaw/1.0)'
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// DuckDuckGo
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function decodeDuckDuckGoUrl(rawUrl: string): string {
|
|
23
|
+
if (!rawUrl) return rawUrl
|
|
24
|
+
try {
|
|
25
|
+
const url = rawUrl.startsWith('http')
|
|
26
|
+
? new URL(rawUrl)
|
|
27
|
+
: new URL(rawUrl, 'https://duckduckgo.com')
|
|
28
|
+
const uddg = url.searchParams.get('uddg')
|
|
29
|
+
if (uddg) return decodeURIComponent(uddg)
|
|
30
|
+
return url.toString()
|
|
31
|
+
} catch {
|
|
32
|
+
const fromQuery = rawUrl.match(/[?&]uddg=([^&]+)/)?.[1]
|
|
33
|
+
if (fromQuery) {
|
|
34
|
+
try { return decodeURIComponent(fromQuery) } catch { /* noop */ }
|
|
35
|
+
}
|
|
36
|
+
return rawUrl
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class DuckDuckGoProvider implements WebSearchProvider {
|
|
41
|
+
id = 'duckduckgo'
|
|
42
|
+
name = 'DuckDuckGo'
|
|
43
|
+
|
|
44
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
45
|
+
const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
headers: { 'User-Agent': UA },
|
|
48
|
+
signal: AbortSignal.timeout(15000),
|
|
49
|
+
})
|
|
50
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
51
|
+
const html = await res.text()
|
|
52
|
+
const $ = cheerio.load(html)
|
|
53
|
+
const results: SearchResult[] = []
|
|
54
|
+
|
|
55
|
+
$('.result').each((_i, el) => {
|
|
56
|
+
if (results.length >= maxResults) return false
|
|
57
|
+
const link = $(el).find('a.result__a').first()
|
|
58
|
+
const rawHref = link.attr('href') || ''
|
|
59
|
+
const title = link.text().replace(/\s+/g, ' ').trim()
|
|
60
|
+
if (!rawHref || !title) return
|
|
61
|
+
const snippet = $(el).find('.result__snippet').first().text().replace(/\s+/g, ' ').trim()
|
|
62
|
+
results.push({ title, url: decodeDuckDuckGoUrl(rawHref), snippet })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (results.length === 0) {
|
|
66
|
+
$('a.result__a').each((_i, el) => {
|
|
67
|
+
if (results.length >= maxResults) return false
|
|
68
|
+
const rawHref = $(el).attr('href') || ''
|
|
69
|
+
const title = $(el).text().replace(/\s+/g, ' ').trim()
|
|
70
|
+
if (!rawHref || !title) return
|
|
71
|
+
results.push({ title, url: decodeDuckDuckGoUrl(rawHref), snippet: '' })
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return results
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Google (scraping)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
class GoogleProvider implements WebSearchProvider {
|
|
84
|
+
id = 'google'
|
|
85
|
+
name = 'Google'
|
|
86
|
+
|
|
87
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
88
|
+
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`
|
|
89
|
+
const res = await fetch(url, {
|
|
90
|
+
headers: { 'User-Agent': UA, 'Accept-Language': 'en-US,en;q=0.9' },
|
|
91
|
+
signal: AbortSignal.timeout(15000),
|
|
92
|
+
})
|
|
93
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
94
|
+
const html = await res.text()
|
|
95
|
+
const $ = cheerio.load(html)
|
|
96
|
+
const results: SearchResult[] = []
|
|
97
|
+
|
|
98
|
+
$('div.g').each((_i, el) => {
|
|
99
|
+
if (results.length >= maxResults) return false
|
|
100
|
+
const anchor = $(el).find('a').first()
|
|
101
|
+
const href = anchor.attr('href') || ''
|
|
102
|
+
if (!href || href.startsWith('/search')) return
|
|
103
|
+
const title = $(el).find('h3').first().text().replace(/\s+/g, ' ').trim()
|
|
104
|
+
if (!title) return
|
|
105
|
+
// Snippet is in various containers depending on Google's layout
|
|
106
|
+
const snippet = $(el).find('[data-sncf], .VwiC3b, .st').first().text().replace(/\s+/g, ' ').trim()
|
|
107
|
+
results.push({ title, url: href, snippet })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return results
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Bing (scraping)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
class BingProvider implements WebSearchProvider {
|
|
119
|
+
id = 'bing'
|
|
120
|
+
name = 'Bing'
|
|
121
|
+
|
|
122
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
123
|
+
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
headers: { 'User-Agent': UA },
|
|
126
|
+
signal: AbortSignal.timeout(15000),
|
|
127
|
+
})
|
|
128
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
129
|
+
const html = await res.text()
|
|
130
|
+
const $ = cheerio.load(html)
|
|
131
|
+
const results: SearchResult[] = []
|
|
132
|
+
|
|
133
|
+
$('li.b_algo').each((_i, el) => {
|
|
134
|
+
if (results.length >= maxResults) return false
|
|
135
|
+
const anchor = $(el).find('h2 a').first()
|
|
136
|
+
const href = anchor.attr('href') || ''
|
|
137
|
+
const title = anchor.text().replace(/\s+/g, ' ').trim()
|
|
138
|
+
if (!href || !title) return
|
|
139
|
+
const snippet = $(el).find('.b_caption p').first().text().replace(/\s+/g, ' ').trim()
|
|
140
|
+
results.push({ title, url: href, snippet })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return results
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// SearXNG (JSON API)
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
class SearXNGProvider implements WebSearchProvider {
|
|
152
|
+
id = 'searxng'
|
|
153
|
+
name = 'SearXNG'
|
|
154
|
+
|
|
155
|
+
constructor(private baseUrl: string) {}
|
|
156
|
+
|
|
157
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
158
|
+
const url = `${this.baseUrl.replace(/\/+$/, '')}/search?q=${encodeURIComponent(query)}&format=json`
|
|
159
|
+
const res = await fetch(url, {
|
|
160
|
+
headers: { 'User-Agent': UA },
|
|
161
|
+
signal: AbortSignal.timeout(15000),
|
|
162
|
+
})
|
|
163
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
164
|
+
const data = await res.json()
|
|
165
|
+
const rawResults = Array.isArray(data.results) ? data.results : []
|
|
166
|
+
return rawResults.slice(0, maxResults).map((r: any) => ({
|
|
167
|
+
title: r.title || '',
|
|
168
|
+
url: r.url || '',
|
|
169
|
+
snippet: r.content || '',
|
|
170
|
+
}))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Tavily (API key required — from secrets)
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
class TavilyProvider implements WebSearchProvider {
|
|
179
|
+
id = 'tavily'
|
|
180
|
+
name = 'Tavily'
|
|
181
|
+
|
|
182
|
+
constructor(private apiKey: string) {}
|
|
183
|
+
|
|
184
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
185
|
+
const res = await fetch('https://api.tavily.com/search', {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
api_key: this.apiKey,
|
|
190
|
+
query,
|
|
191
|
+
max_results: maxResults,
|
|
192
|
+
}),
|
|
193
|
+
signal: AbortSignal.timeout(15000),
|
|
194
|
+
})
|
|
195
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
196
|
+
const data = await res.json()
|
|
197
|
+
const rawResults = Array.isArray(data.results) ? data.results : []
|
|
198
|
+
return rawResults.slice(0, maxResults).map((r: any) => ({
|
|
199
|
+
title: r.title || '',
|
|
200
|
+
url: r.url || '',
|
|
201
|
+
snippet: r.content || '',
|
|
202
|
+
}))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Brave Search (API key required — from secrets)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
class BraveProvider implements WebSearchProvider {
|
|
211
|
+
id = 'brave'
|
|
212
|
+
name = 'Brave Search'
|
|
213
|
+
|
|
214
|
+
constructor(private apiKey: string) {}
|
|
215
|
+
|
|
216
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
217
|
+
const res = await fetch(
|
|
218
|
+
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}`,
|
|
219
|
+
{
|
|
220
|
+
headers: {
|
|
221
|
+
'Accept': 'application/json',
|
|
222
|
+
'Accept-Encoding': 'gzip',
|
|
223
|
+
'X-Subscription-Token': this.apiKey,
|
|
224
|
+
},
|
|
225
|
+
signal: AbortSignal.timeout(15000),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
229
|
+
const data = await res.json()
|
|
230
|
+
const rawResults = Array.isArray(data.web?.results) ? data.web.results : []
|
|
231
|
+
return rawResults.slice(0, maxResults).map((r: any) => ({
|
|
232
|
+
title: r.title || '',
|
|
233
|
+
url: r.url || '',
|
|
234
|
+
snippet: r.description || '',
|
|
235
|
+
}))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Factory
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
export async function getSearchProvider(settings: Partial<AppSettings>): Promise<WebSearchProvider> {
|
|
244
|
+
const providerId = settings.webSearchProvider || 'duckduckgo'
|
|
245
|
+
|
|
246
|
+
switch (providerId) {
|
|
247
|
+
case 'google':
|
|
248
|
+
return new GoogleProvider()
|
|
249
|
+
case 'bing':
|
|
250
|
+
return new BingProvider()
|
|
251
|
+
case 'searxng': {
|
|
252
|
+
const url = settings.searxngUrl || 'http://localhost:8080'
|
|
253
|
+
return new SearXNGProvider(url)
|
|
254
|
+
}
|
|
255
|
+
case 'tavily': {
|
|
256
|
+
const { getSecret } = await import('../storage')
|
|
257
|
+
const secret = await getSecret('tavily')
|
|
258
|
+
if (!secret?.value) throw new Error('Tavily requires an API key. Add a secret named "tavily" in Secrets.')
|
|
259
|
+
return new TavilyProvider(secret.value)
|
|
260
|
+
}
|
|
261
|
+
case 'brave': {
|
|
262
|
+
const { getSecret } = await import('../storage')
|
|
263
|
+
const secret = await getSecret('brave')
|
|
264
|
+
if (!secret?.value) throw new Error('Brave Search requires an API key. Add a secret named "brave" in Secrets.')
|
|
265
|
+
return new BraveProvider(secret.value)
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
return new DuckDuckGoProvider()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
4
|
import { loadSessions, saveSessions, loadAgents } from '../storage'
|
|
5
5
|
import type { ToolBuildContext } from './context'
|
|
6
6
|
|
|
@@ -165,7 +165,7 @@ export function buildSessionInfoTools(bctx: ToolBuildContext): StructuredToolInt
|
|
|
165
165
|
const sourceSession = ctx?.sessionId ? sessions[ctx.sessionId] : null
|
|
166
166
|
const ownerUser = sourceSession?.user || 'system'
|
|
167
167
|
|
|
168
|
-
const id =
|
|
168
|
+
const id = genId()
|
|
169
169
|
const now = Date.now()
|
|
170
170
|
const entry = {
|
|
171
171
|
id,
|