@swarmclawai/swarmclaw 1.4.7 → 1.4.9

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 (34) hide show
  1. package/README.md +17 -2
  2. package/package.json +1 -1
  3. package/scripts/run-next-build.mjs +44 -0
  4. package/src/app/api/swarmfeed/actions/route.ts +101 -0
  5. package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
  6. package/src/app/api/swarmfeed/notifications/route.ts +30 -0
  7. package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
  8. package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
  9. package/src/app/api/swarmfeed/posts/route.ts +12 -52
  10. package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
  11. package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
  12. package/src/app/api/swarmfeed/route.ts +15 -13
  13. package/src/app/api/swarmfeed/search/route.ts +30 -0
  14. package/src/app/api/swarmfeed/suggested/route.ts +25 -0
  15. package/src/cli/index.js +11 -0
  16. package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
  17. package/src/features/swarmfeed/compose-post.tsx +72 -87
  18. package/src/features/swarmfeed/feed-page.tsx +600 -76
  19. package/src/features/swarmfeed/post-card.tsx +205 -73
  20. package/src/features/swarmfeed/post-thread-sheet.tsx +170 -0
  21. package/src/features/swarmfeed/profile-sheet.tsx +179 -0
  22. package/src/features/swarmfeed/queries.ts +191 -8
  23. package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
  24. package/src/lib/server/runtime/heartbeat-service.ts +8 -1
  25. package/src/lib/server/runtime/queue/core.ts +2 -0
  26. package/src/lib/server/session-tools/swarmfeed.ts +226 -63
  27. package/src/lib/server/storage-normalization.ts +1 -0
  28. package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
  29. package/src/lib/server/swarmfeed-runtime.ts +131 -0
  30. package/src/lib/server/tasks/task-route-service.ts +2 -0
  31. package/src/lib/swarmfeed-client.ts +130 -28
  32. package/src/lib/tool-definitions.ts +1 -1
  33. package/src/types/agent.ts +1 -0
  34. package/src/types/swarmfeed.ts +105 -5
@@ -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>