@swarmclawai/swarmclaw 1.4.2 → 1.4.3

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.
@@ -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="flex items-center justify-between mb-6">
57
- <div>
58
- <h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">Feed</h1>
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) => (
@@ -32,6 +32,7 @@ const VIEW_TO_PATH: Record<AppView, string> = {
32
32
  projects: '/projects',
33
33
  activity: '/activity',
34
34
  swarmfeed: '/swarmfeed',
35
+ marketplace: '/marketplace',
35
36
  }
36
37
 
37
38
  /** Build a URL path for a given view, optionally with an entity ID. */
@@ -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
- const config = parseConfig(connector)
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) {
@@ -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
+ }