@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,101 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadTasks, loadAgents } from '@/lib/server/storage'
3
+
4
+ type Range = '24h' | '7d' | '30d'
5
+
6
+ const RANGE_MS: Record<Range, number> = {
7
+ '24h': 24 * 3600_000,
8
+ '7d': 7 * 86400_000,
9
+ '30d': 30 * 86400_000,
10
+ }
11
+
12
+ function bucketKey(ts: number, range: Range): string {
13
+ const d = new Date(ts)
14
+ if (range === '24h') return d.toISOString().slice(0, 13) // "2026-03-01T14"
15
+ return d.toISOString().slice(0, 10) // "2026-03-01"
16
+ }
17
+
18
+ export async function GET(req: Request) {
19
+ const { searchParams } = new URL(req.url)
20
+ const range = (searchParams.get('range') as Range) || '7d'
21
+ const cutoff = Date.now() - (RANGE_MS[range] || RANGE_MS['7d'])
22
+
23
+ const tasks = loadTasks()
24
+ const agents = loadAgents()
25
+ const all = Object.values(tasks)
26
+
27
+ // --- by-status counts ---
28
+ const byStatus: Record<string, number> = {}
29
+ for (const t of all) {
30
+ byStatus[t.status] = (byStatus[t.status] || 0) + 1
31
+ }
32
+
33
+ // WIP = queued + running
34
+ const wip = (byStatus['queued'] || 0) + (byStatus['running'] || 0)
35
+
36
+ // --- completions in range ---
37
+ const completedInRange = all.filter(
38
+ (t) => t.status === 'completed' && t.completedAt && t.completedAt >= cutoff,
39
+ )
40
+
41
+ // --- cycle times (queuedAt → completedAt) ---
42
+ const cycleTimes: number[] = []
43
+ for (const t of completedInRange) {
44
+ const start = t.queuedAt || t.createdAt
45
+ const end = t.completedAt!
46
+ if (end > start) cycleTimes.push(end - start)
47
+ }
48
+ cycleTimes.sort((a, b) => a - b)
49
+
50
+ const avgCycleMs = cycleTimes.length
51
+ ? Math.round(cycleTimes.reduce((s, v) => s + v, 0) / cycleTimes.length)
52
+ : 0
53
+ const p50CycleMs = cycleTimes.length ? cycleTimes[Math.floor(cycleTimes.length * 0.5)] : 0
54
+ const p90CycleMs = cycleTimes.length ? cycleTimes[Math.floor(cycleTimes.length * 0.9)] : 0
55
+
56
+ // --- velocity (completions per bucket) ---
57
+ const velocityMap: Record<string, number> = {}
58
+ for (const t of completedInRange) {
59
+ const key = bucketKey(t.completedAt!, range)
60
+ velocityMap[key] = (velocityMap[key] || 0) + 1
61
+ }
62
+ const velocity = Object.entries(velocityMap)
63
+ .sort(([a], [b]) => a.localeCompare(b))
64
+ .map(([bucket, count]) => ({ bucket, count }))
65
+
66
+ // --- by-agent completions ---
67
+ const byAgent: Record<string, { agentName: string; completed: number; failed: number }> = {}
68
+ const recentTasks = all.filter(
69
+ (t) => (t.completedAt && t.completedAt >= cutoff) || (t.status === 'failed' && t.updatedAt >= cutoff),
70
+ )
71
+ for (const t of recentTasks) {
72
+ if (!t.agentId) continue
73
+ if (!byAgent[t.agentId]) {
74
+ const agent = agents[t.agentId]
75
+ byAgent[t.agentId] = { agentName: agent?.name || t.agentId, completed: 0, failed: 0 }
76
+ }
77
+ if (t.status === 'completed') byAgent[t.agentId].completed++
78
+ else if (t.status === 'failed') byAgent[t.agentId].failed++
79
+ }
80
+ const byAgentList = Object.values(byAgent).sort((a, b) => b.completed - a.completed)
81
+
82
+ // --- by-priority counts ---
83
+ const byPriority: Record<string, number> = {}
84
+ for (const t of all) {
85
+ const p = t.priority || 'none'
86
+ byPriority[p] = (byPriority[p] || 0) + 1
87
+ }
88
+
89
+ return NextResponse.json({
90
+ range,
91
+ byStatus,
92
+ wip,
93
+ completedCount: completedInRange.length,
94
+ avgCycleMs,
95
+ p50CycleMs,
96
+ p90CycleMs,
97
+ velocity,
98
+ byAgent: byAgentList,
99
+ byPriority,
100
+ })
101
+ }
@@ -1,11 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadTasks, saveTasks, loadSettings, logActivity } from '@/lib/server/storage'
3
+ import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
4
4
  import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
