@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.
Files changed (54) hide show
  1. package/README.md +30 -3
  2. package/package.json +1 -1
  3. package/public/provider-logos/hermes-agent.png +0 -0
  4. package/public/provider-logos/openrouter.png +0 -0
  5. package/src/app/api/setup/check-provider/route.ts +18 -2
  6. package/src/app/api/swarmfeed/actions/route.ts +101 -0
  7. package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
  8. package/src/app/api/swarmfeed/notifications/route.ts +30 -0
  9. package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
  10. package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
  11. package/src/app/api/swarmfeed/posts/route.ts +12 -52
  12. package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
  13. package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
  14. package/src/app/api/swarmfeed/route.ts +15 -13
  15. package/src/app/api/swarmfeed/search/route.ts +30 -0
  16. package/src/app/api/swarmfeed/suggested/route.ts +25 -0
  17. package/src/cli/index.js +11 -0
  18. package/src/components/agents/agent-sheet.tsx +10 -3
  19. package/src/components/auth/setup-wizard/step-connect.tsx +6 -0
  20. package/src/components/auth/setup-wizard/utils.test.ts +2 -0
  21. package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
  22. package/src/features/swarmfeed/compose-post.tsx +72 -87
  23. package/src/features/swarmfeed/feed-page.tsx +607 -76
  24. package/src/features/swarmfeed/post-card.tsx +205 -73
  25. package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
  26. package/src/features/swarmfeed/profile-sheet.tsx +179 -0
  27. package/src/features/swarmfeed/queries.ts +191 -8
  28. package/src/lib/app/view-constants.ts +1 -1
  29. package/src/lib/orchestrator-config.test.ts +1 -0
  30. package/src/lib/orchestrator-config.ts +1 -0
  31. package/src/lib/provider-sets.ts +6 -3
  32. package/src/lib/providers/index.ts +35 -0
  33. package/src/lib/providers/openai.ts +5 -4
  34. package/src/lib/server/agents/agent-availability.ts +2 -2
  35. package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
  36. package/src/lib/server/chat-execution/chat-turn-preparation.ts +3 -1
  37. package/src/lib/server/provider-health.test.ts +9 -2
  38. package/src/lib/server/provider-health.ts +8 -3
  39. package/src/lib/server/provider-model-discovery.test.ts +20 -0
  40. package/src/lib/server/runtime/heartbeat-service.ts +8 -1
  41. package/src/lib/server/runtime/queue/core.ts +2 -0
  42. package/src/lib/server/session-tools/swarmfeed.ts +226 -63
  43. package/src/lib/server/storage-normalization.ts +1 -0
  44. package/src/lib/server/storage.ts +1 -1
  45. package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
  46. package/src/lib/server/swarmfeed-runtime.ts +131 -0
  47. package/src/lib/server/tasks/task-route-service.ts +2 -0
  48. package/src/lib/setup-defaults.test.ts +10 -0
  49. package/src/lib/setup-defaults.ts +42 -1
  50. package/src/lib/swarmfeed-client.ts +130 -28
  51. package/src/lib/tool-definitions.ts +1 -1
  52. package/src/types/agent.ts +1 -0
  53. package/src/types/provider.ts +1 -1
  54. 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 === 'openclaw' || (provider === 'ollama' && ollamaMode === 'local')) && (
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
- : 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
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&apos;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&apos;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 { useState, useCallback, useEffect } from 'react'
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
- export function ComposePost({ onPostCreated, onClose }: {
12
- onPostCreated?: (post: SwarmFeedPost) => void
13
- onClose?: () => void
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 = Object.values(agents).filter(
17
- (a: Agent) => a.swarmfeedEnabled && !a.disabled && !a.trashedAt,
17
+ const feedAgents = useMemo(
18
+ () => Object.values(agents).filter((agent: Agent) => agent.swarmfeedEnabled && !agent.disabled && !agent.trashedAt),
19
+ [agents],
18
20
  )
19
-
20
- const [selectedAgentId, setSelectedAgentId] = useState<string>(feedAgents[0]?.id || '')
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
- useEffect(() => {
27
- fetchChannels().then(setChannels).catch(() => { /* channels are optional */ })
28
- }, [])
26
+ const activeAgentId = selectedAgentId || feedAgents[0]?.id || ''
27
+ const activeAgent = activeAgentId ? agents[activeAgentId] : null
29
28
 
30
- const handleSubmit = useCallback(async () => {
31
- if (!selectedAgentId || !content.trim()) return
32
- setPosting(true)
29
+ async function handleSubmit() {
30
+ if (!activeAgentId || !content.trim()) return
33
31
  try {
34
- const post = await submitPost(selectedAgentId, content.trim(), channelId || undefined)
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
- onPostCreated?.(post)
38
- onClose?.()
39
- } catch {
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
- }, [selectedAgentId, content, channelId, onPostCreated, onClose])
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 sm:p-6">
50
- <div className="flex items-center justify-between mb-4">
51
- <h3 className="font-display text-[17px] font-700 tracking-[-0.02em] text-text">Compose Post</h3>
52
- {onClose && (
53
- <button
54
- onClick={onClose}
55
- className="w-8 h-8 rounded-[10px] flex items-center justify-center text-text-3 hover:text-text hover:bg-white/[0.06] transition-all bg-transparent border-none cursor-pointer"
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-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
67
- Post as
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. Enable it in an agent&apos;s settings first.
60
+ No agents have SwarmFeed enabled yet. Turn it on in an agent&apos;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
- onClick={() => setSelectedAgentId(agent.id)}
79
- className={`flex items-center gap-2 px-3 py-2 rounded-[12px] border text-[13px] font-500 transition-all cursor-pointer bg-transparent
80
- ${selectedAgentId === agent.id
81
- ? 'border-accent-bright/40 bg-accent-bright/10 text-accent-bright'
82
- : 'border-white/[0.08] text-text-3 hover:text-text hover:bg-white/[0.04]'
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 seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={22} />
86
- <span className="truncate max-w-[120px]">{agent.name}</span>
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
- {/* Content */}
94
- <div className="mb-4">
95
- <textarea
96
- value={content}
97
- onChange={(e) => setContent(e.target.value)}
98
- placeholder={selectedAgent ? `What's ${selectedAgent.name} thinking?` : 'Write something...'}
99
- className="w-full min-h-[120px] px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow resize-y"
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
- {/* Channel selector */}
107
- {channels.length > 0 && (
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={(e) => setChannelId(e.target.value)}
115
- className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none cursor-pointer"
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
- {channels.map((ch) => (
120
- <option key={ch.id} value={ch.id}>#{ch.handle} - {ch.displayName}</option>
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
- onClick={handleSubmit}
130
- disabled={posting || !selectedAgentId || !content.trim()}
131
- className="px-6 py-2.5 rounded-[12px] bg-accent-bright text-white text-[14px] font-600 transition-all
132
- hover:bg-accent-bright/90 disabled:opacity-40 disabled:cursor-not-allowed border-none cursor-pointer"
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
- {posting ? 'Publishing...' : 'Publish'}
119
+ Publish
135
120
  </button>
136
121
  </div>
137
122
  </div>