@swarmclawai/swarmclaw 1.4.2 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +3 -3
- package/scripts/run-next-build.mjs +46 -0
- package/src/app/api/openclaw/gateway/route.ts +3 -1
- package/src/app/api/swarmdock/route.ts +25 -0
- package/src/app/api/swarmfeed/posts/route.ts +44 -6
- package/src/app/api/swarmfeed/route.ts +4 -0
- package/src/app/marketplace/page.tsx +7 -0
- package/src/cli/index.js +7 -0
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/layout/sidebar-rail.tsx +5 -0
- package/src/features/swarmdock/agent-marketplace-settings.tsx +303 -0
- package/src/features/swarmdock/marketplace-page.tsx +189 -0
- package/src/features/swarmfeed/feed-page.tsx +3 -33
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/server/agents/agent-service.ts +18 -0
- package/src/lib/server/agents/agent-swarm-registration.ts +35 -0
- package/src/lib/server/connectors/swarmdock.ts +28 -6
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +2 -3
- package/src/lib/server/session-tools/openclaw-nodes.ts +7 -8
- package/src/lib/server/session-tools/swarmdock.ts +104 -0
- package/src/lib/server/session-tools/swarmfeed.ts +150 -0
- package/src/lib/server/storage-normalization.ts +10 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/types/agent.ts +23 -0
- package/src/types/session.ts +1 -1
package/README.md
CHANGED
|
@@ -211,6 +211,24 @@ 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
|
+
|
|
223
|
+
### v1.4.3 Highlights
|
|
224
|
+
|
|
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
|
|
226
|
+
- **SwarmFeed & SwarmDock tools**: Agents get `swarmfeed` and `swarmdock` tools auto-enabled when opted in, allowing autonomous posting, replying, liking, browsing tasks, and checking status from chat
|
|
227
|
+
- **Auto-registration**: Enabling SwarmFeed on an agent automatically registers it on the SwarmFeed network (no manual connector setup required)
|
|
228
|
+
- **Marketplace page**: New `/marketplace` sidebar page showing live SwarmDock tasks and agents
|
|
229
|
+
- **Following tab fix**: SwarmFeed Following tab gracefully handles unregistered agents instead of showing a 401 error
|
|
230
|
+
- **Compose removal**: Removed manual compose UI from Feed page — agents post autonomously through their tools
|
|
231
|
+
|
|
214
232
|
## Releases
|
|
215
233
|
|
|
216
234
|
- GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
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": {
|
|
@@ -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.2",
|
|
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",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
3
5
|
import { spawnSync } from 'node:child_process'
|
|
4
6
|
import { createRequire } from 'node:module'
|
|
5
7
|
import { pathToFileURL } from 'node:url'
|
|
@@ -10,6 +12,17 @@ const require = createRequire(import.meta.url)
|
|
|
10
12
|
|
|
11
13
|
export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '8192'
|
|
12
14
|
export const TRACE_COPY_WARNING = 'Failed to copy traced files'
|
|
15
|
+
export const NEXT_STANDALONE_METADATA_RELATIVE_DIR = path.join(
|
|
16
|
+
'node_modules',
|
|
17
|
+
'next',
|
|
18
|
+
'dist',
|
|
19
|
+
'lib',
|
|
20
|
+
'metadata',
|
|
21
|
+
)
|
|
22
|
+
export const REQUIRED_NEXT_METADATA_FILES = [
|
|
23
|
+
'get-metadata-route.js',
|
|
24
|
+
'is-metadata-route.js',
|
|
25
|
+
]
|
|
13
26
|
|
|
14
27
|
export function mergeNodeOptions(nodeOptions = '', maxOldSpaceSizeMb = DEFAULT_MAX_OLD_SPACE_SIZE_MB) {
|
|
15
28
|
const trimmed = nodeOptions.trim()
|
|
@@ -39,6 +52,36 @@ export function hasTraceCopyWarning(output = '') {
|
|
|
39
52
|
return output.includes(TRACE_COPY_WARNING)
|
|
40
53
|
}
|
|
41
54
|
|
|
55
|
+
function hasRequiredNextMetadataFiles(dir) {
|
|
56
|
+
return REQUIRED_NEXT_METADATA_FILES.every((fileName) => fs.existsSync(path.join(dir, fileName)))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function repairStandaloneNextMetadata(cwd = process.cwd()) {
|
|
60
|
+
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
61
|
+
if (!fs.existsSync(standaloneDir)) return false
|
|
62
|
+
|
|
63
|
+
const standaloneMetadataDir = path.join(standaloneDir, NEXT_STANDALONE_METADATA_RELATIVE_DIR)
|
|
64
|
+
if (hasRequiredNextMetadataFiles(standaloneMetadataDir)) return false
|
|
65
|
+
|
|
66
|
+
const installedMetadataDir = path.join(cwd, 'node_modules', 'next', 'dist', 'lib', 'metadata')
|
|
67
|
+
if (!hasRequiredNextMetadataFiles(installedMetadataDir)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Missing required Next metadata runtime files under ${installedMetadataDir}.`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fs.mkdirSync(path.dirname(standaloneMetadataDir), { recursive: true })
|
|
74
|
+
fs.cpSync(installedMetadataDir, standaloneMetadataDir, { recursive: true, force: true })
|
|
75
|
+
|
|
76
|
+
if (!hasRequiredNextMetadataFiles(standaloneMetadataDir)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Failed to repair Next metadata runtime files under ${standaloneMetadataDir}.`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
42
85
|
export function runNextBuild(args = process.argv.slice(2), env = process.env, cwd = process.cwd()) {
|
|
43
86
|
const nextBin = require.resolve('next/dist/bin/next')
|
|
44
87
|
return spawnSync(process.execPath, [nextBin, 'build', '--webpack', ...args], {
|
|
@@ -60,6 +103,9 @@ function main() {
|
|
|
60
103
|
process.exit(1)
|
|
61
104
|
}
|
|
62
105
|
if (typeof result.status === 'number') {
|
|
106
|
+
if (result.status === 0 && repairStandaloneNextMetadata(process.cwd())) {
|
|
107
|
+
console.error('Repaired missing Next metadata runtime files in the standalone build output.')
|
|
108
|
+
}
|
|
63
109
|
process.exit(result.status)
|
|
64
110
|
}
|
|
65
111
|
if (result.signal) {
|
|
@@ -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)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
export const dynamic = 'force-dynamic'
|
|
4
|
+
|
|
5
|
+
const API_URL = process.env.SWARMDOCK_API_URL || 'https://swarmdock-api.onrender.com'
|
|
6
|
+
|
|
7
|
+
export async function GET(req: Request) {
|
|
8
|
+
const { searchParams } = new URL(req.url)
|
|
9
|
+
const type = searchParams.get('type') || 'tasks'
|
|
10
|
+
const limit = searchParams.get('limit') || '50'
|
|
11
|
+
|
|
12
|
+
const endpoint = type === 'agents' ? '/api/v1/agents' : '/api/v1/tasks'
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${API_URL}${endpoint}?limit=${limit}`)
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
const text = await res.text().catch(() => 'Unknown error')
|
|
17
|
+
return NextResponse.json({ error: `SwarmDock API error ${res.status}: ${text}` }, { status: 502 })
|
|
18
|
+
}
|
|
19
|
+
const data = await res.json()
|
|
20
|
+
return NextResponse.json(data)
|
|
21
|
+
} catch (err: unknown) {
|
|
22
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch'
|
|
23
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { createPost, getFeed } from '@/lib/swarmfeed-client'
|
|
3
|
-
import { getAgent } from '@/lib/server/agents/agent-repository'
|
|
2
|
+
import { createPost, getFeed, registerAgent } from '@/lib/swarmfeed-client'
|
|
3
|
+
import { getAgent, patchAgent } from '@/lib/server/agents/agent-repository'
|
|
4
4
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
5
|
+
import { log } from '@/lib/server/logger'
|
|
5
6
|
import type { Agent } from '@/types'
|
|
6
7
|
|
|
7
8
|
export const dynamic = 'force-dynamic'
|
|
@@ -37,15 +38,52 @@ export async function POST(req: Request) {
|
|
|
37
38
|
return NextResponse.json({ error: 'content is required' }, { status: 400 })
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
// Look up the agent
|
|
41
|
-
|
|
42
|
-
if (!agent
|
|
41
|
+
// Look up the agent and auto-register on SwarmFeed if needed
|
|
42
|
+
let agent = getAgent(body.agentId) as Agent | undefined
|
|
43
|
+
if (!agent) {
|
|
44
|
+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
45
|
+
}
|
|
46
|
+
if (!agent.swarmfeedEnabled) {
|
|
43
47
|
return NextResponse.json(
|
|
44
|
-
{ error: '
|
|
48
|
+
{ error: 'SwarmFeed is not enabled for this agent. Enable it in agent settings first.' },
|
|
45
49
|
{ status: 400 },
|
|
46
50
|
)
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
// Auto-register if enabled but no API key yet
|
|
54
|
+
if (!agent.swarmfeedApiKey) {
|
|
55
|
+
const agentName = agent.name
|
|
56
|
+
try {
|
|
57
|
+
log.info('swarmfeed', `Auto-registering agent "${agentName}" on SwarmFeed`)
|
|
58
|
+
const reg = await registerAgent({
|
|
59
|
+
name: agent.name,
|
|
60
|
+
description: agent.description || agent.swarmfeedBio || `${agent.name} agent on SwarmClaw`,
|
|
61
|
+
framework: 'swarmclaw',
|
|
62
|
+
model: agent.model,
|
|
63
|
+
avatar: agent.avatarUrl || undefined,
|
|
64
|
+
bio: agent.swarmfeedBio || undefined,
|
|
65
|
+
})
|
|
66
|
+
patchAgent(agent.id, (current) => {
|
|
67
|
+
if (!current) return null
|
|
68
|
+
return {
|
|
69
|
+
...current,
|
|
70
|
+
swarmfeedApiKey: reg.apiKey,
|
|
71
|
+
swarmfeedAgentId: reg.agentId,
|
|
72
|
+
swarmfeedJoinedAt: current.swarmfeedJoinedAt ?? Date.now(),
|
|
73
|
+
updatedAt: Date.now(),
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
agent = getAgent(body.agentId) as Agent | undefined
|
|
77
|
+
if (!agent?.swarmfeedApiKey) {
|
|
78
|
+
return NextResponse.json({ error: 'Registration succeeded but API key not saved' }, { status: 500 })
|
|
79
|
+
}
|
|
80
|
+
} catch (err: unknown) {
|
|
81
|
+
const message = err instanceof Error ? err.message : 'Registration failed'
|
|
82
|
+
log.error('swarmfeed', `Auto-registration failed for "${agentName}": ${message}`)
|
|
83
|
+
return NextResponse.json({ error: `SwarmFeed registration failed: ${message}` }, { status: 502 })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
49
87
|
try {
|
|
50
88
|
const post = await createPost(agent.swarmfeedApiKey, {
|
|
51
89
|
content: body.content.trim(),
|
|
@@ -25,6 +25,10 @@ export async function GET(req: Request) {
|
|
|
25
25
|
const agents = Object.values(loadAgents()) as Agent[]
|
|
26
26
|
const feedAgent = agents.find((a) => a.swarmfeedEnabled && a.swarmfeedApiKey)
|
|
27
27
|
agentApiKey = feedAgent?.swarmfeedApiKey ?? undefined
|
|
28
|
+
// No registered agent — return empty feed instead of triggering a 401
|
|
29
|
+
if (!agentApiKey) {
|
|
30
|
+
return NextResponse.json({ posts: [], nextCursor: undefined })
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
try {
|
package/src/cli/index.js
CHANGED
|
@@ -806,6 +806,13 @@ const COMMAND_GROUPS = [
|
|
|
806
806
|
cmd('post', 'POST', '/swarmfeed/posts', 'Create a post', { expectsJsonBody: true }),
|
|
807
807
|
],
|
|
808
808
|
},
|
|
809
|
+
{
|
|
810
|
+
name: 'swarmdock',
|
|
811
|
+
description: 'SwarmDock marketplace',
|
|
812
|
+
commands: [
|
|
813
|
+
cmd('browse', 'GET', '/swarmdock', 'Browse SwarmDock marketplace tasks and agents'),
|
|
814
|
+
],
|
|
815
|
+
},
|
|
809
816
|
]
|
|
810
817
|
|
|
811
818
|
const GROUP_MAP = new Map(COMMAND_GROUPS.map((group) => [group.name, group]))
|
|
@@ -29,6 +29,7 @@ import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
|
|
|
29
29
|
import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
30
30
|
import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
|
|
31
31
|
import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
|
|
32
|
+
import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
|
|
32
33
|
|
|
33
34
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
34
35
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
@@ -1983,6 +1984,15 @@ export function AgentSheet() {
|
|
|
1983
1984
|
</SectionCard>
|
|
1984
1985
|
)}
|
|
1985
1986
|
|
|
1987
|
+
{editing && (
|
|
1988
|
+
<SectionCard
|
|
1989
|
+
title="Marketplace"
|
|
1990
|
+
description="SwarmDock integration — list this agent on the AI marketplace to accept tasks and earn USDC."
|
|
1991
|
+
>
|
|
1992
|
+
<AgentMarketplaceSettings agent={editing} />
|
|
1993
|
+
</SectionCard>
|
|
1994
|
+
)}
|
|
1995
|
+
|
|
1986
1996
|
{!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
|
|
1987
1997
|
<AdvancedSettingsSection
|
|
1988
1998
|
open={showAdvancedSettings}
|
|
@@ -267,6 +267,11 @@ export function SidebarRail({
|
|
|
267
267
|
<path d="M4 11a9 9 0 0 1 9 9" /><path d="M4 4a16 16 0 0 1 16 16" /><circle cx="5" cy="19" r="1" />
|
|
268
268
|
</svg>
|
|
269
269
|
</NavItem>
|
|
270
|
+
<NavItem view="marketplace" label="Marketplace" expanded={railExpanded} isActive={isNavActive('marketplace')} onClick={() => handleNavClick('marketplace')}>
|
|
271
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
272
|
+
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><line x1="3" x2="21" y1="6" y2="6" /><path d="M16 10a4 4 0 0 1-8 0" />
|
|
273
|
+
</svg>
|
|
274
|
+
</NavItem>
|
|
270
275
|
</div>
|
|
271
276
|
|
|
272
277
|
<div className={`flex flex-col gap-0.5 ${railExpanded ? '' : 'items-center'}`}>
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useMemo } from 'react'
|
|
4
|
+
import { updateAgent } from '@/lib/agents'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { HintTip } from '@/components/shared/hint-tip'
|
|
7
|
+
import { AdvancedSettingsSection } from '@/components/shared/advanced-settings-section'
|
|
8
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
9
|
+
import type { Agent, SwarmDockMarketplaceConfig } from '@/types'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MARKETPLACE: SwarmDockMarketplaceConfig = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
autoDiscover: false,
|
|
14
|
+
maxBudgetUsdc: '5000000',
|
|
15
|
+
autoBid: false,
|
|
16
|
+
autoBidMaxPrice: '1000000',
|
|
17
|
+
taskNotifications: true,
|
|
18
|
+
preferredCategories: [],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AgentMarketplaceSettings({ agent, onUpdate }: {
|
|
22
|
+
agent: Agent
|
|
23
|
+
onUpdate?: (agent: Agent) => void
|
|
24
|
+
}) {
|
|
25
|
+
const [enabled, setEnabled] = useState(agent.swarmdockEnabled || false)
|
|
26
|
+
const [description, setDescription] = useState(agent.swarmdockDescription || '')
|
|
27
|
+
const [skills, setSkills] = useState<string[]>(agent.swarmdockSkills || [])
|
|
28
|
+
const [walletId, setWalletId] = useState<string | null>(agent.swarmdockWalletId || null)
|
|
29
|
+
const [marketplace, setMarketplace] = useState<SwarmDockMarketplaceConfig>(agent.swarmdockMarketplace || DEFAULT_MARKETPLACE)
|
|
30
|
+
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
31
|
+
const [skillInput, setSkillInput] = useState('')
|
|
32
|
+
const [saving, setSaving] = useState(false)
|
|
33
|
+
|
|
34
|
+
const wallets = useAppStore((s) => s.wallets)
|
|
35
|
+
const loadWallets = useAppStore((s) => s.loadWallets)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
loadWallets()
|
|
39
|
+
}, [loadWallets])
|
|
40
|
+
|
|
41
|
+
const agentWallets = useMemo(() =>
|
|
42
|
+
Object.values(wallets).filter((w) => w.agentId === agent.id),
|
|
43
|
+
[wallets, agent.id])
|
|
44
|
+
|
|
45
|
+
const handleSave = useCallback(async () => {
|
|
46
|
+
setSaving(true)
|
|
47
|
+
try {
|
|
48
|
+
const updated = await updateAgent(agent.id, {
|
|
49
|
+
swarmdockEnabled: enabled,
|
|
50
|
+
swarmdockDescription: description.trim() || null,
|
|
51
|
+
swarmdockSkills: skills,
|
|
52
|
+
swarmdockWalletId: walletId,
|
|
53
|
+
swarmdockListedAt: enabled && !agent.swarmdockListedAt ? Date.now() : agent.swarmdockListedAt,
|
|
54
|
+
swarmdockMarketplace: marketplace.enabled ? marketplace : null,
|
|
55
|
+
})
|
|
56
|
+
toast.success('Marketplace settings saved')
|
|
57
|
+
onUpdate?.(updated)
|
|
58
|
+
} catch {
|
|
59
|
+
toast.error('Failed to save marketplace settings')
|
|
60
|
+
} finally {
|
|
61
|
+
setSaving(false)
|
|
62
|
+
}
|
|
63
|
+
}, [agent.id, agent.swarmdockListedAt, enabled, description, skills, walletId, marketplace, onUpdate])
|
|
64
|
+
|
|
65
|
+
const addSkill = useCallback(() => {
|
|
66
|
+
const trimmed = skillInput.trim().toLowerCase().replace(/\s+/g, '-')
|
|
67
|
+
if (trimmed && !skills.includes(trimmed)) {
|
|
68
|
+
setSkills((prev) => [...prev, trimmed])
|
|
69
|
+
}
|
|
70
|
+
setSkillInput('')
|
|
71
|
+
}, [skillInput, skills])
|
|
72
|
+
|
|
73
|
+
const removeSkill = useCallback((skill: string) => {
|
|
74
|
+
setSkills((prev) => prev.filter((s) => s !== skill))
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
const truncateAddr = (addr: string) =>
|
|
78
|
+
addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="space-y-5">
|
|
82
|
+
{/* Enable/Disable toggle */}
|
|
83
|
+
<div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
|
|
84
|
+
<div className="min-w-0">
|
|
85
|
+
<div className="flex items-center gap-2">
|
|
86
|
+
<p className="text-[14px] font-600 text-text">SwarmDock</p>
|
|
87
|
+
<HintTip text="Enable this agent to list on the SwarmDock AI marketplace" />
|
|
88
|
+
</div>
|
|
89
|
+
<p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
|
|
90
|
+
List this agent on the marketplace to accept tasks and earn USDC.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => setEnabled((c) => !c)}
|
|
96
|
+
className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
97
|
+
aria-pressed={enabled}
|
|
98
|
+
>
|
|
99
|
+
<span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${enabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{enabled && (
|
|
104
|
+
<>
|
|
105
|
+
{/* Description */}
|
|
106
|
+
<div>
|
|
107
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
108
|
+
Marketplace Description <HintTip text="A short description shown on the agent's marketplace profile" />
|
|
109
|
+
</label>
|
|
110
|
+
<textarea
|
|
111
|
+
value={description}
|
|
112
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
113
|
+
placeholder="Describe what this agent specializes in..."
|
|
114
|
+
className="w-full min-h-[80px] px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow resize-y"
|
|
115
|
+
style={{ fontFamily: 'inherit' }}
|
|
116
|
+
maxLength={500}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Skills */}
|
|
121
|
+
<div>
|
|
122
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
123
|
+
Skills <HintTip text="Skill tags for task matching on the marketplace" />
|
|
124
|
+
</label>
|
|
125
|
+
{skills.length > 0 && (
|
|
126
|
+
<div className="flex flex-wrap gap-2 mb-2">
|
|
127
|
+
{skills.map((skill) => (
|
|
128
|
+
<button
|
|
129
|
+
key={skill}
|
|
130
|
+
onClick={() => removeSkill(skill)}
|
|
131
|
+
className="px-3 py-1.5 rounded-[10px] border border-accent-bright/40 bg-accent-bright/10 text-accent-bright text-[12px] font-500 transition-all cursor-pointer bg-transparent hover:bg-red-500/10 hover:border-red-500/40 hover:text-red-400"
|
|
132
|
+
>
|
|
133
|
+
{skill} ×
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
<div className="flex gap-2">
|
|
139
|
+
<input
|
|
140
|
+
value={skillInput}
|
|
141
|
+
onChange={(e) => setSkillInput(e.target.value)}
|
|
142
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addSkill() } }}
|
|
143
|
+
placeholder="e.g. data-analysis, web-design"
|
|
144
|
+
className="flex-1 px-4 py-2.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
145
|
+
style={{ fontFamily: 'inherit' }}
|
|
146
|
+
/>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={addSkill}
|
|
150
|
+
disabled={!skillInput.trim()}
|
|
151
|
+
className="px-4 py-2.5 rounded-[12px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 transition-all hover:bg-white/[0.08] disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
|
|
152
|
+
>
|
|
153
|
+
Add
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Wallet picker */}
|
|
159
|
+
<div>
|
|
160
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
161
|
+
Payout Wallet <HintTip text="Base L2 wallet for receiving USDC payments" />
|
|
162
|
+
</label>
|
|
163
|
+
{agentWallets.length > 0 ? (
|
|
164
|
+
<select
|
|
165
|
+
value={walletId || ''}
|
|
166
|
+
onChange={(e) => setWalletId(e.target.value || null)}
|
|
167
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none cursor-pointer"
|
|
168
|
+
style={{ fontFamily: 'inherit' }}
|
|
169
|
+
>
|
|
170
|
+
<option value="">No wallet selected</option>
|
|
171
|
+
{agentWallets.map((w) => (
|
|
172
|
+
<option key={w.id} value={w.id}>
|
|
173
|
+
{w.label ? `${w.label} (${truncateAddr(w.walletAddress)})` : truncateAddr(w.walletAddress)}
|
|
174
|
+
</option>
|
|
175
|
+
))}
|
|
176
|
+
</select>
|
|
177
|
+
) : (
|
|
178
|
+
<p className="text-[13px] text-text-3/75">
|
|
179
|
+
No wallets linked to this agent. Add a wallet in the Wallets section first.
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Advanced: Marketplace config */}
|
|
185
|
+
<AdvancedSettingsSection
|
|
186
|
+
open={showAdvanced}
|
|
187
|
+
onToggle={() => setShowAdvanced((c) => !c)}
|
|
188
|
+
summary={marketplace.enabled ? 'Active' : undefined}
|
|
189
|
+
badges={marketplace.enabled ? [
|
|
190
|
+
...(marketplace.autoDiscover ? ['auto-discover'] : []),
|
|
191
|
+
...(marketplace.autoBid ? ['auto-bid'] : []),
|
|
192
|
+
] : []}
|
|
193
|
+
>
|
|
194
|
+
<div className="space-y-4">
|
|
195
|
+
<div className="flex items-center justify-between gap-4">
|
|
196
|
+
<div className="min-w-0">
|
|
197
|
+
<div className="flex items-center gap-2">
|
|
198
|
+
<p className="text-[13px] font-600 text-text">Marketplace Automation</p>
|
|
199
|
+
<HintTip text="Enable automated task discovery and bidding on the marketplace" />
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => setMarketplace((m) => ({ ...m, enabled: !m.enabled }))}
|
|
205
|
+
className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${marketplace.enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
206
|
+
aria-pressed={marketplace.enabled}
|
|
207
|
+
>
|
|
208
|
+
<span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${marketplace.enabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{marketplace.enabled && (
|
|
213
|
+
<>
|
|
214
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
215
|
+
<div
|
|
216
|
+
onClick={() => setMarketplace((m) => ({ ...m, autoDiscover: !m.autoDiscover }))}
|
|
217
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoDiscover ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
218
|
+
>
|
|
219
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoDiscover ? 'left-[22px]' : 'left-0.5'}`} />
|
|
220
|
+
</div>
|
|
221
|
+
<span className="flex items-center gap-2 text-[13px] text-text-2">
|
|
222
|
+
Auto-discover <HintTip text="Automatically scan for matching tasks" />
|
|
223
|
+
</span>
|
|
224
|
+
</label>
|
|
225
|
+
|
|
226
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
227
|
+
<div
|
|
228
|
+
onClick={() => setMarketplace((m) => ({ ...m, autoBid: !m.autoBid }))}
|
|
229
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoBid ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
230
|
+
>
|
|
231
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoBid ? 'left-[22px]' : 'left-0.5'}`} />
|
|
232
|
+
</div>
|
|
233
|
+
<span className="flex items-center gap-2 text-[13px] text-text-2">
|
|
234
|
+
Auto-bid <HintTip text="Automatically bid on matching tasks" />
|
|
235
|
+
</span>
|
|
236
|
+
</label>
|
|
237
|
+
|
|
238
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
239
|
+
<div
|
|
240
|
+
onClick={() => setMarketplace((m) => ({ ...m, taskNotifications: !m.taskNotifications }))}
|
|
241
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.taskNotifications ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
242
|
+
>
|
|
243
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.taskNotifications ? 'left-[22px]' : 'left-0.5'}`} />
|
|
244
|
+
</div>
|
|
245
|
+
<span className="flex items-center gap-2 text-[13px] text-text-2">
|
|
246
|
+
Task notifications <HintTip text="Show notifications for new matching tasks" />
|
|
247
|
+
</span>
|
|
248
|
+
</label>
|
|
249
|
+
|
|
250
|
+
<div>
|
|
251
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
|
|
252
|
+
Max budget (USDC) <HintTip text="Maximum budget cap per task. 1000000 = $1.00" />
|
|
253
|
+
</label>
|
|
254
|
+
<input
|
|
255
|
+
value={marketplace.maxBudgetUsdc}
|
|
256
|
+
onChange={(e) => setMarketplace((m) => ({ ...m, maxBudgetUsdc: e.target.value.replace(/[^0-9]/g, '') }))}
|
|
257
|
+
placeholder="5000000"
|
|
258
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
259
|
+
style={{ fontFamily: 'inherit' }}
|
|
260
|
+
/>
|
|
261
|
+
<p className="mt-1 text-[11px] text-text-3/60">
|
|
262
|
+
= ${(parseInt(marketplace.maxBudgetUsdc || '0', 10) / 1_000_000).toFixed(2)} USDC
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{marketplace.autoBid && (
|
|
267
|
+
<div>
|
|
268
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
|
|
269
|
+
Auto-bid max price (USDC) <HintTip text="Maximum amount to auto-bid per task. 1000000 = $1.00" />
|
|
270
|
+
</label>
|
|
271
|
+
<input
|
|
272
|
+
value={marketplace.autoBidMaxPrice}
|
|
273
|
+
onChange={(e) => setMarketplace((m) => ({ ...m, autoBidMaxPrice: e.target.value.replace(/[^0-9]/g, '') }))}
|
|
274
|
+
placeholder="1000000"
|
|
275
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
276
|
+
style={{ fontFamily: 'inherit' }}
|
|
277
|
+
/>
|
|
278
|
+
<p className="mt-1 text-[11px] text-text-3/60">
|
|
279
|
+
= ${(parseInt(marketplace.autoBidMaxPrice || '0', 10) / 1_000_000).toFixed(2)} USDC
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</AdvancedSettingsSection>
|
|
287
|
+
</>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* Save button */}
|
|
291
|
+
<div className="flex justify-end pt-2">
|
|
292
|
+
<button
|
|
293
|
+
onClick={handleSave}
|
|
294
|
+
disabled={saving}
|
|
295
|
+
className="px-6 py-2.5 rounded-[12px] bg-accent-bright text-white text-[14px] font-600 transition-all
|
|
296
|
+
hover:bg-accent-bright/90 disabled:opacity-40 disabled:cursor-not-allowed border-none cursor-pointer"
|
|
297
|
+
>
|
|
298
|
+
{saving ? 'Saving...' : 'Save Marketplace Settings'}
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
}
|