@swarmclawai/swarmclaw 0.4.0 → 0.5.0

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 (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -1,31 +1,29 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSchedules, saveSchedules, deleteSchedule } from '@/lib/server/storage'
3
3
  import { resolveScheduleName } from '@/lib/schedule-name'
4
- import { notify } from '@/lib/server/ws-hub'
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: loadSchedules, save: saveSchedules, deleteFn: deleteSchedule, topic: 'schedules' }
5
8
 
6
9
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
10
  const { id } = await params
8
11
  const body = await req.json()
9
- const schedules = loadSchedules()
10
- if (!schedules[id]) return new NextResponse(null, { status: 404 })
11
-
12
- const origId = id
13
- Object.assign(schedules[id], body)
14
- schedules[id].id = origId
15
- schedules[id].name = resolveScheduleName({
16
- name: schedules[id].name,
17
- taskPrompt: schedules[id].taskPrompt,
12
+ const result = mutateItem(ops, id, (schedule) => {
13
+ Object.assign(schedule, body)
14
+ schedule.id = id
15
+ schedule.name = resolveScheduleName({
16
+ name: schedule.name,
17
+ taskPrompt: schedule.taskPrompt,
18
+ })
19
+ return schedule
18
20
  })
19
- saveSchedules(schedules)
20
- notify('schedules')
21
- return NextResponse.json(schedules[id])
21
+ if (!result) return notFound()
22
+ return NextResponse.json(result)
22
23
  }
23
24
 
24
25
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
25
26
  const { id } = await params
26
- const schedules = loadSchedules()
27
- if (!schedules[id]) return new NextResponse(null, { status: 404 })
28
- deleteSchedule(id)
29
- notify('schedules')
30
- return NextResponse.json('ok')
27
+ if (!deleteItem(ops, id)) return notFound()
28
+ return NextResponse.json({ ok: true })
31
29
  }
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
4
5
  import { enqueueTask } from '@/lib/server/queue'
5
6
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
@@ -14,7 +15,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
14
15
  const { id } = await params
15
16
  const schedules = loadSchedules()
16
17
  const schedule = schedules[id]
17
- if (!schedule) return new NextResponse(null, { status: 404 })
18
+ if (!schedule) return notFound()
18
19
 
19
20
  const agents = loadAgents()
20
21
  const agent = agents[schedule.agentId]
@@ -64,7 +65,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
64
65
  existingTask.validation = null
65
66
  prev.runNumber = schedule.runNumber
66
67
  } else {
67
- taskId = crypto.randomBytes(4).toString('hex')
68
+ taskId = genId()
68
69
  tasks[taskId] = {
69
70
  id: taskId,
70
71
  title: `[Sched] ${schedule.name} (run #${schedule.runNumber})`,
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadSchedules, saveSchedules } from '@/lib/server/storage'
4
4
  import { resolveScheduleName } from '@/lib/schedule-name'
5
5
  import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
@@ -51,7 +51,7 @@ export async function POST(req: Request) {
51
51
  return NextResponse.json(duplicate)
52
52
  }
53
53
 
54
- const id = crypto.randomBytes(4).toString('hex')
54
+ const id = genId()
55
55
 
56
56
  let nextRunAt: number | undefined
57
57
  if (scheduleType === 'once' && body.runAt) {
@@ -1,29 +1,28 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSecrets, saveSecrets } from '@/lib/server/storage'
3
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ const ops: CollectionOps<any> = { load: loadSecrets, save: saveSecrets }
3
7
 
4
8
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
9
  const { id } = await params
6
- const secrets = loadSecrets()
7
- if (!secrets[id]) return new NextResponse(null, { status: 404 })
8
- delete secrets[id]
9
- saveSecrets(secrets)
10
- return NextResponse.json('ok')
10
+ if (!deleteItem(ops, id)) return notFound()
11
+ return NextResponse.json({ ok: true })
11
12
  }
12
13
 
13
14
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
14
15
  const { id } = await params
15
16
  const body = await req.json()