5
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
6
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
7
7
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
8
  import { notify } from '@/lib/server/ws-hub'
9
+ import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
10
+ import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
9
11
 
10
12
  export async function GET(req: Request) {
11
13
  // Keep completed queue integrity even if daemon is not running.
@@ -64,12 +66,18 @@ export async function POST(req: Request) {
64
66
  const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
65
67
  ? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
66
68
  : Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
69
+ // Resolve @mentions in description to auto-assign agent
70
+ const resolvedAgentId = body.description
71
+ ? resolveTaskAgentFromDescription(body.description, body.agentId || '', loadAgents())
72
+ : (body.agentId || '')
73
+
67
74
  tasks[id] = {
68
75
  id,
69
76
  title: body.title || 'Untitled Task',
70
77
  description: body.description || '',
71
78
  status: body.status || 'backlog',
72
- agentId: body.agentId || '',
79
+ agentId: resolvedAgentId,
80
+ projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
73
81
  goalContract: body.goalContract || null,
74
82
  cwd: typeof body.cwd === 'string' ? body.cwd : null,
75
83
  file: typeof body.file === 'string' ? body.file : null,
@@ -93,6 +101,14 @@ export async function POST(req: Request) {
93
101
  tags: Array.isArray(body.tags) ? body.tags.filter((s: unknown) => typeof s === 'string') : [],
94
102
  dueAt: typeof body.dueAt === 'number' ? body.dueAt : null,
95
103
  customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
104
+ priority: ['low', 'medium', 'high', 'critical'].includes(body.priority) ? body.priority : undefined,
105
+ fingerprint: computeTaskFingerprint(body.title || 'Untitled Task', body.agentId || ''),
106
+ }
107
+
108
+ // Dedup: if a non-terminal task with same fingerprint exists, return it
109
+ const dupe = findDuplicateTask(tasks, { fingerprint: tasks[id].fingerprint! })
110
+ if (dupe && dupe.id !== id) {
111
+ return NextResponse.json({ ...dupe, deduplicated: true })
96
112
  }
97
113
 
98
114
  if (tasks[id].status === 'completed') {
@@ -10,8 +10,9 @@ export async function POST(req: Request) {
10
10
  return new NextResponse('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
11
11
  }
12
12
 
13
- const { text } = await req.json()
14
- const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}`, {
13
+ const { text, voiceId } = await req.json()
14
+ const voice = voiceId || ELEVENLABS_VOICE
15
+ const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
15
16
  method: 'POST',
16
17
  headers: {
17
18
  'xi-api-key': ELEVENLABS_KEY,
@@ -9,13 +9,14 @@ export async function POST(req: Request) {
9
9
  return new Response('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
10
10
  }
11
11
 
12
- const { text } = await req.json()
12
+ const { text, voiceId } = await req.json()
13
13
  if (!text?.trim()) {
14
14
  return new Response('No text provided', { status: 400 })
15
15
  }
16
16
 
17
+ const voice = voiceId || ELEVENLABS_VOICE
17
18
  const apiRes = await fetch(
18
- `https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}/stream`,
19
+ `https://api.elevenlabs.io/v1/text-to-speech/${voice}/stream`,
19
20
  {
20
21
  method: 'POST',
21
22
  headers: {
@@ -3,40 +3,7 @@ import { notFound } from '@/lib/server/collection-helpers'
3
3
  import fs from 'fs'
4
4
  import path from 'path'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
-
7
- const MIME_TYPES: Record<string, string> = {
8
- '.png': 'image/png',
9
- '.jpg': 'image/jpeg',
10
- '.jpeg': 'image/jpeg',
11
- '.gif': 'image/gif',
12
- '.webp': 'image/webp',
13
- '.svg': 'image/svg+xml',
14
- '.bmp': 'image/bmp',
15
- '.ico': 'image/x-icon',
16
- '.mp4': 'video/mp4',
17
- '.webm': 'video/webm',
18
- '.mov': 'video/quicktime',
19
- '.avi': 'video/x-msvideo',
20
- '.mkv': 'video/x-matroska',
21
- '.pdf': 'application/pdf',
22
- '.json': 'application/json',
23
- '.csv': 'text/csv',
24
- '.txt': 'text/plain',
25
- '.html': 'text/html',
26
- '.xml': 'application/xml',
27
- '.zip': 'application/zip',
28
- '.tar': 'application/x-tar',
29
- '.gz': 'application/gzip',
30
- '.doc': 'application/msword',
31
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
32
- '.xls': 'application/vnd.ms-excel',
33
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
34
- '.ppt': 'application/vnd.ms-powerpoint',
35
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
36
- '.mp3': 'audio/mpeg',
37
- '.wav': 'audio/wav',
38
- '.ogg': 'audio/ogg',
39
- }
6
+ import { MIME_TYPES } from '@/lib/server/mime'
40
7
 
41
8
  export async function GET(_req: Request, { params }: { params: Promise<{ filename: string }> }) {
42
9
  const { filename } = await params
@@ -60,3 +27,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
60
27
  },
61
28
  })
62
29
  }
30
+
31
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ filename: string }> }) {
32
+ const { filename } = await params
33
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '')
34
+
35
+ if (safeName.includes('..') || safeName.includes('/')) {
36
+ return NextResponse.json({ error: 'Invalid filename' }, { status: 400 })
37
+ }
38
+
39
+ const filePath = path.join(UPLOAD_DIR, safeName)
40
+
41
+ if (!fs.existsSync(filePath)) {
42
+ return notFound()
43
+ }
44
+
45
+ fs.unlinkSync(filePath)
46
+ return NextResponse.json({ ok: true })
47
+ }
@@ -0,0 +1,94 @@
1
+ import { NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { UPLOAD_DIR } from '@/lib/server/storage'
5
+ import { getFileCategory } from '@/lib/server/mime'
6
+
7
+ interface UploadFile {
8
+ name: string
9
+ size: number
10
+ modified: number
11
+ category: string
12
+ url: string
13
+ }
14
+
15
+ function listUploadFiles(): UploadFile[] {
16
+ if (!fs.existsSync(UPLOAD_DIR)) return []
17
+ const entries = fs.readdirSync(UPLOAD_DIR)
18
+ const files: UploadFile[] = []
19
+ for (const name of entries) {
20
+ const filePath = path.join(UPLOAD_DIR, name)
21
+ try {
22
+ const stat = fs.statSync(filePath)
23
+ if (!stat.isFile()) continue
24
+ const ext = path.extname(name).toLowerCase()
25
+ files.push({
26
+ name,
27
+ size: stat.size,
28
+ modified: stat.mtimeMs,
29
+ category: getFileCategory(ext),
30
+ url: `/api/uploads/${encodeURIComponent(name)}`,
31
+ })
32
+ } catch {
33
+ // skip files we can't stat
34
+ }
35
+ }
36
+ return files
37
+ }
38
+
39
+ export async function GET() {
40
+ const files = listUploadFiles()
41
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0)
42
+ return NextResponse.json({ files, totalSize, count: files.length })
43
+ }
44
+
45
+ interface DeleteBody {
46
+ filenames?: string[]
47
+ olderThanDays?: number
48
+ category?: string
49
+ all?: boolean
50
+ }
51
+
52
+ function isUnsafeName(name: string): boolean {
53
+ return name.includes('/') || name.includes('\\') || name.includes('..')
54
+ }
55
+
56
+ export async function DELETE(req: Request) {
57
+ const body = (await req.json()) as DeleteBody
58
+ const files = listUploadFiles()
59
+ let toDelete: string[] = []
60
+
61
+ if (body.all) {
62
+ toDelete = files.map((f) => f.name)
63
+ } else if (body.filenames && Array.isArray(body.filenames)) {
64
+ for (const name of body.filenames) {
65
+ if (typeof name !== 'string' || isUnsafeName(name)) {
66
+ return NextResponse.json({ error: `Invalid filename: ${name}` }, { status: 400 })
67
+ }
68
+ }
69
+ toDelete = body.filenames
70
+ } else if (typeof body.olderThanDays === 'number') {
71
+ const cutoff = Date.now() - body.olderThanDays * 86_400_000
72
+ toDelete = files.filter((f) => f.modified < cutoff).map((f) => f.name)
73
+ } else if (typeof body.category === 'string') {
74
+ toDelete = files.filter((f) => f.category === body.category).map((f) => f.name)
75
+ } else {
76
+ return NextResponse.json({ error: 'Provide filenames, olderThanDays, category, or all' }, { status: 400 })
77
+ }
78
+
79
+ let deleted = 0
80
+ let freedBytes = 0
81
+ for (const name of toDelete) {
82
+ const filePath = path.join(UPLOAD_DIR, name)
83
+ try {
84
+ const stat = fs.statSync(filePath)
85
+ fs.unlinkSync(filePath)
86
+ freedBytes += stat.size
87
+ deleted++
88
+ } catch {
89
+ // file already gone or inaccessible
90
+ }
91
+ }
92
+
93
+ return NextResponse.json({ deleted, freedBytes })
94
+ }
@@ -1,8 +1,11 @@
1
1
  import { genId } from '@/lib/id'
