@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.
Files changed (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. 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}". Wait for the user to grant access before trying to use it.`,
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. Use this when you need a tool from the disabled tools list.',
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 crypto from 'crypto'
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, crypto.randomBytes(6).toString('hex'))
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 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,
@@ -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('mcp_list_tools'), 'index.ts should define mcp_list_tools tool')
135
- assert.ok(src.includes('mcp_call'), 'index.ts should define mcp_call tool')
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. Supports background mode and timeout/yield controls.',
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'),