@swarmclawai/swarmclaw 0.4.0 → 0.5.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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- 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 +2 -2
- 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/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- 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 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -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]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- 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 +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -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/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- 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 +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -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/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- 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 +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- 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 +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -15,6 +15,7 @@ import { buildSessionInfoTools } from './session-info'
|
|
|
15
15
|
import { buildConnectorTools } from './connector'
|
|
16
16
|
import { buildContextTools } from './context-mgmt'
|
|
17
17
|
import { buildSandboxTools } from './sandbox'
|
|
18
|
+
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
18
19
|
|
|
19
20
|
export type { ToolContext, SessionToolsResult }
|
|
20
21
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
@@ -95,6 +96,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
95
96
|
...buildConnectorTools(bctx),
|
|
96
97
|
...buildContextTools(bctx),
|
|
97
98
|
...buildSandboxTools(bctx),
|
|
99
|
+
...buildOpenClawNodeTools(bctx),
|
|
98
100
|
)
|
|
99
101
|
|
|
100
102
|
// ---------------------------------------------------------------------------
|
|
@@ -141,12 +143,12 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
141
143
|
type: 'tool_request',
|
|
142
144
|
toolId,
|
|
143
145
|
reason,
|
|
144
|
-
message: `Tool access request sent to user for "${toolId}".
|
|
146
|
+
message: `Tool access request sent to user for "${toolId}". The user will be prompted to grant access — once granted, a follow-up message will arrive and you should immediately proceed with the original task using the newly available tool.`,
|
|
145
147
|
})
|
|
146
148
|
},
|
|
147
149
|
{
|
|
148
150
|
name: 'request_tool_access',
|
|
149
|
-
description: 'Request access to a tool that is currently disabled. The user will be prompted to grant access.
|
|
151
|
+
description: 'Request access to a tool that is currently disabled. The user will be prompted to grant access, and a follow-up "Continue" message will be sent automatically once granted. End your current response after calling this — do NOT tell the user to "let you know" or ask them to confirm; the continuation is automatic.',
|
|
150
152
|
schema: z.object({
|
|
151
153
|
toolId: z.string().describe('The tool ID to request access for (e.g. manage_tasks, shell, claude_code)'),
|
|
152
154
|
reason: z.string().describe('Brief explanation of why you need this tool'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
-
import
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset } from '../memory-db'
|
|
6
6
|
import { loadSettings } from '../storage'
|
|
7
7
|
import type { ToolBuildContext } from './context'
|
|
@@ -81,7 +81,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
81
81
|
return `Error: image file not found: ${imagePath}`
|
|
82
82
|
}
|
|
83
83
|
try {
|
|
84
|
-
storedImage = await storeMemoryImageAsset(imagePath,
|
|
84
|
+
storedImage = await storeMemoryImageAsset(imagePath, genId(6))
|
|
85
85
|
} catch {
|
|
86
86
|
return `Error: failed to process image at ${imagePath}`
|
|
87
87
|
}
|
|
@@ -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: unknown) {
|
|
32
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
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: unknown) {
|
|
63
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
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: unknown) {
|
|
97
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
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
|
+
}
|
|
@@ -160,5 +160,38 @@ export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
160
160
|
),
|
|
161
161
|
)
|
|
162
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: unknown) {
|
|
181
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
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
|
+
|
|
163
196
|
return tools
|
|
164
197
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
interface RawSearchResult {
|
|
17
|
+
title?: string
|
|
18
|
+
url?: string
|
|
19
|
+
content?: string
|
|
20
|
+
description?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const UA = 'Mozilla/5.0 (compatible; SwarmClaw/1.0)'
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// DuckDuckGo
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function decodeDuckDuckGoUrl(rawUrl: string): string {
|
|
30
|
+
if (!rawUrl) return rawUrl
|
|
31
|
+
try {
|
|
32
|
+
const url = rawUrl.startsWith('http')
|
|
33
|
+
? new URL(rawUrl)
|
|
34
|
+
: new URL(rawUrl, 'https://duckduckgo.com')
|
|
35
|
+
const uddg = url.searchParams.get('uddg')
|
|
36
|
+
if (uddg) return decodeURIComponent(uddg)
|
|
37
|
+
return url.toString()
|
|
38
|
+
} catch {
|
|
39
|
+
const fromQuery = rawUrl.match(/[?&]uddg=([^&]+)/)?.[1]
|
|
40
|
+
if (fromQuery) {
|
|
41
|
+
try { return decodeURIComponent(fromQuery) } catch { /* noop */ }
|
|
42
|
+
}
|
|
43
|
+
return rawUrl
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class DuckDuckGoProvider implements WebSearchProvider {
|
|
48
|
+
id = 'duckduckgo'
|
|
49
|
+
name = 'DuckDuckGo'
|
|
50
|
+
|
|
51
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
52
|
+
const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
headers: { 'User-Agent': UA },
|
|
55
|
+
signal: AbortSignal.timeout(15000),
|
|
56
|
+
})
|
|
57
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
58
|
+
const html = await res.text()
|
|
59
|
+
const $ = cheerio.load(html)
|
|
60
|
+
const results: SearchResult[] = []
|
|
61
|
+
|
|
62
|
+
$('.result').each((_i, el) => {
|
|
63
|
+
if (results.length >= maxResults) return false
|
|
64
|
+
const link = $(el).find('a.result__a').first()
|
|
65
|
+
const rawHref = link.attr('href') || ''
|
|
66
|
+
const title = link.text().replace(/\s+/g, ' ').trim()
|
|
67
|
+
if (!rawHref || !title) return
|
|
68
|
+
const snippet = $(el).find('.result__snippet').first().text().replace(/\s+/g, ' ').trim()
|
|
69
|
+
results.push({ title, url: decodeDuckDuckGoUrl(rawHref), snippet })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (results.length === 0) {
|
|
73
|
+
$('a.result__a').each((_i, el) => {
|
|
74
|
+
if (results.length >= maxResults) return false
|
|
75
|
+
const rawHref = $(el).attr('href') || ''
|
|
76
|
+
const title = $(el).text().replace(/\s+/g, ' ').trim()
|
|
77
|
+
if (!rawHref || !title) return
|
|
78
|
+
results.push({ title, url: decodeDuckDuckGoUrl(rawHref), snippet: '' })
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Google (scraping)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
class GoogleProvider implements WebSearchProvider {
|
|
91
|
+
id = 'google'
|
|
92
|
+
name = 'Google'
|
|
93
|
+
|
|
94
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
95
|
+
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
headers: { 'User-Agent': UA, 'Accept-Language': 'en-US,en;q=0.9' },
|
|
98
|
+
signal: AbortSignal.timeout(15000),
|
|
99
|
+
})
|
|
100
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
101
|
+
const html = await res.text()
|
|
102
|
+
const $ = cheerio.load(html)
|
|
103
|
+
const results: SearchResult[] = []
|
|
104
|
+
|
|
105
|
+
$('div.g').each((_i, el) => {
|
|
106
|
+
if (results.length >= maxResults) return false
|
|
107
|
+
const anchor = $(el).find('a').first()
|
|
108
|
+
const href = anchor.attr('href') || ''
|
|
109
|
+
if (!href || href.startsWith('/search')) return
|
|
110
|
+
const title = $(el).find('h3').first().text().replace(/\s+/g, ' ').trim()
|
|
111
|
+
if (!title) return
|
|
112
|
+
// Snippet is in various containers depending on Google's layout
|
|
113
|
+
const snippet = $(el).find('[data-sncf], .VwiC3b, .st').first().text().replace(/\s+/g, ' ').trim()
|
|
114
|
+
results.push({ title, url: href, snippet })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return results
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Bing (scraping)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
class BingProvider implements WebSearchProvider {
|
|
126
|
+
id = 'bing'
|
|
127
|
+
name = 'Bing'
|
|
128
|
+
|
|
129
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
130
|
+
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
headers: { 'User-Agent': UA },
|
|
133
|
+
signal: AbortSignal.timeout(15000),
|
|
134
|
+
})
|
|
135
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
136
|
+
const html = await res.text()
|
|
137
|
+
const $ = cheerio.load(html)
|
|
138
|
+
const results: SearchResult[] = []
|
|
139
|
+
|
|
140
|
+
$('li.b_algo').each((_i, el) => {
|
|
141
|
+
if (results.length >= maxResults) return false
|
|
142
|
+
const anchor = $(el).find('h2 a').first()
|
|
143
|
+
const href = anchor.attr('href') || ''
|
|
144
|
+
const title = anchor.text().replace(/\s+/g, ' ').trim()
|
|
145
|
+
if (!href || !title) return
|
|
146
|
+
const snippet = $(el).find('.b_caption p').first().text().replace(/\s+/g, ' ').trim()
|
|
147
|
+
results.push({ title, url: href, snippet })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return results
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// SearXNG (JSON API)
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
class SearXNGProvider implements WebSearchProvider {
|
|
159
|
+
id = 'searxng'
|
|
160
|
+
name = 'SearXNG'
|
|
161
|
+
|
|
162
|
+
constructor(private baseUrl: string) {}
|
|
163
|
+
|
|
164
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
165
|
+
const url = `${this.baseUrl.replace(/\/+$/, '')}/search?q=${encodeURIComponent(query)}&format=json`
|
|
166
|
+
const res = await fetch(url, {
|
|
167
|
+
headers: { 'User-Agent': UA },
|
|
168
|
+
signal: AbortSignal.timeout(15000),
|
|
169
|
+
})
|
|
170
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
171
|
+
const data = await res.json()
|
|
172
|
+
const rawResults: RawSearchResult[] = Array.isArray(data.results) ? data.results : []
|
|
173
|
+
return rawResults.slice(0, maxResults).map((r) => ({
|
|
174
|
+
title: r.title || '',
|
|
175
|
+
url: r.url || '',
|
|
176
|
+
snippet: r.content || '',
|
|
177
|
+
}))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Tavily (API key required — from secrets)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
class TavilyProvider implements WebSearchProvider {
|
|
186
|
+
id = 'tavily'
|
|
187
|
+
name = 'Tavily'
|
|
188
|
+
|
|
189
|
+
constructor(private apiKey: string) {}
|
|
190
|
+
|
|
191
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
192
|
+
const res = await fetch('https://api.tavily.com/search', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
api_key: this.apiKey,
|
|
197
|
+
query,
|
|
198
|
+
max_results: maxResults,
|
|
199
|
+
}),
|
|
200
|
+
signal: AbortSignal.timeout(15000),
|
|
201
|
+
})
|
|
202
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
203
|
+
const data = await res.json()
|
|
204
|
+
const rawResults: RawSearchResult[] = Array.isArray(data.results) ? data.results : []
|
|
205
|
+
return rawResults.slice(0, maxResults).map((r) => ({
|
|
206
|
+
title: r.title || '',
|
|
207
|
+
url: r.url || '',
|
|
208
|
+
snippet: r.content || '',
|
|
209
|
+
}))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Brave Search (API key required — from secrets)
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
class BraveProvider implements WebSearchProvider {
|
|
218
|
+
id = 'brave'
|
|
219
|
+
name = 'Brave Search'
|
|
220
|
+
|
|
221
|
+
constructor(private apiKey: string) {}
|
|
222
|
+
|
|
223
|
+
async search(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
224
|
+
const res = await fetch(
|
|
225
|
+
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}`,
|
|
226
|
+
{
|
|
227
|
+
headers: {
|
|
228
|
+
'Accept': 'application/json',
|
|
229
|
+
'Accept-Encoding': 'gzip',
|
|
230
|
+
'X-Subscription-Token': this.apiKey,
|
|
231
|
+
},
|
|
232
|
+
signal: AbortSignal.timeout(15000),
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
236
|
+
const data = await res.json()
|
|
237
|
+
const rawResults: RawSearchResult[] = Array.isArray(data.web?.results) ? data.web.results : []
|
|
238
|
+
return rawResults.slice(0, maxResults).map((r) => ({
|
|
239
|
+
title: r.title || '',
|
|
240
|
+
url: r.url || '',
|
|
241
|
+
snippet: r.description || '',
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Factory
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
export async function getSearchProvider(settings: Partial<AppSettings>): Promise<WebSearchProvider> {
|
|
251
|
+
const providerId = settings.webSearchProvider || 'duckduckgo'
|
|
252
|
+
|
|
253
|
+
switch (providerId) {
|
|
254
|
+
case 'google':
|
|
255
|
+
return new GoogleProvider()
|
|
256
|
+
case 'bing':
|
|
257
|
+
return new BingProvider()
|
|
258
|
+
case 'searxng': {
|
|
259
|
+
const url = settings.searxngUrl || 'http://localhost:8080'
|
|
260
|
+
return new SearXNGProvider(url)
|
|
261
|
+
}
|
|
262
|
+
case 'tavily': {
|
|
263
|
+
const { getSecret } = await import('../storage')
|
|
264
|
+
const secret = await getSecret('tavily')
|
|
265
|
+
if (!secret?.value) throw new Error('Tavily requires an API key. Add a secret named "tavily" in Secrets.')
|
|
266
|
+
return new TavilyProvider(secret.value)
|
|
267
|
+
}
|
|
268
|
+
case 'brave': {
|
|
269
|
+
const { getSecret } = await import('../storage')
|
|
270
|
+
const secret = await getSecret('brave')
|
|
271
|
+
if (!secret?.value) throw new Error('Brave Search requires an API key. Add a secret named "brave" in Secrets.')
|
|
272
|
+
return new BraveProvider(secret.value)
|
|
273
|
+
}
|
|
274
|
+
default:
|
|
275
|
+
return new DuckDuckGoProvider()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -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,
|
|
@@ -131,8 +131,8 @@ describe('MCP tool block type wiring', () => {
|
|
|
131
131
|
'utf-8',
|
|
132
132
|
)
|
|
133
133
|
assert.ok(src.includes('mcpServerIds'), 'index.ts should reference mcpServerIds')
|
|
134
|
-
assert.ok(src.includes('
|
|
135
|
-
assert.ok(src.includes('
|
|
134
|
+
assert.ok(src.includes('connectMcpServer'), 'index.ts should connect configured MCP servers')
|
|
135
|
+
assert.ok(src.includes('mcpToolsToLangChain'), 'index.ts should inject MCP tools dynamically')
|
|
136
136
|
})
|
|
137
137
|
})
|
|
138
138
|
|
|
@@ -49,7 +49,7 @@ export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
name: 'execute_command',
|
|
52
|
-
description: 'Execute a shell command in the session working directory.
|
|
52
|
+
description: 'Execute a shell command in the session working directory. This is the PRIMARY tool for running servers, dev servers, installing packages, running scripts, git operations, and any command the user wants to run or test. Use background=true for long-running processes like servers. Supports timeout/yield controls.',
|
|
53
53
|
schema: z.object({
|
|
54
54
|
command: z.string().describe('The shell command to execute'),
|
|
55
55
|
background: z.boolean().optional().describe('If true, start command in background immediately'),
|