@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.
Files changed (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. 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 crypto from 'crypto'
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 = crypto.randomBytes(4).toString('hex')
168
+ const id = genId()
169
169
  const now = Date.now()
170
170
  const entry = {
171
171
  id,