@swarmclawai/swarmclaw 1.4.5 → 1.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -73,7 +73,7 @@
73
73
  "cli": "node ./bin/swarmclaw.js",
74
74
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
75
75
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
76
- "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
76
+ "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
77
77
  "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
78
78
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
79
79
  "test:e2e": "tsx .workbench/browser-e2e/run.ts",
@@ -91,7 +91,7 @@
91
91
  "@multiavatar/multiavatar": "^1.0.7",
92
92
  "@playwright/mcp": "^0.0.68",
93
93
  "@slack/bolt": "^4.6.0",
94
- "@swarmdock/sdk": "^0.5.2",
94
+ "@swarmdock/sdk": "^0.5.3",
95
95
  "@tailwindcss/postcss": "^4",
96
96
  "@tanstack/react-query": "^5.91.0",
97
97
  "@types/better-sqlite3": "^7.6.13",
@@ -358,7 +358,7 @@ const PLATFORMS: {
358
358
  tokenLabel: 'SwarmDock Identity Key',
359
359
  tokenHelp: 'Encrypted Ed25519 private key used to authenticate this agent on SwarmDock.',
360
360
  configFields: [
361
- { key: 'apiUrl', label: 'API URL', placeholder: 'https://api.swarmdock.ai', help: 'SwarmDock marketplace API endpoint' },
361
+ { key: 'apiUrl', label: 'API URL', placeholder: 'https://swarmdock-api.onrender.com', help: 'SwarmDock marketplace API endpoint' },
362
362
  { key: 'walletAddress', label: 'Base L2 Wallet Address', placeholder: '0x...', help: 'USDC wallet on Base L2 for payments' },
363
363
  { key: 'agentDescription', label: 'Marketplace Description', placeholder: 'Specialized in...', help: 'Description on your SwarmDock profile' },
364
364
  { key: 'skills', label: 'Skills (comma-separated)', placeholder: 'data-analysis,web-design', help: 'Skill IDs for task matching' },
@@ -0,0 +1,200 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import type { Agent } from '@/types/agent'
5
+ import type { Connector } from '@/types/connector'
6
+ import type { AgentWallet } from '@/types/swarmdock'
7
+
8
+ import {
9
+ buildDesiredSwarmDockProfile,
10
+ buildSwarmDockAgentBackfill,
11
+ buildSwarmDockSkillPayload,
12
+ diffSwarmDockProfile,
13
+ resolveSwarmDockConfig,
14
+ resolveSwarmDockWalletAddress,
15
+ syncSwarmDockProfile,
16
+ } from './swarmdock'
17
+
18
+ function makeConnector(config: Record<string, string> = {}): Connector {
19
+ return {
20
+ id: 'conn-1',
21
+ name: 'SwarmDock Analyst',
22
+ platform: 'swarmdock',
23
+ agentId: 'agent-1',
24
+ chatroomId: null,
25
+ credentialId: null,
26
+ config,
27
+ isEnabled: true,
28
+ status: 'running',
29
+ createdAt: 1,
30
+ updatedAt: 1,
31
+ }
32
+ }
33
+
34
+ function makeAgent(overrides: Partial<Agent> = {}): Agent {
35
+ return {
36
+ id: 'agent-1',
37
+ name: 'SwarmDock Analyst',
38
+ description: 'Local agent',
39
+ systemPrompt: 'You are helpful.',
40
+ provider: 'openai',
41
+ model: 'gpt-4.1',
42
+ createdAt: 1,
43
+ updatedAt: 1,
44
+ ...overrides,
45
+ }
46
+ }
47
+
48
+ function makeWallet(overrides: Partial<AgentWallet> = {}): AgentWallet {
49
+ return {
50
+ id: 'wallet-1',
51
+ agentId: 'agent-1',
52
+ walletAddress: '0x000000000000000000000000000000000000dEaD',
53
+ chain: 'base',
54
+ createdAt: 1,
55
+ ...overrides,
56
+ }
57
+ }
58
+
59
+ test('resolveSwarmDockWalletAddress only accepts the selected wallet for the owning agent', () => {
60
+ const agent = makeAgent({ swarmdockWalletId: 'wallet-1' })
61
+
62
+ assert.equal(resolveSwarmDockWalletAddress(agent, makeWallet()), '0x000000000000000000000000000000000000dEaD')
63
+ assert.equal(resolveSwarmDockWalletAddress(agent, makeWallet({ id: 'wallet-2' })), '')
64
+ assert.equal(resolveSwarmDockWalletAddress(agent, makeWallet({ agentId: 'agent-2' })), '')
65
+ })
66
+
67
+ test('resolveSwarmDockConfig uses agent defaults and wallet fallback when connector config is incomplete', () => {
68
+ const connector = makeConnector({ autoDiscover: 'true' })
69
+ const agent = makeAgent({
70
+ swarmdockDescription: 'Marketplace specialist',
71
+ swarmdockSkills: ['data-analysis', 'reporting'],
72
+ swarmdockMarketplace: { enabled: true, autoDiscover: false, maxBudgetUsdc: '2500000', autoBid: false, autoBidMaxPrice: '0', taskNotifications: true, preferredCategories: [] },
73
+ })
74
+
75
+ const config = resolveSwarmDockConfig(connector, agent, '0x000000000000000000000000000000000000dEaD')
76
+
77
+ assert.equal(config.apiUrl, 'https://swarmdock-api.onrender.com')
78
+ assert.equal(config.walletAddress, '0x000000000000000000000000000000000000dEaD')
79
+ assert.equal(config.agentDescription, 'Marketplace specialist')
80
+ assert.equal(config.skills, 'data-analysis,reporting')
81
+ assert.equal(config.autoDiscover, true)
82
+ assert.equal(config.maxBudget, '2500000')
83
+ })
84
+
85
+ test('buildSwarmDockSkillPayload produces stable skill definitions', () => {
86
+ assert.deepEqual(buildSwarmDockSkillPayload('data-analysis'), [{
87
+ skillId: 'data-analysis',
88
+ skillName: 'data analysis',
89
+ description: 'data-analysis capability',
90
+ category: 'data-analysis',
91
+ tags: [],
92
+ inputModes: ['text'],
93
+ outputModes: ['text'],
94
+ pricingModel: 'per-task',
95
+ basePrice: '1000000',
96
+ examplePrompts: [
97
+ 'Perform a data analysis task',
98
+ 'Help me with data analysis',
99
+ 'I need data analysis work done',
100
+ 'Complete a data analysis assignment',
101
+ 'Handle a data analysis request',
102
+ ],
103
+ }])
104
+ })
105
+
106
+ test('diffSwarmDockProfile is a no-op when the live profile already matches local state', () => {
107
+ const connector = makeConnector()
108
+ const agent = makeAgent({ swarmdockDescription: 'Marketplace specialist', swarmdockSkills: ['data-analysis'] })
109
+ const desired = buildDesiredSwarmDockProfile(
110
+ connector,
111
+ resolveSwarmDockConfig(connector, agent, '0x000000000000000000000000000000000000dEaD'),
112
+ agent,
113
+ )
114
+
115
+ const diff = diffSwarmDockProfile({
116
+ id: 'dock-agent-1',
117
+ did: 'did:key:test',
118
+ createdAt: '2026-04-01T12:00:00.000Z',
119
+ displayName: desired.displayName,
120
+ description: desired.description,
121
+ framework: desired.framework,
122
+ modelProvider: desired.modelProvider ?? null,
123
+ modelName: desired.modelName ?? null,
124
+ walletAddress: desired.walletAddress,
125
+ skills: desired.skills,
126
+ }, desired)
127
+
128
+ assert.deepEqual(diff.profileFields, {})
129
+ assert.equal(diff.shouldUpdateSkills, false)
130
+ })
131
+
132
+ test('syncSwarmDockProfile patches drifted fields and updates skills only when needed', async () => {
133
+ const desired = {
134
+ displayName: 'SwarmDock Analyst',
135
+ description: 'Marketplace specialist',
136
+ framework: 'swarmclaw',
137
+ modelProvider: 'openai',
138
+ modelName: 'gpt-4.1',
139
+ walletAddress: '0x000000000000000000000000000000000000dEaD',
140
+ skills: buildSwarmDockSkillPayload('data-analysis'),
141
+ }
142
+
143
+ const calls: {
144
+ profileUpdates: unknown[]
145
+ skillUpdates: unknown[]
146
+ } = {
147
+ profileUpdates: [],
148
+ skillUpdates: [],
149
+ }
150
+
151
+ const result = await syncSwarmDockProfile(
152
+ {
153
+ profile: {
154
+ get: async () => ({
155
+ id: 'dock-agent-1',
156
+ did: 'did:key:test',
157
+ createdAt: '2026-04-01T12:00:00.000Z',
158
+ displayName: 'Old Name',
159
+ description: null,
160
+ framework: null,
161
+ modelProvider: null,
162
+ modelName: null,
163
+ walletAddress: '0x0000000000000000000000000000000000000000',
164
+ skills: [],
165
+ }),
166
+ update: async (fields) => {
167
+ calls.profileUpdates.push(fields)
168
+ },
169
+ updateSkills: async (skills) => {
170
+ calls.skillUpdates.push(skills)
171
+ },
172
+ },
173
+ },
174
+ desired,
175
+ )
176
+
177
+ assert.equal(result.updatedProfile, true)
178
+ assert.equal(result.updatedSkills, true)
179
+ assert.deepEqual(calls.profileUpdates, [{
180
+ displayName: 'SwarmDock Analyst',
181
+ description: 'Marketplace specialist',
182
+ framework: 'swarmclaw',
183
+ modelProvider: 'openai',
184
+ modelName: 'gpt-4.1',
185
+ walletAddress: '0x000000000000000000000000000000000000dEaD',
186
+ }])
187
+ assert.equal(calls.skillUpdates.length, 1)
188
+ })
189
+
190
+ test('buildSwarmDockAgentBackfill uses the live profile createdAt timestamp', () => {
191
+ const backfill = buildSwarmDockAgentBackfill({
192
+ id: 'dock-agent-1',
193
+ did: 'did:key:test',
194
+ createdAt: '2026-04-01T12:00:00.000Z',
195
+ })
196
+
197
+ assert.equal(backfill.swarmdockAgentId, 'dock-agent-1')
198
+ assert.equal(backfill.swarmdockDid, 'did:key:test')
199
+ assert.equal(backfill.swarmdockListedAt, Date.parse('2026-04-01T12:00:00.000Z'))
200
+ })
@@ -3,12 +3,54 @@ 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
5
  import type { Agent } from '@/types/agent'
6
+ import type { AgentWallet } from '@/types/swarmdock'
6
7
  import type { PlatformConnector, ConnectorInstance } from '@/lib/server/connectors/types'
7
8
  import { createBoardTaskFromAssignment, updateBoardTaskFromEvent, findBoardTaskBySwarmdockId } from './swarmdock-tasks'
8
9
  import { shouldAutoBid, submitAutoBid } from './swarmdock-bidding'
9
- import type { Task, SSEEvent, TaskSubmitInput } from '@swarmdock/shared'
10
+ import type {
11
+ Agent as SwarmDockAgentProfile,
12
+ AgentSkill,
13
+ AgentUpdateInput,
14
+ SSEEvent,
15
+ Task,
16
+ TaskSubmitInput,
17
+ } from '@swarmdock/shared'
10
18
 
11
19
  const TAG = 'swarmdock'
20
+ const DEFAULT_SWARMDOCK_API_URL = 'https://swarmdock-api.onrender.com'
21
+
22
+ export interface SwarmDockSkillPayload {
23
+ skillId: string
24
+ skillName: string
25
+ description: string
26
+ category: string
27
+ tags: string[]
28
+ inputModes: string[]
29
+ outputModes: string[]
30
+ pricingModel: string
31
+ basePrice: string
32
+ examplePrompts: string[]
33
+ }
34
+
35
+ export interface DesiredSwarmDockProfile {
36
+ displayName: string
37
+ description: string
38
+ framework: string
39
+ modelProvider?: string
40
+ modelName?: string
41
+ walletAddress: string
42
+ skills: SwarmDockSkillPayload[]
43
+ }
44
+
45
+ type SwarmDockProfileSnapshot = Pick<
46
+ SwarmDockAgentProfile,
47
+ 'id' | 'did' | 'createdAt' | 'displayName' | 'description' | 'framework' | 'modelProvider' | 'modelName' | 'walletAddress'
48
+ > & {
49
+ skills?: Array<Pick<
50
+ AgentSkill,
51
+ 'skillId' | 'skillName' | 'description' | 'category' | 'tags' | 'inputModes' | 'outputModes' | 'pricingModel' | 'basePrice' | 'examplePrompts'
52
+ >>
53
+ }
12
54
 
13
55
  interface SwarmDockConfig {
14
56
  apiUrl: string
@@ -20,19 +62,174 @@ interface SwarmDockConfig {
20
62
  paymentPrivateKey?: string
21
63
  }
22
64
 
23
- function parseConfig(connector: Connector, agent?: Agent): SwarmDockConfig {
65
+ function clean(value: unknown): string {
66
+ return typeof value === 'string' ? value.trim() : ''
67
+ }
68
+
69
+ function parseTimestamp(value: string | null | undefined): number | null {
70
+ if (!value) return null
71
+ const parsed = Date.parse(value)
72
+ return Number.isFinite(parsed) ? parsed : null
73
+ }
74
+
75
+ export function resolveSwarmDockWalletAddress(agent?: Agent, wallet?: AgentWallet | null): string {
76
+ if (!agent?.swarmdockWalletId || !wallet) return ''
77
+ if (wallet.id !== agent.swarmdockWalletId) return ''
78
+ if (wallet.agentId !== agent.id) return ''
79
+ return clean(wallet.walletAddress)
80
+ }
81
+
82
+ export function resolveSwarmDockConfig(
83
+ connector: Connector,
84
+ agent?: Agent,
85
+ fallbackWalletAddress?: string | null,
86
+ ): SwarmDockConfig {
24
87
  const c = connector.config || {}
25
88
  return {
26
- apiUrl: c.apiUrl || 'https://swarmdock-api.onrender.com',
27
- walletAddress: c.walletAddress || '',
28
- agentDescription: c.agentDescription || agent?.swarmdockDescription || connector.name || '',
29
- skills: c.skills || (agent?.swarmdockSkills?.join(',') ?? ''),
89
+ apiUrl: clean(c.apiUrl) || DEFAULT_SWARMDOCK_API_URL,
90
+ walletAddress: clean(c.walletAddress) || clean(fallbackWalletAddress),
91
+ agentDescription: clean(c.agentDescription) || clean(agent?.swarmdockDescription) || clean(connector.name),
92
+ skills: clean(c.skills) || (Array.isArray(agent?.swarmdockSkills) ? agent.swarmdockSkills.join(',') : ''),
30
93
  autoDiscover: c.autoDiscover === 'true' || (agent?.swarmdockMarketplace?.autoDiscover ?? false),
31
- maxBudget: c.maxBudget || agent?.swarmdockMarketplace?.maxBudgetUsdc || '0',
32
- paymentPrivateKey: c.paymentPrivateKey || undefined,
94
+ maxBudget: clean(c.maxBudget) || clean(agent?.swarmdockMarketplace?.maxBudgetUsdc) || '0',
95
+ paymentPrivateKey: clean(c.paymentPrivateKey) || undefined,
33
96
  }
34
97
  }
35
98
 
99
+ export function buildSwarmDockSkillPayload(skills: string): SwarmDockSkillPayload[] {
100
+ return skills
101
+ .split(',')
102
+ .map((value) => value.trim())
103
+ .filter(Boolean)
104
+ .map((skillId) => ({
105
+ skillId,
106
+ skillName: skillId.replace(/-/g, ' '),
107
+ description: `${skillId} capability`,
108
+ category: skillId,
109
+ tags: [],
110
+ basePrice: '1000000',
111
+ inputModes: ['text'],
112
+ outputModes: ['text'],
113
+ pricingModel: 'per-task',
114
+ examplePrompts: generateExamplePrompts(skillId),
115
+ }))
116
+ }
117
+
118
+ export function buildDesiredSwarmDockProfile(
119
+ connector: Connector,
120
+ config: SwarmDockConfig,
121
+ agent?: Agent,
122
+ ): DesiredSwarmDockProfile {
123
+ return {
124
+ displayName: connector.name,
125
+ description: config.agentDescription,
126
+ framework: 'swarmclaw',
127
+ modelProvider: agent?.provider,
128
+ modelName: agent?.model,
129
+ walletAddress: config.walletAddress,
130
+ skills: buildSwarmDockSkillPayload(config.skills),
131
+ }
132
+ }
133
+
134
+ function normalizeComparableSkills(skills: Array<Pick<
135
+ AgentSkill | SwarmDockSkillPayload,
136
+ 'skillId' | 'skillName' | 'description' | 'category' | 'tags' | 'inputModes' | 'outputModes' | 'pricingModel' | 'basePrice' | 'examplePrompts'
137
+ >>): SwarmDockSkillPayload[] {
138
+ return skills
139
+ .map((skill) => ({
140
+ skillId: skill.skillId,
141
+ skillName: skill.skillName,
142
+ description: skill.description,
143
+ category: skill.category,
144
+ tags: [...(skill.tags ?? [])],
145
+ inputModes: [...(skill.inputModes ?? [])],
146
+ outputModes: [...(skill.outputModes ?? [])],
147
+ pricingModel: skill.pricingModel,
148
+ basePrice: String(skill.basePrice),
149
+ examplePrompts: [...(skill.examplePrompts ?? [])],
150
+ }))
151
+ .sort((a, b) => a.skillId.localeCompare(b.skillId))
152
+ }
153
+
154
+ export function diffSwarmDockProfile(
155
+ liveProfile: SwarmDockProfileSnapshot,
156
+ desired: DesiredSwarmDockProfile,
157
+ ): { profileFields: AgentUpdateInput; shouldUpdateSkills: boolean } {
158
+ const profileFields: AgentUpdateInput = {}
159
+
160
+ if (liveProfile.displayName !== desired.displayName) profileFields.displayName = desired.displayName
161
+ if ((liveProfile.description ?? '') !== desired.description) profileFields.description = desired.description
162
+ if ((liveProfile.framework ?? '') !== desired.framework) profileFields.framework = desired.framework
163
+ if ((liveProfile.modelProvider ?? '') !== (desired.modelProvider ?? '')) profileFields.modelProvider = desired.modelProvider ?? ''
164
+ if ((liveProfile.modelName ?? '') !== (desired.modelName ?? '')) profileFields.modelName = desired.modelName ?? ''
165
+ if (liveProfile.walletAddress !== desired.walletAddress) profileFields.walletAddress = desired.walletAddress
166
+
167
+ const liveSkills = normalizeComparableSkills(liveProfile.skills ?? [])
168
+ const desiredSkills = normalizeComparableSkills(desired.skills)
169
+ const shouldUpdateSkills = JSON.stringify(liveSkills) !== JSON.stringify(desiredSkills)
170
+
171
+ return { profileFields, shouldUpdateSkills }
172
+ }
173
+
174
+ export async function syncSwarmDockProfile(
175
+ client: {
176
+ profile: {
177
+ get: () => Promise<SwarmDockProfileSnapshot>
178
+ update: (fields: AgentUpdateInput) => Promise<unknown>
179
+ updateSkills: (skills: SwarmDockSkillPayload[]) => Promise<unknown>
180
+ }
181
+ },
182
+ desired: DesiredSwarmDockProfile,
183
+ ): Promise<{ liveProfile: SwarmDockProfileSnapshot; updatedProfile: boolean; updatedSkills: boolean }> {
184
+ const liveProfile = await client.profile.get()
185
+ const { profileFields, shouldUpdateSkills } = diffSwarmDockProfile(liveProfile, desired)
186
+ const updatedProfile = Object.keys(profileFields).length > 0
187
+
188
+ if (updatedProfile) {
189
+ await client.profile.update(profileFields)
190
+ }
191
+ if (shouldUpdateSkills) {
192
+ await client.profile.updateSkills(desired.skills)
193
+ }
194
+
195
+ return { liveProfile, updatedProfile, updatedSkills: shouldUpdateSkills }
196
+ }
197
+
198
+ export function buildSwarmDockAgentBackfill(
199
+ profile: Pick<SwarmDockAgentProfile, 'id' | 'did'> & { createdAt?: string | null },
200
+ ): Pick<Agent, 'swarmdockAgentId' | 'swarmdockDid' | 'swarmdockListedAt'> {
201
+ return {
202
+ swarmdockAgentId: profile.id,
203
+ swarmdockDid: profile.did,
204
+ swarmdockListedAt: parseTimestamp(profile.createdAt) ?? Date.now(),
205
+ }
206
+ }
207
+
208
+ async function persistSwarmDockAgentBackfill(
209
+ agent: Agent | undefined,
210
+ profile: Pick<SwarmDockAgentProfile, 'id' | 'did'> & { createdAt?: string | null },
211
+ ) {
212
+ if (!agent) return
213
+ const backfill = buildSwarmDockAgentBackfill(profile)
214
+ const { patchAgent } = await import('@/lib/server/agents/agent-repository')
215
+ patchAgent(agent.id, (current) => {
216
+ if (!current) return null
217
+
218
+ const needsId = !current.swarmdockAgentId
219
+ const needsDid = !current.swarmdockDid
220
+ const needsListedAt = current.swarmdockListedAt == null
221
+ if (!needsId && !needsDid && !needsListedAt) return current
222
+
223
+ return {
224
+ ...current,
225
+ ...(needsId ? { swarmdockAgentId: backfill.swarmdockAgentId } : {}),
226
+ ...(needsDid ? { swarmdockDid: backfill.swarmdockDid } : {}),
227
+ ...(needsListedAt ? { swarmdockListedAt: backfill.swarmdockListedAt } : {}),
228
+ updatedAt: Date.now(),
229
+ }
230
+ })
231
+ }
232
+
36
233
  function buildTaskPrompt(task: Task): string {
37
234
  const lines: string[] = [
38
235
  `# SwarmDock Task: ${task.title}`,
@@ -89,7 +286,13 @@ const swarmdock: PlatformConnector = {
89
286
  const { loadAgent } = await import('@/lib/server/agents/agent-repository')
90
287
  agent = (await loadAgent(connector.agentId)) ?? undefined
91
288
  }
92
- const config = parseConfig(connector, agent)
289
+ let walletAddressFallback = ''
290
+ if (agent?.swarmdockWalletId) {
291
+ const { loadWallet } = await import('@/lib/server/wallets/wallet-repository')
292
+ walletAddressFallback = resolveSwarmDockWalletAddress(agent, loadWallet(agent.swarmdockWalletId))
293
+ }
294
+
295
+ const config = resolveSwarmDockConfig(connector, agent, walletAddressFallback)
93
296
  const connectorId = connector.id
94
297
  const agentId = connector.agentId || ''
95
298
  const privateKey = _botToken || ''
@@ -118,47 +321,21 @@ const swarmdock: PlatformConnector = {
118
321
  : {}),
119
322
  })
120
323
 
121
- // Register agent on SwarmDock (Ed25519 challenge-response)
122
- const skillList = config.skills
123
- .split(',')
124
- .map((s) => s.trim())
125
- .filter(Boolean)
126
- .map((skillId) => ({
127
- skillId,
128
- skillName: skillId.replace(/-/g, ' '),
129
- description: `${skillId} capability`,
130
- category: skillId,
131
- basePrice: '1000000', // $1.00 default
132
- inputModes: ['text'],
133
- outputModes: ['text'],
134
- examplePrompts: generateExamplePrompts(skillId),
135
- }))
324
+ const desiredProfile = buildDesiredSwarmDockProfile(connector, config, agent)
136
325
 
137
326
  log.info(TAG, `Registering agent "${connector.name}" on SwarmDock at ${config.apiUrl}`)
138
327
  try {
139
328
  const registration = await client.register({
140
- displayName: connector.name,
141
- description: config.agentDescription,
142
- framework: 'swarmclaw',
143
- walletAddress: config.walletAddress,
144
- skills: skillList,
329
+ displayName: desiredProfile.displayName,
330
+ description: desiredProfile.description,
331
+ framework: desiredProfile.framework,
332
+ modelProvider: desiredProfile.modelProvider,
333
+ modelName: desiredProfile.modelName,
334
+ walletAddress: desiredProfile.walletAddress,
335
+ skills: desiredProfile.skills,
145
336
  })
146
337
  log.info(TAG, `Registered as ${registration.agent.did} (trust level ${registration.agent.trustLevel})`)
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
- }
338
+ await persistSwarmDockAgentBackfill(agent, registration.agent)
162
339
 
163
340
  logActivity({
164
341
  entityType: 'connector',
@@ -171,6 +348,14 @@ const swarmdock: PlatformConnector = {
171
348
  if (err instanceof ConflictError) {
172
349
  log.info(TAG, `Agent already registered, authenticating`)
173
350
  await client.authenticate()
351
+ const syncResult = await syncSwarmDockProfile(client, desiredProfile)
352
+ await persistSwarmDockAgentBackfill(agent, syncResult.liveProfile)
353
+ if (syncResult.updatedProfile || syncResult.updatedSkills) {
354
+ log.info(
355
+ TAG,
356
+ `Synchronized live SwarmDock profile${syncResult.updatedProfile ? ' fields' : ''}${syncResult.updatedProfile && syncResult.updatedSkills ? ' and' : ''}${syncResult.updatedSkills ? ' skills' : ''}`,
357
+ )
358
+ }
174
359
  } else {
175
360
  throw err
176
361
  }
@@ -0,0 +1,85 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import test from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-swarmdock-tool-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ test('swarmdock tool browses tasks with the plural skills filter', () => {
36
+ const output = runWithTempDataDir(`
37
+ const storageMod = await import('./src/lib/server/storage')
38
+ const toolsMod = await import('./src/lib/server/session-tools')
39
+ const storage = storageMod.default || storageMod
40
+ const toolsApi = toolsMod.default || toolsMod
41
+
42
+ let requestedUrl = null
43
+ globalThis.fetch = async (url) => {
44
+ requestedUrl = String(url)
45
+ return new Response(JSON.stringify({ tasks: [{ id: 'task-1' }] }), {
46
+ status: 200,
47
+ headers: { 'content-type': 'application/json' },
48
+ })
49
+ }
50
+
51
+ storage.saveAgents({
52
+ agent_1: {
53
+ id: 'agent_1',
54
+ name: 'SwarmDock Agent',
55
+ description: 'local',
56
+ systemPrompt: 'You are helpful.',
57
+ provider: 'openai',
58
+ model: 'gpt-4.1',
59
+ swarmdockEnabled: true,
60
+ swarmdockSkills: ['data-analysis'],
61
+ createdAt: 1,
62
+ updatedAt: 1,
63
+ },
64
+ })
65
+
66
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['swarmdock'], {
67
+ sessionId: 'session-1',
68
+ agentId: 'agent_1',
69
+ delegationEnabled: false,
70
+ delegationTargetMode: 'all',
71
+ delegationTargetAgentIds: [],
72
+ })
73
+
74
+ try {
75
+ const tool = built.tools.find((entry) => entry.name === 'swarmdock')
76
+ const raw = await tool.invoke({ action: 'browse_tasks', skillFilter: 'data-analysis', limit: 2 })
77
+ console.log(JSON.stringify({ requestedUrl, body: JSON.parse(raw) }))
78
+ } finally {
79
+ await built.cleanup()
80
+ }
81
+ `)
82
+
83
+ assert.match(String(output.requestedUrl || ''), /\/api\/v1\/tasks\?limit=2&skills=data-analysis$/)
84
+ assert.deepEqual(output.body, { tasks: [{ id: 'task-1' }] })
85
+ })
@@ -30,7 +30,7 @@ async function executeSwarmDock(input: SwarmDockInput, bctx: ToolBuildContext):
30
30
  switch (input.action) {
31
31
  case 'browse_tasks': {
32
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}` : ''}`)
33
+ const res = await fetch(`${apiUrl}/api/v1/tasks?limit=${input.limit || 10}${input.skillFilter ? `&skills=${input.skillFilter}` : ''}`)
34
34
  if (!res.ok) {
35
35
  const text = await res.text().catch(() => 'Unknown error')
36
36
  return JSON.stringify({ error: `SwarmDock API error ${res.status}: ${text}` })
@@ -30,7 +30,7 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
30
30
  { id: 'replicate', label: 'Replicate', description: 'Run any AI model on Replicate — image generation, LLMs, audio, video, and more', extensionId: 'replicate' },
31
31
  { id: 'google_workspace', label: 'Google Workspace', description: 'Run Google Workspace CLI (`gws`) commands for Drive, Docs, Sheets, Gmail, Calendar, Chat, and more', extensionId: 'google_workspace' },
32
32
  { id: 'swarmfeed', label: 'SwarmFeed', description: 'Post, reply, like, repost, and browse the SwarmFeed social network (auto-enabled when SwarmFeed is on)' },
33
- { id: 'swarmdock', label: 'SwarmDock', description: 'Browse tasks, check status, and manage marketplace profile on SwarmDock (auto-enabled when SwarmDock is on)' },
33
+ { id: 'swarmdock', label: 'SwarmDock', description: 'Browse tasks and inspect marketplace status/profile on SwarmDock (auto-enabled when SwarmDock is on)' },
34
34
  ]
35
35
 
36
36
  /**