@swarmclawai/swarmclaw 0.3.1 → 0.4.5

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 (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -1,40 +1,42 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSkills, saveSkills, deleteSkill } from '@/lib/server/storage'
3
3
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
4
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const ops: CollectionOps<any> = { load: loadSkills, save: saveSkills, deleteFn: deleteSkill }
4
8
 
5
9
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
10
  const { id } = await params
7
11
  const skills = loadSkills()
8
- if (!skills[id]) return new NextResponse(null, { status: 404 })
12
+ if (!skills[id]) return notFound()
9
13
  return NextResponse.json(skills[id])
10
14
  }
11
15
 
12
16
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
13
17
  const { id } = await params
14
18
  const body = await req.json()
15
- const skills = loadSkills()
16
- if (!skills[id]) return new NextResponse(null, { status: 404 })
17
- const normalized = normalizeSkillPayload({ ...skills[id], ...body })
18
- skills[id] = {
19
- ...skills[id],
20
- ...body,
21
- name: normalized.name,
22
- filename: normalized.filename,
23
- description: normalized.description,
24
- content: normalized.content,
25
- sourceUrl: normalized.sourceUrl,
26
- sourceFormat: normalized.sourceFormat,
27
- id,
28
- updatedAt: Date.now(),
29
- }
30
- saveSkills(skills)
31
- return NextResponse.json(skills[id])
19
+ const result = mutateItem(ops, id, (skill) => {
20
+ const normalized = normalizeSkillPayload({ ...skill, ...body })
21
+ return {
22
+ ...skill,
23
+ ...body,
24
+ name: normalized.name,
25
+ filename: normalized.filename,
26
+ description: normalized.description,
27
+ content: normalized.content,
28
+ sourceUrl: normalized.sourceUrl,
29
+ sourceFormat: normalized.sourceFormat,
30
+ id,
31
+ updatedAt: Date.now(),
32
+ }
33
+ })
34
+ if (!result) return notFound()
35
+ return NextResponse.json(result)
32
36
  }
33
37
 
34
38
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
35
39
  const { id } = await params
36
- const skills = loadSkills()
37
- if (!skills[id]) return new NextResponse(null, { status: 404 })
38
- deleteSkill(id)
40
+ if (!deleteItem(ops, id)) return notFound()
39
41
  return NextResponse.json({ deleted: id })
40
42
  }
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
@@ -47,7 +47,7 @@ export async function POST(req: Request) {
47
47
  })
48
48
 
49
49
  const skills = loadSkills()
50
- const id = crypto.randomBytes(4).toString('hex')
50
+ const id = genId()
51
51
  skills[id] = {
52
52
  id,
53
53
  name: normalized.name,
@@ -1,16 +1,18 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
+ export const dynamic = 'force-dynamic'
5
6
 
6
- export async function GET() {
7
+
8
+ export async function GET(_req: Request) {
7
9
  return NextResponse.json(loadSkills())
8
10
  }
9
11
 
10
12
  export async function POST(req: Request) {
11
13
  const body = await req.json()
12
14
  const skills = loadSkills()
13
- const id = crypto.randomBytes(4).toString('hex')
15
+ const id = genId()
14
16
  const normalized = normalizeSkillPayload(body)
15
17
  skills[id] = {
16
18
  id,
@@ -0,0 +1,74 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadTasks, saveTasks, loadAgents } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
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 approved = body.approved === true
11
+
12
+ const tasks = loadTasks()
13
+ const task = tasks[id]
14
+ if (!task) return notFound()
15
+ if (!task.pendingApproval) {
16
+ return NextResponse.json({ error: 'No pending approval on this task' }, { status: 400 })
17
+ }
18
+
19
+ const { threadId } = task.pendingApproval
20
+
21
+ if (!approved) {
22
+ // Reject: clear approval, delete checkpoint, fail the task
23
+ task.pendingApproval = null
24
+ task.status = 'failed'
25
+ task.error = 'Tool execution rejected by user'
26
+ task.updatedAt = Date.now()
27
+ saveTasks(tasks)
28
+ await getCheckpointSaver().deleteThread(threadId)
29
+ notify('tasks')
30
+ return NextResponse.json({ status: 'rejected' })
31
+ }
32
+
33
+ // Approve: clear pendingApproval, resume the graph
34
+ const agents = loadAgents()
35
+ const agent = agents[task.agentId]
36
+ if (!agent) {
37
+ return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
38
+ }
39
+
40
+ task.pendingApproval = null
41
+ task.updatedAt = Date.now()
42
+ saveTasks(tasks)
43
+ notify('tasks')
44
+
45
+ // Resume in the background
46
+ const sessionId = task.sessionId || ''
47
+ setImmediate(async () => {
48
+ try {
49
+ const { resumeLangGraphOrchestrator } = await import('@/lib/server/orchestrator-lg')
50
+ const result = await resumeLangGraphOrchestrator(agent, sessionId, threadId)
51
+ const t2 = loadTasks()
52
+ if (t2[id] && !t2[id].pendingApproval) {
53
+ // Only mark completed if not paused again
54
+ if (t2[id].status === 'running') {
55
+ t2[id].result = result
56
+ }
57
+ t2[id].updatedAt = Date.now()
58
+ saveTasks(t2)
59
+ notify('tasks')
60
+ }
61
+ } catch (err: any) {
62
+ console.error(`[approve] Resume failed for task ${id}:`, err.message)
63
+ const t2 = loadTasks()
64
+ if (t2[id]) {
65
+ t2[id].error = err.message || String(err)
66
+ t2[id].updatedAt = Date.now()
67
+ saveTasks(t2)
68
+ notify('tasks')
69
+ }
70
+ }
71
+ })
72
+
73
+ return NextResponse.json({ status: 'approved', resuming: true })
74
+ }
@@ -1,10 +1,12 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadTasks, saveTasks } from '@/lib/server/storage'
4
+ import { notFound } from '@/lib/server/collection-helpers'
4
5
  import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
6
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
7
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
7
8
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
9
+ import { notify } from '@/lib/server/ws-hub'
8
10
 
9
11
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
12
  // Keep completed queue integrity even if daemon is not running.
@@ -12,7 +14,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
12
14
 
13
15
  const { id } = await params
14
16
  const tasks = loadTasks()
15
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
17
+ if (!tasks[id]) return notFound()
16
18
  return NextResponse.json(tasks[id])
17
19
  }
