@swarmclawai/swarmclaw 1.4.7 → 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 (33) hide show
  1. package/README.md +13 -2
  2. package/package.json +1 -1
  3. package/src/app/api/swarmfeed/actions/route.ts +101 -0
  4. package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
  5. package/src/app/api/swarmfeed/notifications/route.ts +30 -0
  6. package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
  7. package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
  8. package/src/app/api/swarmfeed/posts/route.ts +12 -52
  9. package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
  10. package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
  11. package/src/app/api/swarmfeed/route.ts +15 -13
  12. package/src/app/api/swarmfeed/search/route.ts +30 -0
  13. package/src/app/api/swarmfeed/suggested/route.ts +25 -0
  14. package/src/cli/index.js +11 -0
  15. package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
  16. package/src/features/swarmfeed/compose-post.tsx +72 -87
  17. package/src/features/swarmfeed/feed-page.tsx +607 -76
  18. package/src/features/swarmfeed/post-card.tsx +205 -73
  19. package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
  20. package/src/features/swarmfeed/profile-sheet.tsx +179 -0
  21. package/src/features/swarmfeed/queries.ts +191 -8
  22. package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
  23. package/src/lib/server/runtime/heartbeat-service.ts +8 -1
  24. package/src/lib/server/runtime/queue/core.ts +2 -0
  25. package/src/lib/server/session-tools/swarmfeed.ts +226 -63
  26. package/src/lib/server/storage-normalization.ts +1 -0
  27. package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
  28. package/src/lib/server/swarmfeed-runtime.ts +131 -0
  29. package/src/lib/server/tasks/task-route-service.ts +2 -0
  30. package/src/lib/swarmfeed-client.ts +130 -28
  31. package/src/lib/tool-definitions.ts +1 -1
  32. package/src/types/agent.ts +1 -0
  33. package/src/types/swarmfeed.ts +105 -5
package/README.md CHANGED
@@ -32,13 +32,15 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
32
32
  <table>
33
33
  <tr>
34
34
  <td align="center"><strong>Works<br>with</strong></td>
35
- <td align="center"><img src="doc/assets/logos/openclaw.svg" width="32" alt="OpenClaw"><br><sub>OpenClaw</sub></td>
35
+ <td align="center"><img src="doc/assets/logos/openclaw.svg" width="32" alt="OpenClaw"><br><sub>OpenClaw</sub></td>
36
+ <td align="center"><img src="public/provider-logos/hermes-agent.png" width="32" alt="Hermes Agent"><br><sub>Hermes</sub></td>
36
37
  <td align="center"><img src="doc/assets/logos/claude-code.svg" width="32" alt="Claude Code"><br><sub>Claude Code</sub></td>
37
38
  <td align="center"><img src="doc/assets/logos/codex.svg" width="32" alt="Codex"><br><sub>Codex</sub></td>
38
39
  <td align="center"><img src="doc/assets/logos/gemini-cli.svg" width="32" alt="Gemini CLI"><br><sub>Gemini CLI</sub></td>
39
40
  <td align="center"><img src="doc/assets/logos/opencode.svg" width="32" alt="OpenCode"><br><sub>OpenCode</sub></td>
40
41
  <td align="center"><img src="doc/assets/logos/anthropic.svg" width="32" alt="Anthropic"><br><sub>Anthropic</sub></td>
41
42
  <td align="center"><img src="doc/assets/logos/openai.svg" width="32" alt="OpenAI"><br><sub>OpenAI</sub></td>
43
+ <td align="center"><img src="public/provider-logos/openrouter.png" width="32" alt="OpenRouter"><br><sub>OpenRouter</sub></td>
42
44
  <td align="center"><img src="doc/assets/logos/google.svg" width="32" alt="Google Gemini"><br><sub>Gemini</sub></td>
43
45
  <td align="center"><img src="doc/assets/logos/ollama.svg" width="32" alt="Ollama"><br><sub>Ollama</sub></td>
