@swarmclawai/swarmclaw 0.4.0 → 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 (144) hide show
  1. package/README.md +13 -2
  2. package/next.config.ts +8 -0
  3. package/package.json +2 -1
  4. package/src/app/api/agents/[id]/route.ts +20 -21
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  6. package/src/app/api/agents/route.ts +3 -2
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/connectors/[id]/route.ts +10 -3
  9. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  10. package/src/app/api/connectors/route.ts +6 -3
  11. package/src/app/api/credentials/[id]/route.ts +2 -1
  12. package/src/app/api/credentials/route.ts +2 -2
  13. package/src/app/api/documents/route.ts +2 -2
  14. package/src/app/api/files/serve/route.ts +8 -0
  15. package/src/app/api/knowledge/[id]/route.ts +5 -4
  16. package/src/app/api/knowledge/upload/route.ts +2 -2
  17. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  18. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  19. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  20. package/src/app/api/mcp-servers/route.ts +2 -2
  21. package/src/app/api/memory/[id]/route.ts +9 -8
  22. package/src/app/api/memory/route.ts +2 -2
  23. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  24. package/src/app/api/openclaw/directory/route.ts +26 -0
  25. package/src/app/api/openclaw/discover/route.ts +61 -0
  26. package/src/app/api/openclaw/sync/route.ts +30 -0
  27. package/src/app/api/orchestrator/run/route.ts +2 -2
  28. package/src/app/api/projects/[id]/route.ts +55 -0
  29. package/src/app/api/projects/route.ts +27 -0
  30. package/src/app/api/providers/[id]/models/route.ts +2 -1
  31. package/src/app/api/providers/[id]/route.ts +13 -15
  32. package/src/app/api/providers/route.ts +2 -2
  33. package/src/app/api/schedules/[id]/route.ts +16 -18
  34. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  35. package/src/app/api/schedules/route.ts +2 -2
  36. package/src/app/api/secrets/[id]/route.ts +16 -17
  37. package/src/app/api/secrets/route.ts +2 -2
  38. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  39. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  40. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  41. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  42. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  43. package/src/app/api/sessions/[id]/route.ts +2 -1
  44. package/src/app/api/sessions/route.ts +2 -2
  45. package/src/app/api/skills/[id]/route.ts +23 -21
  46. package/src/app/api/skills/import/route.ts +2 -2
  47. package/src/app/api/skills/route.ts +2 -2
  48. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  49. package/src/app/api/tasks/[id]/route.ts +6 -5
  50. package/src/app/api/tasks/route.ts +2 -2
  51. package/src/app/api/tts/stream/route.ts +48 -0
  52. package/src/app/api/upload/route.ts +2 -2
  53. package/src/app/api/uploads/[filename]/route.ts +4 -1
  54. package/src/app/api/webhooks/[id]/route.ts +29 -31
  55. package/src/app/api/webhooks/route.ts +2 -2
  56. package/src/app/page.tsx +3 -24
  57. package/src/cli/index.js +28 -0
  58. package/src/cli/index.ts +1 -1
  59. package/src/cli/spec.js +2 -0
  60. package/src/components/agents/agent-list.tsx +3 -1
  61. package/src/components/agents/agent-sheet.tsx +116 -14
  62. package/src/components/chat/chat-area.tsx +27 -4
  63. package/src/components/chat/chat-header.tsx +141 -29
  64. package/src/components/chat/tool-call-bubble.tsx +9 -3
  65. package/src/components/chat/voice-overlay.tsx +80 -0
  66. package/src/components/connectors/connector-list.tsx +6 -2
  67. package/src/components/connectors/connector-sheet.tsx +31 -7
  68. package/src/components/layout/app-layout.tsx +47 -25
  69. package/src/components/projects/project-list.tsx +122 -0
  70. package/src/components/projects/project-sheet.tsx +135 -0
  71. package/src/components/schedules/schedule-list.tsx +3 -1
  72. package/src/components/sessions/new-session-sheet.tsx +6 -6
  73. package/src/components/sessions/session-card.tsx +1 -1
  74. package/src/components/sessions/session-list.tsx +7 -7
  75. package/src/components/shared/connector-platform-icon.tsx +4 -0
  76. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  77. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  78. package/src/components/shared/settings/section-web-search.tsx +56 -0
  79. package/src/components/shared/settings/settings-page.tsx +73 -0
  80. package/src/components/skills/skill-list.tsx +2 -1
  81. package/src/components/tasks/task-list.tsx +5 -2
  82. package/src/hooks/use-continuous-speech.ts +144 -0
  83. package/src/hooks/use-view-router.ts +52 -0
  84. package/src/hooks/use-voice-conversation.ts +80 -0
  85. package/src/lib/id.ts +6 -0
  86. package/src/lib/projects.ts +13 -0
  87. package/src/lib/provider-sets.ts +5 -0
  88. package/src/lib/providers/anthropic.ts +14 -1
  89. package/src/lib/providers/index.ts +6 -0
  90. package/src/lib/providers/ollama.ts +9 -1
  91. package/src/lib/providers/openai.ts +9 -1
  92. package/src/lib/providers/openclaw.ts +11 -0
  93. package/src/lib/server/api-routes.test.ts +5 -6
  94. package/src/lib/server/build-llm.ts +17 -4
  95. package/src/lib/server/chat-execution.ts +38 -4
  96. package/src/lib/server/collection-helpers.ts +54 -0
  97. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  98. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  99. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  100. package/src/lib/server/connectors/googlechat.ts +46 -7
  101. package/src/lib/server/connectors/manager.ts +392 -3
  102. package/src/lib/server/connectors/media.ts +2 -2
  103. package/src/lib/server/connectors/openclaw.ts +64 -0
  104. package/src/lib/server/connectors/pairing.test.ts +99 -0
  105. package/src/lib/server/connectors/pairing.ts +256 -0
  106. package/src/lib/server/connectors/signal.ts +1 -0
  107. package/src/lib/server/connectors/teams.ts +5 -5
  108. package/src/lib/server/connectors/types.ts +10 -0
  109. package/src/lib/server/execution-log.ts +3 -3
  110. package/src/lib/server/heartbeat-service.ts +1 -1
  111. package/src/lib/server/knowledge-db.test.ts +2 -33
  112. package/src/lib/server/main-agent-loop.ts +6 -6
  113. package/src/lib/server/memory-db.ts +6 -6
  114. package/src/lib/server/openclaw-approvals.ts +105 -0
  115. package/src/lib/server/openclaw-sync.ts +496 -0
  116. package/src/lib/server/orchestrator-lg.ts +30 -9
  117. package/src/lib/server/orchestrator.ts +4 -4
  118. package/src/lib/server/process-manager.ts +2 -2
  119. package/src/lib/server/queue.ts +22 -10
  120. package/src/lib/server/scheduler.ts +2 -2
  121. package/src/lib/server/session-mailbox.ts +2 -2
  122. package/src/lib/server/session-run-manager.ts +2 -2
  123. package/src/lib/server/session-tools/connector.ts +51 -4
  124. package/src/lib/server/session-tools/crud.ts +3 -3
  125. package/src/lib/server/session-tools/delegate.ts +3 -3
  126. package/src/lib/server/session-tools/file.ts +176 -3
  127. package/src/lib/server/session-tools/index.ts +2 -0
  128. package/src/lib/server/session-tools/memory.ts +2 -2
  129. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  130. package/src/lib/server/session-tools/sandbox.ts +33 -0
  131. package/src/lib/server/session-tools/search-providers.ts +270 -0
  132. package/src/lib/server/session-tools/session-info.ts +2 -2
  133. package/src/lib/server/session-tools/web.ts +47 -66
  134. package/src/lib/server/storage.ts +12 -0
  135. package/src/lib/server/stream-agent-chat.ts +29 -0
  136. package/src/lib/server/task-result.test.ts +44 -0
  137. package/src/lib/server/task-result.ts +14 -0
  138. package/src/lib/tool-definitions.ts +5 -3
  139. package/src/lib/tts-stream.ts +130 -0
  140. package/src/lib/view-routes.ts +28 -0
  141. package/src/proxy.ts +3 -0
  142. package/src/stores/use-app-store.ts +28 -1
  143. package/src/stores/use-chat-store.ts +9 -1
  144. package/src/types/index.ts +27 -2
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import fs from 'fs'
3
3
  import { NextResponse } from 'next/server'
