@swarmclawai/swarmclaw 0.4.0 → 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 (144) hide show
  1. package/README.md +13 -2
  2. package/next.config.ts +8 -0
  3. package/package.json +2 -1
  4. package/src/app/api/agents/[id]/route.ts +20 -21
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  6. package/src/app/api/agents/route.ts +3 -2
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/connectors/[id]/route.ts +10 -3
  9. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  10. package/src/app/api/connectors/route.ts +6 -3
  11. package/src/app/api/credentials/[id]/route.ts +2 -1
  12. package/src/app/api/credentials/route.ts +2 -2
  13. package/src/app/api/documents/route.ts +2 -2
  14. package/src/app/api/files/serve/route.ts +8 -0
  15. package/src/app/api/knowledge/[id]/route.ts +5 -4
  16. package/src/app/api/knowledge/upload/route.ts +2 -2
  17. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  18. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  19. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  20. package/src/app/api/mcp-servers/route.ts +2 -2
  21. package/src/app/api/memory/[id]/route.ts +9 -8
  22. package/src/app/api/memory/route.ts +2 -2
  23. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  24. package/src/app/api/openclaw/directory/route.ts +26 -0
  25. package/src/app/api/openclaw/discover/route.ts +61 -0
  26. package/src/app/api/openclaw/sync/route.ts +30 -0
  27. package/src/app/api/orchestrator/run/route.ts +2 -2
  28. package/src/app/api/projects/[id]/route.ts +55 -0
  29. package/src/app/api/projects/route.ts +27 -0
  30. package/src/app/api/providers/[id]/models/route.ts +2 -1
  31. package/src/app/api/providers/[id]/route.ts +13 -15
  32. package/src/app/api/providers/route.ts +2 -2
  33. package/src/app/api/schedules/[id]/route.ts +16 -18
  34. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  35. package/src/app/api/schedules/route.ts +2 -2
  36. package/src/app/api/secrets/[id]/route.ts +16 -17
  37. package/src/app/api/secrets/route.ts +2 -2
  38. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  39. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  40. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  41. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  42. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  43. package/src/app/api/sessions/[id]/route.ts +2 -1
  44. package/src/app/api/sessions/route.ts +2 -2
  45. package/src/app/api/skills/[id]/route.ts +23 -21
  46. package/src/app/api/skills/import/route.ts +2 -2
  47. package/src/app/api/skills/route.ts +2 -2
  48. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  49. package/src/app/api/tasks/[id]/route.ts +6 -5
  50. package/src/app/api/tasks/route.ts +2 -2
  51. package/src/app/api/tts/stream/route.ts +48 -0
  52. package/src/app/api/upload/route.ts +2 -2
  53. package/src/app/api/uploads/[filename]/route.ts +4 -1
  54. package/src/app/api/webhooks/[id]/route.ts +29 -31
  55. package/src/app/api/webhooks/route.ts +2 -2
  56. package/src/app/page.tsx +3 -24
  57. package/src/cli/index.js +28 -0
  58. package/src/cli/index.ts +1 -1
  59. package/src/cli/spec.js +2 -0
  60. package/src/components/agents/agent-list.tsx +3 -1
  61. package/src/components/agents/agent-sheet.tsx +116 -14
  62. package/src/components/chat/chat-area.tsx +27 -4
  63. package/src/components/chat/chat-header.tsx +141 -29
  64. package/src/components/chat/tool-call-bubble.tsx +9 -3
  65. package/src/components/chat/voice-overlay.tsx +80 -0
  66. package/src/components/connectors/connector-list.tsx +6 -2
  67. package/src/components/connectors/connector-sheet.tsx +31 -7
  68. package/src/components/layout/app-layout.tsx +47 -25
  69. package/src/components/projects/project-list.tsx +122 -0
  70. package/src/components/projects/project-sheet.tsx +135 -0
  71. package/src/components/schedules/schedule-list.tsx +3 -1
  72. package/src/components/sessions/new-session-sheet.tsx +6 -6
  73. package/src/components/sessions/session-card.tsx +1 -1
  74. package/src/components/sessions/session-list.tsx +7 -7
  75. package/src/components/shared/connector-platform-icon.tsx +4 -0
  76. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  77. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  78. package/src/components/shared/settings/section-web-search.tsx +56 -0
  79. package/src/components/shared/settings/settings-page.tsx +73 -0
  80. package/src/components/skills/skill-list.tsx +2 -1
  81. package/src/components/tasks/task-list.tsx +5 -2
  82. package/src/hooks/use-continuous-speech.ts +144 -0
  83. package/src/hooks/use-view-router.ts +52 -0
  84. package/src/hooks/use-voice-conversation.ts +80 -0
  85. package/src/lib/id.ts +6 -0
  86. package/src/lib/projects.ts +13 -0
  87. package/src/lib/provider-sets.ts +5 -0
  88. package/src/lib/providers/anthropic.ts +14 -1
  89. package/src/lib/providers/index.ts +6 -0
  90. package/src/lib/providers/ollama.ts +9 -1
  91. package/src/lib/providers/openai.ts +9 -1
  92. package/src/lib/providers/openclaw.ts +11 -0
  93. package/src/lib/server/api-routes.test.ts +5 -6
  94. package/src/lib/server/build-llm.ts +17 -4
  95. package/src/lib/server/chat-execution.ts +38 -4
  96. package/src/lib/server/collection-helpers.ts +54 -0
  97. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  98. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  99. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  100. package/src/lib/server/connectors/googlechat.ts +46 -7
  101. package/src/lib/server/connectors/manager.ts +392 -3
  102. package/src/lib/server/connectors/media.ts +2 -2
  103. package/src/lib/server/connectors/openclaw.ts +64 -0
  104. package/src/lib/server/connectors/pairing.test.ts +99 -0
  105. package/src/lib/server/connectors/pairing.ts +256 -0
  106. package/src/lib/server/connectors/signal.ts +1 -0
  107. package/src/lib/server/connectors/teams.ts +5 -5
  108. package/src/lib/server/connectors/types.ts +10 -0
  109. package/src/lib/server/execution-log.ts +3 -3
  110. package/src/lib/server/heartbeat-service.ts +1 -1
  111. package/src/lib/server/knowledge-db.test.ts +2 -33
  112. package/src/lib/server/main-agent-loop.ts +6 -6
  113. package/src/lib/server/memory-db.ts +6 -6
  114. package/src/lib/server/openclaw-approvals.ts +105 -0
  115. package/src/lib/server/openclaw-sync.ts +496 -0
  116. package/src/lib/server/orchestrator-lg.ts +30 -9
  117. package/src/lib/server/orchestrator.ts +4 -4
  118. package/src/lib/server/process-manager.ts +2 -2
  119. package/src/lib/server/queue.ts +22 -10
  120. package/src/lib/server/scheduler.ts +2 -2
  121. package/src/lib/server/session-mailbox.ts +2 -2
  122. package/src/lib/server/session-run-manager.ts +2 -2
  123. package/src/lib/server/session-tools/connector.ts +51 -4
  124. package/src/lib/server/session-tools/crud.ts +3 -3
  125. package/src/lib/server/session-tools/delegate.ts +3 -3
  126. package/src/lib/server/session-tools/file.ts +176 -3
  127. package/src/lib/server/session-tools/index.ts +2 -0
  128. package/src/lib/server/session-tools/memory.ts +2 -2
  129. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  130. package/src/lib/server/session-tools/sandbox.ts +33 -0
  131. package/src/lib/server/session-tools/search-providers.ts +270 -0
  132. package/src/lib/server/session-tools/session-info.ts +2 -2
  133. package/src/lib/server/session-tools/web.ts +47 -66
  134. package/src/lib/server/storage.ts +12 -0
  135. package/src/lib/server/stream-agent-chat.ts +29 -0
  136. package/src/lib/server/task-result.test.ts +44 -0
  137. package/src/lib/server/task-result.ts +14 -0
  138. package/src/lib/tool-definitions.ts +5 -3
  139. package/src/lib/tts-stream.ts +130 -0
  140. package/src/lib/view-routes.ts +28 -0
  141. package/src/proxy.ts +3 -0
  142. package/src/stores/use-app-store.ts +28 -1
  143. package/src/stores/use-chat-store.ts +9 -1
  144. package/src/types/index.ts +27 -2
