@swarmclawai/swarmclaw 1.4.3 → 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/README.md +9 -0
- package/package.json +4 -4
- package/src/app/api/openclaw/gateway/route.ts +3 -1
- package/src/components/connectors/connector-sheet.tsx +1 -1
- package/src/lib/server/connectors/swarmdock.test.ts +200 -0
- package/src/lib/server/connectors/swarmdock.ts +229 -44
- 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.test.ts +85 -0
- package/src/lib/server/session-tools/swarmdock.ts +1 -1
- package/src/lib/tool-definitions.ts +1 -1
package/README.md
CHANGED
|
@@ -211,6 +211,15 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
|
|
|
211
211
|
|
|
212
212
|
Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
|
|
213
213
|
|
|
214
|
+
### v1.4.5 Highlights
|
|
215
|
+
|
|
216
|
+
- **OpenClaw 2026.4.x compatibility**: Fixed WebSocket protocol errors when connecting to OpenClaw 2026.4.2+ gateways (`profileId` was incorrectly included in RPC params)
|
|
217
|
+
- **OpenClaw dependency bump**: Updated minimum OpenClaw from `2026.2.26` to `2026.4.2`
|
|
218
|
+
|
|
219
|
+
### v1.4.4 Highlights
|
|
220
|
+
|
|
221
|
+
- **SwarmDock SDK bump**: Updated `@swarmdock/sdk` from `0.4.1` to `0.5.2`, picking up new error types, skill templates, and agent primitives
|
|
222
|
+
|
|
214
223
|
### v1.4.3 Highlights
|
|
215
224
|
|
|
216
225
|
- **SwarmDock agent opt-in**: Agents can now opt into the SwarmDock marketplace directly from their settings sheet with description, skills, wallet, and auto-bid configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.4.
|
|
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.
|
|
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",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"next": "16.1.7",
|
|
129
129
|
"next-themes": "^0.4.6",
|
|
130
130
|
"nodemailer": "^8.0.1",
|
|
131
|
-
"openclaw": "^2026.2
|
|
131
|
+
"openclaw": "^2026.4.2",
|
|
132
132
|
"pdf-parse": "^2.4.5",
|
|
133
133
|
"qrcode": "^1.5.4",
|
|
134
134
|
"radix-ui": "^1.4.3",
|
|
@@ -61,7 +61,9 @@ export async function POST(req: Request) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
|
-
const
|
|
64
|
+
const rpcParams = { ...params }
|
|
65
|
+
delete rpcParams.profileId
|
|
66
|
+
const result = await gw.rpc(method, rpcParams)
|
|
65
67
|
return NextResponse.json({ ok: true, result })
|
|
66
68
|
} catch (err: unknown) {
|
|
67
69
|
const message = errorMessage(err)
|
|
@@ -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.
|
|
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 {
|
|
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
|
|
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 ||
|
|
27
|
-
walletAddress: c.walletAddress ||
|
|
28
|
-
agentDescription: c.agentDescription || agent?.swarmdockDescription || connector.name
|
|
29
|
-
skills: c.skills || (agent?.swarmdockSkills
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
141
|
-
description:
|
|
142
|
-
framework:
|
|
143
|
-
|
|
144
|
-
|
|
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
|
}
|
|
@@ -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,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 ? `&
|
|
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
|
|
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
|
/**
|