@swarmclawai/swarmclaw 0.7.7 → 0.8.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 (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -2,17 +2,19 @@ import { NextResponse } from 'next/server'
2
2
  import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { getSessionRunState } from '@/lib/server/session-run-manager'
5
- import { pruneStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
5
+ import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
6
+ import { appendSessionNote } from '@/lib/server/session-note'
7
+ import type { Message, Session } from '@/types'
6
8
 
7
9
  export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
10
  const { id } = await params
9
- const session = loadStoredItem('sessions', id)
11
+ const session = loadStoredItem('sessions', id) as Session | null
10
12
  if (!session) return notFound()
11
13
  session.messages = Array.isArray(session.messages) ? session.messages : []
12
14
 
13
15
  const run = getSessionRunState(id)
14
16
  const hasLiveRun = active.has(id) || !!run.runningRunId
15
- if (!hasLiveRun && pruneStreamingAssistantArtifacts(session.messages)) {
17
+ if (!hasLiveRun && materializeStreamingAssistantArtifacts(session.messages)) {
16
18
  upsertStoredItem('sessions', id, session)
17
19
  }
18
20
 
@@ -46,27 +48,49 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
46
48
 
47
49
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
48
50
  const { id } = await params
49
- const body = await req.json() as { kind?: string }
50
- if (body.kind !== 'context-clear') {
51
- return NextResponse.json({ error: 'Only context-clear kind is supported' }, { status: 400 })
51
+ const body = await req.json() as {
52
+ kind?: string
53
+ role?: Message['role']
54
+ text?: string
55
+ messageKind?: Message['kind']
52
56
  }
53
- const session = loadStoredItem('sessions', id)
54
- if (!session) return notFound()
55
57
 
56
- session.messages.push({
57
- role: 'user',
58
- text: '',
59
- kind: 'context-clear',
60
- time: Date.now(),
61
- })
62
- upsertStoredItem('sessions', id, session)
63
- return NextResponse.json({ ok: true })
58
+ if (body.kind === 'context-clear') {
59
+ const session = loadStoredItem('sessions', id) as Session | null
60
+ if (!session) return notFound()
61
+
62
+ session.messages.push({
63
+ role: 'user',
64
+ text: '',
65
+ kind: 'context-clear',
66
+ time: Date.now(),
67
+ })
68
+ upsertStoredItem('sessions', id, session)
69
+ return NextResponse.json({ ok: true })
70
+ }
71
+
72
+ if (body.kind === 'note') {
73
+ const inserted = appendSessionNote({
74
+ sessionId: id,
75
+ text: body.text || '',
76
+ role: body.role || 'assistant',
77
+ kind: body.messageKind || 'system',
78
+ })
79
+ if (!inserted) {
80
+ const session = loadStoredItem('sessions', id) as Session | null
81
+ if (!session) return notFound()
82
+ return NextResponse.json({ error: 'Note text is required' }, { status: 400 })
83
+ }
84
+ return NextResponse.json(inserted)
85
+ }
86
+
87
+ return NextResponse.json({ error: 'Only context-clear and note kinds are supported' }, { status: 400 })
64
88
  }
65
89
 
66
90
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
67
91
  const { id } = await params
68
92
  const body = await req.json() as { messageIndex: number; bookmarked: boolean }
69
- const session = loadStoredItem('sessions', id)
93
+ const session = loadStoredItem('sessions', id) as Session | null
70
94
  if (!session) return notFound()
71
95
 
72
96
  const { messageIndex, bookmarked } = body
@@ -82,7 +106,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
82
106
  export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
83
107
  const { id } = await params
84
108
  const body = await req.json() as { messageIndex: number }
85
- const session = loadStoredItem('sessions', id)
109
+ const session = loadStoredItem('sessions', id) as Session | null
86
110
  if (!session) return notFound()
87
111
 
88
112
  const { messageIndex } = body
@@ -111,7 +111,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
111
111
  const sessions = loadSessions()
112
112
  if (!sessions[id]) return notFound()
113
113
  if (active.has(id)) {
114
- try { active.get(id).kill() } catch {}
114
+ try { active.get(id)?.kill() } catch {}
115
115
  active.delete(id)
116
116
  }
117
117
  deleteSession(id)
@@ -1,17 +1,18 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { pruneStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
2
+ import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
3
3
  import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
4
4
  import { cancelSessionRuns } from '@/lib/server/session-run-manager'
5
+ import type { Session } from '@/types'
5
6
 
6
7
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
8
  const { id } = await params
8
9
  const cancel = cancelSessionRuns(id, 'Stopped by user')
9
- const session = loadStoredItem('sessions', id)
10
- if (session && Array.isArray(session.messages) && pruneStreamingAssistantArtifacts(session.messages)) {
10
+ const session = loadStoredItem('sessions', id) as Session | null
11
+ if (session && Array.isArray(session.messages) && materializeStreamingAssistantArtifacts(session.messages)) {
11
12
  upsertStoredItem('sessions', id, session)
12
13
  }
13
14
  if (active.has(id)) {
14
- try { active.get(id).kill() } catch {}
15
+ try { active.get(id)?.kill() } catch {}
15
16
  active.delete(id)
16
17
  }
17
18
  return NextResponse.json({ ok: true, ...cancel })
@@ -2,22 +2,37 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
- import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
5
+ import { loadSessions, saveSessions, deleteSession, active, loadAgents, upsertStoredItem } from '@/lib/server/storage'
6
6
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import { getSessionRunState } from '@/lib/server/session-run-manager'
9
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
10
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
11
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
12
+ import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
13
+ import { ensureDaemonStarted } from '@/lib/server/daemon-state'
11
14
  export const dynamic = 'force-dynamic'
12
15
 
13
16
 
14
17
  export async function GET(req: Request) {
18
+ ensureDaemonStarted('api/chats:get')
15
19
  const sessions = loadSessions()
20
+ const changedSessionIds: string[] = []
16
21
  for (const id of Object.keys(sessions)) {
17
22
  const run = getSessionRunState(id)
18
23
  sessions[id].active = active.has(id) || !!run.runningRunId
19
24
  sessions[id].queuedCount = run.queueLength
20
25
  sessions[id].currentRunId = run.runningRunId || null
26
+ if (!sessions[id].active && Array.isArray(sessions[id].messages)) {
27
+ if (materializeStreamingAssistantArtifacts(sessions[id].messages)) changedSessionIds.push(id)
28
+ }
29
+ }
30
+ for (const id of changedSessionIds) {
31
+ const persisted = { ...sessions[id] } as Record<string, unknown>
32
+ delete persisted.active
33
+ delete persisted.queuedCount
34
+ delete persisted.currentRunId
35
+ upsertStoredItem('sessions', id, persisted)
21
36
  }
22
37
 
23
38
  const { searchParams } = new URL(req.url)
@@ -32,6 +47,7 @@ export async function GET(req: Request) {
32
47
  }
33
48
 
34
49
  export async function DELETE(req: Request) {
50
+ ensureDaemonStarted('api/chats:delete')
35
51
  const { ids } = await req.json().catch(() => ({ ids: [] })) as { ids: string[] }
36
52
  if (!Array.isArray(ids) || !ids.length) {
37
53
  return new NextResponse('Missing ids', { status: 400 })
@@ -41,7 +57,7 @@ export async function DELETE(req: Request) {
41
57
  for (const id of ids) {
42
58
  if (!sessions[id]) continue
43
59
  if (active.has(id)) {
44
- try { active.get(id).kill() } catch {}
60
+ try { active.get(id)?.kill() } catch {}
45
61
  active.delete(id)
46
62
  }
47
63
  deleteSession(id)
@@ -52,6 +68,7 @@ export async function DELETE(req: Request) {
52
68
  }
53
69
 
54
70
  export async function POST(req: Request) {
71
+ ensureDaemonStarted('api/chats:post')
55
72
  const body = await req.json().catch(() => ({}))
56
73
  let cwd = (body.cwd || '').trim()
57
74
  if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
@@ -61,6 +78,9 @@ export async function POST(req: Request) {
61
78
  const id = body.id || genId()
62
79
  const sessions = loadSessions()
63
80
  const agent = body.agentId ? loadAgents()[body.agentId] : null
81
+ if (isAgentDisabled(agent)) {
82
+ return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'start chats') }, { status: 409 })
83
+ }
64
84
  const routePreferredGatewayTags = Array.isArray(body.routePreferredGatewayTags)
65
85
  ? body.routePreferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
66
86
  : []
@@ -101,6 +121,7 @@ export async function POST(req: Request) {
101
121
  claudeCode: null,
102
122
  codex: null,
103
123
  opencode: null,
124
+ gemini: null,
104
125
  },
105
126
  messages: Array.isArray(body.messages) ? body.messages : [],
106
127
  createdAt: Date.now(), lastActiveAt: Date.now(),
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { fetchSkillContent } from '@/lib/server/clawhub-client'
5
+ import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
6
 
6
7
  export async function POST(req: Request) {
7
8
  const body = await req.json()
@@ -19,18 +20,37 @@ export async function POST(req: Request) {
19
20
  }
20
21
  }
21
22
 
23
+ const normalized = normalizeSkillPayload({
24
+ name,
25
+ description,
26
+ content,
27
+ sourceUrl: url,
28
+ author,
29
+ tags,
30
+ })
31
+
22
32
  const skills = loadSkills()
23
33
  const id = genId()
24
34
  skills[id] = {
25
35
  id,
26
- name,
27
- filename: `skill-${id}.md`,
28
- content,
29
- description: description || '',
30
- sourceFormat: 'openclaw',
31
- sourceUrl: url,
32
- author: author || '',
33
- tags: tags || [],
36
+ name: normalized.name,
37
+ filename: normalized.filename || `skill-${id}.md`,
38
+ content: normalized.content,
39
+ description: normalized.description || '',
40
+ sourceFormat: normalized.sourceFormat,
41
+ sourceUrl: normalized.sourceUrl,
42
+ author: normalized.author || '',
43
+ tags: normalized.tags || [],
44
+ version: normalized.version,
45
+ homepage: normalized.homepage,
46
+ primaryEnv: normalized.primaryEnv,
47
+ skillKey: normalized.skillKey,
48
+ always: normalized.always,
49
+ installOptions: normalized.installOptions,
50
+ skillRequirements: normalized.skillRequirements,
51
+ detectedEnvVars: normalized.detectedEnvVars,
52
+ security: normalized.security,
53
+ frontmatter: normalized.frontmatter,
34
54
  createdAt: Date.now(),
35
55
  updatedAt: Date.now(),
36
56
  }
@@ -2,8 +2,10 @@ import { NextResponse } from 'next/server'
2
2
  import { loadConnectors, saveConnectors, logActivity } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
+ import { ensureDaemonStarted } from '@/lib/server/daemon-state'
5
6
 
6
7
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ ensureDaemonStarted('api/connectors/[id]:get')
7
9
  const { id } = await params
8
10
  const connectors = loadConnectors()
9
11
  const connector = connectors[id]
@@ -11,8 +13,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
11
13
 
12
14
  // Merge runtime status, QR code, and presence
13
15
  try {
14
- const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence } = await import('@/lib/server/connectors/manager')
15
- connector.status = getConnectorStatus(id)
16
+ const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence, getReconnectState } = await import('@/lib/server/connectors/manager')
17
+ const runtimeStatus = getConnectorStatus(id)
18
+ connector.status = runtimeStatus === 'running'
19
+ ? 'running'
20
+ : connector.lastError
21
+ ? 'error'
22
+ : 'stopped'
23
+ const rState = getReconnectState(id)
24
+ if (rState) {
25
+ const ext = connector as unknown as Record<string, unknown>
26
+ ext.reconnectAttempts = rState.attempts
27
+ ext.nextRetryAt = rState.nextRetryAt
28
+ ext.reconnectError = rState.error
29
+ ext.reconnectExhausted = rState.exhausted
30
+ }
16
31
  const qr = getConnectorQR(id)
17
32
  if (qr) connector.qrDataUrl = qr
18
33
  connector.authenticated = isConnectorAuthenticated(id)
@@ -26,6 +41,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
26
41
  }
27
42
 
28
43
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
44
+ ensureDaemonStarted('api/connectors/[id]:put')
29
45
  const { id } = await params
30
46
  const body = await req.json()
31
47
  const connectors = loadConnectors()
@@ -38,12 +54,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
38
54
  try {
39
55
  const manager = await import('@/lib/server/connectors/manager')
40
56
  if (body.action === 'start') {
57
+ manager.clearReconnectState(id)
41
58
  await manager.startConnector(id)
42
59
  logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector started: "${connector.name}"` })
43
60
  } else if (body.action === 'stop') {
44
61
  await manager.stopConnector(id)
45
62
  logActivity({ entityType: 'connector', entityId: id, action: 'stopped', actor: 'user', summary: `Connector stopped: "${connector.name}"` })
46
63
  } else {
64
+ manager.clearReconnectState(id)
47
65
  await manager.repairConnector(id)
48
66
  logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector repaired: "${connector.name}"` })
49
67
  }
@@ -69,8 +87,33 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
69
87
 
70
88
  connectors[id] = connector
71
89
  saveConnectors(connectors)
90
+
91
+ try {
92
+ const manager = await import('@/lib/server/connectors/manager')
93
+ const wasRunning = manager.getConnectorStatus(id) === 'running'
94
+ const shouldStop = body.isEnabled === false
95
+ const shouldReload = wasRunning && (
96
+ body.name !== undefined
97
+ || body.agentId !== undefined
98
+ || body.chatroomId !== undefined
99
+ || body.credentialId !== undefined
100
+ || body.config !== undefined
101
+ || body.isEnabled !== undefined
102
+ )
103
+ const shouldStart = body.isEnabled === true && !wasRunning
104
+
105
+ if (shouldStop) {
106
+ await manager.stopConnector(id)
107
+ } else if (shouldReload || shouldStart) {
108
+ manager.clearReconnectState(id)
109
+ await manager.startConnector(id)
110
+ }
111
+ } catch {
112
+ // Keep the saved connector update even if the runtime reload fails.
113
+ }
114
+
72
115
  notify('connectors')
73
- return NextResponse.json(connector)
116
+ return NextResponse.json(loadConnectors()[id] || connector)
74
117
  }
75
118
 
76
119
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -2,19 +2,26 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadConnectors, saveConnectors } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
+ import { ensureDaemonStarted } from '@/lib/server/daemon-state'
5
6
  import { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
7
  import { z } from 'zod'
7
8
  import type { Connector } from '@/types'
8
9
  export const dynamic = 'force-dynamic'
9
10
 
10
11
 
11
- export async function GET(_req: Request) {
12
+ export async function GET() {
13
+ ensureDaemonStarted('api/connectors:get')
12
14
  const connectors = loadConnectors()
13
15
  // Merge runtime status from manager
14
16
  try {
15
17
  const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
16
18
  for (const c of Object.values(connectors) as Connector[]) {
17
- c.status = getConnectorStatus(c.id)
19
+ const runtimeStatus = getConnectorStatus(c.id)
20
+ c.status = runtimeStatus === 'running'
21
+ ? 'running'
22
+ : c.lastError
23
+ ? 'error'
24
+ : 'stopped'
18
25
  if (c.platform === 'whatsapp') {
19
26
  c.authenticated = isConnectorAuthenticated(c.id)
20
27
  c.hasCredentials = hasConnectorCredentials(c.id)
@@ -28,6 +35,7 @@ export async function GET(_req: Request) {
28
35
  ext.reconnectAttempts = rState.attempts
29
36
  ext.nextRetryAt = rState.nextRetryAt
30
37
  ext.reconnectError = rState.error
38
+ ext.reconnectExhausted = rState.exhausted
31
39
  }
32
40
  }
33
41
  } catch { /* manager not loaded yet */ }
@@ -35,6 +43,7 @@ export async function GET(_req: Request) {
35
43
  }
36
44
 
37
45
  export async function POST(req: Request) {
46
+ ensureDaemonStarted('api/connectors:post')
38
47
  const raw = await req.json()
39
48
  const parsed = ConnectorCreateSchema.safeParse(raw)
40
49
  if (!parsed.success) {
@@ -72,13 +81,8 @@ export async function POST(req: Request) {
72
81
  try {
73
82
  const { startConnector } = await import('@/lib/server/connectors/manager')
74
83
  await startConnector(id)
75
- connector.isEnabled = true
76
- connector.status = 'running'
77
- connectors[id] = connector
78
- saveConnectors(connectors)
79
- notify('connectors')
80
84
  } catch { /* auto-start is best-effort */ }
81
85
  }
82
86
 
83
- return NextResponse.json(connector)
87
+ return NextResponse.json(loadConnectors()[id] || connector)
84
88
  }
@@ -0,0 +1,165 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { GET as listExternalAgents, POST as registerExternalAgent } from './route'
5
+ import { POST as heartbeatExternalAgent } from './[id]/heartbeat/route'
6
+ import { PUT as mutateExternalAgent, DELETE as deleteExternalAgent } from './[id]/route'
7
+ import {
8
+ loadExternalAgents,
9
+ loadGatewayProfiles,
10
+ saveExternalAgents,
11
+ saveGatewayProfiles,
12
+ } from '@/lib/server/storage'
13
+
14
+ const originalExternalAgents = loadExternalAgents()
15
+ const originalGatewayProfiles = loadGatewayProfiles()
16
+
17
+ function routeParams(id: string) {
18
+ return { params: Promise.resolve({ id }) }
19
+ }
20
+
21
+ afterEach(() => {
22
+ saveExternalAgents(originalExternalAgents)
23
+ saveGatewayProfiles(originalGatewayProfiles)
24
+ })
25
+
26
+ test('external agent register + heartbeat derives gateway metadata in listing', async () => {
27
+ const gateways = loadGatewayProfiles()
28
+ gateways['gateway-ext-test'] = {
29
+ id: 'gateway-ext-test',
30
+ name: 'Gateway Test',
31
+ provider: 'openclaw',
32
+ endpoint: 'http://127.0.0.1:19999/v1',
33
+ wsUrl: 'ws://127.0.0.1:19999',
34
+ credentialId: null,
35
+ status: 'healthy',
36
+ notes: null,
37
+ tags: ['lan-remote', 'smoke'],
38
+ lastError: null,
39
+ lastCheckedAt: null,
40
+ lastModelCount: 1,
41
+ discoveredHost: '127.0.0.1',
42
+ discoveredPort: 19999,
43
+ deployment: {
44
+ method: 'imported',
45
+ managedBy: 'external',
46
+ useCase: 'single-vps',
47
+ exposure: 'private-lan',
48
+ targetHost: '127.0.0.1',
49
+ },
50
+ stats: null,
51
+ isDefault: false,
52
+ createdAt: Date.now(),
53
+ updatedAt: Date.now(),
54
+ }
55
+ saveGatewayProfiles(gateways)
56
+
57
+ const registerResponse = await registerExternalAgent(new Request('http://local/api/external-agents/register', {
58
+ method: 'POST',
59
+ headers: { 'content-type': 'application/json' },
60
+ body: JSON.stringify({
61
+ id: 'runtime-ext-test',
62
+ name: 'External Runtime',
63
+ sourceType: 'openclaw',
64
+ transport: 'gateway',
65
+ endpoint: 'http://127.0.0.1:19999/v1',
66
+ agentId: 'agent-ext-test',
67
+ gatewayProfileId: 'gateway-ext-test',
68
+ }),
69
+ }))
70
+ assert.equal(registerResponse.status, 200)
71
+
72
+ const heartbeatResponse = await heartbeatExternalAgent(new Request('http://local/api/external-agents/runtime-ext-test/heartbeat', {
73
+ method: 'POST',
74
+ headers: { 'content-type': 'application/json' },
75
+ body: JSON.stringify({
76
+ status: 'online',
77
+ lastHealthNote: 'Heartbeat OK',
78
+ version: '1.2.3',
79
+ tokenStats: { inputTokens: 5, outputTokens: 7, totalTokens: 12 },
80
+ }),
81
+ }), routeParams('runtime-ext-test'))
82
+ assert.equal(heartbeatResponse.status, 200)
83
+
84
+ const listResponse = await listExternalAgents()
85
+ assert.equal(listResponse.status, 200)
86
+ const listPayload = await listResponse.json() as Array<Record<string, unknown>>
87
+ const runtime = listPayload.find((item) => item.id === 'runtime-ext-test')
88
+
89
+ assert.ok(runtime)
90
+ assert.equal(runtime?.status, 'online')
91
+ assert.equal(runtime?.gatewayUseCase, 'single-vps')
92
+ assert.deepEqual(runtime?.gatewayTags, ['lan-remote', 'smoke'])
93
+ assert.equal(runtime?.lastHealthNote, 'Heartbeat OK')
94
+ assert.equal((runtime?.tokenStats as { totalTokens?: number } | undefined)?.totalTokens, 12)
95
+ })
96
+
97
+ test('external agent lifecycle actions update state and delete removes the runtime', async () => {
98
+ const items = loadExternalAgents()
99
+ items['runtime-lifecycle-test'] = {
100
+ id: 'runtime-lifecycle-test',
101
+ name: 'Lifecycle Runtime',
102
+ sourceType: 'openclaw',
103
+ status: 'online',
104
+ provider: 'openclaw',
105
+ model: 'default',
106
+ workspace: null,
107
+ transport: 'gateway',
108
+ endpoint: 'http://127.0.0.1:18888/v1',
109
+ agentId: 'agent-lifecycle-test',
110
+ gatewayProfileId: null,
111
+ capabilities: [],
112
+ labels: [],
113
+ lifecycleState: 'active',
114
+ gatewayTags: [],
115
+ gatewayUseCase: null,
116
+ version: null,
117
+ lastHealthNote: null,
118
+ metadata: null,
119
+ tokenStats: null,
120
+ lastHeartbeatAt: Date.now(),
121
+ lastSeenAt: Date.now(),
122
+ createdAt: Date.now(),
123
+ updatedAt: Date.now(),
124
+ }
125
+ saveExternalAgents(items)
126
+
127
+ const drainResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
128
+ method: 'PUT',
129
+ headers: { 'content-type': 'application/json' },
130
+ body: JSON.stringify({ action: 'drain' }),
131
+ }), routeParams('runtime-lifecycle-test'))
132
+ const drainPayload = await drainResponse.json() as Record<string, unknown>
133
+ assert.equal(drainPayload.lifecycleState, 'draining')
134
+
135
+ const cordonResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
136
+ method: 'PUT',
137
+ headers: { 'content-type': 'application/json' },
138
+ body: JSON.stringify({ action: 'cordon' }),
139
+ }), routeParams('runtime-lifecycle-test'))
140
+ const cordonPayload = await cordonResponse.json() as Record<string, unknown>
141
+ assert.equal(cordonPayload.lifecycleState, 'cordoned')
142
+
143
+ const restartResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
144
+ method: 'PUT',
145
+ headers: { 'content-type': 'application/json' },
146
+ body: JSON.stringify({ action: 'restart' }),
147
+ }), routeParams('runtime-lifecycle-test'))
148
+ const restartPayload = await restartResponse.json() as Record<string, unknown>
149
+ assert.equal((restartPayload.metadata as { controlRequest?: { action?: string } } | undefined)?.controlRequest?.action, 'restart')
150
+
151
+ const activateResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
152
+ method: 'PUT',
153
+ headers: { 'content-type': 'application/json' },
154
+ body: JSON.stringify({ action: 'activate' }),
155
+ }), routeParams('runtime-lifecycle-test'))
156
+ const activatePayload = await activateResponse.json() as Record<string, unknown>
157
+ assert.equal(activatePayload.lifecycleState, 'active')
158
+
159
+ const deleteResponse = await deleteExternalAgent(
160
+ new Request('http://local/api/external-agents/runtime-lifecycle-test', { method: 'DELETE' }),
161
+ routeParams('runtime-lifecycle-test'),
162
+ )
163
+ assert.equal(deleteResponse.status, 200)
164
+ assert.equal(loadExternalAgents()['runtime-lifecycle-test'], undefined)
165
+ })
@@ -3,34 +3,49 @@ import { probeOpenClawHealth } from '@/lib/server/openclaw-health'
3
3
  import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { notify } from '@/lib/server/ws-hub'
