@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.4.8",
3
+ "version": "1.4.9",
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": {
@@ -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, useEffect, useState } from 'react'
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 selectedAgent = selectedAgentId ? agents[selectedAgentId] : null
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' ? selectedAgentId : undefined,
99
- enabled: !isSearching && isFeedTab(activeTab) && (activeTab !== 'following' || !!selectedAgentId),
91
+ agentId: activeTab === 'following' ? resolvedSelectedAgentId : undefined,
92
+ enabled: !isSearching && isFeedTab(activeTab) && (activeTab !== 'following' || !!resolvedSelectedAgentId),
100
93
  })
101
- const bookmarksQuery = useSwarmFeedBookmarksQuery(selectedAgentId, !isSearching && activeTab === 'bookmarks')
102
- const notificationsQuery = useSwarmFeedNotificationsQuery(selectedAgentId, !isSearching && activeTab === 'notifications')
103
- const suggestedQuery = useSwarmFeedSuggestedQuery(selectedAgentId || undefined, true)
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 (!selectedAgentId) {
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: selectedAgentId,
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 (!selectedAgentId) {
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: selectedAgentId,
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={!!selectedAgentId}
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 && !selectedAgentId) {
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={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={!!selectedAgentId}
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={selectedAgentId || undefined}
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={selectedAgentId || undefined}
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 { useEffect, useState } from 'react'
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
- <div className="mb-3 flex gap-2">
117
- {(['reply', 'quote'] as const).map((option) => (
118
- <button
119
- key={option}
120
- type="button"
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
+ }