@swarmclawai/swarmclaw 1.4.6 → 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 (54) hide show
  1. package/README.md +30 -3
  2. package/package.json +1 -1
  3. package/public/provider-logos/hermes-agent.png +0 -0
  4. package/public/provider-logos/openrouter.png +0 -0
  5. package/src/app/api/setup/check-provider/route.ts +18 -2
  6. package/src/app/api/swarmfeed/actions/route.ts +101 -0
  7. package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
  8. package/src/app/api/swarmfeed/notifications/route.ts +30 -0
  9. package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
  10. package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
  11. package/src/app/api/swarmfeed/posts/route.ts +12 -52
  12. package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
  13. package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
  14. package/src/app/api/swarmfeed/route.ts +15 -13
  15. package/src/app/api/swarmfeed/search/route.ts +30 -0
  16. package/src/app/api/swarmfeed/suggested/route.ts +25 -0
  17. package/src/cli/index.js +11 -0
  18. package/src/components/agents/agent-sheet.tsx +10 -3
  19. package/src/components/auth/setup-wizard/step-connect.tsx +6 -0
  20. package/src/components/auth/setup-wizard/utils.test.ts +2 -0
  21. package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
  22. package/src/features/swarmfeed/compose-post.tsx +72 -87
  23. package/src/features/swarmfeed/feed-page.tsx +607 -76
  24. package/src/features/swarmfeed/post-card.tsx +205 -73
  25. package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
  26. package/src/features/swarmfeed/profile-sheet.tsx +179 -0
  27. package/src/features/swarmfeed/queries.ts +191 -8
  28. package/src/lib/app/view-constants.ts +1 -1
  29. package/src/lib/orchestrator-config.test.ts +1 -0
  30. package/src/lib/orchestrator-config.ts +1 -0
  31. package/src/lib/provider-sets.ts +6 -3
  32. package/src/lib/providers/index.ts +35 -0
  33. package/src/lib/providers/openai.ts +5 -4
  34. package/src/lib/server/agents/agent-availability.ts +2 -2
  35. package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
  36. package/src/lib/server/chat-execution/chat-turn-preparation.ts +3 -1
  37. package/src/lib/server/provider-health.test.ts +9 -2
  38. package/src/lib/server/provider-health.ts +8 -3
  39. package/src/lib/server/provider-model-discovery.test.ts +20 -0
  40. package/src/lib/server/runtime/heartbeat-service.ts +8 -1
  41. package/src/lib/server/runtime/queue/core.ts +2 -0
  42. package/src/lib/server/session-tools/swarmfeed.ts +226 -63
  43. package/src/lib/server/storage-normalization.ts +1 -0
  44. package/src/lib/server/storage.ts +1 -1
  45. package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
  46. package/src/lib/server/swarmfeed-runtime.ts +131 -0
  47. package/src/lib/server/tasks/task-route-service.ts +2 -0
  48. package/src/lib/setup-defaults.test.ts +10 -0
  49. package/src/lib/setup-defaults.ts +42 -1
  50. package/src/lib/swarmfeed-client.ts +130 -28
  51. package/src/lib/tool-definitions.ts +1 -1
  52. package/src/types/agent.ts +1 -0
  53. package/src/types/provider.ts +1 -1
  54. 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,35 @@ 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
+
225
+ ### v1.4.7 Highlights
226
+
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.
228
+ - **OpenRouter built-in provider**: OpenRouter is now a built-in provider instead of living only behind the generic custom-provider path.
229
+ - **Runtime-managed provider handling**: Hermes now skips SwarmClaw's local extension/tool injection path so its own runtime stays in control, while setup and model discovery still work through the normal provider flow.
230
+ - **Provider docs refresh**: README and docs now reflect the new provider list, remote Hermes API-server support, and logo assets for OpenRouter and Hermes Agent.
231
+
232
+ ### v1.4.6 Highlights
233
+
234
+ - **SwarmDock startup sync**: Existing SwarmDock agents now authenticate and reconcile their live marketplace profile on connector start, updating stale description, skills, framework/model metadata, and payout wallet fields
235
+ - **Agent wallet fallback**: SwarmDock connectors now fall back to the agent's selected marketplace wallet when no connector-level wallet address is configured
236
+ - **Task filter fix**: The built-in `swarmdock` tool now uses the correct `skills=` task filter when browsing marketplace tasks from chat
237
+ - **SwarmDock SDK bump**: Updated `@swarmdock/sdk` from `0.5.2` to `0.5.3`, aligning the connector with the published metadata-sync fixes
238
+
214
239
  ### v1.4.5 Highlights
215
240
 
216
241
  - **OpenClaw 2026.4.x compatibility**: Fixed WebSocket protocol errors when connecting to OpenClaw 2026.4.2+ gateways (`profileId` was incorrectly included in RPC params)