16
- const secrets = loadSecrets()
17
- if (!secrets[id]) return new NextResponse(null, { status: 404 })
18
-
19
- // Update metadata only (not the encrypted value unless a new value is provided)
20
- if (body.name !== undefined) secrets[id].name = body.name
21
- if (body.service !== undefined) secrets[id].service = body.service
22
- if (body.scope !== undefined) secrets[id].scope = body.scope
23
- if (body.agentIds !== undefined) secrets[id].agentIds = body.agentIds
24
- secrets[id].updatedAt = Date.now()
25
- saveSecrets(secrets)
26
-
27
- const { encryptedValue, ...safe } = secrets[id]
17
+ const result = mutateItem(ops, id, (secret) => {
18
+ if (body.name !== undefined) secret.name = body.name
19
+ if (body.service !== undefined) secret.service = body.service
20
+ if (body.scope !== undefined) secret.scope = body.scope
21
+ if (body.agentIds !== undefined) secret.agentIds = body.agentIds
22
+ secret.updatedAt = Date.now()
23
+ return secret
24
+ })
25
+ if (!result) return notFound()
26
+ const { encryptedValue, ...safe } = result as Record<string, unknown>
28
27
  return NextResponse.json(safe)
29
28
  }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadSecrets, saveSecrets, encryptKey } from '@/lib/server/storage'
4
4
  export const dynamic = 'force-dynamic'
5
5
 
@@ -18,7 +18,7 @@ export async function GET(_req: Request) {
18
18
 
19
19
  export async function POST(req: Request) {
20
20
  const body = await req.json()
21
- const id = crypto.randomBytes(4).toString('hex')
21
+ const id = genId()
22
22
  const now = Date.now()
23
23
  const secrets = loadSecrets()
24
24
 
@@ -1,10 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
 
4
5
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
6
  const { id } = await params
6
7
  const sessions = loadSessions()
7
- if (!sessions[id]) return new NextResponse(null, { status: 404 })
8
+ if (!sessions[id]) return notFound()
8
9
  sessions[id].messages = []
9
10
  sessions[id].claudeSessionId = null
10
11
  sessions[id].codexThreadId = null
@@ -1,12 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { execSync } from 'child_process'
3
3
  import { loadSessions } from '@/lib/server/storage'
4
+ import { notFound } from '@/lib/server/collection-helpers'
4
5
 
5
6
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
7
8
  const sessions = loadSessions()
8
9
  const session = sessions[id]
9
- if (!session) return new NextResponse(null, { status: 404 })
10
+ if (!session) return notFound()
10
11
 
11
12
  const body = await req.json()
12
13
  const msg = body.message || 'Deploy from SwarmClaw'
@@ -1,12 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { spawn } from 'child_process'
3
3
  import { loadSessions, devServers, localIP } from '@/lib/server/storage'
4
+ import { notFound } from '@/lib/server/collection-helpers'
4
5
 
5
6
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
7
8
  const sessions = loadSessions()
8
9
  const session = sessions[id]
9
- if (!session) return new NextResponse(null, { status: 404 })
10
+ if (!session) return notFound()
10
11
 
11
12
  const { action } = await req.json()
12
13
 
@@ -0,0 +1,22 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const body = await req.json() as { messageIndex: number; newText: string }
8
+ const sessions = loadSessions()
9
+ const session = sessions[id]
10
+ if (!session) return notFound()
11
+
12
+ const { messageIndex, newText } = body
13
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
14
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
15
+ }
16
+
17
+ // Truncate messages to messageIndex (discard that msg + everything after)
18
+ session.messages = session.messages.slice(0, messageIndex)
19
+ saveSessions(sessions)
20
+
21
+ return NextResponse.json({ message: newText })
22
+ }
@@ -0,0 +1,44 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+
6
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const body = await req.json() as { messageIndex: number }
9
+ const sessions = loadSessions()
10
+ const source = sessions[id]
11
+ if (!source) return notFound()
12
+
13
+ const { messageIndex } = body
14
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= source.messages.length) {
15
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
16
+ }
17
+
18
+ const now = Date.now()
19
+ const newId = randomUUID()
20
+ const forked = {
21
+ id: newId,
22
+ name: `Fork of ${source.name}`,
23
+ cwd: source.cwd,
24
+ user: source.user,
25
+ provider: source.provider,
26
+ model: source.model,
27
+ credentialId: source.credentialId ?? null,
28
+ fallbackCredentialIds: source.fallbackCredentialIds,
29
+ apiEndpoint: source.apiEndpoint ?? null,
30
+ claudeSessionId: null,
31
+ messages: source.messages.slice(0, messageIndex + 1),
32
+ createdAt: now,
33
+ lastActiveAt: now,
34
+ agentId: source.agentId ?? null,
35
+ parentSessionId: id,
36
+ tools: source.tools,
37
+ conversationTone: source.conversationTone,
38
+ }
39
+
40
+ sessions[newId] = forked
41
+ saveSessions(sessions)
42
+
43
+ return NextResponse.json(forked)
44
+ }
@@ -1,9 +1,27 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSessions } from '@/lib/server/storage'
2
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
 
