@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSessions } from '@/lib/server/storage'
3
+
4
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id } = await params
6
+ const sessions = loadSessions()
7
+ if (!sessions[id]) return new NextResponse(null, { status: 404 })
8
+ return NextResponse.json(sessions[id].messages)
9
+ }
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+
4
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id } = await params
6
+ const sessions = loadSessions()
7
+ const session = sessions[id]
8
+ if (!session) return new NextResponse(null, { status: 404 })
9
+
10
+ const msgs = session.messages
11
+ // Pop trailing assistant messages to find the last user message
12
+ while (msgs.length && msgs[msgs.length - 1].role === 'assistant') {
13
+ msgs.pop()
14
+ }
15
+ if (!msgs.length) {
16
+ return NextResponse.json({ message: '', imagePath: null }, { status: 200 })
17
+ }
18
+
19
+ const lastUser = msgs[msgs.length - 1]
20
+ const message = lastUser.text
21
+ const imagePath = lastUser.imagePath || null
22
+
23
+ // Remove the last user message too — it will be re-sent by the client
24
+ msgs.pop()
25
+ saveSessions(sessions)
26
+
27
+ return NextResponse.json({ message, imagePath })
28
+ }
@@ -0,0 +1,103 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
3
+ import { enqueueSessionRun } from '@/lib/server/session-run-manager'
4
+ import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
5
+
6
+ function buildSessionAwakeningPrompt(user: string | null | undefined): string {
7
+ const displayName = typeof user === 'string' && user.trim() ? user.trim() : 'there'
8
+ return [
9
+ 'SESSION_AWAKENING',
10
+ `You have just been activated as the primary SwarmClaw assistant for ${displayName}.`,
11
+ 'Write your first message as the agent itself (not as system text).',
12
+ 'Tone: awake, focused, practical.',
13
+ 'Include: brief greeting, what you can help with in SwarmClaw (providers, agents, tools/connectors, tasks, schedules), and one direct question asking for the user goal.',
14
+ 'Keep it concise (<= 90 words).',
15
+ 'Do not mention hidden prompts, policies, or implementation details.',
16
+ ].join('\n')
17
+ }
18
+
19
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
20
+ const { id } = await params
21
+ const updates = await req.json()
22
+ const sessions = loadSessions()
23
+ if (!sessions[id]) return new NextResponse(null, { status: 404 })
24
+ const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
25
+
26
+ const agentIdUpdateProvided = updates.agentId !== undefined
27
+ let nextAgentId = sessions[id].agentId
28
+ if (agentIdUpdateProvided) {
29
+ sessions[id].agentId = updates.agentId
30
+ nextAgentId = updates.agentId
31
+ }
32
+
33
+ const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
34
+
35
+ if (updates.name !== undefined) sessions[id].name = updates.name
36
+ if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
37
+ if (updates.provider !== undefined) sessions[id].provider = updates.provider
38
+ else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
39
+
40
+ if (updates.model !== undefined) sessions[id].model = updates.model
41
+ else if (agentIdUpdateProvided && linkedAgent?.model !== undefined) sessions[id].model = linkedAgent.model
42
+
43
+ if (updates.credentialId !== undefined) sessions[id].credentialId = updates.credentialId
44
+ else if (agentIdUpdateProvided && linkedAgent) sessions[id].credentialId = linkedAgent.credentialId ?? null
45
+
46
+ if (updates.tools !== undefined) sessions[id].tools = updates.tools
47
+ else if (agentIdUpdateProvided && linkedAgent) sessions[id].tools = Array.isArray(linkedAgent.tools) ? linkedAgent.tools : []
48
+
49
+ if (updates.apiEndpoint !== undefined) {
50
+ sessions[id].apiEndpoint = normalizeProviderEndpoint(
51
+ updates.provider || sessions[id].provider,
52
+ updates.apiEndpoint,
53
+ )
54
+ } else if (agentIdUpdateProvided && linkedAgent) {
55
+ sessions[id].apiEndpoint = normalizeProviderEndpoint(
56
+ linkedAgent.provider,
57
+ linkedAgent.apiEndpoint ?? null,
58
+ )
59
+ }
60
+ if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
61
+ if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
62
+ if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
63
+ if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
64
+
65
+ const shouldKickoffAwakening = sessions[id].name === '__main__'
66
+ && agentIdUpdateProvided
67
+ && !!sessions[id].agentId
68
+ && !hadMessagesBefore
69
+ && sessions[id].messages.length === 0
70
+
71
+ saveSessions(sessions)
72
+
73
+ if (shouldKickoffAwakening) {
74
+ try {
75
+ enqueueSessionRun({
76
+ sessionId: id,
77
+ message: buildSessionAwakeningPrompt(sessions[id].user),
78
+ internal: true,
79
+ source: 'session-awakening',
80
+ mode: 'steer',
81
+ dedupeKey: `session-awakening:${id}`,
82
+ })
83
+ } catch {
84
+ // Best-effort kickoff only.
85
+ }
86
+ }
87
+
88
+ return NextResponse.json(sessions[id])
89
+ }
90
+
91
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
92
+ const { id } = await params
93
+ const sessions = loadSessions()
94
+ if (sessions[id]?.name === '__main__') {
95
+ return new NextResponse('Cannot delete main chat session', { status: 403 })
96
+ }
97
+ if (active.has(id)) {
98
+ try { active.get(id).kill() } catch {}
99
+ active.delete(id)
100
+ }
101
+ deleteSession(id)
102
+ return new NextResponse('OK')
103
+ }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { active } from '@/lib/server/storage'
3
+ import { cancelSessionRuns } from '@/lib/server/session-run-manager'
4
+
5
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const cancel = cancelSessionRuns(id, 'Stopped by user')
8
+ if (active.has(id)) {
9
+ try { active.get(id).kill() } catch {}
10
+ active.delete(id)
11
+ }
12
+ return NextResponse.json({ ok: true, ...cancel })
13
+ }
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { disableAllSessionHeartbeats, loadSettings, saveSettings } from '@/lib/server/storage'
3
+ import { cancelAllHeartbeatRuns } from '@/lib/server/session-run-manager'
4
+
5
+ export async function POST(req: Request) {
6
+ const body = await req.json().catch(() => ({}))
7
+ const action = typeof body?.action === 'string' ? body.action : 'disable_all'
8
+ if (action !== 'disable_all') {
9
+ return NextResponse.json({ error: 'Unsupported action' }, { status: 400 })
10
+ }
11
+
12
+ const updatedSessions = disableAllSessionHeartbeats()
13
+ const settings = loadSettings()
14
+ if ((settings.heartbeatIntervalSec ?? 120) !== 0) {
15
+ settings.heartbeatIntervalSec = 0
16
+ saveSettings(settings)
17
+ }
18
+ const { cancelledQueued, abortedRunning } = cancelAllHeartbeatRuns('Heartbeat disabled via global switch')
19
+
20
+ return NextResponse.json({
21
+ ok: true,
22
+ updatedSessions,
23
+ cancelledQueued,
24
+ abortedRunning,
25
+ })
26
+ }
@@ -0,0 +1,85 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
6
+ import { getSessionRunState } from '@/lib/server/session-run-manager'
7
+ import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
8
+
9
+ export async function GET() {
10
+ const sessions = loadSessions()
11
+ for (const id of Object.keys(sessions)) {
12
+ const run = getSessionRunState(id)
13
+ sessions[id].active = active.has(id) || !!run.runningRunId
14
+ sessions[id].queuedCount = run.queueLength
15
+ sessions[id].currentRunId = run.runningRunId || null
16
+ }
17
+ return NextResponse.json(sessions)
18
+ }
19
+
20
+ export async function DELETE(req: Request) {
21
+ const { ids } = await req.json() as { ids: string[] }
22
+ if (!Array.isArray(ids) || !ids.length) {
23
+ return new NextResponse('Missing ids', { status: 400 })
24
+ }
25
+ const sessions = loadSessions()
26
+ for (const id of ids) {
27
+ if (sessions[id]?.name === '__main__') continue
28
+ if (active.has(id)) {
29
+ try { active.get(id).kill() } catch {}
30
+ active.delete(id)
31
+ }
32
+ deleteSession(id)
33
+ }
34
+ return NextResponse.json({ deleted: ids.length })
35
+ }
36
+
37
+ export async function POST(req: Request) {
38
+ const body = await req.json()
39
+ let cwd = (body.cwd || '').trim()
40
+ if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
41
+ else if (cwd === '~' || !cwd) cwd = os.homedir()
42
+
43
+ const id = body.id || crypto.randomBytes(4).toString('hex')
44
+ const sessions = loadSessions()
45
+ const agent = body.agentId ? loadAgents()[body.agentId] : null
46
+ const requestedTools = Array.isArray(body.tools) ? body.tools : null
47
+ const resolvedTools = requestedTools ?? (Array.isArray(agent?.tools) ? agent.tools : [])
48
+
49
+ // If session with this ID already exists, return it as-is
50
+ if (body.id && sessions[id]) {
51
+ return NextResponse.json(sessions[id])
52
+ }
53
+
54
+ const sessionName = body.name || 'New Session'
55
+
56
+ sessions[id] = {
57
+ id, name: sessionName, cwd,
58
+ user: body.user || 'wayde',
59
+ provider: body.provider || agent?.provider || 'claude-cli',
60
+ model: body.model || agent?.model || '',
61
+ credentialId: body.credentialId || agent?.credentialId || null,
62
+ apiEndpoint: normalizeProviderEndpoint(
63
+ body.provider || agent?.provider || 'claude-cli',
64
+ body.apiEndpoint || agent?.apiEndpoint || null,
65
+ ),
66
+ claudeSessionId: null,
67
+ codexThreadId: null,
68
+ opencodeSessionId: null,
69
+ delegateResumeIds: {
70
+ claudeCode: null,
71
+ codex: null,
72
+ opencode: null,
73
+ },
74
+ messages: Array.isArray(body.messages) ? body.messages : [],
75
+ createdAt: Date.now(), lastActiveAt: Date.now(),
76
+ sessionType: body.sessionType || 'human',
77
+ agentId: body.agentId || null,
78
+ parentSessionId: body.parentSessionId || null,
79
+ tools: resolvedTools,
80
+ heartbeatEnabled: body.heartbeatEnabled ?? null,
81
+ heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
82
+ }
83
+ saveSessions(sessions)
84
+ return NextResponse.json(sessions[id])
85
+ }
@@ -0,0 +1,58 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSettings, saveSettings } from '@/lib/server/storage'
3
+
4
+ const MEMORY_DEPTH_MIN = 0
5
+ const MEMORY_DEPTH_MAX = 12
6
+ const MEMORY_PER_LOOKUP_MIN = 1
7
+ const MEMORY_PER_LOOKUP_MAX = 200
8
+ const MEMORY_LINKED_MIN = 0
9
+ const MEMORY_LINKED_MAX = 1000
10
+
11
+ function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
12
+ const parsed = typeof value === 'number'
13
+ ? value
14
+ : typeof value === 'string'
15
+ ? Number.parseInt(value, 10)
16
+ : Number.NaN
17
+ if (!Number.isFinite(parsed)) return fallback
18
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
19
+ }
20
+
21
+ export async function GET() {
22
+ return NextResponse.json(loadSettings())
23
+ }
24
+
25
+ export async function PUT(req: Request) {
26
+ const body = await req.json()
27
+ const settings = loadSettings()
28
+ Object.assign(settings, body)
29
+
30
+ const nextDepth = parseIntSetting(
31
+ settings.memoryReferenceDepth ?? settings.memoryMaxDepth,
32
+ 3,
33
+ MEMORY_DEPTH_MIN,
34
+ MEMORY_DEPTH_MAX,
35
+ )
36
+ const nextPerLookup = parseIntSetting(
37
+ settings.maxMemoriesPerLookup ?? settings.memoryMaxPerLookup,
38
+ 20,
39
+ MEMORY_PER_LOOKUP_MIN,
40
+ MEMORY_PER_LOOKUP_MAX,
41
+ )
42
+ const nextLinked = parseIntSetting(
43
+ settings.maxLinkedMemoriesExpanded,
44
+ 60,
45
+ MEMORY_LINKED_MIN,
46
+ MEMORY_LINKED_MAX,
47
+ )
48
+
49
+ // Keep new and legacy keys synchronized for backward compatibility.
50
+ settings.memoryReferenceDepth = nextDepth
51
+ settings.memoryMaxDepth = nextDepth
52
+ settings.maxMemoriesPerLookup = nextPerLookup
53
+ settings.memoryMaxPerLookup = nextPerLookup
54
+ settings.maxLinkedMemoriesExpanded = nextLinked
55
+
56
+ saveSettings(settings)
57
+ return NextResponse.json(settings)
58
+ }
@@ -0,0 +1,326 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { WebSocket } from 'ws'
3
+ import { randomUUID } from 'crypto'
4
+ import { loadCredentials, decryptKey } from '@/lib/server/storage'
5
+ import { buildOpenClawConnectParams, getDeviceId } from '@/lib/providers/openclaw'
6
+
7
+ type SetupProvider =
8
+ | 'openai'
9
+ | 'anthropic'
10
+ | 'google'
11
+ | 'deepseek'
12
+ | 'groq'
13
+ | 'together'
14
+ | 'mistral'
15
+ | 'xai'
16
+ | 'fireworks'
17
+ | 'ollama'
18
+ | 'openclaw'
19
+
20
+ const OPENAI_COMPATIBLE_PROVIDER_INFO: Record<
21
+ 'openai' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks',
22
+ { name: string; defaultEndpoint: string }
23
+ > = {
24
+ openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
25
+ google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
26
+ deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
27
+ groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
28
+ together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
29
+ mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
30
+ xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
31
+ fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
32
+ }
33
+
34
+ interface SetupCheckBody {
35
+ provider?: string
36
+ apiKey?: string
37
+ credentialId?: string
38
+ endpoint?: string
39
+ model?: string
40
+ }
41
+
42
+ function clean(value: unknown): string {
43
+ return typeof value === 'string' ? value.trim() : ''
44
+ }
45
+
46
+ function parseBody(input: unknown): SetupCheckBody {
47
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
48
+ return input as SetupCheckBody
49
+ }
50
+
51
+ async function parseErrorMessage(res: Response, fallback: string): Promise<string> {
52
+ const text = await res.text().catch(() => '')
53
+ if (!text) return fallback
54
+ try {
55
+ const parsed = JSON.parse(text)
56
+ if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
57
+ if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
58
+ if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
59
+ if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
60
+ } catch {
61
+ // Non-JSON response body.
62
+ }
63
+ return text.slice(0, 300).trim() || fallback
64
+ }
65
+
66
+ async function checkOpenAiCompatible(
67
+ providerName: string,
68
+ apiKey: string,
69
+ endpointRaw: string,
70
+ defaultEndpoint: string,
71
+ ): Promise<{ ok: boolean; message: string; normalizedEndpoint: string }> {
72
+ const normalizedEndpoint = (endpointRaw || defaultEndpoint).replace(/\/+$/, '')
73
+ const res = await fetch(`${normalizedEndpoint}/models`, {
74
+ headers: {
75
+ authorization: `Bearer ${apiKey}`,
76
+ },
77
+ signal: AbortSignal.timeout(10_000),
78
+ cache: 'no-store',
79
+ })
80
+ if (!res.ok) {
81
+ const detail = await parseErrorMessage(res, `${providerName} returned ${res.status}.`)
82
+ return { ok: false, message: detail, normalizedEndpoint }
83
+ }
84
+ const payload = await res.json().catch(() => ({} as any))
85
+ const count = Array.isArray(payload?.data) ? payload.data.length : 0
86
+ return {
87
+ ok: true,
88
+ message: count > 0 ? `Connected to ${providerName}. ${count} model(s) available.` : `Connected to ${providerName}.`,
89
+ normalizedEndpoint,
90
+ }
91
+ }
92
+
93
+ async function checkAnthropic(apiKey: string, modelRaw: string): Promise<{ ok: boolean; message: string }> {
94
+ const model = modelRaw || 'claude-sonnet-4-6'
95
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
96
+ method: 'POST',
97
+ headers: {
98
+ 'x-api-key': apiKey,
99
+ 'anthropic-version': '2023-06-01',
100
+ 'content-type': 'application/json',
101
+ },
102
+ body: JSON.stringify({
103
+ model,
104
+ max_tokens: 12,
105
+ messages: [{ role: 'user', content: 'Reply with ANTHROPIC_SETUP_OK' }],
106
+ }),
107
+ signal: AbortSignal.timeout(15_000),
108
+ cache: 'no-store',
109
+ })
110
+ if (!res.ok) {
111
+ const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
112
+ return { ok: false, message: detail }
113
+ }
114
+ const payload = await res.json().catch(() => ({} as any))
115
+ const text = typeof payload?.content?.[0]?.text === 'string' ? payload.content[0].text : ''
116
+ return { ok: true, message: text ? `Connected to Anthropic. Sample: ${text.slice(0, 120)}` : 'Connected to Anthropic.' }
117
+ }
118
+
119
+ async function checkOllama(endpointRaw: string): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; recommendedModel?: string }> {
120
+ const normalizedEndpoint = (endpointRaw || 'http://localhost:11434').replace(/\/+$/, '')
121
+ const res = await fetch(`${normalizedEndpoint}/api/tags`, {
122
+ signal: AbortSignal.timeout(8_000),
123
+ cache: 'no-store',
124
+ })
125
+ if (!res.ok) {
126
+ const detail = await parseErrorMessage(res, `Ollama returned ${res.status}.`)
127
+ return { ok: false, message: detail, normalizedEndpoint }
128
+ }
129
+ const payload = await res.json().catch(() => ({} as any))
130
+ const models = Array.isArray(payload?.models) ? payload.models : []
131
+ const firstModel = typeof models[0]?.name === 'string'
132
+ ? String(models[0].name).replace(/:latest$/, '')
133
+ : undefined
134
+ if (models.length === 0) {
135
+ return {
136
+ ok: true,
137
+ message: 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
138
+ normalizedEndpoint,
139
+ }
140
+ }
141
+ return {
142
+ ok: true,
143
+ message: `Connected to Ollama. ${models.length} model(s) available.`,
144
+ normalizedEndpoint,
145
+ recommendedModel: firstModel,
146
+ }
147
+ }
148
+
149
+ function normalizeOpenClawUrl(raw: string): { httpUrl: string; wsUrl: string } {
150
+ let url = (raw || 'http://localhost:18789').replace(/\/+$/, '')
151
+ if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
152
+ const httpUrl = url.replace(/^ws:/i, 'http:').replace(/^wss:/i, 'https:')
153
+ const wsUrl = httpUrl.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
154
+ return { httpUrl, wsUrl }
155
+ }
156
+
157
+ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; deviceId?: string; errorCode?: string }> {
158
+ const { httpUrl: normalizedEndpoint, wsUrl } = normalizeOpenClawUrl(endpointRaw)
159
+ const token = apiKey || undefined
160
+ const deviceId = getDeviceId()
161
+
162
+ // Always connect with device auth — the gateway may accept token-only for
163
+ // the connect handshake but still require device identity for agent ops.
164
+ const result = await attemptOpenClawConnect(wsUrl, token, true)
165
+ if (result.ok) {
166
+ return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId }
167
+ }
168
+ if (result.errorCode === 'PAIRING_REQUIRED') {
169
+ return {
170
+ ok: false,
171
+ errorCode: 'PAIRING_REQUIRED',
172
+ message: 'Device needs approval on your gateway.',
173
+ normalizedEndpoint,
174
+ deviceId,
175
+ }
176
+ }
177
+ if (result.errorCode === 'DEVICE_AUTH_INVALID') {
178
+ return {
179
+ ok: false,
180
+ errorCode: 'DEVICE_AUTH_INVALID',
181
+ message: 'Device signature rejected. Your gateway may be running a different protocol version.',
182
+ normalizedEndpoint,
183
+ deviceId,
184
+ }
185
+ }
186
+ return { ok: false, message: result.message, normalizedEndpoint, deviceId, errorCode: result.errorCode }
187
+ }
188
+
189
+ function attemptOpenClawConnect(
190
+ wsUrl: string,
191
+ token: string | undefined,
192
+ useDeviceAuth: boolean,
193
+ ): Promise<{ ok: boolean; message: string; errorCode?: string }> {
194
+ return new Promise((resolve) => {
195
+ let settled = false
196
+ const done = (result: { ok: boolean; message: string; errorCode?: string }) => {
197
+ if (settled) return
198
+ settled = true
199
+ clearTimeout(timer)
200
+ try { ws.close() } catch {}
201
+ resolve(result)
202
+ }
203
+
204
+ const timer = setTimeout(() => {
205
+ done({ ok: false, message: 'Connection timed out. Verify the gateway URL and network access.' })
206
+ }, 10_000)
207
+
208
+ const ws = new WebSocket(wsUrl)
209
+ let connectId: string | null = null
210
+
211
+ ws.on('message', (data) => {
212
+ try {
213
+ const msg = JSON.parse(data.toString())
214
+ if (msg.event === 'connect.challenge') {
215
+ connectId = randomUUID()
216
+ ws.send(JSON.stringify({
217
+ type: 'req',
218
+ id: connectId,
219
+ method: 'connect',
220
+ params: buildOpenClawConnectParams(token, msg.payload?.nonce, { useDeviceAuth }),
221
+ }))
222
+ return
223
+ }
224
+ if (msg.type === 'res' && msg.id === connectId) {
225
+ if (msg.ok) {
226
+ done({ ok: true, message: 'Connected.' })
227
+ } else {
228
+ const message = msg.error?.message || 'Gateway rejected the connection.'
229
+ // Extract error code from structured field or infer from message text
230
+ let errorCode = (msg.error?.details?.code ?? msg.error?.code) as string | undefined
231
+ if (!errorCode) {
232
+ const m = message.toLowerCase()
233
+ if (m.includes('pairing') || m.includes('not paired') || m.includes('pending approval')) errorCode = 'PAIRING_REQUIRED'
234
+ else if (m.includes('signature') || m.includes('device') || m.includes('identity')) errorCode = 'DEVICE_AUTH_INVALID'
235
+ }
236
+ done({ ok: false, message, errorCode })
237
+ }
238
+ return
239
+ }
240
+ } catch {
241
+ done({ ok: false, message: 'Unexpected response from gateway.' })
242
+ }
243
+ })
244
+
245
+ ws.on('error', (err) => {
246
+ done({ ok: false, message: `Connection failed: ${err.message}` })
247
+ })
248
+
249
+ ws.on('close', (code, reason) => {
250
+ if (code === 1008) {
251
+ done({ ok: false, message: `Unauthorized: ${reason?.toString() || 'invalid token'}` })
252
+ } else {
253
+ done({ ok: false, message: `Connection closed (${code})` })
254
+ }
255
+ })
256
+ })
257
+ }
258
+
259
+ export async function POST(req: Request) {
260
+ const body = parseBody(await req.json().catch(() => ({})))
261
+ const provider = clean(body.provider) as SetupProvider
262
+ let apiKey = clean(body.apiKey)
263
+ const credentialId = clean(body.credentialId)
264
+ const endpoint = clean(body.endpoint)
265
+ const model = clean(body.model)
266
+
267
+ // Resolve credentialId to an API key if no raw key was provided
268
+ if (!apiKey && credentialId) {
269
+ try {
270
+ const creds = loadCredentials()
271
+ const cred = creds[credentialId]
272
+ if (cred?.encryptedKey) {
273
+ apiKey = decryptKey(cred.encryptedKey)
274
+ }
275
+ } catch {
276
+ return NextResponse.json({ ok: false, message: 'Failed to decrypt credential.' }, { status: 500 })
277
+ }
278
+ }
279
+
280
+ if (!provider) {
281
+ return NextResponse.json({ ok: false, message: 'Provider is required.' }, { status: 400 })
282
+ }
283
+
284
+ try {
285
+ switch (provider) {
286
+ case 'openai': {
287
+ if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenAI API key is required.' })
288
+ const info = OPENAI_COMPATIBLE_PROVIDER_INFO.openai
289
+ const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
290
+ return NextResponse.json(result)
291
+ }
292
+ case 'anthropic': {
293
+ if (!apiKey) return NextResponse.json({ ok: false, message: 'Anthropic API key is required.' })
294
+ const result = await checkAnthropic(apiKey, model)
295
+ return NextResponse.json(result)
296
+ }
297
+ case 'google':
298
+ case 'deepseek':
299
+ case 'groq':
300
+ case 'together':
301
+ case 'mistral':
302
+ case 'xai':
303
+ case 'fireworks': {
304
+ const info = OPENAI_COMPATIBLE_PROVIDER_INFO[provider]
305
+ if (!apiKey) return NextResponse.json({ ok: false, message: `${info.name} API key is required.` })
306
+ const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
307
+ return NextResponse.json(result)
308
+ }
309
+ case 'ollama': {
310
+ const result = await checkOllama(endpoint)
311
+ return NextResponse.json(result)
312
+ }
313
+ case 'openclaw': {
314
+ const result = await checkOpenClaw(apiKey, endpoint)
315
+ return NextResponse.json(result)
316
+ }
317
+ default:
318
+ return NextResponse.json({ ok: false, message: `Unsupported provider: ${provider}` }, { status: 400 })
319
+ }
320
+ } catch (err: any) {
321
+ const message = err?.name === 'TimeoutError'
322
+ ? 'Connection check timed out. Verify endpoint/network and try again.'
323
+ : (err?.message || 'Failed to validate provider setup.')
324
+ return NextResponse.json({ ok: false, message }, { status: 500 })
325
+ }
326
+ }