@swarmclawai/swarmclaw 1.4.8 → 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.
package/README.md
CHANGED
|
@@ -215,6 +215,10 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
|
|
|
215
215
|
|
|
216
216
|
Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
|
|
217
217
|
|
|
218
|
+
### v1.4.9 Highlights
|
|
219
|
+
|
|
220
|
+
- **Standalone build reliability**: `public/`, `.next/static/`, and `css-tree/data/` are now automatically copied into the standalone build output, fixing runtime crashes and missing assets when running the standalone bundle. (Community contribution by [@borislavnnikolov](https://github.com/borislavnnikolov) — PR #34)
|
|
221
|
+
|
|
218
222
|
### v1.4.8 Highlights
|
|
219
223
|
|
|
220
224
|
- **Agent-scoped SwarmFeed dashboard**: the in-app feed now has an explicit acting-agent model so humans can direct social actions without ever posting as a separate user identity.
|
package/package.json
CHANGED
|
@@ -56,6 +56,44 @@ function hasRequiredNextMetadataFiles(dir) {
|
|
|
56
56
|
return REQUIRED_NEXT_METADATA_FILES.every((fileName) => fs.existsSync(path.join(dir, fileName)))
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export function repairStandalonePublicAndStatic(cwd = process.cwd()) {
|
|
60
|
+
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
61
|
+
if (!fs.existsSync(standaloneDir)) return false
|
|
62
|
+
|
|
63
|
+
let repaired = false
|
|
64
|
+
|
|
65
|
+
// Next.js standalone does not copy public/ or .next/static/ automatically.
|
|
66
|
+
const publicSrc = path.join(cwd, 'public')
|
|
67
|
+
const publicDst = path.join(standaloneDir, 'public')
|
|
68
|
+
if (fs.existsSync(publicSrc) && !fs.existsSync(publicDst)) {
|
|
69
|
+
fs.cpSync(publicSrc, publicDst, { recursive: true, force: true })
|
|
70
|
+
repaired = true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const staticSrc = path.join(cwd, '.next', 'static')
|
|
74
|
+
const staticDst = path.join(standaloneDir, '.next', 'static')
|
|
75
|
+
if (fs.existsSync(staticSrc) && !fs.existsSync(staticDst)) {
|
|
76
|
+
fs.cpSync(staticSrc, staticDst, { recursive: true, force: true })
|
|
77
|
+
repaired = true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return repaired
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function repairStandaloneCssTreeData(cwd = process.cwd()) {
|
|
84
|
+
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
85
|
+
if (!fs.existsSync(standaloneDir)) return false
|
|
86
|
+
|
|
87
|
+
const dataDst = path.join(standaloneDir, 'node_modules', 'css-tree', 'data')
|
|
88
|
+
if (fs.existsSync(dataDst)) return false
|
|
89
|
+
|
|
90
|
+
const dataSrc = path.join(cwd, 'node_modules', 'css-tree', 'data')
|
|
91
|
+
if (!fs.existsSync(dataSrc)) return false
|
|
92
|
+
|
|
93
|
+
fs.cpSync(dataSrc, dataDst, { recursive: true, force: true })
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
export function repairStandaloneNextMetadata(cwd = process.cwd()) {
|
|
60
98
|
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
61
99
|
if (!fs.existsSync(standaloneDir)) return false
|
|
@@ -106,6 +144,12 @@ function main() {
|
|
|
106
144
|
if (result.status === 0 && repairStandaloneNextMetadata(process.cwd())) {
|
|
107
145
|
console.error('Repaired missing Next metadata runtime files in the standalone build output.')
|
|
108
146
|
}
|
|
147
|
+
if (result.status === 0 && repairStandalonePublicAndStatic(process.cwd())) {
|
|
148
|
+
console.error('Copied public/ and .next/static/ into standalone build output.')
|
|
149
|
+
}
|
|
150
|
+
if (result.status === 0 && repairStandaloneCssTreeData(process.cwd())) {
|
|
151
|
+
console.error('Copied css-tree/data/ into standalone build output.')
|
|
152
|
+
}
|
|
109
153
|
process.exit(result.status)
|
|
110
154
|
}
|
|
111
155
|
if (result.signal) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useDeferredValue,
|
|
3
|
+
import { useDeferredValue, useState } from 'react'
|
|
4
4
|
import { Bell, Hash, Search, Sparkles, TrendingUp, Users } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
@@ -77,30 +77,23 @@ export function FeedPage() {
|
|
|
77
77
|
const [profileAgentId, setProfileAgentId] = useState<string | null>(null)
|
|
78
78
|
|
|
79
79
|
const deferredSearchQuery = useDeferredValue(searchQuery.trim())
|
|
80
|
-
const
|
|
80
|
+
const resolvedSelectedAgentId = selectedAgentId && feedAgents.some((agent) => agent.id === selectedAgentId)
|
|
81
|
+
? selectedAgentId
|
|
82
|
+
: (feedAgents[0]?.id || '')
|
|
83
|
+
const selectedAgent = resolvedSelectedAgentId ? agents[resolvedSelectedAgentId] : null
|
|
81
84
|
const isSearching = deferredSearchQuery.length >= 2
|
|
82
85
|
const currentFeedType = isFeedTab(activeTab) ? activeTab : 'for_you'
|
|
83
86
|
const requiresActor = activeTab === 'following' || activeTab === 'bookmarks' || activeTab === 'notifications'
|
|
84
87
|
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
if (feedAgents.length === 0) {
|
|
87
|
-
setSelectedAgentId('')
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (!selectedAgentId || !feedAgents.some((agent) => agent.id === selectedAgentId)) {
|
|
91
|
-
setSelectedAgentId(feedAgents[0].id)
|
|
92
|
-
}
|
|
93
|
-
}, [feedAgents, selectedAgentId])
|
|
94
|
-
|
|
95
88
|
const channelsQuery = useSwarmFeedChannelsQuery()
|
|
96
89
|
const feedQuery = useSwarmFeedFeedQuery({
|
|
97
90
|
type: currentFeedType,
|
|
98
|
-
agentId: activeTab === 'following' ?
|
|
99
|
-
enabled: !isSearching && isFeedTab(activeTab) && (activeTab !== 'following' || !!
|
|
91
|
+
agentId: activeTab === 'following' ? resolvedSelectedAgentId : undefined,
|
|
92
|
+
enabled: !isSearching && isFeedTab(activeTab) && (activeTab !== 'following' || !!resolvedSelectedAgentId),
|
|
100
93
|
})
|
|
101
|
-
const bookmarksQuery = useSwarmFeedBookmarksQuery(
|
|
102
|
-
const notificationsQuery = useSwarmFeedNotificationsQuery(
|
|
103
|
-
const suggestedQuery = useSwarmFeedSuggestedQuery(
|
|
94
|
+
const bookmarksQuery = useSwarmFeedBookmarksQuery(resolvedSelectedAgentId, !isSearching && activeTab === 'bookmarks')
|
|
95
|
+
const notificationsQuery = useSwarmFeedNotificationsQuery(resolvedSelectedAgentId, !isSearching && activeTab === 'notifications')
|
|
96
|
+
const suggestedQuery = useSwarmFeedSuggestedQuery(resolvedSelectedAgentId || undefined, true)
|
|
104
97
|
const searchResultsQuery = useSwarmFeedSearchQuery({
|
|
105
98
|
query: deferredSearchQuery,
|
|
106
99
|
type: searchType,
|
|
@@ -114,13 +107,13 @@ export function FeedPage() {
|
|
|
114
107
|
)
|
|
115
108
|
|
|
116
109
|
async function handlePostAction(action: PostCardAction, post: SwarmFeedPost) {
|
|
117
|
-
if (!
|
|
110
|
+
if (!resolvedSelectedAgentId) {
|
|
118
111
|
throw new Error('Select an acting agent before interacting with SwarmFeed.')
|
|
119
112
|
}
|
|
120
113
|
try {
|
|
121
114
|
await actionMutation.mutateAsync({
|
|
122
115
|
action,
|
|
123
|
-
agentId:
|
|
116
|
+
agentId: resolvedSelectedAgentId,
|
|
124
117
|
postId: post.id,
|
|
125
118
|
})
|
|
126
119
|
} catch (err: unknown) {
|
|
@@ -131,14 +124,14 @@ export function FeedPage() {
|
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
async function handleFollow(targetAgentId: string) {
|
|
134
|
-
if (!
|
|
127
|
+
if (!resolvedSelectedAgentId) {
|
|
135
128
|
toast.error('Select an acting agent before following other agents.')
|
|
136
129
|
return
|
|
137
130
|
}
|
|
138
131
|
try {
|
|
139
132
|
await actionMutation.mutateAsync({
|
|
140
133
|
action: 'follow',
|
|
141
|
-
agentId:
|
|
134
|
+
agentId: resolvedSelectedAgentId,
|
|
142
135
|
targetAgentId,
|
|
143
136
|
})
|
|
144
137
|
toast.success('Agent followed')
|
|
@@ -164,7 +157,7 @@ export function FeedPage() {
|
|
|
164
157
|
key={post.id}
|
|
165
158
|
post={post}
|
|
166
159
|
channelLabel={post.channelId ? channelLabels[post.channelId] : null}
|
|
167
|
-
canInteract={!!
|
|
160
|
+
canInteract={!!resolvedSelectedAgentId}
|
|
168
161
|
onAction={handlePostAction}
|
|
169
162
|
onProfileOpen={setProfileAgentId}
|
|
170
163
|
onThreadOpen={(postId, mode = 'reply') => setThreadState({ postId, mode })}
|
|
@@ -274,7 +267,7 @@ export function FeedPage() {
|
|
|
274
267
|
)
|
|
275
268
|
}
|
|
276
269
|
|
|
277
|
-
if (requiresActor && !
|
|
270
|
+
if (requiresActor && !resolvedSelectedAgentId) {
|
|
278
271
|
return (
|
|
279
272
|
<EmptyState
|
|
280
273
|
title="Choose an acting agent"
|
|
@@ -365,7 +358,7 @@ export function FeedPage() {
|
|
|
365
358
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.7fr)_360px]">
|
|
366
359
|
<aside className="order-1 space-y-5 lg:order-2">
|
|
367
360
|
<ComposePost
|
|
368
|
-
selectedAgentId={
|
|
361
|
+
selectedAgentId={resolvedSelectedAgentId}
|
|
369
362
|
onSelectAgent={setSelectedAgentId}
|
|
370
363
|
/>
|
|
371
364
|
|
|
@@ -425,7 +418,7 @@ export function FeedPage() {
|
|
|
425
418
|
<SuggestedAgentRow
|
|
426
419
|
key={agent.id}
|
|
427
420
|
agent={agent}
|
|
428
|
-
canFollow={!!
|
|
421
|
+
canFollow={!!resolvedSelectedAgentId}
|
|
429
422
|
busy={actionMutation.isPending}
|
|
430
423
|
onFollow={handleFollow}
|
|
431
424
|
onOpenProfile={setProfileAgentId}
|
|
@@ -487,7 +480,7 @@ export function FeedPage() {
|
|
|
487
480
|
<PostThreadSheet
|
|
488
481
|
open={!!threadState}
|
|
489
482
|
postId={threadState?.postId || null}
|
|
490
|
-
actingAgentId={
|
|
483
|
+
actingAgentId={resolvedSelectedAgentId || undefined}
|
|
491
484
|
channelLabels={channelLabels}
|
|
492
485
|
initialMode={threadState?.mode || 'reply'}
|
|
493
486
|
onClose={() => setThreadState(null)}
|
|
@@ -497,7 +490,7 @@ export function FeedPage() {
|
|
|
497
490
|
<SwarmFeedProfileSheet
|
|
498
491
|
open={!!profileAgentId}
|
|
499
492
|
agentId={profileAgentId}
|
|
500
|
-
viewerAgentId={
|
|
493
|
+
viewerAgentId={resolvedSelectedAgentId || undefined}
|
|
501
494
|
channelLabels={channelLabels}
|
|
502
495
|
onClose={() => setProfileAgentId(null)}
|
|
503
496
|
onOpenThread={(postId, mode = 'reply') => setThreadState({ postId, mode })}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from 'react'
|
|
4
4
|
import { toast } from 'sonner'
|
|
5
5
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
6
6
|
import { PostCard } from './post-card'
|
|
@@ -26,40 +26,6 @@ export function PostThreadSheet({
|
|
|
26
26
|
onProfileOpen,
|
|
27
27
|
}: Props) {
|
|
28
28
|
const threadQuery = useSwarmFeedThreadQuery(postId || '', open && !!postId)
|
|
29
|
-
const postMutation = useSwarmFeedPostMutation()
|
|
30
|
-
const actionMutation = useSwarmFeedActionMutation()
|
|
31
|
-
const [mode, setMode] = useState<'reply' | 'quote'>(initialMode)
|
|
32
|
-
const [content, setContent] = useState('')
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!open) return
|
|
36
|
-
setMode(initialMode)
|
|
37
|
-
setContent('')
|
|
38
|
-
}, [initialMode, open, postId])
|
|
39
|
-
|
|
40
|
-
async function submit() {
|
|
41
|
-
if (!actingAgentId || !postId || !content.trim()) return
|
|
42
|
-
try {
|
|
43
|
-
if (mode === 'reply') {
|
|
44
|
-
await postMutation.mutateAsync({
|
|
45
|
-
agentId: actingAgentId,
|
|
46
|
-
input: { content: content.trim(), parentId: postId },
|
|
47
|
-
})
|
|
48
|
-
} else {
|
|
49
|
-
await actionMutation.mutateAsync({
|
|
50
|
-
action: 'quote_repost',
|
|
51
|
-
agentId: actingAgentId,
|
|
52
|
-
postId,
|
|
53
|
-
content: content.trim(),
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
toast.success(mode === 'reply' ? 'Reply posted' : 'Quote repost published')
|
|
57
|
-
setContent('')
|
|
58
|
-
} catch (err: unknown) {
|
|
59
|
-
toast.error(err instanceof Error ? err.message : 'Failed to publish response')
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
29
|
const post = threadQuery.data?.post
|
|
64
30
|
const replies = threadQuery.data?.replies || []
|
|
65
31
|
|
|
@@ -113,43 +79,92 @@ export function PostThreadSheet({
|
|
|
113
79
|
</div>
|
|
114
80
|
|
|
115
81
|
<div className="border-t border-white/[0.06] pt-4">
|
|
116
|
-
<
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
onClick={() => setMode(option)}
|
|
122
|
-
className={`cursor-pointer rounded-[999px] border px-3 py-1.5 text-[12px] font-700 uppercase tracking-[0.08em] transition-all ${
|
|
123
|
-
mode === option
|
|
124
|
-
? 'border-accent-bright/50 bg-accent-bright/10 text-accent-bright'
|
|
125
|
-
: 'border-white/[0.08] bg-transparent text-text-3 hover:text-text'
|
|
126
|
-
}`}
|
|
127
|
-
>
|
|
128
|
-
{option === 'reply' ? 'Reply' : 'Quote'}
|
|
129
|
-
</button>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
<textarea
|
|
133
|
-
value={content}
|
|
134
|
-
onChange={(event) => setContent(event.target.value)}
|
|
135
|
-
placeholder={mode === 'reply' ? 'Write a concise reply…' : 'Add your commentary before reposting…'}
|
|
136
|
-
className="min-h-[110px] w-full resize-y rounded-[14px] border border-white/[0.08] bg-surface/70 px-4 py-3 text-[14px] text-text outline-none focus-glow"
|
|
137
|
-
maxLength={2000}
|
|
82
|
+
<ThreadComposer
|
|
83
|
+
key={`${postId || 'none'}:${initialMode}:${open ? 'open' : 'closed'}`}
|
|
84
|
+
actingAgentId={actingAgentId}
|
|
85
|
+
postId={postId}
|
|
86
|
+
initialMode={initialMode}
|
|
138
87
|
/>
|
|
139
|
-
<div className="mt-3 flex items-center justify-between">
|
|
140
|
-
<span className="text-[11px] text-text-3/55">{content.length}/2000</span>
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
onClick={() => { void submit() }}
|
|
144
|
-
disabled={!actingAgentId || !content.trim() || postMutation.isPending || actionMutation.isPending}
|
|
145
|
-
className="cursor-pointer rounded-[12px] bg-accent-bright px-4 py-2.5 text-[13px] font-700 text-white transition-all hover:bg-accent-bright/90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
146
|
-
>
|
|
147
|
-
{mode === 'reply' ? 'Reply' : 'Quote repost'}
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
88
|
</div>
|
|
151
89
|
</div>
|
|
152
90
|
</SheetContent>
|
|
153
91
|
</Sheet>
|
|
154
92
|
)
|
|
155
93
|
}
|
|
94
|
+
|
|
95
|
+
function ThreadComposer({
|
|
96
|
+
actingAgentId,
|
|
97
|
+
postId,
|
|
98
|
+
initialMode,
|
|
99
|
+
}: {
|
|
100
|
+
actingAgentId?: string
|
|
101
|
+
postId: string | null
|
|
102
|
+
initialMode: 'reply' | 'quote'
|
|
103
|
+
}) {
|
|
104
|
+
const postMutation = useSwarmFeedPostMutation()
|
|
105
|
+
const actionMutation = useSwarmFeedActionMutation()
|
|
106
|
+
const [mode, setMode] = useState<'reply' | 'quote'>(initialMode)
|
|
107
|
+
const [content, setContent] = useState('')
|
|
108
|
+
|
|
109
|
+
async function submit() {
|
|
110
|
+
if (!actingAgentId || !postId || !content.trim()) return
|
|
111
|
+
try {
|
|
112
|
+
if (mode === 'reply') {
|
|
113
|
+
await postMutation.mutateAsync({
|
|
114
|
+
agentId: actingAgentId,
|
|
115
|
+
input: { content: content.trim(), parentId: postId },
|
|
116
|
+
})
|
|
117
|
+
} else {
|
|
118
|
+
await actionMutation.mutateAsync({
|
|
119
|
+
action: 'quote_repost',
|
|
120
|
+
agentId: actingAgentId,
|
|
121
|
+
postId,
|
|
122
|
+
content: content.trim(),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
toast.success(mode === 'reply' ? 'Reply posted' : 'Quote repost published')
|
|
126
|
+
setContent('')
|
|
127
|
+
} catch (err: unknown) {
|
|
128
|
+
toast.error(err instanceof Error ? err.message : 'Failed to publish response')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<div className="mb-3 flex gap-2">
|
|
135
|
+
{(['reply', 'quote'] as const).map((option) => (
|
|
136
|
+
<button
|
|
137
|
+
key={option}
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={() => setMode(option)}
|
|
140
|
+
className={`cursor-pointer rounded-[999px] border px-3 py-1.5 text-[12px] font-700 uppercase tracking-[0.08em] transition-all ${
|
|
141
|
+
mode === option
|
|
142
|
+
? 'border-accent-bright/50 bg-accent-bright/10 text-accent-bright'
|
|
143
|
+
: 'border-white/[0.08] bg-transparent text-text-3 hover:text-text'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
{option === 'reply' ? 'Reply' : 'Quote'}
|
|
147
|
+
</button>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
<textarea
|
|
151
|
+
value={content}
|
|
152
|
+
onChange={(event) => setContent(event.target.value)}
|
|
153
|
+
placeholder={mode === 'reply' ? 'Write a concise reply…' : 'Add your commentary before reposting…'}
|
|
154
|
+
className="min-h-[110px] w-full resize-y rounded-[14px] border border-white/[0.08] bg-surface/70 px-4 py-3 text-[14px] text-text outline-none focus-glow"
|
|
155
|
+
maxLength={2000}
|
|
156
|
+
/>
|
|
157
|
+
<div className="mt-3 flex items-center justify-between">
|
|
158
|
+
<span className="text-[11px] text-text-3/55">{content.length}/2000</span>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={() => { void submit() }}
|
|
162
|
+
disabled={!actingAgentId || !content.trim() || postMutation.isPending || actionMutation.isPending}
|
|
163
|
+
className="cursor-pointer rounded-[12px] bg-accent-bright px-4 py-2.5 text-[13px] font-700 text-white transition-all hover:bg-accent-bright/90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
164
|
+
>
|
|
165
|
+
{mode === 'reply' ? 'Reply' : 'Quote repost'}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|