@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -0,0 +1,284 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { genId } from '@/lib/id'
3
+ import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import { notFound } from '@/lib/server/collection-helpers'
6
+ import { streamAgentChat } from '@/lib/server/stream-agent-chat'
7
+ import { getProvider } from '@/lib/providers'
8
+ import {
9
+ resolveApiKey,
10
+ parseMentions,
11
+ buildChatroomSystemPrompt,
12
+ buildSyntheticSession,
13
+ buildAgentSystemPromptForChatroom,
14
+ buildHistoryForAgent,
15
+ } from '@/lib/server/chatroom-helpers'
16
+ import type { Chatroom, ChatroomMessage, Agent } from '@/types'
17
+
18
+ export const dynamic = 'force-dynamic'
19
+ export const maxDuration = 300
20
+
21
+ const MAX_CHAIN_DEPTH = 5
22
+
23
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
24
+ const { id } = await params
25
+ const body = await req.json()
26
+
27
+ const chatrooms = loadChatrooms()
28
+ const chatroom = chatrooms[id] as Chatroom | undefined
29
+ if (!chatroom) return notFound()
30
+
31
+ const text = typeof body.text === 'string' ? body.text : ''
32
+ const senderId = typeof body.senderId === 'string' ? body.senderId : 'user'
33
+ const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
34
+ const attachedFiles = Array.isArray(body.attachedFiles)
35
+ ? (body.attachedFiles as unknown[]).filter((f): f is string => typeof f === 'string')
36
+ : undefined
37
+ const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
38
+
39
+ if (!text.trim() && !imagePath && !attachedFiles?.length) {
40
+ return NextResponse.json({ error: 'text or attachment is required' }, { status: 400 })
41
+ }
42
+
43
+ const agents = loadAgents() as Record<string, Agent>
44
+
45
+ // Persist incoming message
46
+ const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
47
+ let mentions = parseMentions(text, agents, chatroom.agentIds)
48
+ // Auto-address: if enabled and no explicit mentions, address all agents
49
+ if (chatroom.autoAddress && mentions.length === 0) {
50
+ mentions = [...chatroom.agentIds]
51
+ }
52
+ const userMessage: ChatroomMessage = {
53
+ id: genId(),
54
+ senderId,
55
+ senderName,
56
+ role: senderId === 'user' ? 'user' : 'assistant',
57
+ text,
58
+ mentions,
59
+ reactions: [],
60
+ time: Date.now(),
61
+ ...(imagePath ? { imagePath } : {}),
62
+ ...(attachedFiles ? { attachedFiles } : {}),
63
+ ...(replyToId ? { replyToId } : {}),
64
+ }
65
+ chatroom.messages.push(userMessage)
66
+ chatroom.updatedAt = Date.now()
67
+ chatrooms[id] = chatroom
68
+ saveChatrooms(chatrooms)
69
+ notify('chatrooms')
70
+ notify(`chatroom:${id}`)
71
+
72
+ // Build reply context if replying to a message
73
+ let replyContext = ''
74
+ if (replyToId) {
75
+ const replyMsg = chatroom.messages.find((m) => m.id === replyToId)
76
+ if (replyMsg) {
77
+ const truncated = replyMsg.text.length > 200 ? replyMsg.text.slice(0, 200) + '...' : replyMsg.text
78
+ replyContext = `> [${replyMsg.senderName}]: ${truncated}\n\n`
79
+ }
80
+ }
81
+
82
+ // SSE stream
83
+ const encoder = new TextEncoder()
84
+ const stream = new ReadableStream({
85
+ start(controller) {
86
+ let closed = false
87
+ const writeEvent = (event: Record<string, unknown>) => {
88
+ if (closed) return
89
+ try {
90
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
91
+ } catch {
92
+ closed = true
93
+ }
94
+ }
95
+
96
+ const processAgents = async () => {
97
+ // Build agent queue: start with mentioned agents, then chain
98
+ const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
99
+ const processed = new Set<string>()
100
+ const agentQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = []
101
+
102
+ /** Process a single agent: stream response, persist message, return chained mentions */
103
+ const processOneAgent = async (item: { agentId: string; depth: number; contextMessage?: string }): Promise<string[]> => {
104
+ if (processed.has(item.agentId) || item.depth >= MAX_CHAIN_DEPTH) return []
105
+ processed.add(item.agentId)
106
+
107
+ const agent = agents[item.agentId]
108
+ if (!agent) return []
109
+
110
+ // Pre-flight: check if the agent's provider is usable before attempting to stream
111
+ const providerInfo = getProvider(agent.provider)
112
+ const apiKey = resolveApiKey(agent.credentialId)
113
+ if (providerInfo?.requiresApiKey && !apiKey) {
114
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
115
+ writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
116
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
117
+ return []
118
+ }
119
+ if (providerInfo?.requiresEndpoint && !agent.apiEndpoint) {
120
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
121
+ writeEvent({ t: 'err', text: `${agent.name} has no endpoint configured`, agentId: agent.id, agentName: agent.name })
122
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
123
+ return []
124
+ }
125
+
126
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
127
+
128
+ try {
129
+ const freshChatrooms = loadChatrooms()
130
+ const freshChatroom = freshChatrooms[id] as Chatroom
131
+
132
+ const syntheticSession = buildSyntheticSession(agent, id)
133
+ const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
134
+ const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
135
+ const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
136
+ const history = buildHistoryForAgent(freshChatroom, agent.id, imagePath, attachedFiles)
137
+
138
+ // Use enriched context message for chained agents, or reply context + original text
139
+ const messageForAgent = item.contextMessage || (replyContext + text)
140
+
141
+ let fullText = ''
142
+ let agentError = ''
143
+ const result = await streamAgentChat({
144
+ session: syntheticSession,
145
+ message: messageForAgent,
146
+ imagePath,
147
+ attachedFiles,
148
+ apiKey,
149
+ systemPrompt: fullSystemPrompt,
150
+ write: (raw: string) => {
151
+ const lines = raw.split('\n').filter(Boolean)
152
+ for (const line of lines) {
153
+ if (!line.startsWith('data: ')) continue
154
+ try {
155
+ const parsed = JSON.parse(line.slice(6).trim())
156
+ if (parsed.t === 'd' && parsed.text) {
157
+ fullText += parsed.text
158
+ writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
159
+ } else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
160
+ writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
161
+ } else if (parsed.t === 'err' && parsed.text) {
162
+ agentError = parsed.text
163
+ writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
164
+ }
165
+ } catch {
166
+ // skip malformed lines
167
+ }
168
+ }
169
+ },
170
+ history,
171
+ })
172
+
173
+ const responseText = result.fullText || fullText
174
+
175
+ // Don't persist empty or error-only messages — they pollute chat history
176
+ if (!responseText.trim() && agentError) {
177
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
178
+ return []
179
+ }
180
+
181
+ if (responseText.trim()) {
182
+ const newMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
183
+ const agentMessage: ChatroomMessage = {
184
+ id: genId(),
185
+ senderId: agent.id,
186
+ senderName: agent.name,
187
+ role: 'assistant',
188
+ text: responseText,
189
+ mentions: newMentions,
190
+ reactions: [],
191
+ time: Date.now(),
192
+ }
193
+ const latestChatrooms = loadChatrooms()
194
+ const latestChatroom = latestChatrooms[id] as Chatroom
195
+ latestChatroom.messages.push(agentMessage)
196
+ latestChatroom.updatedAt = Date.now()
197
+ latestChatrooms[id] = latestChatroom
198
+ saveChatrooms(latestChatrooms)
199
+ notify(`chatroom:${id}`)
200
+
201
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
202
+
203
+ // Return chained agent IDs — enriched context is built below when queuing
204
+ return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
205
+ }
206
+
207
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
208
+ return []
209
+ } catch (err: unknown) {
210
+ const msg = err instanceof Error ? err.message : String(err)
211
+ writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
212
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
213
+ return []
214
+ }
215
+ }
216
+
217
+ if (chatroom.chatMode === 'parallel') {
218
+ // Process initial batch in parallel
219
+ const results = await Promise.all(initialQueue.map(processOneAgent))
220
+ // Chained agents from parallel responses queue sequentially
221
+ for (const chainedIds of results) {
222
+ for (const cid of chainedIds) {
223
+ agentQueue.push({ agentId: cid, depth: 1 })
224
+ }
225
+ }
226
+ } else {
227
+ // Sequential: push initial queue items
228
+ agentQueue.push(...initialQueue)
229
+ }
230
+
231
+ // Process remaining chained agents sequentially with enriched context
232
+ while (agentQueue.length > 0) {
233
+ const item = agentQueue.shift()!
234
+
235
+ // Build enriched context for chained agents by looking at the most recent message
236
+ if (item.depth > 0 && !item.contextMessage) {
237
+ const latestChatrooms = loadChatrooms()
238
+ const latestChatroom = latestChatrooms[id] as Chatroom
239
+ const lastAgentMsg = [...latestChatroom.messages].reverse().find(
240
+ (m) => m.role === 'assistant' && m.senderId !== item.agentId
241
+ )
242
+ if (lastAgentMsg) {
243
+ const truncated = lastAgentMsg.text.length > 500 ? lastAgentMsg.text.slice(0, 500) + '...' : lastAgentMsg.text
244
+ item.contextMessage = `${lastAgentMsg.senderName} said: "${truncated}" — They're requesting your help. Review the conversation and respond.`
245
+ }
246
+ }
247
+
248
+ const chainedIds = await processOneAgent(item)
249
+ for (const cid of chainedIds) {
250
+ agentQueue.push({ agentId: cid, depth: item.depth + 1 })
251
+ }
252
+ }
253
+
254
+ writeEvent({ t: 'done' })
255
+ if (!closed) {
256
+ try { controller.close() } catch { /* already closed */ }
257
+ closed = true
258
+ }
259
+ }
260
+
261
+ processAgents().catch((err) => {
262
+ const msg = err instanceof Error ? err.message : String(err)
263
+ writeEvent({ t: 'err', text: msg })
264
+ writeEvent({ t: 'done' })
265
+ if (!closed) {
266
+ try { controller.close() } catch { /* already closed */ }
267
+ closed = true
268
+ }
269
+ })
270
+ },
271
+ cancel() {
272
+ // Client disconnected
273
+ },
274
+ })
275
+
276
+ return new NextResponse(stream, {
277
+ headers: {
278
+ 'Content-Type': 'text/event-stream',
279
+ 'Cache-Control': 'no-cache',
280
+ 'Connection': 'keep-alive',
281
+ 'X-Accel-Buffering': 'no',
282
+ },
283
+ })
284
+ }
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import { genId } from '@/lib/id'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json()
10
+ const chatrooms = loadChatrooms()
11
+ const chatroom = chatrooms[id]
12
+ if (!chatroom) return notFound()
13
+
14
+ const agentId = body.agentId as string
15
+ if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
16
+
17
+ if (!chatroom.agentIds.includes(agentId)) {
18
+ chatroom.agentIds.push(agentId)
19
+
20
+ // Inject a system event message
21
+ const agents = loadAgents()
22
+ const agentName = agents[agentId]?.name || 'Unknown agent'
23
+ if (!Array.isArray(chatroom.messages)) chatroom.messages = []
24
+ chatroom.messages.push({
25
+ id: genId(),
26
+ senderId: 'system',
27
+ senderName: 'System',
28
+ role: 'assistant',
29
+ text: `${agentName} has joined the chat`,
30
+ mentions: [],
31
+ reactions: [],
32
+ time: Date.now(),
33
+ })
34
+
35
+ chatroom.updatedAt = Date.now()
36
+ chatrooms[id] = chatroom
37
+ saveChatrooms(chatrooms)
38
+ notify('chatrooms')
39
+ notify(`chatroom:${id}`)
40
+ }
41
+
42
+ return NextResponse.json(chatroom)
43
+ }
44
+
45
+ export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
46
+ const { id } = await params
47
+ const body = await req.json()
48
+ const chatrooms = loadChatrooms()
49
+ const chatroom = chatrooms[id]
50
+ if (!chatroom) return notFound()
51
+
52
+ const agentId = body.agentId as string
53
+ if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
54
+
55
+ const wasPresent = chatroom.agentIds.includes(agentId)
56
+ chatroom.agentIds = chatroom.agentIds.filter((aid: string) => aid !== agentId)
57
+
58
+ // Inject a system event message
59
+ if (wasPresent) {
60
+ const agents = loadAgents()
61
+ const agentName = agents[agentId]?.name || 'Unknown agent'
62
+ if (!Array.isArray(chatroom.messages)) chatroom.messages = []
63
+ chatroom.messages.push({
64
+ id: genId(),
65
+ senderId: 'system',
66
+ senderName: 'System',
67
+ role: 'assistant',
68
+ text: `${agentName} has left the chat`,
69
+ mentions: [],
70
+ reactions: [],
71
+ time: Date.now(),
72
+ })
73
+ }
74
+
75
+ chatroom.updatedAt = Date.now()
76
+ chatrooms[id] = chatroom
77
+ saveChatrooms(chatrooms)
78
+ notify('chatrooms')
79
+ notify(`chatroom:${id}`)
80
+
81
+ return NextResponse.json(chatroom)
82
+ }
@@ -0,0 +1,39 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import type { Chatroom } from '@/types'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json()
10
+ const chatrooms = loadChatrooms()
11
+ const chatroom = chatrooms[id] as Chatroom | undefined
12
+ if (!chatroom) return notFound()
13
+
14
+ const messageId = body.messageId as string
15
+ if (!messageId) {
16
+ return NextResponse.json({ error: 'messageId is required' }, { status: 400 })
17
+ }
18
+
19
+ const message = chatroom.messages.find((m) => m.id === messageId)
20
+ if (!message) {
21
+ return NextResponse.json({ error: 'Message not found' }, { status: 404 })
22
+ }
23
+
24
+ // Toggle: remove if pinned, add if not
25
+ if (!chatroom.pinnedMessageIds) chatroom.pinnedMessageIds = []
26
+ const idx = chatroom.pinnedMessageIds.indexOf(messageId)
27
+ if (idx >= 0) {
28
+ chatroom.pinnedMessageIds.splice(idx, 1)
29
+ } else {
30
+ chatroom.pinnedMessageIds.push(messageId)
31
+ }
32
+
33
+ chatroom.updatedAt = Date.now()
34
+ chatrooms[id] = chatroom
35
+ saveChatrooms(chatrooms)
36
+ notify(`chatroom:${id}`)
37
+
38
+ return NextResponse.json({ ok: true, pinnedMessageIds: chatroom.pinnedMessageIds })
39
+ }
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import type { Chatroom, ChatroomMessage, ChatroomReaction } from '@/types'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json()
10
+ const chatrooms = loadChatrooms()
11
+ const chatroom = chatrooms[id] as Chatroom | undefined
12
+ if (!chatroom) return notFound()
13
+
14
+ const messageId = body.messageId as string
15
+ const emoji = body.emoji as string
16
+ const reactorId = (body.reactorId as string) || 'user'
17
+ if (!messageId || !emoji) {
18
+ return NextResponse.json({ error: 'messageId and emoji are required' }, { status: 400 })
19
+ }
20
+
21
+ const message = chatroom.messages.find((m: ChatroomMessage) => m.id === messageId)
22
+ if (!message) {
23
+ return NextResponse.json({ error: 'Message not found' }, { status: 404 })
24
+ }
25
+
26
+ // Toggle: remove if already exists, add if not
27
+ const existingIdx = message.reactions.findIndex(
28
+ (r: ChatroomReaction) => r.emoji === emoji && r.reactorId === reactorId
29
+ )
30
+ if (existingIdx >= 0) {
31
+ message.reactions.splice(existingIdx, 1)
32
+ } else {
33
+ message.reactions.push({ emoji, reactorId, time: Date.now() })
34
+ }
35
+
36
+ chatroom.updatedAt = Date.now()
37
+ chatrooms[id] = chatroom
38
+ saveChatrooms(chatrooms)
39
+ notify(`chatroom:${id}`)
40
+
41
+ return NextResponse.json({ ok: true, reactions: message.reactions })
42
+ }
@@ -0,0 +1,84 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import { genId } from '@/lib/id'
6
+
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const chatrooms = loadChatrooms()
10
+ const chatroom = chatrooms[id]
11
+ if (!chatroom) return notFound()
12
+ return NextResponse.json(chatroom)
13
+ }
14
+
15
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
16
+ const { id } = await params
17
+ const body = await req.json()
18
+ const chatrooms = loadChatrooms()
19
+ const chatroom = chatrooms[id]
20
+ if (!chatroom) return notFound()
21
+
22
+ if (body.name !== undefined) chatroom.name = body.name
23
+ if (body.description !== undefined) chatroom.description = body.description
24
+
25
+ // Diff agentIds and inject join/leave system messages
26
+ if (Array.isArray(body.agentIds)) {
27
+ const oldIds = new Set(chatroom.agentIds)
28
+ const newIds = new Set(body.agentIds as string[])
29
+ const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
30
+ const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
31
+
32
+ if (added.length > 0 || removed.length > 0) {
33
+ const agents = loadAgents()
34
+ if (!Array.isArray(chatroom.messages)) chatroom.messages = []
35
+ const now = Date.now()
36
+ let offset = 0
37
+ for (const aid of added) {
38
+ chatroom.messages.push({
39
+ id: genId(),
40
+ senderId: 'system',
41
+ senderName: 'System',
42
+ role: 'assistant',
43
+ text: `${agents[aid]?.name || 'Unknown agent'} has joined the chat`,
44
+ mentions: [],
45
+ reactions: [],
46
+ time: now + offset++,
47
+ })
48
+ }
49
+ for (const aid of removed) {
50
+ chatroom.messages.push({
51
+ id: genId(),
52
+ senderId: 'system',
53
+ senderName: 'System',
54
+ role: 'assistant',
55
+ text: `${agents[aid]?.name || 'Unknown agent'} has left the chat`,
56
+ mentions: [],
57
+ reactions: [],
58
+ time: now + offset++,
59
+ })
60
+ }
61
+ }
62
+
63
+ chatroom.agentIds = body.agentIds
64
+ }
65
+
66
+ chatroom.updatedAt = Date.now()
67
+
68
+ chatrooms[id] = chatroom
69
+ saveChatrooms(chatrooms)
70
+ notify('chatrooms')
71
+ notify(`chatroom:${id}`)
72
+ return NextResponse.json(chatroom)
73
+ }
74
+
75
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
76
+ const { id } = await params
77
+ const chatrooms = loadChatrooms()
78
+ if (!chatrooms[id]) return notFound()
79
+
80
+ delete chatrooms[id]
81
+ saveChatrooms(chatrooms)
82
+ notify('chatrooms')
83
+ return NextResponse.json({ ok: true })
84
+ }
@@ -0,0 +1,50 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { genId } from '@/lib/id'
3
+ import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import type { Chatroom, ChatroomMessage } from '@/types'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export async function GET() {
10
+ const chatrooms = loadChatrooms()
11
+ return NextResponse.json(chatrooms)
12
+ }
13
+
14
+ export async function POST(req: Request) {
15
+ const body = await req.json()
16
+ const chatrooms = loadChatrooms()
17
+ const id = genId()
18
+
19
+ const agentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
20
+ const now = Date.now()
21
+
22
+ // Generate join messages for initial agents
23
+ const agents = agentIds.length > 0 ? loadAgents() : {}
24
+ const joinMessages: ChatroomMessage[] = agentIds.map((agentId: string, i: number) => ({
25
+ id: genId(),
26
+ senderId: 'system',
27
+ senderName: 'System',
28
+ role: 'assistant',
29
+ text: `${agents[agentId]?.name || 'Unknown agent'} has joined the chat`,
30
+ mentions: [],
31
+ reactions: [],
32
+ time: now + i, // offset by 1ms so they sort in order
33
+ }))
34
+
35
+ const chatroom: Chatroom = {
36
+ id,
37
+ name: body.name || 'New Chatroom',
38
+ description: body.description || '',
39
+ agentIds,
40
+ messages: joinMessages,
41
+ createdAt: now,
42
+ updatedAt: now,
43
+ }
44
+
45
+ chatrooms[id] = chatroom
46
+ saveChatrooms(chatrooms)
47
+ notify('chatrooms')
48
+
49
+ return NextResponse.json(chatroom)
50
+ }
@@ -61,6 +61,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
61
61
  // Regular update
