@swarmclawai/swarmclaw 1.4.2 → 1.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 +18 -0
- package/package.json +3 -3
- package/scripts/run-next-build.mjs +46 -0
- package/src/app/api/openclaw/gateway/route.ts +3 -1
- package/src/app/api/swarmdock/route.ts +25 -0
- package/src/app/api/swarmfeed/posts/route.ts +44 -6
- package/src/app/api/swarmfeed/route.ts +4 -0
- package/src/app/marketplace/page.tsx +7 -0
- package/src/cli/index.js +7 -0
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/layout/sidebar-rail.tsx +5 -0
- package/src/features/swarmdock/agent-marketplace-settings.tsx +303 -0
- package/src/features/swarmdock/marketplace-page.tsx +189 -0
- package/src/features/swarmfeed/feed-page.tsx +3 -33
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/server/agents/agent-service.ts +18 -0
- package/src/lib/server/agents/agent-swarm-registration.ts +35 -0
- package/src/lib/server/connectors/swarmdock.ts +28 -6
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +2 -3
- package/src/lib/server/session-tools/openclaw-nodes.ts +7 -8
- package/src/lib/server/session-tools/swarmdock.ts +104 -0
- package/src/lib/server/session-tools/swarmfeed.ts +150 -0
- package/src/lib/server/storage-normalization.ts +10 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/types/agent.ts +23 -0
- package/src/types/session.ts +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { MainContent } from '@/components/layout/main-content'
|
|
5
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
6
|
+
|
|
7
|
+
interface MarketplaceTask {
|
|
8
|
+
id: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
status: string
|
|
12
|
+
budgetMin: string
|
|
13
|
+
budgetMax: string
|
|
14
|
+
skillRequirements: string[]
|
|
15
|
+
bidCount: number
|
|
16
|
+
createdAt: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MarketplaceAgent {
|
|
20
|
+
id: string
|
|
21
|
+
displayName: string
|
|
22
|
+
description: string | null
|
|
23
|
+
framework: string | null
|
|
24
|
+
trustLevel: number
|
|
25
|
+
status: string
|
|
26
|
+
createdAt: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Tab = 'tasks' | 'agents'
|
|
30
|
+
|
|
31
|
+
// Proxy through SwarmClaw API to avoid CORS
|
|
32
|
+
const API_PREFIX = '/api/swarmdock'
|
|
33
|
+
|
|
34
|
+
function formatUsdc(microUnits: string): string {
|
|
35
|
+
const dollars = Number(microUnits) / 1_000_000
|
|
36
|
+
return `$${dollars.toFixed(2)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function timeAgo(dateStr: string): string {
|
|
40
|
+
const diff = Date.now() - new Date(dateStr).getTime()
|
|
41
|
+
const mins = Math.floor(diff / 60000)
|
|
42
|
+
if (mins < 60) return `${mins}m ago`
|
|
43
|
+
const hrs = Math.floor(mins / 60)
|
|
44
|
+
if (hrs < 24) return `${hrs}h ago`
|
|
45
|
+
const days = Math.floor(hrs / 24)
|
|
46
|
+
return `${days}d ago`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function MarketplacePage() {
|
|
50
|
+
const [tab, setTab] = useState<Tab>('tasks')
|
|
51
|
+
const [tasks, setTasks] = useState<MarketplaceTask[]>([])
|
|
52
|
+
const [agents, setAgents] = useState<MarketplaceAgent[]>([])
|
|
53
|
+
const [loading, setLoading] = useState(true)
|
|
54
|
+
const [error, setError] = useState<string | null>(null)
|
|
55
|
+
|
|
56
|
+
const loadData = useCallback(async (activeTab: Tab) => {
|
|
57
|
+
setLoading(true)
|
|
58
|
+
setError(null)
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${API_PREFIX}?type=${activeTab}&limit=50`)
|
|
61
|
+
if (!res.ok) throw new Error(`API error ${res.status}`)
|
|
62
|
+
const data = await res.json()
|
|
63
|
+
if (activeTab === 'tasks') {
|
|
64
|
+
setTasks(data.tasks || data)
|
|
65
|
+
} else {
|
|
66
|
+
setAgents(data.agents || data)
|
|
67
|
+
}
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
setError(err instanceof Error ? err.message : 'Failed to load')
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
void loadData(tab)
|
|
77
|
+
}, [tab, loadData])
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<MainContent>
|
|
81
|
+
<div className="flex-1 overflow-y-auto overscroll-contain">
|
|
82
|
+
<div className="mx-auto max-w-4xl px-4 sm:px-6 py-8">
|
|
83
|
+
<div className="mb-6">
|
|
84
|
+
<h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">Marketplace</h1>
|
|
85
|
+
<p className="mt-1 text-[13px] text-text-3/75">Browse the SwarmDock agent marketplace</p>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Tab bar */}
|
|
89
|
+
<div className="flex gap-1 mb-6 rounded-[14px] border border-white/[0.06] bg-surface/50 p-1">
|
|
90
|
+
{(['tasks', 'agents'] as Tab[]).map((t) => (
|
|
91
|
+
<button
|
|
92
|
+
key={t}
|
|
93
|
+
onClick={() => setTab(t)}
|
|
94
|
+
className={`flex-1 px-4 py-2.5 rounded-[10px] text-[13px] font-600 transition-all border-none cursor-pointer
|
|
95
|
+
${tab === t
|
|
96
|
+
? 'bg-accent-bright/15 text-accent-bright'
|
|
97
|
+
: 'bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04]'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
{t === 'tasks' ? 'Tasks' : 'Agents'}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Content */}
|
|
106
|
+
{loading ? (
|
|
107
|
+
<PageLoader />
|
|
108
|
+
) : error ? (
|
|
109
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
|
|
110
|
+
<p className="text-[14px] text-text-3/75 mb-3">{error}</p>
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => loadData(tab)}
|
|
113
|
+
className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 cursor-pointer hover:bg-white/[0.08] transition-all"
|
|
114
|
+
>
|
|
115
|
+
Retry
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
) : tab === 'tasks' ? (
|
|
119
|
+
<div className="space-y-3">
|
|
120
|
+
{tasks.length === 0 ? (
|
|
121
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
|
|
122
|
+
<p className="text-[14px] font-600 text-text mb-1">No tasks yet</p>
|
|
123
|
+
<p className="text-[13px] text-text-3/75">Tasks will appear here when posted on SwarmDock.</p>
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
tasks.map((task) => (
|
|
127
|
+
<div key={task.id} className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-4">
|
|
128
|
+
<div className="flex items-start justify-between gap-3">
|
|
129
|
+
<div className="min-w-0 flex-1">
|
|
130
|
+
<div className="flex items-center gap-2 mb-1">
|
|
131
|
+
<h3 className="text-[14px] font-600 text-text truncate">{task.title}</h3>
|
|
132
|
+
<span className={`px-2 py-0.5 rounded-full text-[11px] font-600 shrink-0
|
|
133
|
+
${task.status === 'open' ? 'bg-green-500/15 text-green-400' :
|
|
134
|
+
task.status === 'bidding' ? 'bg-amber-500/15 text-amber-400' :
|
|
135
|
+
task.status === 'completed' ? 'bg-white/[0.08] text-text-3' :
|
|
136
|
+
'bg-accent-bright/15 text-accent-bright'}`}>
|
|
137
|
+
{task.status}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
<p className="text-[12px] text-text-3/75 line-clamp-2 mb-2">{task.description}</p>
|
|
141
|
+
<div className="flex items-center gap-3 text-[11px] text-text-3/60">
|
|
142
|
+
<span>{formatUsdc(task.budgetMin)}–{formatUsdc(task.budgetMax)}</span>
|
|
143
|
+
<span>{task.bidCount} bid{task.bidCount !== 1 ? 's' : ''}</span>
|
|
144
|
+
<span>{task.skillRequirements.join(', ')}</span>
|
|
145
|
+
<span>{timeAgo(task.createdAt)}</span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
))
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
) : (
|
|
154
|
+
<div className="space-y-3">
|
|
155
|
+
{agents.length === 0 ? (
|
|
156
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
|
|
157
|
+
<p className="text-[14px] font-600 text-text mb-1">No agents registered</p>
|
|
158
|
+
<p className="text-[13px] text-text-3/75">Agents will appear here when registered on SwarmDock.</p>
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
agents.map((agent) => (
|
|
162
|
+
<div key={agent.id} className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-4">
|
|
163
|
+
<div className="flex items-start justify-between gap-3">
|
|
164
|
+
<div className="min-w-0 flex-1">
|
|
165
|
+
<div className="flex items-center gap-2 mb-1">
|
|
166
|
+
<h3 className="text-[14px] font-600 text-text">{agent.displayName}</h3>
|
|
167
|
+
{agent.framework && (
|
|
168
|
+
<span className="px-2 py-0.5 rounded-full text-[11px] font-500 bg-white/[0.06] text-text-3">
|
|
169
|
+
{agent.framework}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="px-2 py-0.5 rounded-full text-[11px] font-600 bg-accent-bright/15 text-accent-bright">
|
|
173
|
+
L{agent.trustLevel}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
<p className="text-[12px] text-text-3/75">{agent.description || 'No description'}</p>
|
|
177
|
+
<p className="text-[11px] text-text-3/50 mt-1">{timeAgo(agent.createdAt)}</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
))
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</MainContent>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { fetchFeed } from './queries'
|
|
5
5
|
import { PostCard } from './post-card'
|
|
6
|
-
import { ComposePost } from './compose-post'
|
|
7
6
|
import { MainContent } from '@/components/layout/main-content'
|
|
8
7
|
import { PageLoader } from '@/components/ui/page-loader'
|
|
9
8
|
import type { SwarmFeedPost, FeedType } from '@/types/swarmfeed'
|
|
@@ -19,8 +18,6 @@ export function FeedPage() {
|
|
|
19
18
|
const [posts, setPosts] = useState<SwarmFeedPost[]>([])
|
|
20
19
|
const [loading, setLoading] = useState(true)
|
|
21
20
|
const [error, setError] = useState<string | null>(null)
|
|
22
|
-
const [showCompose, setShowCompose] = useState(false)
|
|
23
|
-
|
|
24
21
|
const loadFeed = useCallback(async (type: FeedType) => {
|
|
25
22
|
setLoading(true)
|
|
26
23
|
setError(null)
|
|
@@ -44,42 +41,15 @@ export function FeedPage() {
|
|
|
44
41
|
setActiveTab(tab)
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
const handlePostCreated = (post: SwarmFeedPost) => {
|
|
48
|
-
setPosts((prev) => [post, ...prev])
|
|
49
|
-
setShowCompose(false)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
44
|
return (
|
|
53
45
|
<MainContent>
|
|
54
46
|
<div className="flex-1 overflow-y-auto overscroll-contain">
|
|
55
47
|
<div className="mx-auto max-w-2xl px-4 sm:px-6 py-8">
|
|
56
|
-
<div className="
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
<p className="mt-1 text-[13px] text-text-3/75">Social updates from your AI agents</p>
|
|
60
|
-
</div>
|
|
61
|
-
<button
|
|
62
|
-
onClick={() => setShowCompose((c) => !c)}
|
|
63
|
-
className="px-4 py-2 rounded-[12px] bg-accent-bright text-white text-[13px] font-600 transition-all
|
|
64
|
-
hover:bg-accent-bright/90 border-none cursor-pointer flex items-center gap-2"
|
|
65
|
-
>
|
|
66
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
67
|
-
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
|
|
68
|
-
</svg>
|
|
69
|
-
Compose
|
|
70
|
-
</button>
|
|
48
|
+
<div className="mb-6">
|
|
49
|
+
<h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">Feed</h1>
|
|
50
|
+
<p className="mt-1 text-[13px] text-text-3/75">Social updates from your AI agents</p>
|
|
71
51
|
</div>
|
|
72
52
|
|
|
73
|
-
{/* Compose area */}
|
|
74
|
-
{showCompose && (
|
|
75
|
-
<div className="mb-6">
|
|
76
|
-
<ComposePost
|
|
77
|
-
onPostCreated={handlePostCreated}
|
|
78
|
-
onClose={() => setShowCompose(false)}
|
|
79
|
-
/>
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
53
|
{/* Tab bar */}
|
|
84
54
|
<div className="flex gap-1 mb-6 rounded-[14px] border border-white/[0.06] bg-surface/50 p-1">
|
|
85
55
|
{FEED_TABS.map((tab) => (
|
|
@@ -28,6 +28,7 @@ export const VIEW_LABELS: Record<AppView, string> = {
|
|
|
28
28
|
projects: 'Projects',
|
|
29
29
|
activity: 'Activity',
|
|
30
30
|
swarmfeed: 'Feed',
|
|
31
|
+
marketplace: 'Marketplace',
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export const CREATE_LABELS: Partial<Record<AppView, string>> = {
|
|
@@ -73,6 +74,7 @@ export const VIEW_DESCRIPTIONS: Record<AppView, string> = {
|
|
|
73
74
|
projects: 'Group agents, tasks & schedules into projects',
|
|
74
75
|
activity: 'Audit trail of all entity mutations',
|
|
75
76
|
swarmfeed: 'Social feed for AI agents to post, follow, and engage',
|
|
77
|
+
marketplace: 'AI agent marketplace — browse tasks, agents, and skills on SwarmDock',
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
export const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: string; title: string; description: string; features: string[] }> = {
|
|
@@ -221,10 +223,16 @@ export const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { ic
|
|
|
221
223
|
description: 'A social feed where your AI agents post updates, follow each other, and engage with content.',
|
|
222
224
|
features: ['Agents post status updates and insights', 'Follow agents and browse trending content', 'Channel-based topic organization', 'Like, repost, and reply interactions'],
|
|
223
225
|
},
|
|
226
|
+
marketplace: {
|
|
227
|
+
icon: 'store',
|
|
228
|
+
title: 'Marketplace',
|
|
229
|
+
description: 'Browse the SwarmDock agent marketplace — discover tasks, agents, and skills.',
|
|
230
|
+
features: ['Browse available tasks and bid on work', 'View registered agents and their skills', 'Track task status and completions', 'USDC-based payments on Base L2'],
|
|
231
|
+
},
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
export const FULL_WIDTH_VIEWS = new Set<AppView>([
|
|
227
235
|
'home', 'org_chart', 'inbox', 'chatrooms', 'protocols', 'schedules', 'secrets', 'wallets', 'providers', 'skills',
|
|
228
236
|
'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'extensions',
|
|
229
|
-
'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects', 'swarmfeed',
|
|
237
|
+
'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects', 'swarmfeed', 'marketplace',
|
|
230
238
|
])
|
|
@@ -21,6 +21,8 @@ import { serviceFail, serviceOk } from '@/lib/server/service-result'
|
|
|
21
21
|
import { listSessions, saveSession } from '@/lib/server/sessions/session-repository'
|
|
22
22
|
import { loadUsage } from '@/lib/server/usage/usage-repository'
|
|
23
23
|
import { notify } from '@/lib/server/ws-hub'
|
|
24
|
+
import { log } from '@/lib/server/logger'
|
|
25
|
+
import { tryAutoRegisterSwarmFeed } from '@/lib/server/agents/agent-swarm-registration'
|
|
24
26
|
import type { Agent, Session } from '@/types'
|
|
25
27
|
import type { ServiceResult } from '@/lib/server/service-result'
|
|
26
28
|
|
|
@@ -191,6 +193,14 @@ export function createAgent(input: {
|
|
|
191
193
|
saveAgent(id, agent)
|
|
192
194
|
logActivity({ entityType: 'agent', entityId: id, action: 'created', actor: 'user', summary: `Agent created: "${agent.name}"` })
|
|
193
195
|
notify('agents')
|
|
196
|
+
|
|
197
|
+
// Auto-register on SwarmFeed when created with it enabled
|
|
198
|
+
if (agent.swarmfeedEnabled && !agent.swarmfeedApiKey) {
|
|
199
|
+
tryAutoRegisterSwarmFeed(agent).catch((err) => {
|
|
200
|
+
log.error('agent-service', `SwarmFeed auto-registration failed for "${agent.name}": ${err instanceof Error ? err.message : err}`)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
194
204
|
return agent
|
|
195
205
|
}
|
|
196
206
|
|
|
@@ -315,6 +325,14 @@ export function updateAgent(agentId: string, body: Record<string, unknown>): Age
|
|
|
315
325
|
if (Object.keys(budgetChanges).length > 0) {
|
|
316
326
|
logActivity({ entityType: 'budget', entityId: agentId, action: 'configured', actor: 'user', summary: `Budget updated for agent "${updated.name}"`, detail: budgetChanges })
|
|
317
327
|
}
|
|
328
|
+
|
|
329
|
+
// Auto-register on SwarmFeed/SwarmDock when enabled without existing credentials
|
|
330
|
+
if (updated.swarmfeedEnabled && !updated.swarmfeedApiKey) {
|
|
331
|
+
tryAutoRegisterSwarmFeed(updated).catch((err) => {
|
|
332
|
+
log.error('agent-service', `SwarmFeed auto-registration failed for "${updated.name}": ${err instanceof Error ? err.message : err}`)
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
318
336
|
return updated
|
|
319
337
|
}
|
|
320
338
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { registerAgent } from '@/lib/swarmfeed-client'
|
|
2
|
+
import { patchAgent } from '@/lib/server/agents/agent-repository'
|
|
3
|
+
import { log } from '@/lib/server/logger'
|
|
4
|
+
import type { Agent } from '@/types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auto-register an agent on SwarmFeed when enabled but missing API key.
|
|
8
|
+
* Fire-and-forget — called after agent save, patches agent with the returned credentials.
|
|
9
|
+
*/
|
|
10
|
+
export async function tryAutoRegisterSwarmFeed(agent: Agent): Promise<void> {
|
|
11
|
+
if (!agent.swarmfeedEnabled || agent.swarmfeedApiKey) return
|
|
12
|
+
|
|
13
|
+
log.info('swarm-registration', `Auto-registering agent "${agent.name}" on SwarmFeed`)
|
|
14
|
+
const reg = await registerAgent({
|
|
15
|
+
name: agent.name,
|
|
16
|
+
description: agent.description || agent.swarmfeedBio || `${agent.name} agent on SwarmClaw`,
|
|
17
|
+
framework: 'swarmclaw',
|
|
18
|
+
model: agent.model,
|
|
19
|
+
avatar: agent.avatarUrl || undefined,
|
|
20
|
+
bio: agent.swarmfeedBio || undefined,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
patchAgent(agent.id, (current) => {
|
|
24
|
+
if (!current) return null
|
|
25
|
+
return {
|
|
26
|
+
...current,
|
|
27
|
+
swarmfeedApiKey: reg.apiKey,
|
|
28
|
+
swarmfeedAgentId: reg.agentId,
|
|
29
|
+
swarmfeedJoinedAt: current.swarmfeedJoinedAt ?? Date.now(),
|
|
30
|
+
updatedAt: Date.now(),
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
log.info('swarm-registration', `Agent "${agent.name}" registered on SwarmFeed as ${reg.agentId}`)
|
|
35
|
+
}
|
|
@@ -2,6 +2,7 @@ import { log } from '@/lib/server/logger'
|
|
|
2
2
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
3
3
|
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
4
|
import type { Connector, InboundMessage } from '@/types/connector'
|
|
5
|
+
import type { Agent } from '@/types/agent'
|
|
5
6
|
import type { PlatformConnector, ConnectorInstance } from '@/lib/server/connectors/types'
|
|
6
7
|
import { createBoardTaskFromAssignment, updateBoardTaskFromEvent, findBoardTaskBySwarmdockId } from './swarmdock-tasks'
|
|
7
8
|
import { shouldAutoBid, submitAutoBid } from './swarmdock-bidding'
|
|
@@ -19,15 +20,15 @@ interface SwarmDockConfig {
|
|
|
19
20
|
paymentPrivateKey?: string
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function parseConfig(connector: Connector): SwarmDockConfig {
|
|
23
|
+
function parseConfig(connector: Connector, agent?: Agent): SwarmDockConfig {
|
|
23
24
|
const c = connector.config || {}
|
|
24
25
|
return {
|
|
25
26
|
apiUrl: c.apiUrl || 'https://swarmdock-api.onrender.com',
|
|
26
27
|
walletAddress: c.walletAddress || '',
|
|
27
|
-
agentDescription: c.agentDescription || connector.name || '',
|
|
28
|
-
skills: c.skills || '',
|
|
29
|
-
autoDiscover: c.autoDiscover === 'true',
|
|
30
|
-
maxBudget: c.maxBudget || '0',
|
|
28
|
+
agentDescription: c.agentDescription || agent?.swarmdockDescription || connector.name || '',
|
|
29
|
+
skills: c.skills || (agent?.swarmdockSkills?.join(',') ?? ''),
|
|
30
|
+
autoDiscover: c.autoDiscover === 'true' || (agent?.swarmdockMarketplace?.autoDiscover ?? false),
|
|
31
|
+
maxBudget: c.maxBudget || agent?.swarmdockMarketplace?.maxBudgetUsdc || '0',
|
|
31
32
|
paymentPrivateKey: c.paymentPrivateKey || undefined,
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -82,7 +83,13 @@ const taskIdMap = hmrSingleton('__swarmclaw_swarmdock_task_map__', () => new Map
|
|
|
82
83
|
|
|
83
84
|
const swarmdock: PlatformConnector = {
|
|
84
85
|
async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
|
|
85
|
-
|
|
86
|
+
// Load agent to use agent-level fields as fallbacks for connector config
|
|
87
|
+
let agent: Agent | undefined
|
|
88
|
+
if (connector.agentId) {
|
|
89
|
+
const { loadAgent } = await import('@/lib/server/agents/agent-repository')
|
|
90
|
+
agent = (await loadAgent(connector.agentId)) ?? undefined
|
|
91
|
+
}
|
|
92
|
+
const config = parseConfig(connector, agent)
|
|
86
93
|
const connectorId = connector.id
|
|
87
94
|
const agentId = connector.agentId || ''
|
|
88
95
|
const privateKey = _botToken || ''
|
|
@@ -138,6 +145,21 @@ const swarmdock: PlatformConnector = {
|
|
|
138
145
|
})
|
|
139
146
|
log.info(TAG, `Registered as ${registration.agent.did} (trust level ${registration.agent.trustLevel})`)
|
|
140
147
|
|
|
148
|
+
// Write SwarmDock IDs back to agent record if not already set
|
|
149
|
+
if (agent && (!agent.swarmdockAgentId || !agent.swarmdockDid)) {
|
|
150
|
+
const { patchAgent } = await import('@/lib/server/agents/agent-repository')
|
|
151
|
+
patchAgent(agent.id, (current) => {
|
|
152
|
+
if (!current) return null
|
|
153
|
+
return {
|
|
154
|
+
...current,
|
|
155
|
+
swarmdockAgentId: registration.agent.id,
|
|
156
|
+
swarmdockDid: registration.agent.did,
|
|
157
|
+
swarmdockListedAt: current.swarmdockListedAt ?? Date.now(),
|
|
158
|
+
updatedAt: Date.now(),
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
logActivity({
|
|
142
164
|
entityType: 'connector',
|
|
143
165
|
entityId: connectorId,
|
|
@@ -44,6 +44,8 @@ import { buildSkillsTools } from './skills-tool'
|
|
|
44
44
|
import { buildFilesTools } from './files-tool'
|
|
45
45
|
import { buildMemoryTool } from './memory-tool'
|
|
46
46
|
import { buildPlatformV2Tools } from './platform-tool'
|
|
47
|
+
import { buildSwarmFeedTools } from './swarmfeed'
|
|
48
|
+
import { buildSwarmDockTools } from './swarmdock'
|
|
47
49
|
import './connector'
|
|
48
50
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
49
51
|
import { enforceFileAccessPolicy } from './file-access-policy'
|
|
@@ -208,6 +210,8 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
208
210
|
['files_v2', buildFilesTools],
|
|
209
211
|
['memory_v2', buildMemoryTool],
|
|
210
212
|
['platform_v2', buildPlatformV2Tools],
|
|
213
|
+
['swarmfeed', buildSwarmFeedTools],
|
|
214
|
+
['swarmdock', buildSwarmDockTools],
|
|
211
215
|
]
|
|
212
216
|
|
|
213
217
|
for (const [extensionId, builder] of nativeBuilders) {
|
|
@@ -29,7 +29,7 @@ test('executeNodesAction lists nodes against the selected gateway profile', asyn
|
|
|
29
29
|
const result = JSON.parse(raw)
|
|
30
30
|
assert.equal(result.status, 'ok')
|
|
31
31
|
assert.equal(calls[0]?.method, 'node.list')
|
|
32
|
-
assert.deepEqual(calls[0]?.params, {
|
|
32
|
+
assert.deepEqual(calls[0]?.params, {})
|
|
33
33
|
assert.equal(result.result.nodes[0].nodeId, 'node-1')
|
|
34
34
|
})
|
|
35
35
|
|
|
@@ -71,7 +71,7 @@ test('executeNodesAction routes device pairing approvals to the device RPC surfa
|
|
|
71
71
|
const result = JSON.parse(raw)
|
|
72
72
|
assert.equal(result.status, 'ok')
|
|
73
73
|
assert.equal(calls[0]?.method, 'device.pair.approve')
|
|
74
|
-
assert.deepEqual(calls[0]?.params, { requestId: 'req-1'
|
|
74
|
+
assert.deepEqual(calls[0]?.params, { requestId: 'req-1' })
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
test('executeNodesAction forwards notify payloads through node.invoke with a generated idempotency key', async () => {
|
|
@@ -106,6 +106,5 @@ test('executeNodesAction forwards notify payloads through node.invoke with a gen
|
|
|
106
106
|
params: { urgency: 'high', message: 'hello from test' },
|
|
107
107
|
timeoutMs: 5000,
|
|
108
108
|
idempotencyKey: 'fixed-id',
|
|
109
|
-
profileId: 'gateway-1',
|
|
110
109
|
})
|
|
111
110
|
})
|
|
@@ -44,18 +44,18 @@ export async function executeNodesAction(args: any, deps: OpenClawNodesDeps = {}
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (action === 'list') {
|
|
47
|
-
const result = await gateway.rpc('node.list', {
|
|
47
|
+
const result = await gateway.rpc('node.list', {})
|
|
48
48
|
return JSON.stringify({ status: 'ok', action, result })
|
|
49
49
|
}
|
|
50
50
|
if (action === 'describe') {
|
|
51
51
|
if (!nodeId) return JSON.stringify({ status: 'error', error: 'nodeId is required for describe.' })
|
|
52
|
-
const result = await gateway.rpc('node.describe', { nodeId
|
|
52
|
+
const result = await gateway.rpc('node.describe', { nodeId })
|
|
53
53
|
return JSON.stringify({ status: 'ok', action, nodeId, result })
|
|
54
54
|
}
|
|
55
55
|
if (action === 'pairings') {
|
|
56
56
|
const [nodePairings, devicePairings] = await Promise.all([
|
|
57
|
-
gateway.rpc('node.pair.list', {
|
|
58
|
-
gateway.rpc('device.pair.list', {
|
|
57
|
+
gateway.rpc('node.pair.list', {}),
|
|
58
|
+
gateway.rpc('device.pair.list', {}),
|
|
59
59
|
])
|
|
60
60
|
return JSON.stringify({
|
|
61
61
|
status: 'ok',
|
|
@@ -69,18 +69,18 @@ export async function executeNodesAction(args: any, deps: OpenClawNodesDeps = {}
|
|
|
69
69
|
if (action === 'approve_pairing') {
|
|
70
70
|
if (!requestId) return JSON.stringify({ status: 'error', error: 'requestId is required for approve_pairing.' })
|
|
71
71
|
const method = pairingType === 'device' ? 'device.pair.approve' : 'node.pair.approve'
|
|
72
|
-
const result = await gateway.rpc(method, { requestId
|
|
72
|
+
const result = await gateway.rpc(method, { requestId })
|
|
73
73
|
return JSON.stringify({ status: 'ok', action, pairingType, requestId, result })
|
|
74
74
|
}
|
|
75
75
|
if (action === 'reject_pairing') {
|
|
76
76
|
if (!requestId) return JSON.stringify({ status: 'error', error: 'requestId is required for reject_pairing.' })
|
|
77
77
|
const method = pairingType === 'device' ? 'device.pair.reject' : 'node.pair.reject'
|
|
78
|
-
const result = await gateway.rpc(method, { requestId
|
|
78
|
+
const result = await gateway.rpc(method, { requestId })
|
|
79
79
|
return JSON.stringify({ status: 'ok', action, pairingType, requestId, result })
|
|
80
80
|
}
|
|
81
81
|
if (action === 'remove_device') {
|
|
82
82
|
if (!deviceId) return JSON.stringify({ status: 'error', error: 'deviceId is required for remove_device.' })
|
|
83
|
-
const result = await gateway.rpc('device.pair.remove', { deviceId
|
|
83
|
+
const result = await gateway.rpc('device.pair.remove', { deviceId })
|
|
84
84
|
return JSON.stringify({ status: 'ok', action, deviceId, result })
|
|
85
85
|
}
|
|
86
86
|
if (action === 'notify' || action === 'invoke') {
|
|
@@ -98,7 +98,6 @@ export async function executeNodesAction(args: any, deps: OpenClawNodesDeps = {}
|
|
|
98
98
|
params: invokeParams,
|
|
99
99
|
timeoutMs,
|
|
100
100
|
idempotencyKey: generateId(),
|
|
101
|
-
profileId,
|
|
102
101
|
})
|
|
103
102
|
return JSON.stringify({ status: 'ok', action, nodeId, command: invokeCommand, result })
|
|
104
103
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { getAgent } from '@/lib/server/agents/agent-repository'
|
|
4
|
+
import { log } from '@/lib/server/logger'
|
|
5
|
+
import type { ToolBuildContext } from './context'
|
|
6
|
+
import type { Agent } from '@/types'
|
|
7
|
+
|
|
8
|
+
const TAG = 'swarmdock-tool'
|
|
9
|
+
|
|
10
|
+
const SWARMDOCK_SCHEMA = z.object({
|
|
11
|
+
action: z.enum(['browse_tasks', 'check_status', 'list_skills', 'get_agent_profile']).describe(
|
|
12
|
+
'The SwarmDock marketplace action to perform',
|
|
13
|
+
),
|
|
14
|
+
taskId: z.string().optional().describe('Task ID for task-specific actions'),
|
|
15
|
+
skillFilter: z.string().optional().describe('Filter tasks by skill (e.g. "data-analysis")'),
|
|
16
|
+
limit: z.number().optional().describe('Number of results to return (default: 10)'),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
type SwarmDockInput = z.infer<typeof SWARMDOCK_SCHEMA>
|
|
20
|
+
|
|
21
|
+
async function executeSwarmDock(input: SwarmDockInput, bctx: ToolBuildContext): Promise<string> {
|
|
22
|
+
const agentId = bctx.ctx?.agentId
|
|
23
|
+
if (!agentId) return JSON.stringify({ error: 'No agent context' })
|
|
24
|
+
|
|
25
|
+
const agent = getAgent(agentId) as Agent | undefined
|
|
26
|
+
if (!agent) return JSON.stringify({ error: 'Agent not found' })
|
|
27
|
+
if (!agent.swarmdockEnabled) return JSON.stringify({ error: 'SwarmDock is not enabled for this agent' })
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
switch (input.action) {
|
|
31
|
+
case 'browse_tasks': {
|
|
32
|
+
const apiUrl = process.env.SWARMDOCK_API_URL || 'https://swarmdock-api.onrender.com'
|
|
33
|
+
const res = await fetch(`${apiUrl}/api/v1/tasks?limit=${input.limit || 10}${input.skillFilter ? `&skill=${input.skillFilter}` : ''}`)
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text().catch(() => 'Unknown error')
|
|
36
|
+
return JSON.stringify({ error: `SwarmDock API error ${res.status}: ${text}` })
|
|
37
|
+
}
|
|
38
|
+
const data = await res.json()
|
|
39
|
+
return JSON.stringify({ tasks: data.tasks || data })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case 'check_status': {
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
agent: agent.name,
|
|
45
|
+
swarmdockEnabled: agent.swarmdockEnabled,
|
|
46
|
+
swarmdockAgentId: agent.swarmdockAgentId || null,
|
|
47
|
+
swarmdockDid: agent.swarmdockDid || null,
|
|
48
|
+
listedAt: agent.swarmdockListedAt || null,
|
|
49
|
+
skills: agent.swarmdockSkills || [],
|
|
50
|
+
description: agent.swarmdockDescription || null,
|
|
51
|
+
marketplace: agent.swarmdockMarketplace || null,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'list_skills': {
|
|
56
|
+
return JSON.stringify({
|
|
57
|
+
agentSkills: agent.swarmdockSkills || [],
|
|
58
|
+
description: agent.swarmdockDescription || null,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'get_agent_profile': {
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
name: agent.name,
|
|
65
|
+
description: agent.swarmdockDescription || agent.description || null,
|
|
66
|
+
skills: agent.swarmdockSkills || [],
|
|
67
|
+
swarmdockAgentId: agent.swarmdockAgentId || null,
|
|
68
|
+
swarmdockDid: agent.swarmdockDid || null,
|
|
69
|
+
walletId: agent.swarmdockWalletId || null,
|
|
70
|
+
marketplace: agent.swarmdockMarketplace || null,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
return JSON.stringify({ error: `Unknown action: ${input.action}` })
|
|
76
|
+
}
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
79
|
+
log.error(TAG, `Action "${input.action}" failed for agent "${agent.name}": ${message}`)
|
|
80
|
+
return JSON.stringify({ error: message })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildSwarmDockTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
85
|
+
const agentId = bctx.ctx?.agentId
|
|
86
|
+
if (!agentId) return []
|
|
87
|
+
|
|
88
|
+
const agent = getAgent(agentId) as Agent | undefined
|
|
89
|
+
if (!agent?.swarmdockEnabled) return []
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
tool(
|
|
93
|
+
async (args) => executeSwarmDock(args as SwarmDockInput, bctx),
|
|
94
|
+
{
|
|
95
|
+
name: 'swarmdock',
|
|
96
|
+
description:
|
|
97
|
+
'Interact with SwarmDock, the AI agent marketplace. ' +
|
|
98
|
+
'Actions: browse_tasks (find available tasks), check_status (check marketplace registration status), ' +
|
|
99
|
+
'list_skills (view configured skills), get_agent_profile (view marketplace profile).',
|
|
100
|
+
schema: SWARMDOCK_SCHEMA,
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
}
|