@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.
- package/README.md +13 -2
- package/package.json +1 -1
- package/src/app/api/swarmfeed/actions/route.ts +101 -0
- package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
- package/src/app/api/swarmfeed/notifications/route.ts +30 -0
- package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
- package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
- package/src/app/api/swarmfeed/posts/route.ts +12 -52
- package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
- package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
- package/src/app/api/swarmfeed/route.ts +15 -13
- package/src/app/api/swarmfeed/search/route.ts +30 -0
- package/src/app/api/swarmfeed/suggested/route.ts +25 -0
- package/src/cli/index.js +11 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
- package/src/features/swarmfeed/compose-post.tsx +72 -87
- package/src/features/swarmfeed/feed-page.tsx +607 -76
- package/src/features/swarmfeed/post-card.tsx +205 -73
- package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
- package/src/features/swarmfeed/profile-sheet.tsx +179 -0
- package/src/features/swarmfeed/queries.ts +191 -8
- package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
- package/src/lib/server/runtime/heartbeat-service.ts +8 -1
- package/src/lib/server/runtime/queue/core.ts +2 -0
- package/src/lib/server/session-tools/swarmfeed.ts +226 -63
- package/src/lib/server/storage-normalization.ts +1 -0
- package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
- package/src/lib/server/swarmfeed-runtime.ts +131 -0
- package/src/lib/server/tasks/task-route-service.ts +2 -0
- package/src/lib/swarmfeed-client.ts +130 -28
- package/src/lib/tool-definitions.ts +1 -1
- package/src/types/agent.ts +1 -0
- 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
|
-
|
|
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
|
-
- **
|
|
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
|
@@ -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
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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'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>
|