@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
@@ -25,7 +25,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
25
25
  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
26
26
  }
27
27
 
28
- const { title, content, tags } = body as Record<string, unknown>
28
+ const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
29
29
 
30
30
  const updates: Record<string, unknown> = {}
31
31
  if (typeof title === 'string' && title.trim()) {
@@ -36,13 +36,24 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
36
36
  }
37
37
 
38
38
  const existingMeta = (existing.metadata || {}) as Record<string, unknown>
39
+ const metaUpdates: Record<string, unknown> = { ...existingMeta }
40
+
39
41
  if (Array.isArray(tags)) {
40
42
  const normalizedTags = (tags as unknown[]).filter(
41
43
  (t): t is string => typeof t === 'string' && t.trim().length > 0,
42
44
  )
43
- updates.metadata = { ...existingMeta, tags: normalizedTags }
45
+ metaUpdates.tags = normalizedTags
46
+ }
47
+
48
+ if (scope === 'global' || scope === 'agent') {
49
+ metaUpdates.scope = scope
50
+ metaUpdates.agentIds = scope === 'agent' && Array.isArray(agentIds)
51
+ ? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
52
+ : []
44
53
  }
45
54
 
55
+ updates.metadata = metaUpdates
56
+
46
57
  const updated = db.update(id, updates)
47
58
  if (!updated) {
48
59
  return notFound()
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
25
25
  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
26
26
  }
27
27
 
28
- const { title, content, tags } = body as Record<string, unknown>
28
+ const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
29
29
 