@@ -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: 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
+
163
196
  return tools
164
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,
@@ -5,29 +5,9 @@ import path from 'path'
5
5
  import * as cheerio from 'cheerio'
6
6
  import { UPLOAD_DIR } from '../storage'
7
7
  import type { ToolBuildContext } from './context'
8
- import { safePath, truncate, MAX_OUTPUT } from './context'
9
-
10
- // ---------------------------------------------------------------------------
11
- // DuckDuckGo redirect-URL decoder
12
- // ---------------------------------------------------------------------------
13
-
14
- function decodeDuckDuckGoUrl(rawUrl: string): string {
15
- if (!rawUrl) return rawUrl
16
- try {
17
- const url = rawUrl.startsWith('http')
18
- ? new URL(rawUrl)
19
- : new URL(rawUrl, 'https://duckduckgo.com')
20
- const uddg = url.searchParams.get('uddg')
21
- if (uddg) return decodeURIComponent(uddg)
22
- return url.toString()
23
- } catch {
24
- const fromQuery = rawUrl.match(/[?&]uddg=([^&]+)/)?.[1]
25
- if (fromQuery) {
26
- try { return decodeURIComponent(fromQuery) } catch { /* noop */ }
27
- }
28
- return rawUrl
29
- }
30
- }
8
+ import { spawnSync } from 'child_process'
9
+ import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
10
+ import { getSearchProvider } from './search-providers'
31
11
 
