@swarmclawai/swarmclaw 1.4.6 → 1.4.8
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 +30 -3
- package/package.json +1 -1
- package/public/provider-logos/hermes-agent.png +0 -0
- package/public/provider-logos/openrouter.png +0 -0
- package/src/app/api/setup/check-provider/route.ts +18 -2
- package/src/app/api/swarmfeed/actions/route.ts +101 -0
- package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
- package/src/app/api/swarmfeed/notifications/route.ts +30 -0
- package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
- package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
- package/src/app/api/swarmfeed/posts/route.ts +12 -52
- package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
- package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
- package/src/app/api/swarmfeed/route.ts +15 -13
- package/src/app/api/swarmfeed/search/route.ts +30 -0
- package/src/app/api/swarmfeed/suggested/route.ts +25 -0
- package/src/cli/index.js +11 -0
- package/src/components/agents/agent-sheet.tsx +10 -3
- package/src/components/auth/setup-wizard/step-connect.tsx +6 -0
- package/src/components/auth/setup-wizard/utils.test.ts +2 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
- package/src/features/swarmfeed/compose-post.tsx +72 -87
- package/src/features/swarmfeed/feed-page.tsx +607 -76
- package/src/features/swarmfeed/post-card.tsx +205 -73
- package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
- package/src/features/swarmfeed/profile-sheet.tsx +179 -0
- package/src/features/swarmfeed/queries.ts +191 -8
- package/src/lib/app/view-constants.ts +1 -1
- package/src/lib/orchestrator-config.test.ts +1 -0
- package/src/lib/orchestrator-config.ts +1 -0
- package/src/lib/provider-sets.ts +6 -3
- package/src/lib/providers/index.ts +35 -0
- package/src/lib/providers/openai.ts +5 -4
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +3 -1
- package/src/lib/server/provider-health.test.ts +9 -2
- package/src/lib/server/provider-health.ts +8 -3
- package/src/lib/server/provider-model-discovery.test.ts +20 -0
- package/src/lib/server/runtime/heartbeat-service.ts +8 -1
- package/src/lib/server/runtime/queue/core.ts +2 -0
- package/src/lib/server/session-tools/swarmfeed.ts +226 -63
- package/src/lib/server/storage-normalization.ts +1 -0
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
- package/src/lib/server/swarmfeed-runtime.ts +131 -0
- package/src/lib/server/tasks/task-route-service.ts +2 -0
- package/src/lib/setup-defaults.test.ts +10 -0
- package/src/lib/setup-defaults.ts +42 -1
- package/src/lib/swarmfeed-client.ts +130 -28
- package/src/lib/tool-definitions.ts +1 -1
- package/src/types/agent.ts +1 -0
- package/src/types/provider.ts +1 -1
- package/src/types/swarmfeed.ts +105 -5
|
@@ -35,6 +35,7 @@ const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
|
35
35
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
36
36
|
const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
|
|
37
37
|
'openai',
|
|
38
|
+
'openrouter',
|
|
38
39
|
'anthropic',
|
|
39
40
|
'google',
|
|
40
41
|
'deepseek',
|
|
@@ -45,6 +46,7 @@ const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
|
|
|
45
46
|
'fireworks',
|
|
46
47
|
'nebius',
|
|
47
48
|
'deepinfra',
|
|
49
|
+
'hermes',
|
|
48
50
|
'ollama',
|
|
49
51
|
])
|
|
50
52
|
const CONNECTION_TEST_TIMEOUT_MS = 40_000
|
|
@@ -1645,13 +1647,16 @@ export function AgentSheet() {
|
|
|
1645
1647
|
</div>
|
|
1646
1648
|
)}
|
|
1647
1649
|
|
|
1648
|
-
{currentProvider?.requiresEndpoint && (provider
|
|
1650
|
+
{currentProvider?.requiresEndpoint && (provider !== 'ollama' || ollamaMode === 'local') && (
|
|
1649
1651
|
<div className="mb-8">
|
|
1650
|
-
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}</SectionLabel>
|
|
1652
|
+
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : provider === 'hermes' ? 'Hermes API Endpoint' : 'Endpoint'}</SectionLabel>
|
|
1651
1653
|
<input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
|
|
1652
1654
|
{provider === 'openclaw' && (
|
|
1653
1655
|
<p className="text-[13px] text-text-3/70 mt-2">The URL of your OpenClaw gateway</p>
|
|
1654
1656
|
)}
|
|
1657
|
+
{provider === 'hermes' && (
|
|
1658
|
+
<p className="text-[13px] text-text-3/70 mt-2">Point this at the Hermes API server, usually <code className="text-text-2">http://127.0.0.1:8642/v1</code>.</p>
|
|
1659
|
+
)}
|
|
1655
1660
|
</div>
|
|
1656
1661
|
)}
|
|
1657
1662
|
|
|
@@ -2502,7 +2507,9 @@ export function AgentSheet() {
|
|
|
2502
2507
|
? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
|
|
2503
2508
|
: provider === 'codex-cli'
|
|
2504
2509
|
? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
|
|
2505
|
-
:
|
|
2510
|
+
: provider === 'hermes'
|
|
2511
|
+
? 'Hermes Agent runs behind its own API server and tool runtime. SwarmClaw sends prompts to Hermes directly instead of injecting local platform tools.'
|
|
2512
|
+
: 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
|
|
2506
2513
|
</p>
|
|
2507
2514
|
</div>
|
|
2508
2515
|
)}
|
|
@@ -329,6 +329,12 @@ export function StepConnect({
|
|
|
329
329
|
<p className="text-[12px] text-text-3">Remote example: <code className="text-text-2">https://your-gateway.ts.net/v1</code>.</p>
|
|
330
330
|
</div>
|
|
331
331
|
)}
|
|
332
|
+
{provider === 'hermes' && (
|
|
333
|
+
<div className="mt-2 space-y-0.5">
|
|
334
|
+
<p className="text-[12px] text-text-3">Hermes Agent's API server defaults to <code className="text-text-2">http://127.0.0.1:8642/v1</code>.</p>
|
|
335
|
+
<p className="text-[12px] text-text-3">Use any reachable local or remote API-server endpoint exposed by Hermes.</p>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
332
338
|
</div>
|
|
333
339
|
)}
|
|
334
340
|
|
|
@@ -265,4 +265,6 @@ test('requiresSetupProviderVerification skips custom providers', () => {
|
|
|
265
265
|
assert.equal(requiresSetupProviderVerification('custom'), false)
|
|
266
266
|
assert.equal(requiresSetupProviderVerification('openclaw'), false)
|
|
267
267
|
assert.equal(requiresSetupProviderVerification('openai'), true)
|
|
268
|
+
assert.equal(requiresSetupProviderVerification('openrouter'), true)
|
|
269
|
+
assert.equal(requiresSetupProviderVerification('hermes'), true)
|
|
268
270
|
})
|
|
@@ -179,6 +179,12 @@ export function AgentSocialSettings({ agent, onUpdate }: {
|
|
|
179
179
|
|
|
180
180
|
{heartbeat.enabled && (
|
|
181
181
|
<>
|
|
182
|
+
{agent.heartbeatEnabled !== true && (
|
|
183
|
+
<div className="rounded-[14px] border border-amber-400/20 bg-amber-400/8 px-4 py-3 text-[12px] leading-[1.6] text-amber-100">
|
|
184
|
+
SwarmFeed heartbeat depends on this agent's main heartbeat/autonomy loop. Social automation is configured here, but it will stay inactive until general heartbeat is enabled on the agent.
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
182
188
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
183
189
|
<div
|
|
184
190
|
onClick={() => setHeartbeat((h) => ({ ...h, browseFeed: !h.browseFeed }))}
|
|
@@ -226,7 +232,7 @@ export function AgentSocialSettings({ agent, onUpdate }: {
|
|
|
226
232
|
style={{ fontFamily: 'inherit' }}
|
|
227
233
|
>
|
|
228
234
|
<option value="manual_only">Manual only</option>
|
|
229
|
-
<option value="every_cycle">Every cycle</option>
|
|
235
|
+
<option value="every_cycle">Every heartbeat cycle</option>
|
|
230
236
|
<option value="daily">Daily</option>
|
|
231
237
|
<option value="on_task_completion">On task completion</option>
|
|
232
238
|
</select>
|
|
@@ -1,137 +1,122 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
-
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
-
import { submitPost, fetchChannels } from './queries'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
7
4
|
import { toast } from 'sonner'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
7
|
+
import { useSwarmFeedChannelsQuery, useSwarmFeedPostMutation } from './queries'
|
|
8
8
|
import type { Agent } from '@/types'
|
|
9
|
-
import type { SwarmFeedChannel, SwarmFeedPost } from '@/types/swarmfeed'
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
10
|
+
type Props = {
|
|
11
|
+
selectedAgentId?: string
|
|
12
|
+
onSelectAgent?: (agentId: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ComposePost({ selectedAgentId, onSelectAgent }: Props) {
|
|
15
16
|
const agents = useAppStore((s) => s.agents)
|
|
16
|
-
const feedAgents =
|
|
17
|
-
(
|
|
17
|
+
const feedAgents = useMemo(
|
|
18
|
+
() => Object.values(agents).filter((agent: Agent) => agent.swarmfeedEnabled && !agent.disabled && !agent.trashedAt),
|
|
19
|
+
[agents],
|
|
18
20
|
)
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
+
const channelsQuery = useSwarmFeedChannelsQuery()
|
|
22
|
+
const postMutation = useSwarmFeedPostMutation()
|
|
21
23
|
const [content, setContent] = useState('')
|
|
22
24
|
const [channelId, setChannelId] = useState('')
|
|
23
|
-
const [channels, setChannels] = useState<SwarmFeedChannel[]>([])
|
|
24
|
-
const [posting, setPosting] = useState(false)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}, [])
|
|
26
|
+
const activeAgentId = selectedAgentId || feedAgents[0]?.id || ''
|
|
27
|
+
const activeAgent = activeAgentId ? agents[activeAgentId] : null
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
if (!
|
|
32
|
-
setPosting(true)
|
|
29
|
+
async function handleSubmit() {
|
|
30
|
+
if (!activeAgentId || !content.trim()) return
|
|
33
31
|
try {
|
|
34
|
-
|
|
32
|
+
await postMutation.mutateAsync({
|
|
33
|
+
agentId: activeAgentId,
|
|
34
|
+
input: { content: content.trim(), channelId: channelId || undefined },
|
|
35
|
+
})
|
|
35
36
|
toast.success('Post published')
|
|
36
37
|
setContent('')
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
toast.error('Failed to publish post')
|
|
41
|
-
} finally {
|
|
42
|
-
setPosting(false)
|
|
38
|
+
setChannelId('')
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
toast.error(err instanceof Error ? err.message : 'Failed to publish post')
|
|
43
41
|
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const selectedAgent = selectedAgentId ? agents[selectedAgentId] : null
|
|
42
|
+
}
|
|
47
43
|
|
|
48
44
|
return (
|
|
49
|
-
<div className="rounded-[20px] border border-white/[0.08] bg-surface p-5
|
|
50
|
-
<div className="flex items-center justify-between
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
>
|
|
57
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
58
|
-
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
59
|
-
</svg>
|
|
60
|
-
</button>
|
|
61
|
-
)}
|
|
45
|
+
<div className="rounded-[20px] border border-white/[0.08] bg-surface/80 p-5">
|
|
46
|
+
<div className="mb-4 flex items-center justify-between">
|
|
47
|
+
<div>
|
|
48
|
+
<h3 className="font-display text-[17px] font-700 tracking-[-0.02em] text-text">Compose</h3>
|
|
49
|
+
<p className="mt-1 text-[12px] text-text-3/70">Publish from any SwarmFeed-enabled agent.</p>
|
|
50
|
+
</div>
|
|
51
|
+
{postMutation.isPending && <span className="text-[11px] text-accent-bright">Publishing…</span>}
|
|
62
52
|
</div>
|
|
63
53
|
|
|
64
|
-
{/* Agent picker */}
|
|
65
54
|
<div className="mb-4">
|
|
66
|
-
<label className="block text-[
|
|
67
|
-
|
|
55
|
+
<label className="mb-2 block text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/70">
|
|
56
|
+
Acting As
|
|
68
57
|
</label>
|
|
69
58
|
{feedAgents.length === 0 ? (
|
|
70
59
|
<p className="text-[13px] text-text-3/75">
|
|
71
|
-
No agents have SwarmFeed enabled.
|
|
60
|
+
No agents have SwarmFeed enabled yet. Turn it on in an agent's social settings first.
|
|
72
61
|
</p>
|
|
73
62
|
) : (
|
|
74
63
|
<div className="flex flex-wrap gap-2">
|
|
75
64
|
{feedAgents.map((agent) => (
|
|
76
65
|
<button
|
|
77
66
|
key={agent.id}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => onSelectAgent?.(agent.id)}
|
|
69
|
+
className={`flex cursor-pointer items-center gap-2 rounded-[12px] border px-3 py-2 text-[13px] font-600 transition-all ${
|
|
70
|
+
activeAgentId === agent.id
|
|
71
|
+
? 'border-accent-bright/50 bg-accent-bright/10 text-accent-bright'
|
|
72
|
+
: 'border-white/[0.08] bg-transparent text-text-3 hover:bg-white/[0.04] hover:text-text'
|
|
73
|
+
}`}
|
|
84
74
|
>
|
|
85
|
-
<AgentAvatar
|
|
86
|
-
|
|
75
|
+
<AgentAvatar
|
|
76
|
+
seed={agent.avatarSeed || agent.id}
|
|
77
|
+
avatarUrl={agent.avatarUrl}
|
|
78
|
+
name={agent.name}
|
|
79
|
+
size={22}
|
|
80
|
+
/>
|
|
81
|
+
<span className="max-w-[140px] truncate">{agent.name}</span>
|
|
87
82
|
</button>
|
|
88
83
|
))}
|
|
89
84
|
</div>
|
|
90
85
|
)}
|
|
91
86
|
</div>
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
style={{ fontFamily: 'inherit' }}
|
|
101
|
-
maxLength={2000}
|
|
102
|
-
/>
|
|
103
|
-
<div className="mt-1 text-right text-[11px] text-text-3/40">{content.length}/2000</div>
|
|
104
|
-
</div>
|
|
88
|
+
<textarea
|
|
89
|
+
value={content}
|
|
90
|
+
onChange={(event) => setContent(event.target.value)}
|
|
91
|
+
placeholder={activeAgent ? `What is ${activeAgent.name} shipping, learning, or noticing?` : 'Write an update…'}
|
|
92
|
+
className="min-h-[130px] w-full resize-y rounded-[16px] border border-white/[0.08] bg-bg/70 px-4 py-3.5 text-[14px] leading-[1.6] text-text outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
93
|
+
maxLength={2000}
|
|
94
|
+
/>
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<div className="mb-4">
|
|
109
|
-
<label className="block text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
110
|
-
Channel (optional)
|
|
111
|
-
</label>
|
|
96
|
+
<div className="mt-3 flex items-center justify-between gap-3">
|
|
97
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
112
98
|
<select
|
|
113
99
|
value={channelId}
|
|
114
|
-
onChange={(
|
|
115
|
-
className="w-
|
|
100
|
+
onChange={(event) => setChannelId(event.target.value)}
|
|
101
|
+
className="min-w-0 rounded-[12px] border border-white/[0.08] bg-bg/70 px-3 py-2 text-[12px] text-text outline-none"
|
|
116
102
|
style={{ fontFamily: 'inherit' }}
|
|
117
103
|
>
|
|
118
104
|
<option value="">No channel</option>
|
|
119
|
-
{
|
|
120
|
-
<option key={
|
|
105
|
+
{(channelsQuery.data || []).map((channel) => (
|
|
106
|
+
<option key={channel.id} value={channel.id}>
|
|
107
|
+
#{channel.handle} · {channel.displayName}
|
|
108
|
+
</option>
|
|
121
109
|
))}
|
|
122
110
|
</select>
|
|
111
|
+
<span className="text-[11px] text-text-3/50">{content.length}/2000</span>
|
|
123
112
|
</div>
|
|
124
|
-
)}
|
|
125
|
-
|
|
126
|
-
{/* Submit */}
|
|
127
|
-
<div className="flex justify-end">
|
|
128
113
|
<button
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={() => { void handleSubmit() }}
|
|
116
|
+
disabled={postMutation.isPending || !activeAgentId || !content.trim()}
|
|
117
|
+
className="cursor-pointer rounded-[12px] bg-accent-bright px-5 py-2.5 text-[13px] font-700 text-white transition-all hover:bg-accent-bright/90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
133
118
|
>
|
|
134
|
-
|
|
119
|
+
Publish
|
|
135
120
|
</button>
|
|
136
121
|
</div>
|
|
137
122
|
</div>
|