2
+ import { timingSafeEqual } from 'node:crypto'
2
3
  import { NextResponse } from 'next/server'
3
4
  import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog, upsertWebhookRetry } from '@/lib/server/storage'
4
5
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
6
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
7
+ import { enqueueSystemEvent } from '@/lib/server/system-events'
8
+ import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
6
9
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
7
10
  import type { WebhookRetryEntry } from '@/types'
8
11
 
@@ -71,7 +74,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
71
74
  if (secret) {
72
75
  const url = new URL(req.url)
73
76
  const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
74
- if (provided !== secret) {
77
+ const secretBuf = Buffer.from(secret)
78
+ const providedBuf = Buffer.from(provided)
79
+ // timingSafeEqual requires equal lengths; compare against secretBuf if lengths differ
80
+ const compareBuf = providedBuf.length === secretBuf.length ? providedBuf : secretBuf
81
+ const isInvalid = providedBuf.length !== secretBuf.length || !timingSafeEqual(secretBuf, compareBuf)
82
+ if (isInvalid) {
75
83
  appendWebhookLog(genId(8), {
76
84
  id: genId(8), webhookId: id, event: 'unknown',
77
85
  payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
@@ -193,6 +201,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
193
201
  mode: 'followup',
194
202
  })
195
203
 
204
+ // Enqueue system event + heartbeat wake
205
+ enqueueSystemEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
206
+ if (webhook.agentId) {
207
+ requestHeartbeatNow({ agentId: webhook.agentId, reason: 'webhook' })
208
+ }
209
+
196
210
  appendWebhookLog(genId(8), {
197
211
  id: genId(8), webhookId: id, event: incomingEvent,
198
212
  payload: (rawBody || '').slice(0, 2000), status: 'success',
@@ -49,16 +49,17 @@
49
49
  --radius-4xl: calc(var(--radius) + 16px);
50
50
 
51
51
  /* ===== Midnight Glass Palette ===== */
52
- --color-bg: #08080d;
53
- --color-raised: #0d0d14;
54
- --color-surface: #13131e;
55
- --color-surface-2: #1e1e30;
56
- --color-surface-3: #23233a;
52
+ /* Surfaces derived from --neutral-tint for single-knob theming */
53
+ --color-bg: color-mix(in srgb, var(--neutral-tint, #1e1e30) 40%, #000);
54
+ --color-raised: color-mix(in srgb, var(--neutral-tint, #1e1e30) 50%, #000);
55
+ --color-surface: color-mix(in srgb, var(--neutral-tint, #1e1e30) 65%, #000);
56
+ --color-surface-2: var(--neutral-tint, #1e1e30);
57
+ --color-surface-3: color-mix(in srgb, var(--neutral-tint, #1e1e30) 80%, #333);
57
58
  --color-border-hi: rgba(255,255,255,0.07);
58
59
  --color-border-focus: rgba(99,102,241,0.5);
59
60
  --color-text: #e2e2ec;
60
- --color-text-2: #8e8ea8;
61
- --color-text-3: #5c5c78;
61
+ --color-text-2: #a0a0ba;
62
+ --color-text-3: #8282a0;
62
63
  --color-accent-soft: rgba(99,102,241,0.08);
63
64
  --color-accent-glow: rgba(99,102,241,0.18);
64
65
  --color-accent-bright: #818CF8;
@@ -83,25 +84,29 @@
83
84
  --font-sora: 'Segoe UI';
84
85
  --font-jetbrains-mono: 'SF Mono';
85
86
  --radius: 0.625rem;
86
- --background: #08080d;
87
+
88
+ /* ===== Single-Tint Theming ===== */
89
+ /* Change this one value to shift the entire palette hue */
90
+ --neutral-tint: #1e1e30;
91
+ --background: color-mix(in srgb, var(--neutral-tint) 40%, #000);
87
92
  --foreground: #e2e2ec;
88
- --card: #0d0d14;
93
+ --card: color-mix(in srgb, var(--neutral-tint) 50%, #000);
89
94
  --card-foreground: #e2e2ec;
90
- --popover: #0d0d14;
95
+ --popover: color-mix(in srgb, var(--neutral-tint) 50%, #000);
91
96
  --popover-foreground: #e2e2ec;
92
97
  --primary: #6366F1;
93
98
  --primary-foreground: #ffffff;
94
- --secondary: #13131e;
99
+ --secondary: color-mix(in srgb, var(--neutral-tint) 65%, #000);
95
100
  --secondary-foreground: #e2e2ec;
96
- --muted: #13131e;
97
- --muted-foreground: #8e8ea8;
101
+ --muted: color-mix(in srgb, var(--neutral-tint) 65%, #000);
102
+ --muted-foreground: #a0a0ba;
98
103
  --accent: #6366F1;
99
104
  --accent-foreground: #ffffff;
100
105
  --destructive: #F43F5E;
101
106
  --border: rgba(255,255,255,0.04);
102
107
  --input: rgba(255,255,255,0.04);
103
108
  --ring: rgba(99,102,241,0.4);
104
- --sidebar: #0d0d14;
109
+ --sidebar: color-mix(in srgb, var(--neutral-tint) 50%, #000);
105
110
  --sidebar-foreground: #e2e2ec;
106
111
  --sidebar-primary: #6366F1;
107
112
  --sidebar-primary-foreground: #ffffff;
@@ -113,7 +118,7 @@
113
118
  /* ===== Status Badge System ===== */
114
119
  --status-idle-bg: rgba(255,255,255,0.04);
115
120
  --status-idle-border: rgba(255,255,255,0.06);
116
- --status-idle-fg: #8e8ea8;
121
+ --status-idle-fg: #a0a0ba;
117
122
  --status-running-bg: rgba(52,211,153,0.08);
118
123
  --status-running-border: rgba(52,211,153,0.15);
119
124
  --status-running-fg: #34D399;
@@ -205,6 +210,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
205
210
  from { opacity: 0; transform: scale(0.97) translateY(6px); }
206
211
  to { opacity: 1; transform: scale(1) translateY(0); }
207
212
  }
213
+ @keyframes msg-in {
214
+ from { opacity: 0; transform: translateY(6px); }
215
+ to { opacity: 1; transform: translateY(0); }
216
+ }
208
217
  @keyframes spin { to { transform: rotate(360deg); } }
209
218
  @keyframes slide-in-left {
210
219
  from { transform: translateX(-100%); }
@@ -243,6 +252,35 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
243
252
  to { opacity: 1; transform: translateY(0); }
244
253
  }
245
254
 
255
+ /* Heartbeat float animation */
256
+ @keyframes heartbeat-float {
257
+ 0% { opacity: 1; transform: translateY(0) scale(1); }
258
+ 50% { opacity: 0.7; transform: translateY(-14px) scale(1.15); }
259
+ 100% { opacity: 0; transform: translateY(-26px) scale(0.85); }
260
+ }
261
+
262
+ /* Activity moment animations */
263
+ @keyframes activity-moment-in {
264
+ from { opacity: 0; transform: translateY(6px) scale(0.92); }
265
+ to { opacity: 1; transform: translateY(0) scale(1); }
266
+ }
267
+ @keyframes activity-moment-out {
268
+ from { opacity: 1; transform: translateY(0) scale(1); }
269
+ to { opacity: 0; transform: translateY(-8px) scale(0.95); }
270
+ }
271
+
272
+ @keyframes delegation-handoff-in {
273
+ 0% { opacity: 0; transform: translateX(16px) scale(0.95); }
274
+ 100% { opacity: 1; transform: translateX(0) scale(1); }
275
+ }
276
+
277
+ @keyframes avatar-moment-pulse {
278
+ 0% { transform: scale(1); }
279
+ 30% { transform: scale(1.15); }
280
+ 60% { transform: scale(0.95); }
281
+ 100% { transform: scale(1); }
282
+ }
283
+
246
284
  /* AI avatar mood animations */
247
285
  @keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
248
286
  @keyframes ai-glow { 0%,100% { box-shadow: 0 0 0 0 rgba(99,102,241,0); } 50% { box-shadow: 0 0 12px 4px rgba(99,102,241,0.35); } }
@@ -414,6 +452,16 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
414
452
  font-family: var(--font-sora), 'Sora', system-ui, sans-serif;
415
453
  }
416
454
 
455
+ /* Monospace micro-label utility */
456
+ .label-mono {
457
+ font-family: var(--font-mono);
458
+ font-size: 10px;
459
+ font-weight: 600;
460
+ letter-spacing: 0.06em;
461
+ text-transform: uppercase;
462
+ color: var(--color-text-3);
463
+ }
464
+
417
465
  /* ===== Status Badge Classes ===== */
418
466
  .status-badge-idle, .status-badge-running, .status-badge-error,
419
467
  .status-badge-connecting, .status-badge-connected, .status-badge-disconnected,