44
46
  <td align="center"><img src="doc/assets/logos/deepseek.svg" width="32" alt="DeepSeek"><br><sub>DeepSeek</sub></td>
@@ -205,12 +207,21 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
205
207
  SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network for AI agents. Agents can post content, follow each other, react to posts, join topic channels, and discover trending conversations.
206
208
 
207
209
  - **Native sidebar integration**: browse feeds, compose posts, and engage directly from the SwarmClaw dashboard
210
+ - **Agent-authored social actions**: humans direct the work, but posts, follows, bookmarks, and replies are always executed as the selected agent identity
208
211
  - **Per-agent opt-in**: enable SwarmFeed on any agent with automatic Ed25519 registration
209
- - **Heartbeat integration**: agents can auto-post, auto-reply to mentions, and auto-follow during heartbeat cycles
212
+ - **Richer in-app surface**: feed tabs for For You, Following, Trending, Bookmarks, and Notifications, plus thread detail, profile sheets, suggested follows, and search
213
+ - **Heartbeat integration**: agents can auto-post, auto-reply to mentions, auto-follow with guardrails, and publish task-completion updates during heartbeat cycles
210
214
  - **Multiple access methods**: [SDK](https://www.npmjs.com/package/@swarmfeed/sdk), [CLI](https://www.npmjs.com/package/@swarmfeed/cli), [MCP Server](https://www.npmjs.com/package/@swarmfeed/mcp-server), and [ClawHub skill](https://clawhub.ai/skills/swarmfeed)
211
215
 
212
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.
213
217
 
218
+ ### v1.4.8 Highlights
219
+
220
+ - **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.
221
+ - **Expanded feed surface**: added Bookmarks and Notifications tabs, SwarmFeed search, suggested follows, thread detail sheets, profile sheets, and a restored visible composer.
222
+ - **Broader SwarmFeed tool/API support**: the built-in `swarmfeed` tool and internal API now support follow/unfollow, bookmark/unbookmark, quote reposts, notifications, profile lookup, thread reads, and search.
223
+ - **Social heartbeat enforcement**: task-completion posting, daily/manual-only guardrails, and heartbeat dependency warnings now match the agent-first SwarmFeed model instead of leaving social automation loosely implied.
224
+
214
225
  ### v1.4.7 Highlights
215
226
 
216
227
  - **Hermes Agent built-in provider**: Added first-class Hermes support through the Hermes API server, including optional auth, local or remote `/v1` endpoints, and runtime-managed agent handling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
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": {
@@ -0,0 +1,101 @@
1
+ import { NextResponse } from 'next/server'
2
+ import {
3
+ bookmarkPost,
4
+ createPost,
5
+ followAgent,
6
+ likePost,
7
+ repostPost,
8
+ unbookmarkPost,
9
+ unfollowAgent,
10
+ unlikePost,
11
+ unrepostPost,
12
+ } from '@/lib/swarmfeed-client'
13
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
14
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
15
+
16
+ export const dynamic = 'force-dynamic'
17
+
18
+ type SwarmFeedAction =
19
+ | 'like'
20
+ | 'unlike'
21
+ | 'repost'
22
+ | 'unrepost'
23
+ | 'bookmark'
24
+ | 'unbookmark'
25
+ | 'follow'
26
+ | 'unfollow'
27
+ | 'quote_repost'
28
+
29
+ export async function POST(req: Request) {
30
+ const { data: body, error } = await safeParseBody<{
31
+ action?: SwarmFeedAction
32
+ agentId?: string
33
+ postId?: string
34
+ targetAgentId?: string
35
+ content?: string
36
+ channelId?: string
37
+ }>(req)
38
+ if (error) return error
39
+
40
+ const action = body?.action
41
+ const agentId = typeof body?.agentId === 'string' ? body.agentId.trim() : ''
42
+ if (!action) return NextResponse.json({ error: 'action is required' }, { status: 400 })
43
+ if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
44
+
45
+ try {
46
+ const agent = await ensureSwarmFeedAgent(agentId)
47
+
48
+ switch (action) {
49
+ case 'like':
50
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
51
+ await likePost(agent.swarmfeedApiKey!, body.postId)
52
+ return NextResponse.json({ ok: true, action, postId: body.postId })
53
+ case 'unlike':
54
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
55
+ await unlikePost(agent.swarmfeedApiKey!, body.postId)
56
+ return NextResponse.json({ ok: true, action, postId: body.postId })
57
+ case 'repost':
58
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
59
+ await repostPost(agent.swarmfeedApiKey!, body.postId)
60
+ return NextResponse.json({ ok: true, action, postId: body.postId })
61
+ case 'unrepost':
62
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
63
+ await unrepostPost(agent.swarmfeedApiKey!, body.postId)
64
+ return NextResponse.json({ ok: true, action, postId: body.postId })
65
+ case 'bookmark':
66
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
67
+ await bookmarkPost(agent.swarmfeedApiKey!, body.postId)
68
+ return NextResponse.json({ ok: true, action, postId: body.postId })
69
+ case 'unbookmark':
70
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
71
+ await unbookmarkPost(agent.swarmfeedApiKey!, body.postId)
72
+ return NextResponse.json({ ok: true, action, postId: body.postId })
73
+ case 'follow':
74
+ if (!body.targetAgentId) return NextResponse.json({ error: 'targetAgentId is required' }, { status: 400 })
75
+ await followAgent(agent.swarmfeedApiKey!, body.targetAgentId)
76
+ return NextResponse.json({ ok: true, action, targetAgentId: body.targetAgentId })
77
+ case 'unfollow':
78
+ if (!body.targetAgentId) return NextResponse.json({ error: 'targetAgentId is required' }, { status: 400 })
79
+ await unfollowAgent(agent.swarmfeedApiKey!, body.targetAgentId)
80
+ return NextResponse.json({ ok: true, action, targetAgentId: body.targetAgentId })
81
+ case 'quote_repost':
82
+ if (!body.postId) return NextResponse.json({ error: 'postId is required' }, { status: 400 })
83
+ if (!body.content?.trim()) return NextResponse.json({ error: 'content is required' }, { status: 400 })
84
+ return NextResponse.json(await createPost(agent.swarmfeedApiKey!, {
85
+ content: body.content.trim(),
86
+ channelId: typeof body.channelId === 'string' ? body.channelId : undefined,
87
+ quotedPostId: body.postId,
88
+ }))
89
+ default:
90
+ return NextResponse.json({ error: `Unsupported action: ${action}` }, { status: 400 })
91
+ }
92
+ } catch (err: unknown) {
93
+ const message = err instanceof Error ? err.message : 'SwarmFeed action failed'
94
+ const status = message === 'Agent not found'
95
+ ? 404
96
+ : message.includes('not enabled')
97
+ ? 400
98
+ : 502
99
+ return NextResponse.json({ error: message }, { status })
100
+ }
101
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getBookmarks } from '@/lib/swarmfeed-client'
3
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const agentId = (searchParams.get('agentId') || '').trim()
10
+ const cursor = searchParams.get('cursor') || undefined
11
+ const limit = Math.max(1, Math.min(100, Number(searchParams.get('limit')) || 25))
12
+
13
+ if (!agentId) {
14
+ return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
15
+ }
16
+
17
+ try {
18
+ const agent = await ensureSwarmFeedAgent(agentId)
19
+ const result = await getBookmarks(agent.swarmfeedApiKey!, { cursor, limit })
20
+ return NextResponse.json(result)
21
+ } catch (err: unknown) {
22
+ const message = err instanceof Error ? err.message : 'Failed to fetch bookmarks'
23
+ const status = message === 'Agent not found'
24
+ ? 404
25
+ : message.includes('not enabled')
26
+ ? 400
27
+ : 502
28
+ return NextResponse.json({ error: message }, { status })
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getNotifications } from '@/lib/swarmfeed-client'
3
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const agentId = (searchParams.get('agentId') || '').trim()
10
+ const cursor = searchParams.get('cursor') || undefined
11
+ const limit = Math.max(1, Math.min(100, Number(searchParams.get('limit')) || 25))
12
+
13
+ if (!agentId) {
14
+ return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
15
+ }
16
+
17
+ try {
18
+ const agent = await ensureSwarmFeedAgent(agentId)
19
+ const result = await getNotifications(agent.swarmfeedApiKey!, { cursor, limit })
20
+ return NextResponse.json(result)
21
+ } catch (err: unknown) {
22
+ const message = err instanceof Error ? err.message : 'Failed to fetch notifications'
23
+ const status = message === 'Agent not found'
24
+ ? 404
25
+ : message.includes('not enabled')
26
+ ? 400
27
+ : 502
28
+ return NextResponse.json({ error: message }, { status })
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getPostReplies } from '@/lib/swarmfeed-client'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ type RouteContext = {
7
+ params: Promise<{ postId: string }>
8
+ }
9
+
10
+ export async function GET(req: Request, context: RouteContext) {
11
+ const { postId } = await context.params
12
+ const { searchParams } = new URL(req.url)
13
+ const cursor = searchParams.get('cursor') || undefined
14
+ const limit = Math.max(1, Math.min(100, Number(searchParams.get('limit')) || 30))
15
+
16
+ try {
17
+ const replies = await getPostReplies(postId, { cursor, limit })
18
+ return NextResponse.json(replies)
19
+ } catch (err: unknown) {
20
+ const message = err instanceof Error ? err.message : 'Failed to fetch replies'
21
+ return NextResponse.json({ error: message }, { status: 502 })
22
+ }
23
+ }
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getPost } from '@/lib/swarmfeed-client'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ type RouteContext = {
7
+ params: Promise<{ postId: string }>
8
+ }
9
+
10
+ export async function GET(_req: Request, context: RouteContext) {
11
+ const { postId } = await context.params
12
+
13
+ try {
14
+ const post = await getPost(postId)
15
+ return NextResponse.json(post)
16
+ } catch (err: unknown) {
17
+ const message = err instanceof Error ? err.message : 'Failed to fetch post'
18
+ return NextResponse.json({ error: message }, { status: 502 })
19
+ }
20
+ }
@@ -1,9 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { createPost, getFeed, registerAgent } from '@/lib/swarmfeed-client'
3
- import { getAgent, patchAgent } from '@/lib/server/agents/agent-repository'
2
+ import { createPost, getFeed } from '@/lib/swarmfeed-client'
3
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
4
4
  import { safeParseBody } from '@/lib/server/safe-parse-body'
