@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.
- package/README.md +30 -3
- package/package.json +1 -1
- package/public/provider-logos/hermes-agent.png +0 -0
- package/public/provider-logos/openrouter.png +0 -0
- package/src/app/api/setup/check-provider/route.ts +18 -2
- 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/components/agents/agent-sheet.tsx +10 -3
- package/src/components/auth/setup-wizard/step-connect.tsx +6 -0
- package/src/components/auth/setup-wizard/utils.test.ts +2 -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/app/view-constants.ts +1 -1
- package/src/lib/orchestrator-config.test.ts +1 -0
- package/src/lib/orchestrator-config.ts +1 -0
- package/src/lib/provider-sets.ts +6 -3
- package/src/lib/providers/index.ts +35 -0
- package/src/lib/providers/openai.ts +5 -4
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +3 -1
- package/src/lib/server/provider-health.test.ts +9 -2
- package/src/lib/server/provider-health.ts +8 -3
- package/src/lib/server/provider-model-discovery.test.ts +20 -0
- 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/storage.ts +1 -1
- 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/setup-defaults.test.ts +10 -0
- package/src/lib/setup-defaults.ts +42 -1
- 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/provider.ts +1 -1
- 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,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
|
-
- **
|
|
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
|
Binary file
|
|
Binary file
|
|
@@ -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:
|
|
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
|
|
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
|
{
|