@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
@@ -0,0 +1,109 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { mergeHistoryMessages, isValidSessionKey } from '@/lib/server/openclaw-history-merge'
4
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+ import type { GatewaySessionPreview } from '@/types'
7
+
8
+ /**
9
+ * Extract a single session preview from the gateway response.
10
+ * The gateway may return:
11
+ * - A map: { [sessionKey]: preview }
12
+ * - An array: [preview, ...]
13
+ * - A single object with sessionKey field
14
+ */
15
+ function extractPreview(
16
+ raw: unknown,
17
+ sessionKey: string,
18
+ ): GatewaySessionPreview | undefined {
19
+ if (!raw || typeof raw !== 'object') return undefined
20
+
21
+ // Direct object with messages array
22
+ if ('messages' in (raw as Record<string, unknown>)) {
23
+ return raw as GatewaySessionPreview
24
+ }
25
+
26
+ // Map keyed by session key
27
+ const asMap = raw as Record<string, unknown>
28
+ if (asMap[sessionKey] && typeof asMap[sessionKey] === 'object') {
29
+ return asMap[sessionKey] as GatewaySessionPreview
30
+ }
31
+
32
+ // Array — find matching session
33
+ if (Array.isArray(raw)) {
34
+ return raw.find(
35
+ (p: unknown) =>
36
+ p && typeof p === 'object' && (p as GatewaySessionPreview).sessionKey === sessionKey,
37
+ ) as GatewaySessionPreview | undefined
38
+ }
39
+
40
+ return undefined
41
+ }
42
+
43
+ /** GET ?sessionKey=X — preview gateway session history */
44
+ export async function GET(req: Request) {
45
+ const { searchParams } = new URL(req.url)
46
+ const sessionKey = searchParams.get('sessionKey')
47
+ if (!sessionKey || !isValidSessionKey(sessionKey)) {
48
+ return NextResponse.json({ error: 'Missing or invalid sessionKey' }, { status: 400 })
49
+ }
50
+
51
+ const gw = await ensureGatewayConnected()
52
+ if (!gw) {
53
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
54
+ }
55
+
56
+ try {
57
+ const raw = await gw.rpc('sessions.preview', { keys: [sessionKey], limit: 100 })
58
+ const preview = extractPreview(raw, sessionKey)
59
+ return NextResponse.json(preview ?? { sessionKey, epoch: 0, messages: [] })
60
+ } catch (err: unknown) {
61
+ const message = err instanceof Error ? err.message : String(err)
62
+ return NextResponse.json({ error: message }, { status: 502 })
63
+ }
64
+ }
65
+
66
+ /** POST { sessionKey, epoch, localSessionId } — merge gateway history into local session */
67
+ export async function POST(req: Request) {
68
+ const body = await req.json()
69
+ const { sessionKey, localSessionId } = body as {
70
+ sessionKey?: string
71
+ epoch?: number
72
+ localSessionId?: string
73
+ }
74
+ if (!sessionKey || !localSessionId) {
75
+ return NextResponse.json({ error: 'Missing sessionKey or localSessionId' }, { status: 400 })
76
+ }
77
+
78
+ const gw = await ensureGatewayConnected()
79
+ if (!gw) {
80
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
81
+ }
82
+
83
+ try {
84
+ const raw = await gw.rpc('sessions.preview', { keys: [sessionKey], limit: 100 })
85
+ const preview = extractPreview(raw, sessionKey)
86
+ if (!preview?.messages?.length) {
87
+ return NextResponse.json({ ok: true, merged: 0 })
88
+ }
89
+
90
+ const sessions = loadSessions()
91
+ const session = sessions[localSessionId]
92
+ if (!session) {
93
+ return NextResponse.json({ error: 'Local session not found' }, { status: 404 })
94
+ }
95
+
96
+ const merged = mergeHistoryMessages(session.messages, preview)
97
+ const newCount = merged.length - session.messages.length
98
+ session.messages = merged
99
+ session.lastActiveAt = Date.now()
100
+ sessions[localSessionId] = session
101
+ saveSessions(sessions)
102
+ notify('sessions')
103
+
104
+ return NextResponse.json({ ok: true, merged: newCount })
105
+ } catch (err: unknown) {
106
+ const message = err instanceof Error ? err.message : String(err)
107
+ return NextResponse.json({ error: message }, { status: 502 })
108
+ }
109
+ }
@@ -0,0 +1,53 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { lookup } from 'mime-types'
4
+
5
+ const MAX_SIZE = 25 * 1024 * 1024 // 25MB
6
+
7
+ /** GET ?path=... — proxy agent files (images etc.) from gateway */
8
+ export async function GET(req: Request) {
9
+ const { searchParams } = new URL(req.url)
10
+ const filePath = searchParams.get('path')
11
+ if (!filePath) {
12
+ return NextResponse.json({ error: 'Missing path' }, { status: 400 })
13
+ }
14
+
15
+ // Security: path must be under ~/.openclaw/
16
+ if (!filePath.includes('.openclaw') && !filePath.includes('.clawdbot')) {
17
+ return NextResponse.json({ error: 'Path not allowed' }, { status: 403 })
18
+ }
19
+
20
+ const gw = await ensureGatewayConnected()
21
+ if (!gw) {
22
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
23
+ }
24
+
25
+ try {
26
+ const result = await gw.rpc('files.read', { path: filePath }) as { content?: string; encoding?: string } | undefined
27
+ if (!result?.content) {
28
+ return NextResponse.json({ error: 'File not found' }, { status: 404 })
29
+ }
30
+
31
+ const isBase64 = result.encoding === 'base64'
32
+ const buf = isBase64
33
+ ? Buffer.from(result.content, 'base64')
34
+ : Buffer.from(result.content, 'utf-8')
35
+
36
+ if (buf.length > MAX_SIZE) {
37
+ return NextResponse.json({ error: 'File too large' }, { status: 413 })
38
+ }
39
+
40
+ const mimeType = lookup(filePath) || 'application/octet-stream'
41
+
42
+ return new NextResponse(buf, {
43
+ headers: {
44
+ 'Content-Type': mimeType,
45
+ 'Content-Length': String(buf.length),
46
+ 'Cache-Control': 'public, max-age=300',
47
+ },
48
+ })
49
+ } catch (err: unknown) {
50
+ const message = err instanceof Error ? err.message : String(err)
51
+ return NextResponse.json({ error: message }, { status: 502 })
52
+ }
53
+ }
@@ -0,0 +1,12 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { fetchGatewayModelPolicy, buildAllowedModelKeys } from '@/lib/server/openclaw-models'
3
+
4
+ /** GET — fetch allowed models for OpenClaw agents from gateway policy */
5
+ export async function GET() {
6
+ const policy = await fetchGatewayModelPolicy()
7
+ const models = buildAllowedModelKeys(policy)
8
+ return NextResponse.json({
9
+ models: models ?? ['default'],
10
+ defaultModel: policy?.defaultModel ?? 'default',
11
+ })
12
+ }
@@ -0,0 +1,39 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { PermissionPreset } from '@/types'
3
+ import { getExecConfig } from '@/lib/server/openclaw-exec-config'
4
+ import { resolvePresetFromConfig, applyPreset } from '@/lib/server/openclaw-permission-presets'
5
+
6
+ /** GET ?agentId=X — resolve current permission preset */
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const agentId = searchParams.get('agentId')
10
+ if (!agentId) {
11
+ return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
12
+ }
13
+
14
+ try {
15
+ const snap = await getExecConfig(agentId)
16
+ const preset = resolvePresetFromConfig(snap.file)
17
+ return NextResponse.json({ preset, config: snap.file })
18
+ } catch (err: unknown) {
19
+ const message = err instanceof Error ? err.message : String(err)
20
+ return NextResponse.json({ error: message }, { status: 502 })
21
+ }
22
+ }
23
+
24
+ /** PUT { agentId, preset } — apply a permission preset */
25
+ export async function PUT(req: Request) {
26
+ const body = await req.json()
27
+ const { agentId, preset } = body as { agentId?: string; preset?: PermissionPreset }
28
+ if (!agentId || !preset) {
29
+ return NextResponse.json({ error: 'Missing agentId or preset' }, { status: 400 })
30
+ }
31
+
32
+ try {
33
+ await applyPreset(agentId, preset)
34
+ return NextResponse.json({ ok: true })
35
+ } catch (err: unknown) {
36
+ const message = err instanceof Error ? err.message : String(err)
37
+ return NextResponse.json({ error: message }, { status: 502 })
38
+ }
39
+ }
@@ -0,0 +1,69 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** GET — list available and allowed env keys for sandbox */
5
+ export async function GET() {
6
+ const gw = await ensureGatewayConnected()
7
+ if (!gw) {
8
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
9
+ }
10
+
11
+ try {
12
+ // Get available keys from dotenv
13
+ const available = await gw.rpc('env.keys') as string[] | undefined
14
+
15
+ // Get current config to find allowed keys
16
+ const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
17
+ const agents = (config as Record<string, unknown>)?.agents as Record<string, unknown> | undefined
18
+ const defaults = agents?.defaults as Record<string, unknown> | undefined
19
+ const sandbox = defaults?.sandbox as Record<string, unknown> | undefined
20
+ const docker = sandbox?.docker as Record<string, unknown> | undefined
21
+ const envList = docker?.env as string[] | undefined
22
+
23
+ // Parse allowed keys from ${KEY} format
24
+ const allowed = (envList ?? [])
25
+ .map((entry) => {
26
+ const m = entry.match(/^\$\{(.+)\}$/)
27
+ return m ? m[1] : entry
28
+ })
29
+
30
+ return NextResponse.json({ available: available ?? [], allowed })
31
+ } catch (err: unknown) {
32
+ const message = err instanceof Error ? err.message : String(err)
33
+ return NextResponse.json({ error: message }, { status: 502 })
34
+ }
35
+ }
36
+
37
+ /** PUT { allowed: string[] } — update sandbox env allowlist */
38
+ export async function PUT(req: Request) {
39
+ const body = await req.json()
40
+ const { allowed } = body as { allowed?: string[] }
41
+ if (!allowed || !Array.isArray(allowed)) {
42
+ return NextResponse.json({ error: 'Missing allowed array' }, { status: 400 })
43
+ }
44
+
45
+ const gw = await ensureGatewayConnected()
46
+ if (!gw) {
47
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
48
+ }
49
+
50
+ try {
51
+ // Format as ${KEY} for gateway config
52
+ const envEntries = allowed.map((key) => `\${${key}}`)
53
+
54
+ // Fetch current config hash for conflict detection
55
+ const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
56
+ const configHash = (config as Record<string, unknown>)?._hash as string | undefined
57
+
58
+ await gw.rpc('config.set', {
59
+ key: 'agents.defaults.sandbox.docker.env',
60
+ value: envEntries,
61
+ baseHash: configHash,
62
+ })
63
+
64
+ return NextResponse.json({ ok: true })
65
+ } catch (err: unknown) {
66
+ const message = err instanceof Error ? err.message : String(err)
67
+ return NextResponse.json({ error: message }, { status: 502 })
68
+ }
69
+ }
@@ -0,0 +1,32 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** POST { name, installId, timeoutMs? } — install a skill via gateway */
5
+ export async function POST(req: Request) {
6
+ const body = await req.json()
7
+ const { name, installId, timeoutMs } = body as {
8
+ name?: string
9
+ installId?: string
10
+ timeoutMs?: number
11
+ }
12
+ if (!name) {
13
+ return NextResponse.json({ error: 'Missing skill name' }, { status: 400 })
14
+ }
15
+
16
+ const gw = await ensureGatewayConnected()
17
+ if (!gw) {
18
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
19
+ }
20
+
21
+ try {
22
+ const result = await gw.rpc('skills.install', {
23
+ name,
24
+ installId,
25
+ timeoutMs: timeoutMs ?? 120_000,
26
+ }, (timeoutMs ?? 120_000) + 5_000)
27
+ return NextResponse.json({ ok: true, result })
28
+ } catch (err: unknown) {
29
+ const message = err instanceof Error ? err.message : String(err)
30
+ return NextResponse.json({ error: message }, { status: 502 })
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** POST { skillKey, source } — remove a skill via gateway */
5
+ export async function POST(req: Request) {
6
+ const body = await req.json()
7
+ const { skillKey, source } = body as { skillKey?: string; source?: string }
8
+ if (!skillKey) {
9
+ return NextResponse.json({ error: 'Missing skillKey' }, { status: 400 })
10
+ }
11
+
12
+ const gw = await ensureGatewayConnected()
13
+ if (!gw) {
14
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
15
+ }
16
+
17
+ try {
18
+ await gw.rpc('skills.remove', { skillKey, source })
19
+ return NextResponse.json({ ok: true })
20
+ } catch (err: unknown) {
21
+ const message = err instanceof Error ? err.message : String(err)
22
+ return NextResponse.json({ error: message }, { status: 502 })
23
+ }
24
+ }
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { loadAgents, saveAgents } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
6
+
7
+ /** GET ?agentId=X — fetch skills from gateway with eligibility */
8
+ export async function GET(req: Request) {
9
+ const { searchParams } = new URL(req.url)
10
+ const agentId = searchParams.get('agentId')
11
+ if (!agentId) {
12
+ return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
13
+ }
14
+
15
+ const gw = await ensureGatewayConnected()
16
+ if (!gw) {
17
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
18
+ }
19
+
20
+ try {
21
+ const result = await gw.rpc('skills.status', { agentId }) as OpenClawSkillEntry[] | undefined
22
+ return NextResponse.json(result ?? [])
23
+ } catch (err: unknown) {
24
+ const message = err instanceof Error ? err.message : String(err)
25
+ return NextResponse.json({ error: message }, { status: 502 })
26
+ }
27
+ }
28
+
29
+ /** PATCH { skillKey, enabled?, apiKey? } — update a skill's config on gateway */
30
+ export async function PATCH(req: Request) {
31
+ const body = await req.json()
32
+ const { skillKey, enabled, apiKey } = body as {
33
+ skillKey?: string
34
+ enabled?: boolean
35
+ apiKey?: string
36
+ }
37
+ if (!skillKey) {
38
+ return NextResponse.json({ error: 'Missing skillKey' }, { status: 400 })
39
+ }
40
+
41
+ const gw = await ensureGatewayConnected()
42
+ if (!gw) {
43
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
44
+ }
45
+
46
+ try {
47
+ await gw.rpc('skills.update', { skillKey, enabled, apiKey })
48
+ return NextResponse.json({ ok: true })
49
+ } catch (err: unknown) {
50
+ const message = err instanceof Error ? err.message : String(err)
51
+ return NextResponse.json({ error: message }, { status: 502 })
52
+ }
53
+ }
54
+
55
+ /** PUT { agentId, mode, allowedSkills } — save skill allowlist config to agent */
56
+ export async function PUT(req: Request) {
57
+ const body = await req.json()
58
+ const { agentId, mode, allowedSkills } = body as {
59
+ agentId?: string
60
+ mode?: SkillAllowlistMode
61
+ allowedSkills?: string[]
62
+ }
63
+
64
+ if (!agentId || !mode) {
65
+ return NextResponse.json({ error: 'Missing agentId or mode' }, { status: 400 })
66
+ }
67
+
68
+ const agents = loadAgents({ includeTrashed: true })
69
+ const agent = agents[agentId]
70
+ if (!agent) {
71
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
72
+ }
73
+
74
+ agent.openclawSkillMode = mode
75
+ agent.openclawAllowedSkills = mode === 'selected' ? (allowedSkills ?? []) : undefined
76
+ agent.updatedAt = Date.now()
77
+ agents[agentId] = agent
78
+ saveAgents(agents)
79
+ notify('agents')
80
+
81
+ return NextResponse.json({ ok: true })
82
+ }
@@ -0,0 +1,31 @@
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: unknown) {
28
+ const message = err instanceof Error ? err.message : 'Sync failed'
29
+ return NextResponse.json({ error: message }, { status: 500 })
30
+ }
31
+ }
@@ -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, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => 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() {
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',