@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,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
4
4
  import { enqueueTask } from '@/lib/server/queue'
5
5
 
@@ -16,7 +16,7 @@ export async function POST(req: Request) {
16
16
  }
17
17
 
18
18
  // Create a board task and enqueue it
19
- const taskId = crypto.randomBytes(4).toString('hex')
19
+ const taskId = genId()
20
20
  const now = Date.now()
21
21
  const tasks = loadTasks()
22
22
  tasks[taskId] = {
@@ -1,11 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
+ export const dynamic = 'force-dynamic'
3
+
2
4
 
3
5
  const REGISTRY_URL = 'https://swarmclaw.ai/registry/plugins.json'
4
6
  const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
5
7
 
6
8
  let cache: { data: any; fetchedAt: number } | null = null
7
9
 
8
- export async function GET() {
10
+ export async function GET(_req: Request) {
9
11
  const now = Date.now()
10
12
 
11
13
  if (cache && now - cache.fetchedAt < CACHE_TTL) {
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager } from '@/lib/server/plugins'
3
+ export const dynamic = 'force-dynamic'
3
4
 
4
- export async function GET() {
5
+
6
+ export async function GET(_req: Request) {
5
7
  const manager = getPluginManager()
6
8
  return NextResponse.json(manager.listPlugins())
7
9
  }
@@ -0,0 +1,55 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills } from '@/lib/server/storage'
3
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const ops: CollectionOps<any> = { load: loadProjects, save: saveProjects, deleteFn: deleteProject, topic: 'projects' }
8
+
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const projects = loadProjects()
12
+ if (!projects[id]) return notFound()
13
+ return NextResponse.json(projects[id])
14
+ }
15
+
16
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
17
+ const { id } = await params
18
+ const body = await req.json()
19
+ const result = mutateItem(ops, id, (project) => {
20
+ Object.assign(project, body, { updatedAt: Date.now() })
21
+ delete (project as Record<string, unknown>).id
22
+ project.id = id
23
+ return project
24
+ })
25
+ if (!result) return notFound()
26
+ return NextResponse.json(result)
27
+ }
28
+
29
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
30
+ const { id } = await params
31
+ if (!deleteItem(ops, id)) return notFound()
32
+
33
+ // Clear projectId from referencing entities
34
+ const clearProjectId = (load: () => Record<string, any>, save: (d: Record<string, any>) => void, topic: string) => {
35
+ const items = load()
36
+ let changed = false
37
+ for (const item of Object.values(items)) {
38
+ if (item.projectId === id) {
39
+ item.projectId = undefined
40
+ changed = true
41
+ }
42
+ }
43
+ if (changed) {
44
+ save(items)
45
+ notify(topic)
46
+ }
47
+ }
48
+
49
+ clearProjectId(loadAgents, saveAgents, 'agents')
50
+ clearProjectId(loadTasks, saveTasks, 'tasks')
51
+ clearProjectId(loadSchedules, saveSchedules, 'schedules')
52
+ clearProjectId(loadSkills, saveSkills, 'skills')
53
+
54
+ return NextResponse.json({ ok: true })
55
+ }
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { genId } from '@/lib/id'
3
+ import { loadProjects, saveProjects } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(_req: Request) {
8
+ return NextResponse.json(loadProjects())
9
+ }
10
+
11
+ export async function POST(req: Request) {
12
+ const body = await req.json()
13
+ const id = genId()
14
+ const now = Date.now()
15
+ const projects = loadProjects()
16
+ projects[id] = {
17
+ id,
18
+ name: body.name || 'Unnamed Project',
19
+ description: body.description || '',
20
+ color: body.color || undefined,
21
+ createdAt: now,
22
+ updatedAt: now,
23
+ }
24
+ saveProjects(projects)
25
+ notify('projects')
26
+ return NextResponse.json(projects[id])
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadModelOverrides, saveModelOverrides } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { getProviderList } from '@/lib/providers'
4
5
 
5
6
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -7,7 +8,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
7
8
  const overrides = loadModelOverrides()
8
9
  const providers = getProviderList()
9
10
  const provider = providers.find((p) => p.id === id)
10
- if (!provider) return new NextResponse(null, { status: 404 })
11
+ if (!provider) return notFound()
11
12
  return NextResponse.json({ models: provider.models, hasOverride: !!overrides[id] })
12
13
  }
13
14
 
@@ -1,34 +1,35 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
3
+ import { mutateItem, deleteItem, notFound, badRequest, type CollectionOps } from '@/lib/server/collection-helpers'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ const ops: CollectionOps<any> = { load: loadProviderConfigs, save: saveProviderConfigs, topic: 'providers' }
3
7
 
4
8
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
9
  const { id } = await params
6
10
  const configs = loadProviderConfigs()
7
11
  const config = configs[id]