18
20
 
@@ -20,7 +22,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
20
22
  const { id } = await params
21
23
  const body = await req.json()
22
24
  const tasks = loadTasks()
23
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
25
+ if (!tasks[id]) return notFound()
24
26
 
25
27
  const prevStatus = tasks[id].status
26
28
 
@@ -54,7 +56,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
54
56
  tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
55
57
  if (!tasks[id].comments) tasks[id].comments = []
56
58
  tasks[id].comments.push({
57
- id: crypto.randomBytes(4).toString('hex'),
59
+ id: genId(),
58
60
  author: 'System',
59
61
  text: `Completion validation failed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
60
62
  createdAt: Date.now(),
@@ -80,13 +82,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
80
82
  enqueueTask(id)
81
83
  }
82
84
 
85
+ notify('tasks')
83
86
  return NextResponse.json(tasks[id])
84
87
  }
85
88
 
86
89
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
87
90
  const { id } = await params
88
91
  const tasks = loadTasks()
89
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
92
+ if (!tasks[id]) return notFound()
90
93
 
91
94
  // Soft delete: move to archived status instead of hard delete
92
95
  tasks[id].status = 'archived'
@@ -98,5 +101,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
98
101
  text: `Task archived: "${tasks[id].title}" (${id}).`,
99
102
  })
100
103
 
104
+ notify('tasks')
101
105
  return NextResponse.json(tasks[id])
102
106
  }
@@ -1,10 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadTasks, saveTasks, loadSettings } 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
+ import { notify } from '@/lib/server/ws-hub'
8
9
 
9
10
  export async function GET(req: Request) {
10
11
  // Keep completed queue integrity even if daemon is not running.
@@ -47,12 +48,13 @@ export async function DELETE(req: Request) {
47
48
  removed++
48
49
  }
49
50
  }
51
+ notify('tasks')
50
52
  return NextResponse.json({ removed, remaining: Object.keys(tasks).length - removed })
51
53
  }
52
54
 
53
55
  export async function POST(req: Request) {
54
56
  const body = await req.json()
55
- const id = crypto.randomBytes(4).toString('hex')
57
+ const id = genId()
56
58
  const now = Date.now()
57
59
  const tasks = loadTasks()
58
60
  const settings = loadSettings()
@@ -111,5 +113,6 @@ export async function POST(req: Request) {
111
113
  if (tasks[id].status === 'queued') {
112
114
  enqueueTask(id)
113
115
  }
116
+ notify('tasks')
114
117
  return NextResponse.json(tasks[id])
115
118
  }
@@ -0,0 +1,48 @@
1
+ import { loadSettings } from '@/lib/server/storage'
2
+
3
+ export async function POST(req: Request) {
4
+ const settings = loadSettings()
5
+ const ELEVENLABS_KEY = settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY
6
+ const ELEVENLABS_VOICE = settings.elevenLabsVoiceId || process.env.ELEVENLABS_VOICE || 'JBFqnCBsd6RMkjVDRZzb'
7
+
8
+ if (!ELEVENLABS_KEY) {
9
+ return new Response('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
10
+ }
11
+
12
+ const { text } = await req.json()
13
+ if (!text?.trim()) {
14
+ return new Response('No text provided', { status: 400 })
15
+ }
16
+
17
+ const apiRes = await fetch(
18
+ `https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}/stream`,
19
+ {
20
+ method: 'POST',
21
+ headers: {
22
+ 'xi-api-key': ELEVENLABS_KEY,
23
+ 'Content-Type': 'application/json',
24
+ 'Accept': 'audio/mpeg',
25
+ },
26
+ body: JSON.stringify({
27
+ text: text.slice(0, 2000),
28
+ model_id: 'eleven_multilingual_v2',
29
+ voice_settings: { stability: 0.5, similarity_boost: 0.75 },
30
+ output_format: 'mp3_22050_32',
31
+ }),
32
+ },
33
+ )
34
+
35
+ if (!apiRes.ok) {
36
+ const err = await apiRes.text()
37
+ return new Response(err, { status: apiRes.status })
38
+ }
39
+
40
+ // Pipe the streaming response directly
41
+ return new Response(apiRes.body, {
42
+ headers: {
43
+ 'Content-Type': 'audio/mpeg',
44
+ 'Transfer-Encoding': 'chunked',
45
+ 'Cache-Control': 'no-cache',
46
+ },
47
+ })
48
+ }
@@ -1,13 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
- import crypto from 'crypto'
4
+ import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
6
 
7
7
  export async function POST(req: Request) {
8
8
  const filename = req.headers.get('x-filename') || 'image.png'
9
9
  const buf = Buffer.from(await req.arrayBuffer())
10
- const name = crypto.randomBytes(4).toString('hex') + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
10
+ const name = genId() + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
11
11
  const filePath = path.join(UPLOAD_DIR, name)
12
12
 
13
13
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
2
3
  import fs from 'fs'
3
4
  import path from 'path'
4
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
@@ -43,16 +44,18 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
43
44
  const filePath = path.join(UPLOAD_DIR, safeName)
44
45
 
45
46
  if (!fs.existsSync(filePath)) {
46
- return new NextResponse(null, { status: 404 })
47
+ return notFound()
47
48
  }
48
49
 
49
50
  const ext = path.extname(safeName).toLowerCase()
50
51
  const contentType = MIME_TYPES[ext] || 'application/octet-stream'
51
52
  const data = fs.readFileSync(filePath)
52
53
 
54
+ const inline = contentType.startsWith('image/') || contentType.startsWith('video/') || contentType.startsWith('text/') || contentType === 'application/pdf'
53
55
  return new NextResponse(data, {
54
56
  headers: {
55
57
  'Content-Type': contentType,
58
+ 'Content-Disposition': inline ? 'inline' : `attachment; filename="${path.basename(safeName)}"`,
56
59
  'Cache-Control': 'public, max-age=86400',
57
60
  },
58
61
  })
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadUsage } from '@/lib/server/storage'
3
+ export const dynamic = 'force-dynamic'
3
4
 
4
- export async function GET() {
5
+
6
+ export async function GET(_req: Request) {
5
7
  const usage = loadUsage()
6
8
  // Compute summary
7
9
  let totalTokens = 0
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { execSync } from 'child_process'
3
+ export const dynamic = 'force-dynamic'
4
+
3
5
 
4
6
  let cachedRemote: {
5
7
  sha: string
@@ -31,7 +33,7 @@ function getHeadStableTag(): string | null {
31
33
  return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
32
34
  }
33
35
 
34
- export async function GET() {
36
+ export async function GET(_req: Request) {
35
37
  try {
36
38
  const localSha = run('git rev-parse --short HEAD')
37
39
  const localTag = getHeadStableTag()
@@ -1,7 +1,12 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog } from '@/lib/server/storage'
4
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
4
5
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
6
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ const ops: CollectionOps<any> = { load: loadWebhooks, save: saveWebhooks }
5
10
 
6
11
  function normalizeEvents(value: unknown): string[] {
7
12
  if (!Array.isArray(value)) return []
@@ -21,36 +26,30 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
21
26
  const { id } = await params
22
27
  const webhooks = loadWebhooks()
23
28
  const webhook = webhooks[id]
24
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
29
+ if (!webhook) return notFound('Webhook not found')
25
30
  return NextResponse.json(webhook)
26
31
  }
27
32
 
28
33
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
29
34
  const { id } = await params
30
35
  const body = await req.json().catch(() => ({}))
31
- const webhooks = loadWebhooks()
32
- const webhook = webhooks[id]
33
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
34
-
35
- if (body.name !== undefined) webhook.name = body.name
36
- if (body.source !== undefined) webhook.source = body.source
37
- if (body.events !== undefined) webhook.events = normalizeEvents(body.events)
38
- if (body.agentId !== undefined) webhook.agentId = body.agentId
39
- if (body.secret !== undefined) webhook.secret = body.secret
40
- if (body.isEnabled !== undefined) webhook.isEnabled = !!body.isEnabled
41
- webhook.updatedAt = Date.now()
42
-
43
- webhooks[id] = webhook
44
- saveWebhooks(webhooks)
45
- return NextResponse.json(webhook)
36
+ const result = mutateItem(ops, id, (webhook) => {
37
+ if (body.name !== undefined) webhook.name = body.name
38
+ if (body.source !== undefined) webhook.source = body.source
39
+ if (body.events !== undefined) webhook.events = normalizeEvents(body.events)
40
+ if (body.agentId !== undefined) webhook.agentId = body.agentId
41
+ if (body.secret !== undefined) webhook.secret = body.secret
42
+ if (body.isEnabled !== undefined) webhook.isEnabled = !!body.isEnabled
43
+ webhook.updatedAt = Date.now()
44
+ return webhook
45
+ })
46
+ if (!result) return notFound('Webhook not found')
47
+ return NextResponse.json(result)
46
48
  }
47
49
 
48
50
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
49
51
  const { id } = await params
50
- const webhooks = loadWebhooks()
51
- if (!webhooks[id]) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
52
- delete webhooks[id]
53
- saveWebhooks(webhooks)
52
+ if (!deleteItem(ops, id)) return notFound('Webhook not found')
54
53
  return NextResponse.json({ ok: true })
55
54
  }
56
55
 
@@ -58,10 +57,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
58
57
  const { id } = await params
59
58
  const webhooks = loadWebhooks()
60
59
  const webhook = webhooks[id]
61
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
60
+ if (!webhook) return notFound('Webhook not found')
62
61
  if (webhook.isEnabled === false) {
63
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
64
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: 'unknown',
62
+ appendWebhookLog(genId(8), {
63
+ id: genId(8), webhookId: id, event: 'unknown',
65
64
  payload: '', status: 'error', error: 'Webhook is disabled', timestamp: Date.now(),
66
65
  })
67
66
  return NextResponse.json({ error: 'Webhook is disabled' }, { status: 409 })
@@ -72,8 +71,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
72
71
  const url = new URL(req.url)
73
72
  const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
74
73
  if (provided !== secret) {
75
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
76
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: 'unknown',
74
+ appendWebhookLog(genId(8), {
75
+ id: genId(8), webhookId: id, event: 'unknown',
77
76
  payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
78
77
  })
79
78
  return NextResponse.json({ error: 'Invalid webhook secret' }, { status: 401 })
@@ -121,8 +120,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
121
120
  const agents = loadAgents()
122
121
  const agent = webhook.agentId ? agents[webhook.agentId] : null
123
122
  if (!agent) {
124
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
125
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: incomingEvent,
123
+ appendWebhookLog(genId(8), {
124
+ id: genId(8), webhookId: id, event: incomingEvent,
126
125
  payload: (rawBody || '').slice(0, 2000), status: 'error', error: 'Webhook agent is not configured or missing', timestamp: Date.now(),
127
126
  })
128
127
  return NextResponse.json({ error: 'Webhook agent is not configured or missing' }, { status: 400 })
@@ -132,12 +131,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
132
131
  const sessionName = `webhook:${id}`
133
132
  let session = Object.values(sessions).find((s: any) => s.name === sessionName && s.agentId === agent.id) as any
134
133
  if (!session) {
135
- const sessionId = crypto.randomBytes(4).toString('hex')
134
+ const sessionId = genId()
136
135
  const now = Date.now()
137
136
  session = {
138
137
  id: sessionId,
139
138
  name: sessionName,
140
- cwd: process.cwd(),
139
+ cwd: WORKSPACE_DIR,
141
140
  user: 'system',
142
141
  provider: agent.provider || 'claude-cli',
143
142
  model: agent.model || '',
@@ -188,8 +187,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
188
187
  mode: 'followup',
189
188
  })
190
189
 
191
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
192
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: incomingEvent,
190
+ appendWebhookLog(genId(8), {
191
+ id: genId(8), webhookId: id, event: incomingEvent,
193
192
  payload: (rawBody || '').slice(0, 2000), status: 'success',
194
193
  sessionId: session.id, runId: run.runId, timestamp: Date.now(),
195
194
  })
@@ -1,6 +1,8 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadWebhooks, saveWebhooks } from '@/lib/server/storage'
4
+ export const dynamic = 'force-dynamic'
5
+
4
6
 
5
7
  function normalizeEvents(value: unknown): string[] {
6
8
  if (!Array.isArray(value)) return []
@@ -10,14 +12,14 @@ function normalizeEvents(value: unknown): string[] {
10
12
  .filter(Boolean)
11
13
  }
12
14
 
13
- export async function GET() {
15
+ export async function GET(_req: Request) {
14
16
  return NextResponse.json(loadWebhooks())
15
17
  }
16
18
 
17
19
  export async function POST(req: Request) {
18
20
  const body = await req.json().catch(() => ({}))
19
21
  const webhooks = loadWebhooks()
20
- const id = crypto.randomBytes(4).toString('hex')
22
+ const id = genId()
21
23
  const now = Date.now()
22
24
 
23
25
  webhooks[id] = {
@@ -0,0 +1,58 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
2
+ <title id="title">SwarmClaw Lobster Avatar</title>
3
+ <desc id="desc">SwarmClaw org avatar using an OpenClaw-inspired lobster mark with swarm accents.</desc>
4
+
5
+ <defs>
6
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
7
+ <stop offset="0%" stop-color="#050B18"/>
8
+ <stop offset="100%" stop-color="#111827"/>
9
+ </linearGradient>
10
+ <radialGradient id="glow" cx="50%" cy="38%" r="62%">
11
+ <stop offset="0%" stop-color="#22d3ee" stop-opacity="0.22"/>
12
+ <stop offset="100%" stop-color="#22d3ee" stop-opacity="0"/>
13
+ </radialGradient>
14
+ <linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
15
+ <stop offset="0%" stop-color="#ff6a5f"/>
16
+ <stop offset="100%" stop-color="#a41318"/>
17
+ </linearGradient>
18
+ <filter id="soft-shadow" x="-20%" y="-20%" width="140%" height="140%">
19
+ <feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#020617" flood-opacity="0.55"/>
20
+ </filter>
21
+ </defs>
22
+
23
+ <rect x="40" y="40" width="944" height="944" rx="216" fill="url(#bg)"/>
24
+ <rect x="40" y="40" width="944" height="944" rx="216" fill="url(#glow)"/>
25
+ <rect x="56" y="56" width="912" height="912" rx="200" fill="none" stroke="#334155" stroke-width="6"/>
26
+
27
+ <!-- swarm accents -->
28
+ <g stroke="#22d3ee" stroke-opacity="0.8" stroke-width="10" fill="none" stroke-linecap="round">
29
+ <path d="M182 286 C232 236, 314 224, 378 252"/>
30
+ <path d="M842 286 C792 236, 710 224, 646 252"/>
31
+ <path d="M202 760 C270 814, 350 826, 420 806"/>
32
+ <path d="M822 760 C754 814, 674 826, 604 806"/>
33
+ </g>
34
+ <g fill="#67e8f9">
35
+ <circle cx="172" cy="282" r="14"/>
36
+ <circle cx="852" cy="282" r="14"/>
37
+ <circle cx="198" cy="760" r="12"/>
38
+ <circle cx="826" cy="760" r="12"/>
39
+ </g>
40
+
41
+ <!-- OpenClaw-inspired lobster mark -->
42
+ <g transform="translate(152 156) scale(6)" filter="url(#soft-shadow)">
43
+ <!-- Body -->
44
+ <path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
45
+ <!-- Left Claw -->
46
+ <path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
47
+ <!-- Right Claw -->
48
+ <path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
49
+ <!-- Antenna -->
50
+ <path d="M45 15 Q35 5 30 8" stroke="#ff8b84" stroke-width="3" stroke-linecap="round"/>
51
+ <path d="M75 15 Q85 5 90 8" stroke="#ff8b84" stroke-width="3" stroke-linecap="round"/>
52
+ <!-- Eyes -->
53
+ <circle cx="45" cy="35" r="6" fill="#030712"/>
54
+ <circle cx="75" cy="35" r="6" fill="#030712"/>
55
+ <circle cx="46" cy="34" r="2.5" fill="#22d3ee"/>
56
+ <circle cx="76" cy="34" r="2.5" fill="#22d3ee"/>
57
+ </g>
58
+ </svg>