@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.
- package/README.md +13 -2
- package/next.config.ts +8 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +20 -21
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +10 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +2 -2
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +28 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +2 -0
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +116 -14
- package/src/components/chat/chat-area.tsx +27 -4
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/tool-call-bubble.tsx +9 -3
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +38 -4
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +392 -3
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +6 -6
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +22 -10
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +29 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +9 -1
- 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
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
4
|
import { loadSessions, saveSessions, loadAgents } from '../storage'
|
|
5
5
|
import type { ToolBuildContext } from './context'
|
|
6
6
|
|
|
@@ -165,7 +165,7 @@ export function buildSessionInfoTools(bctx: ToolBuildContext): StructuredToolInt
|
|
|
165
165
|
const sourceSession = ctx?.sessionId ? sessions[ctx.sessionId] : null
|
|
166
166
|
const ownerUser = sourceSession?.user || 'system'
|
|
167
167
|
|
|
168
|
-
const id =
|
|
168
|
+
const id = genId()
|
|
169
169
|
const now = Date.now()
|
|
170
170
|
const entry = {
|
|
171
171
|
id,
|
|
@@ -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 {
|
|
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
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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
|
|