@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { dispatchWake } from '@/lib/server/runtime/wake-dispatcher'
|
|
2
|
+
import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
3
|
+
import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-session'
|
|
4
|
+
import { getAgent, patchAgent } from '@/lib/server/agents/agent-repository'
|
|
5
|
+
import type { Agent, BoardTask } from '@/types'
|
|
6
|
+
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
8
|
+
|
|
9
|
+
function formatChannels(channelIds: string[] | undefined): string {
|
|
10
|
+
if (!Array.isArray(channelIds) || channelIds.length === 0) return 'any relevant channel'
|
|
11
|
+
return channelIds.map((id) => `#${id}`).join(', ')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildSwarmFeedHeartbeatGuidance(agent: Agent | null | undefined): string {
|
|
15
|
+
if (!agent?.swarmfeedEnabled || !agent.swarmfeedHeartbeat?.enabled) return ''
|
|
16
|
+
|
|
17
|
+
const config = agent.swarmfeedHeartbeat
|
|
18
|
+
const lines = [
|
|
19
|
+
'### SwarmFeed Social Guidance',
|
|
20
|
+
'SwarmFeed is enabled for this agent. Use the built-in `swarmfeed` tool only when the policy below allows it.',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
if (agent.heartbeatEnabled !== true) {
|
|
24
|
+
lines.push('SwarmFeed social automation is configured but currently inactive because the agent heartbeat is disabled.')
|
|
25
|
+
lines.push('Do not do autonomous SwarmFeed work until the general heartbeat is enabled again.')
|
|
26
|
+
return lines.join('\n')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.browseFeed) {
|
|
30
|
+
lines.push(`Browse the feed when helpful, prioritizing ${formatChannels(config.channelsToMonitor)}.`)
|
|
31
|
+
} else {
|
|
32
|
+
lines.push('Do not browse SwarmFeed unless the recent event context or direct user/task context makes it necessary.')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.autoReply) {
|
|
36
|
+
lines.push('Auto-reply is allowed, but only when there is a specific mention, thread, or high-signal post worth responding to.')
|
|
37
|
+
} else {
|
|
38
|
+
lines.push('Do not reply automatically unless the user explicitly asked for it.')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (config.autoFollow) {
|
|
42
|
+
lines.push('Auto-follow is allowed only after you first browsed or searched supporting context during this tick.')
|
|
43
|
+
} else {
|
|
44
|
+
lines.push('Do not auto-follow agents during this tick.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch (config.postFrequency) {
|
|
48
|
+
case 'manual_only':
|
|
49
|
+
lines.push('Posting policy: manual only. Do not author new SwarmFeed posts or replies during autonomous heartbeat work.')
|
|
50
|
+
break
|
|
51
|
+
case 'daily':
|
|
52
|
+
if (typeof agent.swarmfeedLastAutoPostAt === 'number' && Date.now() - agent.swarmfeedLastAutoPostAt < DAY_MS) {
|
|
53
|
+
lines.push('Posting policy: daily. A daily auto-post already happened in the last 24 hours, so do not author another new SwarmFeed post this tick.')
|
|
54
|
+
} else {
|
|
55
|
+
lines.push(`Posting policy: daily. At most one authored SwarmFeed post this tick, ideally in ${formatChannels(agent.swarmfeedAutoPostChannels)}.`)
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
case 'on_task_completion':
|
|
59
|
+
lines.push('Posting policy: on task completion. Only author a new SwarmFeed post if this tick was triggered by a newly completed task or the recent event context explicitly references a completed task.')
|
|
60
|
+
break
|
|
61
|
+
case 'every_cycle':
|
|
62
|
+
lines.push(`Posting policy: every cycle. You may author at most one SwarmFeed post this tick if there is a worthwhile update for ${formatChannels(agent.swarmfeedAutoPostChannels)}.`)
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push('Hard limits: max one authored SwarmFeed post this tick, no recursive reply chains, and skip SwarmFeed entirely when there is nothing socially useful to add.')
|
|
67
|
+
return lines.join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function canAutoPostToSwarmFeed(agent: Agent | null | undefined): { allowed: boolean; reason?: string } {
|
|
71
|
+
if (!agent?.swarmfeedEnabled || !agent.swarmfeedHeartbeat?.enabled || agent.heartbeatEnabled !== true) {
|
|
72
|
+
return { allowed: true }
|
|
73
|
+
}
|
|
74
|
+
switch (agent.swarmfeedHeartbeat.postFrequency) {
|
|
75
|
+
case 'manual_only':
|
|
76
|
+
return { allowed: false, reason: 'SwarmFeed heartbeat is set to manual_only for this agent.' }
|
|
77
|
+
case 'daily':
|
|
78
|
+
if (typeof agent.swarmfeedLastAutoPostAt === 'number' && Date.now() - agent.swarmfeedLastAutoPostAt < DAY_MS) {
|
|
79
|
+
return { allowed: false, reason: 'This agent already made its daily autonomous SwarmFeed post in the last 24 hours.' }
|
|
80
|
+
}
|
|
81
|
+
return { allowed: true }
|
|
82
|
+
default:
|
|
83
|
+
return { allowed: true }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function markSwarmFeedAutoPost(agentId: string): void {
|
|
88
|
+
patchAgent(agentId, (agent) => {
|
|
89
|
+
if (!agent) return null
|
|
90
|
+
return {
|
|
91
|
+
...agent,
|
|
92
|
+
swarmfeedLastAutoPostAt: Date.now(),
|
|
93
|
+
updatedAt: Date.now(),
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function summarizeTask(task: BoardTask): string {
|
|
99
|
+
const title = task.title.trim() || task.id
|
|
100
|
+
const result = typeof task.result === 'string' ? task.result.trim() : ''
|
|
101
|
+
if (!result) return `Completed task: ${title}.`
|
|
102
|
+
return `Completed task: ${title}. Result summary: ${result.slice(0, 300)}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function queueSwarmFeedTaskCompletionWake(task: BoardTask): void {
|
|
106
|
+
const agent = task.agentId ? (getAgent(task.agentId) as Agent | undefined) : undefined
|
|
107
|
+
if (!agent?.swarmfeedEnabled || !agent.swarmfeedHeartbeat?.enabled) return
|
|
108
|
+
if (agent.heartbeatEnabled !== true) return
|
|
109
|
+
if (agent.swarmfeedHeartbeat.postFrequency !== 'on_task_completion') return
|
|
110
|
+
|
|
111
|
+
const session = ensureAgentThreadSession(agent.id, 'default', agent)
|
|
112
|
+
if (!session) return
|
|
113
|
+
|
|
114
|
+
const summary = summarizeTask(task)
|
|
115
|
+
enqueueSystemEvent(
|
|
116
|
+
session.id,
|
|
117
|
+
`${summary} Consider whether it merits one concise SwarmFeed update in ${formatChannels(agent.swarmfeedAutoPostChannels)}.`,
|
|
118
|
+
`swarmfeed-task:${task.id}`,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
dispatchWake({
|
|
122
|
+
mode: 'immediate',
|
|
123
|
+
agentId: agent.id,
|
|
124
|
+
sessionId: session.id,
|
|
125
|
+
eventId: `swarmfeed-task:${task.id}`,
|
|
126
|
+
reason: 'task-completed-social',
|
|
127
|
+
source: `swarmfeed:${task.id}`,
|
|
128
|
+
resumeMessage: `A completed task may merit one SwarmFeed update: ${task.title}`,
|
|
129
|
+
detail: summary,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { resolveTaskAgentFromDescription } from '@/lib/server/tasks/task-mention'
|
|
26
26
|
import { applyTaskPatch, prepareTaskCreation } from '@/lib/server/tasks/task-service'
|
|
27
27
|
import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
28
|
+
import { queueSwarmFeedTaskCompletionWake } from '@/lib/server/swarmfeed-runtime'
|
|
28
29
|
import { notify } from '@/lib/server/ws-hub'
|
|
29
30
|
import type { BoardTask, BoardTaskStatus, TaskComment } from '@/types'
|
|
30
31
|
import type { ServiceResult } from '@/lib/server/service-result'
|
|
@@ -152,6 +153,7 @@ export function updateTaskFromRoute(id: string, body: Record<string, unknown>):
|
|
|
152
153
|
{ taskId: id, result: tasks[id].result },
|
|
153
154
|
{ enabledIds: agentExtensions },
|
|
154
155
|
)
|
|
156
|
+
queueSwarmFeedTaskCompletionWake(tasks[id])
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
if (tasks[id].sessionId) {
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CreatePostInput,
|
|
4
|
+
FeedType,
|
|
5
|
+
SwarmFeedChannel,
|
|
6
|
+
SwarmFeedFeedResponse,
|
|
7
|
+
SwarmFeedFollowState,
|
|
8
|
+
SwarmFeedNotificationsResponse,
|
|
9
|
+
SwarmFeedPost,
|
|
10
|
+
SwarmFeedProfile,
|
|
11
|
+
SwarmFeedReactionType,
|
|
12
|
+
SwarmFeedSearchResponse,
|
|
13
|
+
SwarmFeedSearchType,
|
|
14
|
+
SwarmFeedSuggestedResponse,
|
|
15
|
+
} from '@/types/swarmfeed'
|
|
3
16
|
|
|
4
17
|
interface SwarmFeedConfig {
|
|
5
18
|
apiUrl: string
|
|
@@ -9,10 +22,6 @@ const config = hmrSingleton<SwarmFeedConfig>('swarmfeed_config', () => ({
|
|
|
9
22
|
apiUrl: process.env.SWARMFEED_API_URL || 'https://swarmfeed-api.onrender.com',
|
|
10
23
|
}))
|
|
11
24
|
|
|
12
|
-
/**
|
|
13
|
-
* Internal fetch helper for SwarmFeed API.
|
|
14
|
-
* @param agentApiKey - Per-agent API key (sf_live_*) for authenticated requests. Omit for public endpoints.
|
|
15
|
-
*/
|
|
16
25
|
async function sfFetch<T>(path: string, agentApiKey?: string, init?: RequestInit): Promise<T> {
|
|
17
26
|
const url = `${config.apiUrl}${path}`
|
|
18
27
|
const headers: Record<string, string> = {
|
|
@@ -27,11 +36,10 @@ async function sfFetch<T>(path: string, agentApiKey?: string, init?: RequestInit
|
|
|
27
36
|
const text = await res.text().catch(() => 'Unknown error')
|
|
28
37
|
throw new Error(`SwarmFeed API error ${res.status}: ${text}`)
|
|
29
38
|
}
|
|
39
|
+
if (res.status === 204) return undefined as T
|
|
30
40
|
return res.json() as Promise<T>
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
// --- Feed (public, no auth needed) ---
|
|
34
|
-
|
|
35
43
|
const FEED_TYPE_URL: Record<FeedType, string> = {
|
|
36
44
|
for_you: 'for-you',
|
|
37
45
|
following: 'following',
|
|
@@ -43,10 +51,9 @@ export async function getFeed(
|
|
|
43
51
|
type: FeedType,
|
|
44
52
|
params?: { channelId?: string; cursor?: string; limit?: number },
|
|
45
53
|
agentApiKey?: string,
|
|
46
|
-
): Promise<
|
|
54
|
+
): Promise<SwarmFeedFeedResponse> {
|
|
47
55
|
const urlSegment = FEED_TYPE_URL[type] ?? type
|
|
48
56
|
|
|
49
|
-
// Channel feed uses path param: /feed/channel/:channelId
|
|
50
57
|
if (type === 'channel' && params?.channelId) {
|
|
51
58
|
const searchParams = new URLSearchParams()
|
|
52
59
|
if (params.cursor) searchParams.set('cursor', params.cursor)
|
|
@@ -62,8 +69,6 @@ export async function getFeed(
|
|
|
62
69
|
return sfFetch(`/api/v1/feed/${urlSegment}${qs ? `?${qs}` : ''}`, agentApiKey)
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
// --- Posts (auth required for writes) ---
|
|
66
|
-
|
|
67
72
|
export async function createPost(agentApiKey: string, input: CreatePostInput): Promise<SwarmFeedPost> {
|
|
68
73
|
return sfFetch('/api/v1/posts', agentApiKey, {
|
|
69
74
|
method: 'POST',
|
|
@@ -71,34 +76,138 @@ export async function createPost(agentApiKey: string, input: CreatePostInput): P
|
|
|
71
76
|
content: input.content,
|
|
72
77
|
channelId: input.channelId,
|
|
73
78
|
parentId: input.parentId,
|
|
79
|
+
quotedPostId: input.quotedPostId,
|
|
74
80
|
}),
|
|
75
81
|
})
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
export async function getPost(postId: string): Promise<SwarmFeedPost> {
|
|
85
|
+
return sfFetch(`/api/v1/posts/${postId}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function getPostReplies(
|
|
89
|
+
postId: string,
|
|
90
|
+
params?: { cursor?: string; limit?: number },
|
|
91
|
+
): Promise<SwarmFeedFeedResponse> {
|
|
92
|
+
const searchParams = new URLSearchParams()
|
|
93
|
+
if (params?.cursor) searchParams.set('cursor', params.cursor)
|
|
94
|
+
if (params?.limit) searchParams.set('limit', String(params.limit))
|
|
95
|
+
const qs = searchParams.toString()
|
|
96
|
+
return sfFetch(`/api/v1/posts/${postId}/replies${qs ? `?${qs}` : ''}`)
|
|
97
|
+
}
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
async function addReaction(agentApiKey: string, postId: string, reactionType: SwarmFeedReactionType): Promise<void> {
|
|
100
|
+
await sfFetch(`/api/v1/posts/${postId}/like`, agentApiKey, {
|
|
82
101
|
method: 'POST',
|
|
83
|
-
body: JSON.stringify({ reactionType
|
|
102
|
+
body: JSON.stringify({ reactionType }),
|
|
84
103
|
})
|
|
85
104
|
}
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
method: '
|
|
90
|
-
body: JSON.stringify({ reactionType: 'repost' }),
|
|
106
|
+
async function removeReaction(agentApiKey: string, postId: string, reactionType: SwarmFeedReactionType): Promise<void> {
|
|
107
|
+
await sfFetch(`/api/v1/posts/${postId}/like?reactionType=${encodeURIComponent(reactionType)}`, agentApiKey, {
|
|
108
|
+
method: 'DELETE',
|
|
91
109
|
})
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
|
|
112
|
+
export async function likePost(agentApiKey: string, postId: string): Promise<void> {
|
|
113
|
+
await addReaction(agentApiKey, postId, 'like')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function unlikePost(agentApiKey: string, postId: string): Promise<void> {
|
|
117
|
+
await removeReaction(agentApiKey, postId, 'like')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function repostPost(agentApiKey: string, postId: string): Promise<void> {
|
|
121
|
+
await addReaction(agentApiKey, postId, 'repost')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function unrepostPost(agentApiKey: string, postId: string): Promise<void> {
|
|
125
|
+
await removeReaction(agentApiKey, postId, 'repost')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function bookmarkPost(agentApiKey: string, postId: string): Promise<void> {
|
|
129
|
+
await addReaction(agentApiKey, postId, 'bookmark')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function unbookmarkPost(agentApiKey: string, postId: string): Promise<void> {
|
|
133
|
+
await removeReaction(agentApiKey, postId, 'bookmark')
|
|
134
|
+
}
|
|
95
135
|
|
|
96
136
|
export async function getChannels(): Promise<SwarmFeedChannel[]> {
|
|
97
137
|
const result = await sfFetch<{ channels: SwarmFeedChannel[] }>('/api/v1/channels')
|
|
98
138
|
return result.channels
|
|
99
139
|
}
|
|
100
140
|
|
|
101
|
-
|
|
141
|
+
export async function getProfile(agentId: string): Promise<SwarmFeedProfile> {
|
|
142
|
+
return sfFetch(`/api/v1/agents/${agentId}/profile`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function getProfilePosts(
|
|
146
|
+
agentId: string,
|
|
147
|
+
params?: { cursor?: string; limit?: number; filter?: 'posts' | 'replies' },
|
|
148
|
+
): Promise<SwarmFeedFeedResponse> {
|
|
149
|
+
const searchParams = new URLSearchParams()
|
|
150
|
+
if (params?.cursor) searchParams.set('cursor', params.cursor)
|
|
151
|
+
if (params?.limit) searchParams.set('limit', String(params.limit))
|
|
152
|
+
if (params?.filter) searchParams.set('filter', params.filter)
|
|
153
|
+
const qs = searchParams.toString()
|
|
154
|
+
return sfFetch(`/api/v1/agents/${agentId}/posts${qs ? `?${qs}` : ''}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function getBookmarks(
|
|
158
|
+
agentApiKey: string,
|
|
159
|
+
params?: { cursor?: string; limit?: number },
|
|
160
|
+
): Promise<SwarmFeedFeedResponse> {
|
|
161
|
+
const searchParams = new URLSearchParams()
|
|
162
|
+
if (params?.cursor) searchParams.set('cursor', params.cursor)
|
|
163
|
+
if (params?.limit) searchParams.set('limit', String(params.limit))
|
|
164
|
+
const qs = searchParams.toString()
|
|
165
|
+
return sfFetch(`/api/v1/bookmarks${qs ? `?${qs}` : ''}`, agentApiKey)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function getNotifications(
|
|
169
|
+
agentApiKey: string,
|
|
170
|
+
params?: { cursor?: string; limit?: number },
|
|
171
|
+
): Promise<SwarmFeedNotificationsResponse> {
|
|
172
|
+
const searchParams = new URLSearchParams()
|
|
173
|
+
if (params?.cursor) searchParams.set('cursor', params.cursor)
|
|
174
|
+
if (params?.limit) searchParams.set('limit', String(params.limit))
|
|
175
|
+
const qs = searchParams.toString()
|
|
176
|
+
return sfFetch(`/api/v1/notifications${qs ? `?${qs}` : ''}`, agentApiKey)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function getSuggestedFollows(agentApiKey?: string, limit?: number): Promise<SwarmFeedSuggestedResponse> {
|
|
180
|
+
const searchParams = new URLSearchParams()
|
|
181
|
+
if (limit) searchParams.set('limit', String(limit))
|
|
182
|
+
const qs = searchParams.toString()
|
|
183
|
+
return sfFetch(`/api/v1/agents/suggested${qs ? `?${qs}` : ''}`, agentApiKey)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function followAgent(agentApiKey: string, targetAgentId: string): Promise<void> {
|
|
187
|
+
await sfFetch(`/api/v1/agents/${targetAgentId}/follow`, agentApiKey, { method: 'POST' })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function unfollowAgent(agentApiKey: string, targetAgentId: string): Promise<void> {
|
|
191
|
+
await sfFetch(`/api/v1/agents/${targetAgentId}/follow`, agentApiKey, { method: 'DELETE' })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function getFollowState(agentId: string, targetAgentId: string): Promise<SwarmFeedFollowState> {
|
|
195
|
+
return sfFetch(`/api/v1/agents/${agentId}/is-following?targetId=${encodeURIComponent(targetAgentId)}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function searchSwarmFeed(params: {
|
|
199
|
+
query: string
|
|
200
|
+
type?: SwarmFeedSearchType
|
|
201
|
+
limit?: number
|
|
202
|
+
offset?: number
|
|
203
|
+
}): Promise<SwarmFeedSearchResponse> {
|
|
204
|
+
const searchParams = new URLSearchParams()
|
|
205
|
+
searchParams.set('q', params.query)
|
|
206
|
+
if (params.type) searchParams.set('type', params.type)
|
|
207
|
+
if (params.limit) searchParams.set('limit', String(params.limit))
|
|
208
|
+
if (params.offset) searchParams.set('offset', String(params.offset))
|
|
209
|
+
return sfFetch(`/api/v1/search?${searchParams.toString()}`)
|
|
210
|
+
}
|
|
102
211
|
|
|
103
212
|
interface RegisterResult {
|
|
104
213
|
agentId: string
|
|
@@ -107,10 +216,6 @@ interface RegisterResult {
|
|
|
107
216
|
challengeExpiresAt: string
|
|
108
217
|
}
|
|
109
218
|
|
|
110
|
-
/**
|
|
111
|
-
* Register a SwarmClaw agent on SwarmFeed.
|
|
112
|
-
* Generates an Ed25519 keypair, registers, verifies, and returns the API key.
|
|
113
|
-
*/
|
|
114
219
|
export async function registerAgent(agent: {
|
|
115
220
|
name: string
|
|
116
221
|
description?: string
|
|
@@ -119,13 +224,11 @@ export async function registerAgent(agent: {
|
|
|
119
224
|
avatar?: string
|
|
120
225
|
bio?: string
|
|
121
226
|
}): Promise<{ agentId: string; apiKey: string }> {
|
|
122
|
-
// Dynamic import tweetnacl (available in SwarmClaw's deps)
|
|
123
227
|
const naclModule = await import('tweetnacl')
|
|
124
228
|
const nacl = naclModule.default ?? naclModule
|
|
125
229
|
const keypair = nacl.sign.keyPair()
|
|
126
230
|
const publicKeyHex = Buffer.from(keypair.publicKey).toString('hex')
|
|
127
231
|
|
|
128
|
-
// Step 1: Register
|
|
129
232
|
const reg = await sfFetch<RegisterResult>('/api/v1/register', undefined, {
|
|
130
233
|
method: 'POST',
|
|
131
234
|
body: JSON.stringify({
|
|
@@ -139,7 +242,6 @@ export async function registerAgent(agent: {
|
|
|
139
242
|
}),
|
|
140
243
|
})
|
|
141
244
|
|
|
142
|
-
// Step 2: Sign the challenge and verify
|
|
143
245
|
const messageBytes = new TextEncoder().encode(reg.challenge)
|
|
144
246
|
const signature = nacl.sign.detached(messageBytes, keypair.secretKey)
|
|
145
247
|
const signatureHex = Buffer.from(signature).toString('hex')
|
|
@@ -29,7 +29,7 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
|
|
|
29
29
|
{ id: 'email', label: 'Email', description: 'Send emails via SMTP with plain text and HTML support', extensionId: 'email' },
|
|
30
30
|
{ id: 'replicate', label: 'Replicate', description: 'Run any AI model on Replicate — image generation, LLMs, audio, video, and more', extensionId: 'replicate' },
|
|
31
31
|
{ id: 'google_workspace', label: 'Google Workspace', description: 'Run Google Workspace CLI (`gws`) commands for Drive, Docs, Sheets, Gmail, Calendar, Chat, and more', extensionId: 'google_workspace' },
|
|
32
|
-
{ id: 'swarmfeed', label: 'SwarmFeed', description: 'Post, reply,
|
|
32
|
+
{ id: 'swarmfeed', label: 'SwarmFeed', description: 'Post, reply, quote repost, bookmark, follow, search, browse feeds, read threads, and check notifications on the SwarmFeed agent network (auto-enabled when SwarmFeed is on)' },
|
|
33
33
|
{ id: 'swarmdock', label: 'SwarmDock', description: 'Browse tasks and inspect marketplace status/profile on SwarmDock (auto-enabled when SwarmDock is on)' },
|
|
34
34
|
]
|
|
35
35
|
|
package/src/types/agent.ts
CHANGED
|
@@ -212,6 +212,7 @@ export interface Agent {
|
|
|
212
212
|
swarmfeedAutoPostChannels?: string[]
|
|
213
213
|
swarmfeedApiKey?: string | null
|
|
214
214
|
swarmfeedAgentId?: string | null
|
|
215
|
+
swarmfeedLastAutoPostAt?: number | null
|
|
215
216
|
origin?: 'swarmdock' | 'swarmfeed' | 'swarmclaw' | 'external'
|
|
216
217
|
swarmfeedHeartbeat?: SwarmFeedHeartbeatConfig | null
|
|
217
218
|
|
package/src/types/swarmfeed.ts
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
|
+
export type FeedType = 'for_you' | 'following' | 'channel' | 'trending'
|
|
2
|
+
export type SwarmFeedSearchType = 'posts' | 'agents' | 'channels' | 'hashtags'
|
|
3
|
+
export type SwarmFeedNotificationType = 'mention' | 'reaction' | 'follow'
|
|
4
|
+
export type SwarmFeedReactionType = 'like' | 'repost' | 'bookmark'
|
|
5
|
+
|
|
6
|
+
export interface SwarmFeedBadge {
|
|
7
|
+
id: string
|
|
8
|
+
badgeType: string
|
|
9
|
+
displayName: string
|
|
10
|
+
emoji: string
|
|
11
|
+
color: string
|
|
12
|
+
isActive: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SwarmFeedAgentSummary {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
avatar?: string | null
|
|
19
|
+
framework?: string | null
|
|
20
|
+
bio?: string | null
|
|
21
|
+
followerCount?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SwarmFeedLinkPreview {
|
|
25
|
+
url: string
|
|
26
|
+
title?: string
|
|
27
|
+
description?: string
|
|
28
|
+
image?: string
|
|
29
|
+
siteName?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
1
32
|
export interface SwarmFeedPost {
|
|
2
33
|
id: string
|
|
3
34
|
agentId: string
|
|
4
35
|
content: string
|
|
5
|
-
channelId?: string
|
|
6
|
-
parentId?: string
|
|
36
|
+
channelId?: string | null
|
|
37
|
+
parentId?: string | null
|
|
38
|
+
quotedPostId?: string | null
|
|
7
39
|
likeCount: number
|
|
8
40
|
replyCount: number
|
|
9
41
|
repostCount: number
|
|
10
42
|
bookmarkCount: number
|
|
43
|
+
contentQualityScore?: number
|
|
44
|
+
isFlagged?: boolean
|
|
11
45
|
createdAt: string
|
|
12
|
-
|
|
46
|
+
updatedAt?: string
|
|
47
|
+
agent?: SwarmFeedAgentSummary
|
|
48
|
+
quotedPost?: SwarmFeedPost
|
|
49
|
+
linkPreview?: SwarmFeedLinkPreview
|
|
13
50
|
}
|
|
14
51
|
|
|
15
52
|
export interface SwarmFeedChannel {
|
|
@@ -17,14 +54,77 @@ export interface SwarmFeedChannel {
|
|
|
17
54
|
handle: string
|
|
18
55
|
displayName: string
|
|
19
56
|
description?: string
|
|
57
|
+
avatar?: string
|
|
20
58
|
memberCount: number
|
|
21
59
|
postCount: number
|
|
60
|
+
rules?: string
|
|
61
|
+
isModerated?: boolean
|
|
62
|
+
creatorAgentId?: string
|
|
63
|
+
createdAt?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SwarmFeedProfile {
|
|
67
|
+
id: string
|
|
68
|
+
name: string
|
|
69
|
+
description?: string
|
|
70
|
+
avatar?: string | null
|
|
71
|
+
bio?: string | null
|
|
72
|
+
model?: string
|
|
73
|
+
framework?: string
|
|
74
|
+
origin?: string
|
|
75
|
+
postCount: number
|
|
76
|
+
followerCount: number
|
|
77
|
+
followingCount: number
|
|
78
|
+
totalTipsReceived?: number
|
|
79
|
+
badges?: SwarmFeedBadge[]
|
|
80
|
+
channelMemberships?: string[]
|
|
81
|
+
isFollowing?: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SwarmFeedNotification {
|
|
85
|
+
id: string
|
|
86
|
+
type: SwarmFeedNotificationType
|
|
87
|
+
actorId: string
|
|
88
|
+
actorName: string | null
|
|
89
|
+
postId: string | null
|
|
90
|
+
content: string | null
|
|
91
|
+
createdAt: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SwarmFeedHashtag {
|
|
95
|
+
tag: string
|
|
96
|
+
postCount: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SwarmFeedSearchResponse {
|
|
100
|
+
posts?: SwarmFeedPost[]
|
|
101
|
+
agents?: SwarmFeedProfile[]
|
|
102
|
+
channels?: SwarmFeedChannel[]
|
|
103
|
+
hashtags?: SwarmFeedHashtag[]
|
|
104
|
+
total: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SwarmFeedFollowState {
|
|
108
|
+
isFollowing: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SwarmFeedFeedResponse {
|
|
112
|
+
posts: SwarmFeedPost[]
|
|
113
|
+
nextCursor?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SwarmFeedNotificationsResponse {
|
|
117
|
+
notifications: SwarmFeedNotification[]
|
|
118
|
+
nextCursor?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface SwarmFeedSuggestedResponse {
|
|
122
|
+
agents: SwarmFeedAgentSummary[]
|
|
22
123
|
}
|
|
23
124
|
|
|
24
125
|
export interface CreatePostInput {
|
|
25
126
|
content: string
|
|
26
127
|
channelId?: string
|
|
27
128
|
parentId?: string
|
|
129
|
+
quotedPostId?: string
|
|
28
130
|
}
|
|
29
|
-
|
|
30
|
-
export type FeedType = 'for_you' | 'following' | 'channel' | 'trending'
|