62
62
  if (body.name !== undefined) connector.name = body.name
63
63
  if (body.agentId !== undefined) connector.agentId = body.agentId
64
+ if (body.chatroomId !== undefined) connector.chatroomId = body.chatroomId
64
65
  if (body.credentialId !== undefined) connector.credentialId = body.credentialId
65
66
  if (body.config !== undefined) connector.config = body.config
66
67
  if (body.isEnabled !== undefined) connector.isEnabled = body.isEnabled
@@ -33,7 +33,8 @@ export async function POST(req: Request) {
33
33
  id,
34
34
  name: body.name || `${body.platform} Connector`,
35
35
  platform: body.platform,
36
- agentId: body.agentId,
36
+ agentId: body.agentId || null,
37
+ chatroomId: body.chatroomId || null,
37
38
  credentialId: body.credentialId || null,
38
39
  config: body.config || {},
39
40
  isEnabled: false,
@@ -6,8 +6,8 @@ export const dynamic = 'force-dynamic'
6
6
 
7
7
  export async function GET(_req: Request) {
8
8
  const creds = loadCredentials()
9
- const safe: Record<string, any> = {}
10
- for (const [id, c] of Object.entries(creds) as [string, any][]) {
9
+ const safe: Record<string, Record<string, unknown>> = {}
10
+ for (const [id, c] of Object.entries(creds) as [string, Record<string, unknown>][]) {
11
11
  safe[id] = { id: c.id, provider: c.provider, name: c.name, createdAt: c.createdAt }
12
12
  }
13
13
  return NextResponse.json(safe)
@@ -28,6 +28,5 @@ export async function POST(req: Request) {
28
28
  createdAt: Date.now(),
29
29
  }
30
30
  saveCredentials(creds)
31
- console.log(`[credentials] stored ${id} for ${provider}`)
32
31
  return NextResponse.json({ id, provider, name: creds[id].name, createdAt: creds[id].createdAt })
33
32
  }
@@ -0,0 +1,43 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { exec } from 'child_process'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+
6
+ export async function POST(req: Request) {
7
+ const { path: targetPath } = await req.json() as { path?: string }
8
+ if (!targetPath || typeof targetPath !== 'string') {
9
+ return NextResponse.json({ error: 'path is required' }, { status: 400 })
10
+ }
11
+
12
+ const resolved = path.resolve(targetPath)
13
+
14
+ // Verify the path exists
15
+ if (!fs.existsSync(resolved)) {
16
+ return NextResponse.json({ error: 'Path does not exist' }, { status: 404 })
17
+ }
18
+
19
+ const isDir = fs.statSync(resolved).isDirectory()
20
+ const platform = process.platform
21
+
22
+ // Determine the command to reveal in the OS file manager
23
+ let cmd: string
24
+ if (platform === 'darwin') {
25
+ // macOS: -R reveals in Finder (selects the item), for dirs just open the dir
26
+ cmd = isDir ? `open "${resolved}"` : `open -R "${resolved}"`
27
+ } else if (platform === 'win32') {
28
+ cmd = isDir ? `explorer "${resolved}"` : `explorer /select,"${resolved}"`
29
+ } else {
30
+ // Linux: xdg-open on the directory containing the file
31
+ cmd = `xdg-open "${isDir ? resolved : path.dirname(resolved)}"`
32
+ }
33
+
34
+ return new Promise<NextResponse>((resolve) => {
35
+ exec(cmd, (err) => {
36
+ if (err) {
37
+ resolve(NextResponse.json({ error: err.message }, { status: 500 }))
38
+ } else {
39
+ resolve(NextResponse.json({ ok: true }))
40
+ }
41
+ })
42
+ })
43
+ }