4
5
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
6
  const { id } = await params
6
7
  const sessions = loadSessions()
7
- if (!sessions[id]) return new NextResponse(null, { status: 404 })
8
+ if (!sessions[id]) return notFound()
8
9
  return NextResponse.json(sessions[id].messages)
9
10
  }
11
+
12
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
13
+ const { id } = await params
14
+ const body = await req.json() as { messageIndex: number; bookmarked: boolean }
15
+ const sessions = loadSessions()
16
+ const session = sessions[id]
17
+ if (!session) return notFound()
18
+
19
+ const { messageIndex, bookmarked } = body
20
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
21
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
22
+ }
23
+
24
+ session.messages[messageIndex].bookmarked = bookmarked
25
+ saveSessions(sessions)
26
+ return NextResponse.json(session.messages[messageIndex])
27
+ }
@@ -1,11 +1,12 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
 
4
5
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
6
  const { id } = await params
6
7
  const sessions = loadSessions()
7
8
  const session = sessions[id]
8
- if (!session) return new NextResponse(null, { status: 404 })
9
+ if (!session) return notFound()
9
10
 
10
11
  const msgs = session.messages
11
12
  // Pop trailing assistant messages to find the last user message
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
4
5
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
6
+ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
5
7
 
6
8
  function buildSessionAwakeningPrompt(user: string | null | undefined): string {
7
9
  const displayName = typeof user === 'string' && user.trim() ? user.trim() : 'there'
@@ -20,7 +22,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
20
22
  const { id } = await params
21
23
  const updates = await req.json()
22
24
  const sessions = loadSessions()
23
- if (!sessions[id]) return new NextResponse(null, { status: 404 })
25
+ if (!sessions[id]) return notFound()
26
+ const wasProtectedMain = isProtectedMainSession(sessions[id])
24
27
  const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
25
28
 
26
29
  const agentIdUpdateProvided = updates.agentId !== undefined
@@ -32,7 +35,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
32
35
 
33
36
  const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
34
37
 
35
- if (updates.name !== undefined) sessions[id].name = updates.name
38
+ if (updates.name !== undefined) {
39
+ const nextName = typeof updates.name === 'string' ? updates.name.trim() : String(updates.name || '')
40
+ if (wasProtectedMain && nextName !== '__main__') {
41
+ return new NextResponse('Cannot rename main chat session', { status: 400 })
42
+ }
43
+ sessions[id].name = updates.name
44
+ }
36
45
  if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
37
46
  if (updates.provider !== undefined) sessions[id].provider = updates.provider
38
47
  else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
@@ -61,8 +70,9 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
61
70
  if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
62
71
  if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
63
72
  if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
73
+ ensureMainSessionFlag(sessions[id])
64
74
 
65
- const shouldKickoffAwakening = sessions[id].name === '__main__'
75
+ const shouldKickoffAwakening = isProtectedMainSession(sessions[id])
66
76
  && agentIdUpdateProvided
67
77
  && !!sessions[id].agentId
68
78
  && !hadMessagesBefore
@@ -91,7 +101,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
91
101
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
92
102
  const { id } = await params
93
103
  const sessions = loadSessions()
94
- if (sessions[id]?.name === '__main__') {
104
+ if (isProtectedMainSession(sessions[id])) {
95
105
  return new NextResponse('Cannot delete main chat session', { status: 403 })
96
106
  }
97
107
  if (active.has(id)) {
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
5
  import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
@@ -7,6 +7,7 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import { getSessionRunState } from '@/lib/server/session-run-manager'
9
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
10
11
  export const dynamic = 'force-dynamic'
11
12
 
12
13
 
@@ -27,16 +28,18 @@ export async function DELETE(req: Request) {
27
28
  return new NextResponse('Missing ids', { status: 400 })
28
29
  }
29
30
  const sessions = loadSessions()
31
+ let deleted = 0
30
32
  for (const id of ids) {
31
- if (sessions[id]?.name === '__main__') continue
33
+ if (isProtectedMainSession(sessions[id])) continue
32
34
  if (active.has(id)) {
33
35
  try { active.get(id).kill() } catch {}
34
36
  active.delete(id)
35
37
  }
36
38
  deleteSession(id)
39
+ deleted += 1
37
40
  }
38
41
  notify('sessions')
39
- return NextResponse.json({ deleted: ids.length })
42
+ return NextResponse.json({ deleted, requested: ids.length })
40
43
  }
41
44
 
42
45
  export async function POST(req: Request) {
@@ -46,7 +49,7 @@ export async function POST(req: Request) {
46
49
  else if (cwd === '~') cwd = os.homedir()
47
50
  else if (!cwd) cwd = WORKSPACE_DIR
48
51
 
49
- const id = body.id || crypto.randomBytes(4).toString('hex')
52
+ const id = body.id || genId()
50
53
  const sessions = loadSessions()
51
54
  const agent = body.agentId ? loadAgents()[body.agentId] : null
52
55
  const requestedTools = Array.isArray(body.tools) ? body.tools : null
@@ -86,6 +89,7 @@ export async function POST(req: Request) {
86
89
  heartbeatEnabled: body.heartbeatEnabled ?? null,
87
90
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
88
91
  }
92
+ ensureMainSessionFlag(sessions[id])
89
93
  saveSessions(sessions)
90
94
  notify('sessions')
91
95
  return NextResponse.json(sessions[id])
@@ -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,5 +1,5 @@
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
5
  export const dynamic = 'force-dynamic'
@@ -12,7 +12,7 @@ export async function GET(_req: Request) {
12
12
  export async function POST(req: Request) {
13
13
  const body = await req.json()
14
14
  const skills = loadSkills()
15
- const id = crypto.randomBytes(4).toString('hex')
15
+ const id = genId()
16
16
  const normalized = normalizeSkillPayload(body)
17
17
  skills[id] = {
18
18
  id,
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadTasks, saveTasks, loadAgents } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { notify } from '@/lib/server/ws-hub'
4
5
  import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
5
6
 
@@ -10,7 +11,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
10
11
 
11
12
  const tasks = loadTasks()
12
13
  const task = tasks[id]
13
- if (!task) return new NextResponse(null, { status: 404 })
14
+ if (!task) return notFound()
14
15
  if (!task.pendingApproval) {
15
16
  return NextResponse.json({ error: 'No pending approval on this task' }, { status: 400 })
16
17
  }
@@ -1,6 +1,7 @@
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'
@@ -13,7 +14,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
13
14
 
14
15
  const { id } = await params
15
16
  const tasks = loadTasks()
16
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
17
+ if (!tasks[id]) return notFound()
17
18
  return NextResponse.json(tasks[id])
18
19
  }
19
20
 
@@ -21,7 +22,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
21
22
  const { id } = await params
22
23
  const body = await req.json()
23
24
  const tasks = loadTasks()
24
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
25
+ if (!tasks[id]) return notFound()
25
26
 
26
27
  const prevStatus = tasks[id].status
27
28
 
@@ -55,7 +56,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
55
56
  tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
56
57
  if (!tasks[id].comments) tasks[id].comments = []
57
58
  tasks[id].comments.push({
58
- id: crypto.randomBytes(4).toString('hex'),
59
+ id: genId(),
59
60
  author: 'System',
60
61
  text: `Completion validation failed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
61
62
  createdAt: Date.now(),
@@ -88,7 +89,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
88
89
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
89
90
  const { id } = await params
90
91
  const tasks = loadTasks()
91
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
92
+ if (!tasks[id]) return notFound()
92
93
 
93
94
  // Soft delete: move to archived status instead of hard delete
94
95
  tasks[id].status = 'archived'
@@ -1,5 +1,5 @@
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'
@@ -54,7 +54,7 @@ export async function DELETE(req: Request) {
54
54
 
55
55
  export async function POST(req: Request) {
56
56
  const body = await req.json()
57
- const id = crypto.randomBytes(4).toString('hex')
57
+ const id = genId()
58
58
  const now = Date.now()
59
59
  const tasks = loadTasks()
60
60
  const settings = loadSettings()
@@ -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
+ }