32
12
  // ---------------------------------------------------------------------------
33
13
  // Global registry of active browser instances for cleanup sweeps
@@ -86,48 +66,10 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
86
66
  async ({ query, maxResults }) => {
87
67
  try {
88
68
  const limit = Math.min(maxResults || 5, 10)
89
- const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`
90
- const res = await fetch(url, {
91
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
92
- signal: AbortSignal.timeout(15000),
93
- })
94
- if (!res.ok) {
95
- return `Error searching web: HTTP ${res.status} ${res.statusText}`
96
- }
97
- const html = await res.text()
98
- const $ = cheerio.load(html)
99
- const results: { title: string; url: string; snippet: string }[] = []
100
-
101
- // Primary parser: DuckDuckGo result cards
102
- $('.result').each((_i, el) => {
103
- if (results.length >= limit) return false
104
- const link = $(el).find('a.result__a').first()
105
- const rawHref = link.attr('href') || ''
106
- const title = link.text().replace(/\s+/g, ' ').trim()
107
- if (!rawHref || !title) return
108
- const snippet = $(el).find('.result__snippet').first().text().replace(/\s+/g, ' ').trim()
109
- results.push({
110
- title,
111
- url: decodeDuckDuckGoUrl(rawHref),
112
- snippet,
113
- })
114
- })
115
-
116
- // Fallback parser: any result__a anchors
117
- if (results.length === 0) {
118
- $('a.result__a').each((_i, el) => {
119
- if (results.length >= limit) return false
120
- const rawHref = $(el).attr('href') || ''
121
- const title = $(el).text().replace(/\s+/g, ' ').trim()
122
- if (!rawHref || !title) return
123
- results.push({
124
- title,
125
- url: decodeDuckDuckGoUrl(rawHref),
126
- snippet: '',
127
- })
128
- })
129
- }
130
-
69
+ const { loadSettings } = await import('../storage')
70
+ const settings = loadSettings()
71
+ const provider = await getSearchProvider(settings)
72
+ const results = await provider.search(query, limit)
131
73
  return results.length > 0
132
74
  ? JSON.stringify(results, null, 2)
133
75
  : 'No results found.'
@@ -137,7 +79,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
137
79
  },
138
80
  {
139
81
  name: 'web_search',
140
- description: 'Search the web using DuckDuckGo. Returns an array of results with title, url, and snippet.',
82
+ description: 'Search the web. Returns an array of results with title, url, and snippet.',
141
83
  schema: z.object({
142
84
  query: z.string().describe('Search query'),
143
85
  maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
@@ -404,5 +346,44 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
404
346
  )
405
347
  }
406
348
 
349
+ // ---- openclaw_browser (CLI passthrough) -----------------------------------
350
+
351
+ if (bctx.hasTool('browser') || bctx.hasTool('openclaw_browser')) {
352
+ const openclawPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
353
+ if (openclawPath) {
354
+ tools.push(
355
+ tool(
356
+ async ({ command, args: cmdArgs }) => {
357
+ try {
358
+ const spawnArgs = ['browser', command, '--json']
359
+ if (cmdArgs) spawnArgs.push(...cmdArgs.split(/\s+/).filter(Boolean))
360
+ const result = spawnSync(openclawPath, spawnArgs, {
361
+ encoding: 'utf-8',
362
+ timeout: 60_000,
363
+ maxBuffer: MAX_OUTPUT,
364
+ })
365
+ const stdout = (result.stdout || '').trim()
366
+ const stderr = (result.stderr || '').trim()
367
+ if (result.status !== 0) {
368
+ return `Error (exit ${result.status}): ${stderr || stdout || 'unknown error'}`
369
+ }
370
+ return truncate(stdout || '(no output)', MAX_OUTPUT)
371
+ } catch (err: any) {
372
+ return `Error: ${err.message}`
373
+ }
374
+ },
375
+ {
376
+ name: 'openclaw_browser',
377
+ description: 'Control a browser through the OpenClaw CLI. Requires openclaw/clawdbot CLI on PATH. Passes through to `openclaw browser <command> --json`.',
378
+ schema: z.object({
379
+ command: z.string().describe('Browser command (navigate, screenshot, click, type, evaluate, etc.)'),
380
+ args: z.string().optional().describe('Additional arguments as a space-separated string'),
381
+ }),
382
+ },
383
+ ),
384
+ )
385
+ }
386
+ }
387
+
407
388
  return tools
408
389
  }
@@ -43,6 +43,7 @@ const COLLECTIONS = [
43
43
  'model_overrides',
44
44
  'mcp_servers',
45
45
  'webhook_logs',
46
+ 'projects',
46
47
  ] as const
47
48
 
48
49
  for (const table of COLLECTIONS) {
@@ -576,6 +577,17 @@ export function saveModelOverrides(m: Record<string, string[]>) {
576
577
  saveCollection('model_overrides', m)
577
578
  }
578
579
 
580
+ // --- Projects ---
581
+ export function loadProjects(): Record<string, any> {
582
+ return loadCollection('projects')
583
+ }
584
+
585
+ export function saveProjects(s: Record<string, any>) {
586
+ saveCollection('projects', s)
587
+ }
588
+
589
+ export function deleteProject(id: string) { deleteCollectionItem('projects', id) }
590
+
579
591
  // --- Skills ---
580
592
  export function loadSkills(): Record<string, any> {
581
593
  return loadCollection('skills')
@@ -168,11 +168,20 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
168
168
 
169
169
  // fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
170
170
  void fallbackCredentialIds
171
+
172
+ // Resolve agent's thinking level for provider-native params
173
+ let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
174
+ if (session.agentId) {
175
+ const agentsForThinking = loadAgents()
176
+ agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
177
+ }
178
+
171
179
  const llm = buildChatModel({
172
180
  provider: session.provider,
173
181
  model: session.model,
174
182
  apiKey,
175
183
  apiEndpoint: session.apiEndpoint,
184
+ thinkingLevel: agentThinkingLevel,
176
185
  })
177
186
 
178
187
  // Build stateModifier
@@ -228,6 +237,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
228
237
  stateModifierParts.push('You are a capable AI assistant with tool access. Be execution-oriented and outcome-focused.')
229
238
  }
230
239
 
240
+ // Thinking level guidance (applies to all providers via system prompt)
241
+ if (agentThinkingLevel) {
242
+ const thinkingGuidance: Record<string, string> = {
243
+ minimal: 'Be direct and concise. Skip extended analysis.',
244
+ low: 'Keep reasoning brief. Focus on key conclusions.',
245
+ medium: 'Provide moderate depth of analysis and reasoning.',
246
+ high: 'Think deeply and thoroughly. Show detailed reasoning.',
247
+ }
248
+ stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
249
+ }
250
+
231
251
  if ((session.tools || []).includes('memory') && session.agentId) {
232
252
  try {
233
253
  const memDb = getMemoryDb()
@@ -612,6 +632,15 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
612
632
  // Plugin hooks: afterAgentComplete
613
633
  await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
614
634
 
635
+ // OpenClaw auto-sync: push memory if enabled
636
+ try {
637
+ const { loadSyncConfig, pushMemoryToOpenClaw } = await import('./openclaw-sync')
638
+ const syncConfig = loadSyncConfig()
639
+ if (syncConfig.autoSyncMemory) {
640
+ pushMemoryToOpenClaw(session.agentId || undefined)
641
+ }
642
+ } catch { /* OpenClaw sync not available — ignore */ }
643
+
615
644
  // Clean up browser and other session resources
616
645
  await cleanup()
617
646
 
@@ -0,0 +1,44 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { extractTaskResult } from './task-result'
4
+
5
+ describe('extractTaskResult', () => {
6
+ it('limits artifact extraction to messages from the current run window', () => {
7
+ const session = {
8
+ messages: [
9
+ {
10
+ role: 'assistant',
11
+ time: 1_000,
12
+ text: 'old run artifact: /api/uploads/wiki-old.png',
13
+ },
14
+ {
15
+ role: 'assistant',
16
+ time: 2_000,
17
+ text: 'new run artifact: /api/uploads/wiki-new.png',
18
+ },
19
+ ],
20
+ }
21
+
22
+ const result = extractTaskResult(session, 'done', { sinceTime: 1_500 })
23
+ assert.deepEqual(result.artifacts.map((a) => a.url), ['/api/uploads/wiki-new.png'])
24
+ })
25
+
26
+ it('excludes messages without timestamps when sinceTime is provided', () => {
27
+ const session = {
28
+ messages: [
29
+ {
30
+ role: 'assistant',
31
+ text: 'undated artifact: /api/uploads/undated.png',
32
+ },
33
+ {
34
+ role: 'assistant',
35
+ time: 5_000,
36
+ text: 'dated artifact: /api/uploads/dated.png',
37
+ },
38
+ ],
39
+ }
40
+
41
+ const result = extractTaskResult(session, 'done', { sinceTime: 4_000 })
42
+ assert.deepEqual(result.artifacts.map((a) => a.url), ['/api/uploads/dated.png'])
43
+ })
44
+ })
@@ -43,6 +43,7 @@ function classifyArtifact(filename: string): Artifact['type'] {
43
43
  interface MessageLike {
44
44
  role?: string
45
45
  text?: string
46
+ time?: number
46
47
  imageUrl?: string
47
48
  imagePath?: string
48
49
  toolEvents?: Array<{ name?: string; output?: string }>
@@ -52,6 +53,10 @@ interface SessionLike {
52
53
  messages?: MessageLike[]
53
54
  }
54
55
 
56
+ interface ExtractTaskResultOptions {
57
+ sinceTime?: number | null
58
+ }
59
+
55
60
  // ---------------------------------------------------------------------------
56
61
  // Core extraction
57
62
  // ---------------------------------------------------------------------------
@@ -64,9 +69,13 @@ interface SessionLike {
64
69
  export function extractTaskResult(
65
70
  session: SessionLike | null | undefined,
66
71
  rawResultText: string | null | undefined,
72
+ options?: ExtractTaskResultOptions,
67
73
  ): TaskResult {
68
74
  const seen = new Set<string>()
69
75
  const artifacts: Artifact[] = []
76
+ const sinceTime = typeof options?.sinceTime === 'number' && Number.isFinite(options.sinceTime)
77
+ ? options.sinceTime
78
+ : null
70
79
 
71
80
  function addUrl(raw: string) {
72
81
  const url = stripSandbox(raw)
@@ -79,6 +88,11 @@ export function extractTaskResult(
79
88
  // Walk session messages to collect all artifact URLs
80
89
  if (Array.isArray(session?.messages)) {
81
90
  for (const msg of session.messages) {
91
+ if (sinceTime !== null) {
92
+ const msgTime = typeof msg.time === 'number' && Number.isFinite(msg.time) ? msg.time : null
93
+ if (msgTime === null || msgTime < sinceTime) continue
94
+ }
95
+
82
96
  // Explicit image fields
83
97
  if (msg.imageUrl) addUrl(msg.imageUrl)
84
98
  if (msg.imagePath) {
@@ -18,8 +18,10 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
18
18
  { id: 'codex_cli', label: 'Codex CLI', description: 'Delegate complex tasks to OpenAI Codex CLI' },
19
19
  { id: 'opencode_cli', label: 'OpenCode CLI', description: 'Delegate complex tasks to OpenCode CLI' },
20
20
  { id: 'browser', label: 'Browser', description: 'Playwright — browse, scrape, interact with web pages' },
21
- { id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across sessions' },
21
+ { id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across conversations' },
22
22
  { id: 'sandbox', label: 'Sandbox', description: 'Run JS/TS/Python code in an isolated Deno sandbox' },
23
+ { id: 'create_document', label: 'Create Document', description: 'Render markdown to PDF, HTML, or image' },
24
+ { id: 'create_spreadsheet', label: 'Create Spreadsheet', description: 'Create Excel or CSV files from structured data' },
23
25
  ]
24
26
 
25
27
  export const PLATFORM_TOOLS: ToolDefinition[] = [
@@ -28,9 +30,9 @@ export const PLATFORM_TOOLS: ToolDefinition[] = [
28
30
  { id: 'manage_schedules', label: 'Schedules', description: 'Create, edit, and delete schedules' },
29
31
  { id: 'manage_skills', label: 'Skills', description: 'Create, edit, and delete skills' },
30
32
  { id: 'manage_documents', label: 'Documents', description: 'Upload, search, and delete indexed documents' },
31
- { id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent sessions' },
33
+ { id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent workflows' },
32
34
  { id: 'manage_connectors', label: 'Connectors', description: 'Create, edit, and delete connectors' },
33
- { id: 'manage_sessions', label: 'Sessions', description: 'List sessions, send messages, and spawn session work' },
35
+ { id: 'manage_sessions', label: 'Chats', description: 'List chats, send messages, and spawn agent work' },
34
36
  { id: 'manage_secrets', label: 'Secrets', description: 'Store and retrieve encrypted service secrets' },
35
37
  ]
36
38