4
4
  import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset, storeMemoryImageFromDataUrl } from '@/lib/server/memory-db'
@@ -72,7 +72,7 @@ export async function POST(req: Request) {
72
72
  }
73
73
 
74
74
  const db = getMemoryDb()
75
- const draftId = crypto.randomBytes(6).toString('hex')
75
+ const draftId = genId(6)
76
76
 
77
77
  let image = body.image
78
78
  const inputImagePath = typeof body.imagePath === 'string' ? body.imagePath.trim() : ''
@@ -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
 
@@ -20,7 +21,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
20
21
  const filePath = path.join(IMAGES_DIR, safeName)
21
22
 
22
23
  if (!fs.existsSync(filePath)) {
23
- return new NextResponse(null, { status: 404 })
24
+ return notFound()
24
25
  }
25
26
 
26
27
  const ext = path.extname(safeName).toLowerCase()
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from 'next/server'
2
+ export const dynamic = 'force-dynamic'
3
+
4
+ export async function GET() {
5
+ try {
6
+ const { listRunningConnectors, getRunningInstance } = await import('@/lib/server/connectors/manager')
7
+ const openclawConnectors = listRunningConnectors('openclaw')
8
+
9
+ if (!openclawConnectors.length) {
10
+ return NextResponse.json({ devices: [], note: 'No running OpenClaw connector.' })
11
+ }
12
+
13
+ // The directory.list RPC requires gateway support — degrade gracefully
14
+ return NextResponse.json({
15
+ devices: [],
16
+ connectors: openclawConnectors.map((c) => ({
17
+ id: c.id,
18
+ name: c.name,
19
+ platform: c.platform,
20
+ })),
21
+ note: 'Directory listing requires OpenClaw gateway directory.list RPC support.',
22
+ })
23
+ } catch (err: any) {
24
+ return NextResponse.json({ error: err.message || 'Directory listing failed' }, { status: 500 })
25
+ }
26
+ }
@@ -0,0 +1,61 @@
1
+ import { NextResponse } from 'next/server'
2
+ import os from 'node:os'
3
+ import { probeOpenClawHealth } from '@/lib/server/openclaw-health'
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ const DEFAULT_PORTS = [18789, 18790]
7
+ const PROBE_TIMEOUT_MS = 4000
8
+
9
+ function getLanIps(): string[] {
10
+ const interfaces = os.networkInterfaces()
11
+ const ips: string[] = []
12
+ for (const iface of Object.values(interfaces)) {
13
+ if (!iface) continue
14
+ for (const info of iface) {
15
+ if (info.family === 'IPv4' && !info.internal) {
16
+ ips.push(info.address)
17
+ }
18
+ }
19
+ }
20
+ return ips
21
+ }
22
+
23
+ export async function GET() {
24
+ try {
25
+ const hosts = ['127.0.0.1', ...getLanIps()]
26
+ const probes: Array<Promise<{
27
+ host: string
28
+ port: number
29
+ healthy: boolean
30
+ models?: string[]
31
+ error?: string
32
+ }>> = []
33
+
34
+ for (const host of hosts) {
35
+ for (const port of DEFAULT_PORTS) {
36
+ probes.push(
37
+ probeOpenClawHealth({
38
+ endpoint: `http://${host}:${port}`,
39
+ timeoutMs: PROBE_TIMEOUT_MS,
40
+ }).then((result) => ({
41
+ host,
42
+ port,
43
+ healthy: result.ok,
44
+ models: result.models.length > 0 ? result.models : undefined,
45
+ error: result.error || undefined,
46
+ })).catch(() => ({
47
+ host,
48
+ port,
49
+ healthy: false,
50
+ error: 'unreachable',
51
+ })),
52
+ )
53
+ }
54
+ }
55
+
56
+ const results = await Promise.all(probes)
57
+ return NextResponse.json({ gateways: results })
58
+ } catch (err: any) {
59
+ return NextResponse.json({ error: err.message || 'Discovery failed' }, { status: 500 })
60
+ }
61
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { runSync, type SyncType } from '@/lib/server/openclaw-sync'
3
+ export const dynamic = 'force-dynamic'
4
+
5
+ const VALID_ACTIONS = new Set(['push', 'pull', 'both'])
6
+ const VALID_TYPES: SyncType[] = ['memory', 'workspace', 'schedules', 'credentials', 'plugins']
7
+
8
+ export async function POST(req: Request) {
9
+ try {
10
+ const body = await req.json()
11
+ const action = body.action
12
+ const types = body.types
13
+
14
+ if (!action || !VALID_ACTIONS.has(action)) {
15
+ return NextResponse.json({ error: 'Invalid action. Use push, pull, or both.' }, { status: 400 })
16
+ }
17
+ if (!Array.isArray(types) || types.length === 0) {
18
+ return NextResponse.json({ error: 'types must be a non-empty array.' }, { status: 400 })
19
+ }
20
+ const validTypes = types.filter((t: string) => VALID_TYPES.includes(t as SyncType)) as SyncType[]
21
+ if (validTypes.length === 0) {
22
+ return NextResponse.json({ error: `No valid types. Use: ${VALID_TYPES.join(', ')}` }, { status: 400 })
23
+ }
24
+
25
+ const results = await runSync({ action, types: validTypes })
26
+ return NextResponse.json({ ok: true, results })
27
+ } catch (err: any) {
28
+ return NextResponse.json({ error: err.message || 'Sync failed' }, { status: 500 })
29
+ }
30
+ }
@@ -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] = {
@@ -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,37 +1,35 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
3
- import { notify } from '@/lib/server/ws-hub'
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' }
4
7
 
5
8
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
9
  const { id } = await params
7
10
  const configs = loadProviderConfigs()
8
11
  const config = configs[id]
9
- if (!config) return new NextResponse(null, { status: 404 })
12
+ if (!config) return notFound()
10
13
  return NextResponse.json(config)
11
14
  }