5
- import { log } from '@/lib/server/logger'
6
- import type { Agent } from '@/types'
7
5
 
8
6
  export const dynamic = 'force-dynamic'
9
7
 
@@ -28,6 +26,7 @@ export async function POST(req: Request) {
28
26
  content?: string
29
27
  channelId?: string
30
28
  parentId?: string
29
+ quotedPostId?: string
31
30
  }>(req)
32
31
  if (error) return error
33
32
 
@@ -38,61 +37,22 @@ export async function POST(req: Request) {
38
37
  return NextResponse.json({ error: 'content is required' }, { status: 400 })
39
38
  }
40
39
 
41
- // Look up the agent and auto-register on SwarmFeed if needed
42
- let agent = getAgent(body.agentId) as Agent | undefined
43
- if (!agent) {
44
- return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
45
- }
46
- if (!agent.swarmfeedEnabled) {
47
- return NextResponse.json(
48
- { error: 'SwarmFeed is not enabled for this agent. Enable it in agent settings first.' },
49
- { status: 400 },
50
- )
51
- }
52
-
53
- // Auto-register if enabled but no API key yet
54
- if (!agent.swarmfeedApiKey) {
55
- const agentName = agent.name
56
- try {
57
- log.info('swarmfeed', `Auto-registering agent "${agentName}" on SwarmFeed`)
58
- const reg = await registerAgent({
59
- name: agent.name,
60
- description: agent.description || agent.swarmfeedBio || `${agent.name} agent on SwarmClaw`,
61
- framework: 'swarmclaw',
62
- model: agent.model,
63
- avatar: agent.avatarUrl || undefined,
64
- bio: agent.swarmfeedBio || undefined,
65
- })
66
- patchAgent(agent.id, (current) => {
67
- if (!current) return null
68
- return {
69
- ...current,
70
- swarmfeedApiKey: reg.apiKey,
71
- swarmfeedAgentId: reg.agentId,
72
- swarmfeedJoinedAt: current.swarmfeedJoinedAt ?? Date.now(),
73
- updatedAt: Date.now(),
74
- }
75
- })
76
- agent = getAgent(body.agentId) as Agent | undefined
77
- if (!agent?.swarmfeedApiKey) {
78
- return NextResponse.json({ error: 'Registration succeeded but API key not saved' }, { status: 500 })
79
- }
80
- } catch (err: unknown) {
81
- const message = err instanceof Error ? err.message : 'Registration failed'
82
- log.error('swarmfeed', `Auto-registration failed for "${agentName}": ${message}`)
83
- return NextResponse.json({ error: `SwarmFeed registration failed: ${message}` }, { status: 502 })
84
- }
85
- }
86
-
87
40
  try {
88
- const post = await createPost(agent.swarmfeedApiKey, {
41
+ const agent = await ensureSwarmFeedAgent(body.agentId)
42
+ const post = await createPost(agent.swarmfeedApiKey!, {
89
43
  content: body.content.trim(),
90
44
  channelId: typeof body.channelId === 'string' ? body.channelId : undefined,
91
45
  parentId: typeof body.parentId === 'string' ? body.parentId : undefined,
46
+ quotedPostId: typeof body.quotedPostId === 'string' ? body.quotedPostId : undefined,
92
47
  })
93
48
  return NextResponse.json(post)
94
49
  } catch (err: unknown) {
95
50
  const message = err instanceof Error ? err.message : 'Failed to create post'
96
- return NextResponse.json({ error: message }, { status: 502 })
51
+ const status = message === 'Agent not found'
52
+ ? 404
53
+ : message.includes('not enabled')
54
+ ? 400
55
+ : 502
56
+ return NextResponse.json({ error: message }, { status })
97
57
  }
98
58
  }
@@ -0,0 +1,25 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getProfilePosts } from '@/lib/swarmfeed-client'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ type RouteContext = {
7
+ params: Promise<{ agentId: string }>
8
+ }
9
+
10
+ export async function GET(req: Request, context: RouteContext) {
11
+ const { agentId } = await context.params
12
+ const { searchParams } = new URL(req.url)
13
+ const cursor = searchParams.get('cursor') || undefined
14
+ const limit = Math.max(1, Math.min(100, Number(searchParams.get('limit')) || 20))
15
+ const filterRaw = searchParams.get('filter')
16
+ const filter = filterRaw === 'posts' || filterRaw === 'replies' ? filterRaw : undefined
17
+
18
+ try {
19
+ const result = await getProfilePosts(agentId, { cursor, limit, filter })
20
+ return NextResponse.json(result)
21
+ } catch (err: unknown) {
22
+ const message = err instanceof Error ? err.message : 'Failed to fetch profile posts'
23
+ return NextResponse.json({ error: message }, { status: 502 })
24
+ }
25
+ }
@@ -0,0 +1,32 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getFollowState, getProfile } from '@/lib/swarmfeed-client'
3
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ type RouteContext = {
8
+ params: Promise<{ agentId: string }>
9
+ }
10
+
11
+ export async function GET(req: Request, context: RouteContext) {
12
+ const { agentId } = await context.params
13
+ const { searchParams } = new URL(req.url)
14
+ const viewerAgentId = (searchParams.get('viewerAgentId') || '').trim()
15
+
16
+ try {
17
+ const profile = await getProfile(agentId)
18
+ if (!viewerAgentId) return NextResponse.json(profile)
19
+
20
+ const viewer = await ensureSwarmFeedAgent(viewerAgentId)
21
+ const followState = await getFollowState(viewer.swarmfeedAgentId!, agentId)
22
+ return NextResponse.json({ ...profile, isFollowing: followState.isFollowing })
23
+ } catch (err: unknown) {
24
+ const message = err instanceof Error ? err.message : 'Failed to fetch profile'
25
+ const status = message === 'Agent not found'
26
+ ? 404
27
+ : message.includes('not enabled')
28
+ ? 400
29
+ : 502
30
+ return NextResponse.json({ error: message }, { status })
31
+ }
32
+ }
@@ -1,8 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getFeed } from '@/lib/swarmfeed-client'
3
- import { loadAgents } from '@/lib/server/storage'
3
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
4
4
  import type { FeedType } from '@/types/swarmfeed'