8
- if (!config) return new NextResponse(null, { status: 404 })
12
+ if (!config) return notFound()
9
13
  return NextResponse.json(config)
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 configs = loadProviderConfigs()
16
- const existing = configs[id]
17
- if (!existing) return new NextResponse(null, { status: 404 })
18
- configs[id] = { ...existing, ...body, id, updatedAt: Date.now() }
19
- saveProviderConfigs(configs)
20
- return NextResponse.json(configs[id])
19
+ const result = mutateItem(ops, id, (existing) => ({
20
+ ...existing, ...body, id, updatedAt: Date.now(),
21
+ }))
22
+ if (!result) return notFound()
23
+ return NextResponse.json(result)
21
24
  }
22
25
 
23
26
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
24
27
  const { id } = await params
25
28
  const configs = loadProviderConfigs()
26
- if (!configs[id]) return new NextResponse(null, { status: 404 })
27
- // Only allow deleting custom providers
29
+ if (!configs[id]) return notFound()
28
30
  if (configs[id].type === 'builtin') {
29
- return NextResponse.json({ error: 'Cannot delete built-in providers' }, { status: 400 })
31
+ return badRequest('Cannot delete built-in providers')
30
32
  }
31
- delete configs[id]
32
- saveProviderConfigs(configs)
33
+ if (!deleteItem(ops, id)) return notFound()
33
34
  return NextResponse.json({ ok: true })
34
35
  }
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadProviderConfigs } 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 configs = loadProviderConfigs()
6
8
  return NextResponse.json(Object.values(configs))
7
9
  }
@@ -1,16 +1,19 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { getProviderList } from '@/lib/providers'
4
4
  import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+ export const dynamic = 'force-dynamic'
5
7
 