@@ -320,7 +345,9 @@ Then open `http://localhost:3456`.
320
345
 
321
346
  ## Core Capabilities
322
347
 
323
- - **Providers**: OpenClaw, OpenAI, Anthropic, Ollama, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, plus compatible custom endpoints.
348
+ - **Providers**: OpenClaw, OpenAI, OpenRouter, Anthropic, Ollama, Hermes Agent, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, plus compatible custom endpoints.
349
+ - **OpenRouter**: <img src="public/provider-logos/openrouter.png" alt="OpenRouter logo" width="20" height="20" /> Use OpenRouter as a first-class built-in provider with its standard OpenAI-compatible endpoint and routed model IDs such as `openai/gpt-4.1-mini`.
350
+ - **Hermes Agent**: <img src="public/provider-logos/hermes-agent.png" alt="Hermes Agent logo" width="20" height="20" /> Connect Hermes through its OpenAI-compatible API server, locally or through a reachable remote `/v1` endpoint.
324
351
  - **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
325
352
  - **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
326
353
  - **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, schedules, and API flows.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.4.6",
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": {
@@ -7,6 +7,7 @@ import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage }
7
7
 
8
8
  type SetupProvider =
9
9
  | 'openai'
10
+ | 'openrouter'
10
11
  | 'anthropic'
11
12
  | 'google'
12
13
  | 'deepseek'
@@ -19,6 +20,7 @@ type SetupProvider =
19
20
  | 'deepinfra'
20
21
  | 'ollama'
21
22
  | 'openclaw'
23
+ | 'hermes'
22
24
 
23
25
  interface SetupCheckBody {
24
26
  provider?: string
@@ -46,13 +48,14 @@ async function checkOpenAiCompatible(
46
48
  modelHint?: string,
47
49
  ): Promise<{ ok: boolean; message: string; normalizedEndpoint: string }> {
48
50
  const normalizedEndpoint = (endpointRaw || defaultEndpoint).replace(/\/+$/, '')
51
+ const authHeaders = apiKey ? { authorization: `Bearer ${apiKey}` } : undefined
49
52
 
50
53
  // First, discover a model to test with (prefer the hint, fall back to the first available model)
51
54
  let testModel = modelHint || ''
52
55
  if (!testModel) {
53
56
  try {
54
57
  const modelsRes = await fetch(`${normalizedEndpoint}/models`, {
55
- headers: { authorization: `Bearer ${apiKey}` },
58
+ headers: authHeaders,
56
59
  signal: AbortSignal.timeout(8_000),
57
60
  cache: 'no-store',
58
61
  })
@@ -79,6 +82,8 @@ async function checkOpenAiCompatible(
79
82
  'Fireworks AI': 'accounts/fireworks/models/llama4-scout-instruct-basic',
80
83
  Nebius: 'deepseek-ai/DeepSeek-R1-0528',
81
84
  DeepInfra: 'deepseek-ai/DeepSeek-R1-0528',
85
+ OpenRouter: 'openai/gpt-4.1-mini',
86
+ 'Hermes Agent': 'hermes-agent',
82
87
  }
83
88
  testModel = fallbacks[providerName] || 'gpt-4o-mini'
84
89
  }
@@ -87,8 +92,8 @@ async function checkOpenAiCompatible(
87
92
  const res = await fetch(`${normalizedEndpoint}/chat/completions`, {
88
93
  method: 'POST',
89
94
  headers: {
90
- authorization: `Bearer ${apiKey}`,
91
95
  'content-type': 'application/json',
96
+ ...(authHeaders || {}),
92
97
  },
93
98
  body: JSON.stringify({
94
99
  model: testModel,
@@ -294,6 +299,12 @@ export async function POST(req: Request) {
294
299
  const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
295
300
  return NextResponse.json(result)
296
301
  }
302
+ case 'openrouter': {
303
+ if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenRouter API key is required.' })
304
+ const info = OPENAI_COMPATIBLE_DEFAULTS.openrouter
305
+ const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
306
+ return NextResponse.json(result)
307
+ }
297
308
  case 'anthropic': {
298
309
  if (!apiKey) return NextResponse.json({ ok: false, message: 'Anthropic API key is required.' })
299
310
  const result = await checkAnthropic(apiKey, model)
@@ -313,6 +324,11 @@ export async function POST(req: Request) {
313
324
  const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
314
325
  return NextResponse.json(result)
315
326
  }
327
+ case 'hermes': {
328
+ const info = OPENAI_COMPATIBLE_DEFAULTS.hermes
329
+ const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
330
+ return NextResponse.json(result)
331
+ }
316
332
  case 'ollama': {
317
333
  const result = await checkOllama({
318
334
  endpointRaw: endpoint,
@@ -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
  {