6
+ import type { GatewayProfile } from '@/types'
7
+ import type { OpenClawHealthResult } from '@/lib/server/openclaw-health'
6
8
  export const dynamic = 'force-dynamic'
7
9
 
8
- export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
- const { id } = await params
10
+ export function persistGatewayHealthResult(
11
+ id: string,
12
+ result: OpenClawHealthResult,
13
+ now = Date.now(),
14
+ ): GatewayProfile | null {
10
15
  const gateways = loadGatewayProfiles()
11
16
  const gateway = gateways[id]
12
- if (!gateway) return notFound()
13
-
14
- const result = await probeOpenClawHealth({
15
- endpoint: gateway.endpoint,
16
- credentialId: gateway.credentialId || null,
17
- })
17
+ if (!gateway) return null
18
18
 
19
19
  gateway.status = result.ok ? 'healthy' : (result.authProvided ? 'degraded' : 'offline')
20
- gateway.lastCheckedAt = Date.now()
20
+ gateway.lastCheckedAt = now
21
21
  gateway.lastError = result.ok ? null : (result.error || result.hint || 'Gateway health check failed.')
22
22
  gateway.lastModelCount = Array.isArray(result.models) ? result.models.length : 0
23
23
  gateway.deployment = {
24
24
  ...(gateway.deployment || {}),
25
- lastVerifiedAt: Date.now(),
25
+ lastVerifiedAt: now,
26
26
  lastVerifiedOk: result.ok,
27
27
  lastVerifiedMessage: result.ok
28
- ? `Verified ${Array.isArray(result.models) ? result.models.length : 0} model${result.models?.length === 1 ? '' : 's'}`
28
+ ? result.message
29
29
  : (result.error || result.hint || 'Gateway health check failed.'),
30
30
  }
31
- gateway.updatedAt = Date.now()
31
+ gateway.updatedAt = now
32
32
  saveGatewayProfiles(gateways)
33
33
  notify('gateways')
34
+ return gateway
35
+ }
36
+
37
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
38
+ const { id } = await params
39
+ const gateways = loadGatewayProfiles()
40
+ const gateway = gateways[id]
41
+ if (!gateway) return notFound()
42
+
43
+ const result = await probeOpenClawHealth({
44
+ endpoint: gateway.endpoint,
45
+ credentialId: gateway.credentialId || null,
46
+ })
47
+
48
+ persistGatewayHealthResult(id, result)
34
49
 
35
50
  return NextResponse.json(result)
36
51
  }
@@ -35,6 +35,8 @@ function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
35
35
  useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
36
36
  exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
37
37
  managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
38
+ localInstanceId: normalizeText(deployment.localInstanceId),
39
+ localPort: normalizeNullableNumber(deployment.localPort),
38
40
  targetHost: normalizeText(deployment.targetHost),
39
41
  sshHost: normalizeText(deployment.sshHost),
40
42
  sshUser: normalizeText(deployment.sshUser),