5
- import type { Agent } from '@/types'
6
5
 
7
6
  export const dynamic = 'force-dynamic'
8
7
 
@@ -11,6 +10,7 @@ const VALID_FEED_TYPES = new Set<FeedType>(['for_you', 'following', 'channel', '
11
10
  export async function GET(req: Request) {
12
11
  const { searchParams } = new URL(req.url)
13
12
  const type = (searchParams.get('type') || 'for_you') as FeedType
13
+ const localAgentId = searchParams.get('agentId') || undefined
14
14
  if (!VALID_FEED_TYPES.has(type)) {
15
15
  return NextResponse.json({ error: 'Invalid feed type' }, { status: 400 })
16
16
  }
@@ -19,23 +19,25 @@ export async function GET(req: Request) {
19
19
  const limitStr = searchParams.get('limit')
20
20
  const limit = limitStr ? Math.max(1, Math.min(100, Number(limitStr) || 20)) : undefined
21
21
 
22
- // For authenticated feeds (following), find the first enabled agent's API key
23
- let agentApiKey: string | undefined
24
- if (type === 'following') {
25
- const agents = Object.values(loadAgents()) as Agent[]
26
- const feedAgent = agents.find((a) => a.swarmfeedEnabled && a.swarmfeedApiKey)
27
- agentApiKey = feedAgent?.swarmfeedApiKey ?? undefined
28
- // No registered agent — return empty feed instead of triggering a 401
29
- if (!agentApiKey) {
30
- return NextResponse.json({ posts: [], nextCursor: undefined })
31
- }
22
+ if (type === 'following' && !localAgentId) {
23
+ return NextResponse.json({ error: 'agentId is required for following feeds' }, { status: 400 })
32
24
  }
33
25
 
34
26
  try {
27
+ let agentApiKey: string | undefined
28
+ if (localAgentId) {
29
+ const scopedAgent = await ensureSwarmFeedAgent(localAgentId)
30
+ agentApiKey = scopedAgent.swarmfeedApiKey || undefined
31
+ }
35
32
  const result = await getFeed(type, { channelId, cursor, limit }, agentApiKey)
36
33
  return NextResponse.json(result)
37
34
  } catch (err: unknown) {
38
35
  const message = err instanceof Error ? err.message : 'Failed to fetch feed'
39
- return NextResponse.json({ error: message }, { status: 502 })
36
+ const status = message === 'Agent not found'
37
+ ? 404
38
+ : message.includes('not enabled')
39
+ ? 400
40
+ : 502
41
+ return NextResponse.json({ error: message }, { status })
40
42
  }
41
43
  }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { searchSwarmFeed } from '@/lib/swarmfeed-client'
3
+ import type { SwarmFeedSearchType } from '@/types/swarmfeed'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ const VALID_SEARCH_TYPES = new Set<SwarmFeedSearchType>(['posts', 'agents', 'channels', 'hashtags'])
8
+
9
+ export async function GET(req: Request) {
10
+ const { searchParams } = new URL(req.url)
11
+ const query = (searchParams.get('q') || '').trim()
12
+ const typeRaw = searchParams.get('type') || undefined
13
+ const type = typeRaw && VALID_SEARCH_TYPES.has(typeRaw as SwarmFeedSearchType)
14
+ ? typeRaw as SwarmFeedSearchType
15
+ : undefined
16
+ const limit = Math.max(1, Math.min(50, Number(searchParams.get('limit')) || 12))
17
+ const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
18
+
19
+ if (!query) {
20
+ return NextResponse.json({ error: 'q is required' }, { status: 400 })
21
+ }
22
+
23
+ try {
24
+ const results = await searchSwarmFeed({ query, type, limit, offset })
25
+ return NextResponse.json(results)
26
+ } catch (err: unknown) {
27
+ const message = err instanceof Error ? err.message : 'Failed to search SwarmFeed'
28
+ return NextResponse.json({ error: message }, { status: 502 })
29
+ }
30
+ }
@@ -0,0 +1,25 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getSuggestedFollows } from '@/lib/swarmfeed-client'
3
+ import { ensureSwarmFeedAgent } from '@/lib/server/agents/agent-swarm-registration'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const localAgentId = (searchParams.get('agentId') || '').trim()
10
+ const limit = Math.max(1, Math.min(20, Number(searchParams.get('limit')) || 5))
11
+
12
+ try {
13
+ const agent = localAgentId ? await ensureSwarmFeedAgent(localAgentId) : null
14
+ const result = await getSuggestedFollows(agent?.swarmfeedApiKey || undefined, limit)
15
+ return NextResponse.json(result)
16
+ } catch (err: unknown) {
17
+ const message = err instanceof Error ? err.message : 'Failed to fetch suggestions'
18
+ const status = message === 'Agent not found'
19
+ ? 404
20
+ : message.includes('not enabled')
21
+ ? 400
22
+ : 502
23
+ return NextResponse.json({ error: message }, { status })
24
+ }
25
+ }
package/src/cli/index.js CHANGED
@@ -801,9 +801,20 @@ const COMMAND_GROUPS = [
801
801
  description: 'SwarmFeed social network',
802
802
  commands: [
803
803
  cmd('feed', 'GET', '/swarmfeed', 'Get SwarmFeed timeline'),
804
+ cmd('search', 'GET', '/swarmfeed/search', 'Search SwarmFeed posts, agents, channels, or hashtags'),
804
805
  cmd('channels', 'GET', '/swarmfeed/channels', 'List SwarmFeed channels'),
806
+ cmd('bookmarks', 'GET', '/swarmfeed/bookmarks', 'Get bookmarked SwarmFeed posts for an agent'),
807
+ cmd('notifications', 'GET', '/swarmfeed/notifications', 'Get SwarmFeed notifications for an agent'),
808
+ cmd('suggested', 'GET', '/swarmfeed/suggested', 'Get suggested SwarmFeed follows'),
805
809
  cmd('posts', 'GET', '/swarmfeed/posts', 'Get recent posts'),
810
+ cmd('post-get', 'GET', '/swarmfeed/posts/:postId', 'Get a SwarmFeed post by id'),
811
+ cmd('replies', 'GET', '/swarmfeed/posts/:postId/replies', 'Get replies for a SwarmFeed post'),
806
812
  cmd('post', 'POST', '/swarmfeed/posts', 'Create a post', { expectsJsonBody: true }),
813
+ cmd('profile', 'GET', '/swarmfeed/profiles/:agentId', 'Get a SwarmFeed agent profile'),
814
+ cmd('profile-posts', 'GET', '/swarmfeed/profiles/:agentId/posts', 'Get recent posts for a SwarmFeed agent profile'),
815
+ cmd('action', 'POST', '/swarmfeed/actions', 'Run a SwarmFeed action such as follow, bookmark, repost, or quote repost', {
816
+ expectsJsonBody: true,
817
+ }),
807
818
  ],
808
819
  },
809
820
  {
@@ -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>