30
30
  if (typeof title !== 'string' || !title.trim()) {
31
31
  return NextResponse.json({ error: 'title is required.' }, { status: 400 })
@@ -38,10 +38,17 @@ export async function POST(req: Request) {
38
38
  ? (tags as unknown[]).filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
39
39
  : undefined
40
40
 
41
+ const normalizedScope = scope === 'agent' ? 'agent' as const : 'global' as const
42
+ const normalizedAgentIds = Array.isArray(agentIds)
43
+ ? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
44
+ : []
45
+
41
46
  const entry = addKnowledge({
42
47
  title: title.trim(),
43
48
  content,
44
49
  tags: normalizedTags,
50
+ scope: normalizedScope,
51
+ agentIds: normalizedAgentIds,
45
52
  })
46
53
 
47
54
  return NextResponse.json(entry)
@@ -26,7 +26,13 @@ export async function GET(req: Request) {
26
26
  const requestedLimit = parseOptionalInt(searchParams.get('limit'))
27
27
  const requestedLinkedLimit = parseOptionalInt(searchParams.get('linkedLimit'))
28
28
 
29
+ const counts = searchParams.get('counts') === 'true'
29
30
  const db = getMemoryDb()
31
+
32
+ if (counts) {
33
+ return NextResponse.json(db.countsByAgent())
34
+ }
35
+
30
36
  const defaults = getMemoryLookupLimits()
31
37
  const limits = resolveLookupRequest(defaults, {
32
38
  depth: requestedDepth,
@@ -106,6 +112,8 @@ export async function POST(req: Request) {
106
112
  image: image as MemoryImage | null | undefined,
107
113
  imagePath: image && typeof image === 'object' && 'path' in image ? String((image as { path: string }).path) : null,
108
114
  linkedMemoryIds: body.linkedMemoryIds as string[] | undefined,
115
+ pinned: body.pinned === true,
116
+ sharedWith: Array.isArray(body.sharedWith) ? body.sharedWith as string[] : undefined,
109
117
  })
110
118
  return NextResponse.json(entry)
111
119
  }
@@ -25,12 +25,16 @@ export async function GET(req: Request) {
25
25
 
26
26
  export async function POST(req: Request) {
27
27
  const body = (await req.json()) as Record<string, unknown>
28
+ const actionLabel = typeof body.actionLabel === 'string' ? body.actionLabel : undefined
29
+ const actionUrl = typeof body.actionUrl === 'string' ? body.actionUrl : undefined
28
30
  const id = genId()
29
31
  const notification: AppNotification = {
30
32
  id,
31
33
  type: (['info', 'success', 'warning', 'error'].includes(body.type as string) ? body.type : 'info') as AppNotification['type'],
32
34
  title: typeof body.title === 'string' ? body.title : 'Notification',
33
35
  message: typeof body.message === 'string' ? body.message : undefined,
36
+ actionLabel,
37
+ actionUrl,
34
38
  entityType: typeof body.entityType === 'string' ? body.entityType : undefined,
35
39
  entityId: typeof body.entityId === 'string' ? body.entityId : undefined,
36
40
  read: false,
@@ -4,7 +4,7 @@ import { loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
4
4
  import { enqueueTask } from '@/lib/server/queue'
5
5
 
6
6
  export async function POST(req: Request) {
7
- const { agentId, task } = await req.json()
7
+ const { agentId, task } = await req.json().catch(() => ({}))
8
8
  if (!agentId || !task) {
9
9
  return NextResponse.json({ error: 'agentId and task are required' }, { status: 400 })
10
10
  }
@@ -49,9 +49,9 @@ export async function POST(req: Request) {
49
49
  fs.writeFileSync(dest, code, 'utf8')
50
50
 
51
51
  return NextResponse.json({ ok: true, filename: sanitized })
52
- } catch (err: any) {
52
+ } catch (err: unknown) {
53
53
  return NextResponse.json(
54
- { error: 'Failed to install plugin', message: err.message },
54
+ { error: 'Failed to install plugin', message: err instanceof Error ? err.message : String(err) },
55
55
  { status: 500 },
56
56
  )
57
57
  }
@@ -9,11 +9,12 @@ import {
9
9
  } from '@/lib/server/storage'
10
10
 
11
11
  interface SearchResult {
12
- type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill'
12
+ type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill' | 'message'
13
13
  id: string
14
14
  title: string
15
15
  description?: string
16
16
  status?: string
17
+ messageIndex?: number
17
18
  }
18
19
 
19
20
  const MAX_RESULTS = 20
@@ -49,6 +50,56 @@ function searchCollection(
49
50
  return results
50
51
  }
51
52
 
53
+ function searchMessages(
54
+ sessions: Record<string, Record<string, unknown>>,
55
+ agents: Record<string, Record<string, unknown>>,
56
+ needle: string,
57
+ ): SearchResult[] {
58
+ const results: SearchResult[] = []
59
+ const MAX_MSG_RESULTS = 10
60
+ for (const [sessionId, session] of Object.entries(sessions)) {
61
+ if (results.length >= MAX_MSG_RESULTS) break
62
+ if (!Array.isArray(session.messages) || !session.messages.length) continue
63
+ const messages = session.messages as Array<Record<string, unknown>>
64
+ const agentId = session.agentId as string | undefined
65
+ const agentName = agentId && agents[agentId] ? (agents[agentId].name as string) : undefined
66
+ const sessionName = (session.name as string) || 'Untitled'
67
+ for (let i = 0; i < messages.length; i++) {
68
+ if (results.length >= MAX_MSG_RESULTS) break
69
+ const msg = messages[i]
70
+ const text = typeof msg?.text === 'string' ? msg.text : ''
71
+ if (!text) continue
72
+ const idx = text.toLowerCase().indexOf(needle)
73
+ if (idx === -1) continue
74
+ // Build snippet with context around match
75
+ const start = Math.max(0, idx - 30)
76
+ const end = Math.min(text.length, idx + needle.length + 50)
77
+ const snippet = (start > 0 ? '...' : '') + text.slice(start, end).replace(/\n/g, ' ') + (end < text.length ? '...' : '')
78
+ const msgTime = typeof msg.time === 'number' ? msg.time : 0
79
+ const timeAgo = msgTime ? formatTimeAgo(msgTime) : ''
80
+ results.push({
81
+ type: 'message',
82
+ id: sessionId,
83
+ title: snippet,
84
+ description: [agentName || sessionName, timeAgo].filter(Boolean).join(' · '),
85
+ messageIndex: i,
86
+ })
87
+ }
88
+ }
89
+ return results
90
+ }
91
+
92
+ function formatTimeAgo(ts: number): string {
93
+ const diff = Date.now() - ts
94
+ const mins = Math.floor(diff / 60000)
95
+ if (mins < 1) return 'just now'
96
+ if (mins < 60) return `${mins}m ago`
97
+ const hours = Math.floor(mins / 60)
98
+ if (hours < 24) return `${hours}h ago`
99
+ const days = Math.floor(hours / 24)
100
+ return `${days}d ago`
101
+ }
102
+
52
103
  export async function GET(req: Request) {
53
104
  const { searchParams } = new URL(req.url)
54
105
  const q = (searchParams.get('q') || '').trim().toLowerCase()
@@ -71,6 +122,7 @@ export async function GET(req: Request) {
71
122
  searchCollection(schedules, 'schedule', q, 'name', 'taskPrompt', 'status'),
72
123
  searchCollection(webhooks, 'webhook', q, 'name', 'source'),
73
124
  searchCollection(skills, 'skill', q, 'name', 'description'),
125
+ searchMessages(sessions, agents, q),
74
126
  ]
75
127
 
76
128
  // Proportional allocation across types
@@ -17,6 +17,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
17
17
  const attachedFiles = Array.isArray(body.attachedFiles) ? body.attachedFiles.filter((f: unknown) => typeof f === 'string') as string[] : undefined
18
18
  const internal = body.internal === true
19
19
  const queueMode = normalizeQueueMode(body.queueMode, internal)
20
+ const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
20
21
 
21
22
  const hasFiles = !!(imagePath || imageUrl || (attachedFiles && attachedFiles.length > 0))
22
23
  if (!message.trim() && !hasFiles) {
@@ -46,6 +47,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
46
47
  source: internal ? 'heartbeat' : 'chat',
47
48
  mode: queueMode,
48
49
  onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
50
+ replyToId,
49
51
  })
50
52
 
51
53
  log.info('chat', `Enqueued session run ${run.runId}`, {
@@ -4,7 +4,7 @@ import { notFound } from '@/lib/server/collection-helpers'
4
4
 
5
5
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
6
  const { id } = await params
7
- const body = await req.json() as { messageIndex: number; newText: string }
7
+ const body = await req.json().catch(() => ({})) as { messageIndex: number; newText: string }
8
8
  const sessions = loadSessions()
9
9
  const session = sessions[id]
10
10
  if (!session) return notFound()
@@ -5,7 +5,7 @@ import { notFound } from '@/lib/server/collection-helpers'
5
5
 
6
6
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
7
  const { id } = await params
8
- const body = await req.json() as { messageIndex: number }
8
+ const body = await req.json().catch(() => ({})) as { messageIndex: number }
9
9
  const sessions = loadSessions()
10
10
  const source = sessions[id]
11
11
  if (!source) return notFound()
@@ -2,11 +2,57 @@ import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
 
5
- export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
6
  const { id } = await params
7
7
  const sessions = loadSessions()
8
8
  if (!sessions[id]) return notFound()
9
- return NextResponse.json(sessions[id].messages)
9
+
10
+ const url = new URL(req.url)
11
+ const limitParam = url.searchParams.get('limit')
12
+ const beforeParam = url.searchParams.get('before')
13
+
14
+ const allMessages = sessions[id].messages
15
+ const total = allMessages.length
16
+
17
+ // If no limit param, return all messages (backward compatible)
18
+ if (!limitParam) {
19
+ return NextResponse.json(allMessages)
20
+ }
21
+
22
+ const limit = Math.max(1, Math.min(500, parseInt(limitParam) || 100))
23
+ const before = beforeParam !== null ? parseInt(beforeParam) : total
24
+
25
+ // Return `limit` messages ending just before `before` index
26
+ const start = Math.max(0, before - limit)
27
+ const end = Math.max(0, before)
28
+ const messages = allMessages.slice(start, end)
29
+
30
+ return NextResponse.json({
31
+ messages,
32
+ total,
33
+ hasMore: start > 0,
34
+ startIndex: start,
35
+ })
36
+ }
37
+
38
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
39
+ const { id } = await params
40
+ const body = await req.json() as { kind?: string }
41
+ if (body.kind !== 'context-clear') {
42
+ return NextResponse.json({ error: 'Only context-clear kind is supported' }, { status: 400 })
43
+ }
44
+ const sessions = loadSessions()
45
+ const session = sessions[id]
46
+ if (!session) return notFound()
47
+
48
+ session.messages.push({
49
+ role: 'user',
50
+ text: '',
51
+ kind: 'context-clear',
52
+ time: Date.now(),
53
+ })
54
+ saveSessions(sessions)
55
+ return NextResponse.json({ ok: true })
10
56
  }
11
57
 
12
58
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -25,3 +71,25 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
25
71
  saveSessions(sessions)
26
72
  return NextResponse.json(session.messages[messageIndex])
27
73
  }
74
+
75
+ export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
76
+ const { id } = await params
77
+ const body = await req.json() as { messageIndex: number }
78
+ const sessions = loadSessions()
79
+ const session = sessions[id]
80
+ if (!session) return notFound()
81
+
82
+ const { messageIndex } = body
83
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
84
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
85
+ }
86
+
87
+ // Only allow deleting context-clear markers (safety guard)
88
+ if (session.messages[messageIndex].kind !== 'context-clear') {
89
+ return NextResponse.json({ error: 'Only context-clear markers can be removed' }, { status: 400 })
90
+ }
91
+
92
+ session.messages.splice(messageIndex, 1)
93
+ saveSessions(sessions)
94
+ return NextResponse.json({ ok: true })
95
+ }
@@ -69,6 +69,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
69
69
  if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
70
70
  if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
71
71
  if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
72
+ if (updates.claudeSessionId !== undefined) sessions[id].claudeSessionId = updates.claudeSessionId
73
+ if (updates.codexThreadId !== undefined) sessions[id].codexThreadId = updates.codexThreadId
74
+ if (updates.opencodeSessionId !== undefined) sessions[id].opencodeSessionId = updates.opencodeSessionId
75
+ if (updates.delegateResumeIds !== undefined) sessions[id].delegateResumeIds = updates.delegateResumeIds
72
76
  if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
73
77
  ensureMainSessionFlag(sessions[id])
74
78
 
@@ -23,7 +23,7 @@ export async function GET(_req: Request) {
23
23
  }
24
24
 
25
25
  export async function DELETE(req: Request) {
26
- const { ids } = await req.json() as { ids: string[] }
26
+ const { ids } = await req.json().catch(() => ({ ids: [] })) as { ids: string[] }
27
27
  if (!Array.isArray(ids) || !ids.length) {
28
28
  return new NextResponse('Missing ids', { status: 400 })
29
29
  }
@@ -43,7 +43,7 @@ export async function DELETE(req: Request) {
43
43
  }
44
44
 
45
45
  export async function POST(req: Request) {
46
- const body = await req.json()
46
+ const body = await req.json().catch(() => ({}))
47
47
  let cwd = (body.cwd || '').trim()
48
48
  if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
49
49
  else if (cwd === '~') cwd = os.homedir()
@@ -64,7 +64,7 @@ export async function POST(req: Request) {
64
64
 
65
65
  sessions[id] = {
66
66
  id, name: sessionName, cwd,
67
- user: body.user || 'wayde',
67
+ user: body.user || 'user',
68
68
  provider: body.provider || agent?.provider || 'claude-cli',
69
69
  model: body.model || agent?.model || '',
70
70
  credentialId: body.credentialId || agent?.credentialId || null,
@@ -56,5 +56,14 @@ export async function PUT(req: Request) {
56
56
  settings.maxLinkedMemoriesExpanded = nextLinked
57
57
 
58
58
  saveSettings(settings)
59
+
60
+ // Restart heartbeat service when heartbeat-related settings change
61
+ const heartbeatKeys = ['heartbeatIntervalSec', 'heartbeatInterval', 'heartbeatPrompt', 'heartbeatEnabled', 'heartbeatActiveStart', 'heartbeatActiveEnd']
62
+ if (heartbeatKeys.some((k) => k in body)) {
63
+ import('@/lib/server/heartbeat-service').then(({ restartHeartbeatService }) => {
64
+ restartHeartbeatService()
65
+ }).catch(() => { /* heartbeat service may not be initialized yet */ })
66
+ }
67
+
59
68
  return NextResponse.json(settings)
60
69
  }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, decryptKey } from '@/lib/server/storage'
3
3
  import { getDeviceId, wsConnect } from '@/lib/providers/openclaw'
4
+ import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
4
5
 
5
6
  type SetupProvider =
6
7
  | 'openai'
@@ -15,20 +16,6 @@ type SetupProvider =
15
16
  | 'ollama'
16
17
  | 'openclaw'
17
18
 
18
- const OPENAI_COMPATIBLE_PROVIDER_INFO: Record<
19
- 'openai' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks',
20
- { name: string; defaultEndpoint: string }
21
- > = {
22
- openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
23
- google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
24
- deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
25
- groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
26
- together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
27
- mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
28
- xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
29
- fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
30
- }
31
-
32
19
  interface SetupCheckBody {
33
20
  provider?: string
34
21
  apiKey?: string
@@ -196,7 +183,7 @@ export async function POST(req: Request) {
196
183
  switch (provider) {
197
184
  case 'openai': {
198
185
  if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenAI API key is required.' })
199
- const info = OPENAI_COMPATIBLE_PROVIDER_INFO.openai
186
+ const info = OPENAI_COMPATIBLE_DEFAULTS.openai
200
187
  const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
201
188
  return NextResponse.json(result)
202
189
  }
@@ -212,7 +199,7 @@ export async function POST(req: Request) {
212
199
  case 'mistral':
213
200
  case 'xai':
214
201
  case 'fireworks': {
215
- const info = OPENAI_COMPATIBLE_PROVIDER_INFO[provider]
202
+ const info = OPENAI_COMPATIBLE_DEFAULTS[provider]
216
203
  if (!apiKey) return NextResponse.json({ ok: false, message: `${info.name} API key is required.` })
217
204
  const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
218
205
  return NextResponse.json(result)
@@ -18,6 +18,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
18
18
  const body = await req.json()
19
19
  const result = mutateItem(ops, id, (skill) => {
20
20
  const normalized = normalizeSkillPayload({ ...skill, ...body })
21
+ const updatedScope = body.scope === 'agent' ? 'agent' as const : body.scope === 'global' ? 'global' as const : skill.scope
22
+ const updatedAgentIds = updatedScope === 'agent' && Array.isArray(body.agentIds)
23
+ ? (body.agentIds as unknown[]).filter((aid): aid is string => typeof aid === 'string')
24
+ : updatedScope === 'agent' ? (skill.agentIds || []) : []
21
25
  return {
22
26
  ...skill,
23
27
  ...body,
@@ -27,6 +31,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
27
31
  content: normalized.content,
28
32
  sourceUrl: normalized.sourceUrl,
29
33
  sourceFormat: normalized.sourceFormat,
34
+ scope: updatedScope,
35
+ agentIds: updatedAgentIds,
30
36
  id,
31
37
  updatedAt: Date.now(),
32
38
  }
@@ -14,6 +14,10 @@ export async function POST(req: Request) {
14
14
  const skills = loadSkills()
15
15
  const id = genId()
16
16
  const normalized = normalizeSkillPayload(body)
17
+ const scope = body.scope === 'agent' ? 'agent' as const : 'global' as const
18
+ const agentIds = scope === 'agent' && Array.isArray(body.agentIds)
19
+ ? (body.agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
20
+ : []
17
21
  skills[id] = {
18
22
  id,
19
23
  name: normalized.name,
@@ -22,6 +26,8 @@ export async function POST(req: Request) {
22
26
  description: normalized.description || '',
23
27
  sourceUrl: normalized.sourceUrl,
24
28
  sourceFormat: normalized.sourceFormat,
29
+ scope,
30
+ agentIds,
25
31
  createdAt: Date.now(),
26
32
  updatedAt: Date.now(),
27
33
  }
@@ -8,6 +8,8 @@ import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/ta
8
8
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
9
9
  import { notify } from '@/lib/server/ws-hub'
10
10
  import { createNotification } from '@/lib/server/create-notification'
11
+ import { enqueueSystemEvent } from '@/lib/server/system-events'
12
+ import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
11
13
 
12
14
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
13
15
  // Keep completed queue integrity even if daemon is not running.
@@ -34,6 +36,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
34
36
  tasks[id].updatedAt = Date.now()
35
37
  } else {
36
38
  Object.assign(tasks[id], body, { updatedAt: Date.now() })
39
+ // Explicitly clear nullable fields when sent as null (Object.assign copies null but not undefined)
40
+ if (body.projectId === null) delete tasks[id].projectId
37
41
  }
38
42
  tasks[id].id = id // prevent id overwrite
39
43
 
@@ -84,6 +88,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
84
88
  entityType: 'task',
85
89
  entityId: id,
86
90
  })
91
+
92
+ // Enqueue system event + heartbeat wake
93
+ if (tasks[id].sessionId) {
94
+ enqueueSystemEvent(tasks[id].sessionId, `Task ${tasks[id].status}: ${tasks[id].title}`)
95
+ }
96
+ if (tasks[id].agentId) {
97
+ requestHeartbeatNow({ agentId: tasks[id].agentId, reason: 'task-completed' })
98
+ }
87
99
  }
88
100
 
89
101
  // Dependency check: cannot queue a task if any blocker is incomplete
@@ -0,0 +1,100 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
3
+ import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
4
+ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+ import { createNotification } from '@/lib/server/create-notification'
7
+ import type { BoardTaskStatus } from '@/types'
8
+
9
+ const VALID_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
10
+
11
+ /**
12
+ * Bulk update tasks — batch status changes, agent/project reassignment, or archive/delete.
13
+ *
14
+ * POST body:
15
+ * ids: string[] — required, task IDs to update
16
+ * status?: BoardTaskStatus — move all to this status
17
+ * agentId?: string | null — reassign agent (null to clear)
18
+ * projectId?: string | null — reassign project (null to clear)
19
+ */
20
+ export async function POST(req: Request) {
21
+ const body = await req.json()
22
+ const ids: unknown = body.ids
23
+ if (!Array.isArray(ids) || ids.length === 0) {
24
+ return NextResponse.json({ error: 'ids must be a non-empty array' }, { status: 400 })
25
+ }
26
+
27
+ const taskIds = ids.filter((id): id is string => typeof id === 'string')
28
+ if (taskIds.length === 0) {
29
+ return NextResponse.json({ error: 'No valid task IDs provided' }, { status: 400 })
30
+ }
31
+
32
+ const tasks = loadTasks()
33
+ let updated = 0
34
+ const results: string[] = []
35
+
36
+ for (const id of taskIds) {
37
+ if (!tasks[id]) continue
38
+ const prevStatus = tasks[id].status
39
+
40
+ if (typeof body.status === 'string' && VALID_STATUSES.includes(body.status as BoardTaskStatus)) {
41
+ tasks[id].status = body.status as BoardTaskStatus
42
+ if (body.status === 'archived' && prevStatus !== 'archived') {
43
+ tasks[id].archivedAt = Date.now()
44
+ }
45
+ }
46
+
47
+ if ('agentId' in body) {
48
+ tasks[id].agentId = body.agentId === null ? '' : String(body.agentId)
49
+ }
50
+
51
+ if ('projectId' in body) {
52
+ if (body.projectId === null) {
53
+ delete tasks[id].projectId
54
+ } else {
55
+ tasks[id].projectId = String(body.projectId)
56
+ }
57
+ }
58
+
59
+ tasks[id].updatedAt = Date.now()
60
+ updated++
61
+ results.push(id)
62
+
63
+ // Side-effects for status transitions
64
+ if (prevStatus !== tasks[id].status) {
65
+ logActivity({
66
+ entityType: 'task',
67
+ entityId: id,
68
+ action: 'updated',
69
+ actor: 'user',
70
+ summary: `Bulk update: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})`,
71
+ })
72
+ pushMainLoopEventToMainSessions({
73
+ type: 'task_status_changed',
74
+ text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
75
+ })
76
+ if (tasks[id].status === 'completed' || tasks[id].status === 'failed') {
77
+ disableSessionHeartbeat(tasks[id].sessionId)
78
+ }
79
+ if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
80
+ enqueueTask(id)
81
+ }
82
+ }
83
+ }
84
+
85
+ saveTasks(tasks)
86
+
87
+ if (updated > 0) {
88
+ const action = body.status
89
+ ? `moved ${updated} task(s) to ${body.status}`
90
+ : `updated ${updated} task(s)`
91
+ createNotification({
92
+ type: 'success',
93
+ title: `Bulk update: ${action}`,
94
+ entityType: 'task',
95
+ })
96
+ }
97
+
98
+ notify('tasks')
99
+ return NextResponse.json({ updated, ids: results })
100
+ }