12
15
 
13
16
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
14
17
  const { id } = await params
15
18
  const body = await req.json()
16
- const configs = loadProviderConfigs()
17
- const existing = configs[id]
18
- if (!existing) return new NextResponse(null, { status: 404 })
19
- configs[id] = { ...existing, ...body, id, updatedAt: Date.now() }
20
- saveProviderConfigs(configs)
21
- notify('providers')
22
- 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)
23
24
  }
24
25
 
25
26
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
26
27
  const { id } = await params
27
28
  const configs = loadProviderConfigs()
28
- if (!configs[id]) return new NextResponse(null, { status: 404 })
29
- // Only allow deleting custom providers
29
+ if (!configs[id]) return notFound()
30
30
  if (configs[id].type === 'builtin') {
31
- return NextResponse.json({ error: 'Cannot delete built-in providers' }, { status: 400 })
31
+ return badRequest('Cannot delete built-in providers')
32
32
  }
33
- delete configs[id]
34
- saveProviderConfigs(configs)
35
- notify('providers')
33
+ if (!deleteItem(ops, id)) return notFound()
36
34
  return NextResponse.json({ ok: true })
37
35
  }
@@ -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 { getProviderList } from '@/lib/providers'
4
4
  import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
5
5
  import { notify } from '@/lib/server/ws-hub'
@@ -13,7 +13,7 @@ export async function GET(_req: Request) {
13
13
  export async function POST(req: Request) {
14
14
  const body = await req.json()
15
15
  const configs = loadProviderConfigs()
16
- const id = body.id || `custom-${crypto.randomBytes(4).toString('hex')}`
16
+ const id = body.id || `custom-${genId()}`
17
17
  configs[id] = {
18
18
  id,
19
19
  name: body.name || 'Custom Provider',
@@ -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, any>
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
 
@@ -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,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'
@@ -46,7 +46,7 @@ export async function POST(req: Request) {
46
46
  else if (cwd === '~') cwd = os.homedir()
47
47
  else if (!cwd) cwd = WORKSPACE_DIR
48
48
 
49
- const id = body.id || crypto.randomBytes(4).toString('hex')
49
+ const id = body.id || genId()
50
50
  const sessions = loadSessions()
51
51
  const agent = body.agentId ? loadAgents()[body.agentId] : null
52
52
  const requestedTools = Array.isArray(body.tools) ? body.tools : null