6
- export async function GET() {
8
+
9
+ export async function GET(_req: Request) {
7
10
  return NextResponse.json(getProviderList())
8
11
  }
9
12
 
10
13
  export async function POST(req: Request) {
11
14
  const body = await req.json()
12
15
  const configs = loadProviderConfigs()
13
- const id = body.id || `custom-${crypto.randomBytes(4).toString('hex')}`
16
+ const id = body.id || `custom-${genId()}`
14
17
  configs[id] = {
15
18
  id,
16
19
  name: body.name || 'Custom Provider',
@@ -24,5 +27,6 @@ export async function POST(req: Request) {
24
27
  updatedAt: Date.now(),
25
28
  }
26
29
  saveProviderConfigs(configs)
30
+ notify('providers')
27
31
  return NextResponse.json(configs[id])
28
32
  }
@@ -1,28 +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 { 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' }
4
8
 
5
9
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
10
  const { id } = await params
7
11
  const body = await req.json()
8
- const schedules = loadSchedules()
9
- if (!schedules[id]) return new NextResponse(null, { status: 404 })
10
-
11
- const origId = id
12
- Object.assign(schedules[id], body)
13
- schedules[id].id = origId
14
- schedules[id].name = resolveScheduleName({
15
- name: schedules[id].name,
16
- 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
17
20
  })
18
- saveSchedules(schedules)
19
- return NextResponse.json(schedules[id])
21
+ if (!result) return notFound()
22
+ return NextResponse.json(result)
20
23
  }
21
24
 
22
25
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
23
26
  const { id } = await params
24
- const schedules = loadSchedules()
25
- if (!schedules[id]) return new NextResponse(null, { status: 404 })
26
- deleteSchedule(id)
27
- return NextResponse.json('ok')
27
+ if (!deleteItem(ops, id)) return notFound()
28
+ return NextResponse.json({ ok: true })
28
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,10 +1,13 @@
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'
6
+ import { notify } from '@/lib/server/ws-hub'
7
+ export const dynamic = 'force-dynamic'
6
8
 
7
- export async function GET() {
9
+
10
+ export async function GET(_req: Request) {
8
11
  return NextResponse.json(loadSchedules())
9
12
  }
10
13
 
@@ -43,11 +46,12 @@ export async function POST(req: Request) {
43
46
  mutableDuplicate.updatedAt = now
44
47
  if (duplicateId) schedules[duplicateId] = duplicate
45
48
  saveSchedules(schedules)
49
+ notify('schedules')
46
50
  }
47
51
  return NextResponse.json(duplicate)
48
52
  }
49
53
 
50
- const id = crypto.randomBytes(4).toString('hex')
54
+ const id = genId()
51
55
 
52
56
  let nextRunAt: number | undefined
53
57
  if (scheduleType === 'once' && body.runAt) {
@@ -74,5 +78,6 @@ export async function POST(req: Request) {
74
78
  createdAt: now,
75
79
  }
76
80
  saveSchedules(schedules)
81
+ notify('schedules')
77
82
  return NextResponse.json(schedules[id])
78
83
  }
@@ -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, any>
28
27
  return NextResponse.json(safe)
29
28
  }
@@ -1,8 +1,10 @@
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
+ export const dynamic = 'force-dynamic'
4
5
 
5
- export async function GET() {
6
+
7
+ export async function GET(_req: Request) {
6
8
  // Return secrets WITHOUT the encrypted values (just metadata)
7
9
  const secrets = loadSecrets()
8
10
  const safe = Object.fromEntries(
@@ -16,7 +18,7 @@ export async function GET() {
16
18
 
17
19
  export async function POST(req: Request) {
18
20
  const body = await req.json()
19
- const id = crypto.randomBytes(4).toString('hex')
21
+ const id = genId()
20
22
  const now = Date.now()
21
23
  const secrets = loadSecrets()
22
24
 
@@ -14,11 +14,13 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
14
14
  const message = typeof body.message === 'string' ? body.message : ''
15
15
  const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
16
16
  const imageUrl = typeof body.imageUrl === 'string' ? body.imageUrl : undefined
17
+ const attachedFiles = Array.isArray(body.attachedFiles) ? body.attachedFiles.filter((f: unknown) => typeof f === 'string') as string[] : undefined
17
18
  const internal = body.internal === true
18
19
  const queueMode = normalizeQueueMode(body.queueMode, internal)
19
20
 
20
- if (!message.trim()) {
21
- return NextResponse.json({ error: 'message is required' }, { status: 400 })
21
+ const hasFiles = !!(imagePath || imageUrl || (attachedFiles && attachedFiles.length > 0))
22
+ if (!message.trim() && !hasFiles) {
23
+ return NextResponse.json({ error: 'message or file is required' }, { status: 400 })
22
24
  }
23
25
 
24
26
  const encoder = new TextEncoder()
@@ -39,6 +41,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
39
41
  message,
40
42
  imagePath,
41
43
  imageUrl,
44
+ attachedFiles,
42
45
  internal,
43
46
  source: internal ? 'heartbeat' : 'chat',
44
47
  mode: queueMode,
@@ -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
 
@@ -1,9 +1,10 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSessions } 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
  }
@@ -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,5 +1,6 @@
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'
5
6
 
@@ -20,7 +21,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
20
21
  const { id } = await params
21
22
  const updates = await req.json()
22
23
  const sessions = loadSessions()
23
- if (!sessions[id]) return new NextResponse(null, { status: 404 })
24
+ if (!sessions[id]) return notFound()
24
25
  const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
25
26
 
26
27
  const agentIdUpdateProvided = updates.agentId !== undefined
@@ -1,12 +1,16 @@
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'
6
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
+ import { notify } from '@/lib/server/ws-hub'
6
8
  import { getSessionRunState } from '@/lib/server/session-run-manager'
7
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ export const dynamic = 'force-dynamic'
8
11
 
9
- export async function GET() {
12
+
13
+ export async function GET(_req: Request) {
10
14
  const sessions = loadSessions()
11
15
  for (const id of Object.keys(sessions)) {
12
16
  const run = getSessionRunState(id)
@@ -31,6 +35,7 @@ export async function DELETE(req: Request) {
31
35
  }
32
36
  deleteSession(id)
33
37
  }
38
+ notify('sessions')
34
39
  return NextResponse.json({ deleted: ids.length })
35
40
  }
36
41
 
@@ -38,9 +43,10 @@ export async function POST(req: Request) {
38
43
  const body = await req.json()
39
44
  let cwd = (body.cwd || '').trim()
40
45
  if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
41
- else if (cwd === '~' || !cwd) cwd = os.homedir()
46
+ else if (cwd === '~') cwd = os.homedir()
47
+ else if (!cwd) cwd = WORKSPACE_DIR
42
48
 
43
- const id = body.id || crypto.randomBytes(4).toString('hex')
49
+ const id = body.id || genId()
44
50
  const sessions = loadSessions()
45
51
  const agent = body.agentId ? loadAgents()[body.agentId] : null
46
52
  const requestedTools = Array.isArray(body.tools) ? body.tools : null
@@ -81,5 +87,6 @@ export async function POST(req: Request) {
81
87
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
82
88
  }
83
89
  saveSessions(sessions)
90
+ notify('sessions')
84
91
  return NextResponse.json(sessions[id])
85
92
  }
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSettings, saveSettings } from '@/lib/server/storage'
3
+ export const dynamic = 'force-dynamic'
4
+
3
5
 
4
6
  const MEMORY_DEPTH_MIN = 0
5
7
  const MEMORY_DEPTH_MAX = 12
@@ -18,7 +20,7 @@ function parseIntSetting(value: unknown, fallback: number, min: number, max: num
18
20
  return Math.max(min, Math.min(max, Math.trunc(parsed)))
19
21
  }
20
22
 
21
- export async function GET() {
23
+ export async function GET(_req: Request) {
22
24
  return NextResponse.json(loadSettings())
23
25
  }
24
26
 
@@ -165,6 +165,7 @@ export async function GET(req: Request) {
165
165
  { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
166
166
  { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
167
167
  { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
168
+ { id: 'deno', label: 'Deno (sandbox runtime)', command: 'deno' },
168
169
  ]
169
170
 
170
171
  for (const binary of optionalBinaries) {
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getDeviceId } from '@/lib/providers/openclaw'
3
+ export const dynamic = 'force-dynamic'
3
4
 
4
- export async function GET() {
5
+
6
+ export async function GET(_req: Request) {
5
7
  try {
6
8
  const deviceId = getDeviceId()
7
9
  return NextResponse.json({ deviceId })