@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
@@ -0,0 +1,135 @@
1
+ import assert from 'node:assert/strict'
2
+ import { afterEach, test } from 'node:test'
3
+
4
+ import { persistGatewayHealthResult } from './[id]/health/route'
5
+ import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
6
+
7
+ const originalGateways = loadGatewayProfiles()
8
+
9
+ afterEach(() => {
10
+ saveGatewayProfiles(originalGateways)
11
+ })
12
+
13
+ test('persistGatewayHealthResult stores healthy verification details', () => {
14
+ const gateways = loadGatewayProfiles()
15
+ gateways['gateway-health-test'] = {
16
+ id: 'gateway-health-test',
17
+ name: 'Gateway Health Test',
18
+ provider: 'openclaw',
19
+ endpoint: 'http://127.0.0.1:18789/v1',
20
+ wsUrl: 'ws://127.0.0.1:18789',
21
+ credentialId: 'credential-1',
22
+ status: 'unknown',
23
+ notes: null,
24
+ tags: ['smoke'],
25
+ lastError: 'previous error',
26
+ lastCheckedAt: null,
27
+ lastModelCount: null,
28
+ discoveredHost: '127.0.0.1',
29
+ discoveredPort: 18789,
30
+ deployment: {
31
+ method: 'imported',
32
+ useCase: 'single-vps',
33
+ },
34
+ stats: null,
35
+ isDefault: false,
36
+ createdAt: 1,
37
+ updatedAt: 1,
38
+ }
39
+ saveGatewayProfiles(gateways)
40
+
41
+ const now = 1_777_777_777_000
42
+ const updated = persistGatewayHealthResult('gateway-health-test', {
43
+ ok: true,
44
+ endpoint: 'http://127.0.0.1:18789/v1',
45
+ wsUrl: 'ws://127.0.0.1:18789',
46
+ wsConnected: true,
47
+ httpCompatible: true,
48
+ authProvided: true,
49
+ model: 'default',
50
+ models: ['default', 'glm-5:cloud'],
51
+ modelsStatus: 200,
52
+ chatStatus: 200,
53
+ message: 'Gateway verified successfully.',
54
+ }, now)
55
+
56
+ const saved = loadGatewayProfiles()['gateway-health-test']
57
+ assert.ok(updated)
58
+ assert.equal(saved.status, 'healthy')
59
+ assert.equal(saved.lastCheckedAt, now)
60
+ assert.equal(saved.lastError, null)
61
+ assert.equal(saved.lastModelCount, 2)
62
+ assert.equal(saved.updatedAt, now)
63
+ assert.equal(saved.deployment?.method, 'imported')
64
+ assert.equal(saved.deployment?.lastVerifiedAt, now)
65
+ assert.equal(saved.deployment?.lastVerifiedOk, true)
66
+ assert.equal(saved.deployment?.lastVerifiedMessage, 'Gateway verified successfully.')
67
+ })
68
+
69
+ test('persistGatewayHealthResult stores degraded/offline failures with the right message', () => {
70
+ const gateways = loadGatewayProfiles()
71
+ gateways['gateway-health-failure-test'] = {
72
+ id: 'gateway-health-failure-test',
73
+ name: 'Gateway Health Failure Test',
74
+ provider: 'openclaw',
75
+ endpoint: 'http://127.0.0.1:18888/v1',
76
+ wsUrl: 'ws://127.0.0.1:18888',
77
+ credentialId: null,
78
+ status: 'unknown',
79
+ notes: null,
80
+ tags: [],
81
+ lastError: null,
82
+ lastCheckedAt: null,
83
+ lastModelCount: null,
84
+ discoveredHost: '127.0.0.1',
85
+ discoveredPort: 18888,
86
+ deployment: null,
87
+ stats: null,
88
+ isDefault: false,
89
+ createdAt: 1,
90
+ updatedAt: 1,
91
+ }
92
+ saveGatewayProfiles(gateways)
93
+
94
+ const degraded = persistGatewayHealthResult('gateway-health-failure-test', {
95
+ ok: false,
96
+ endpoint: 'http://127.0.0.1:18888/v1',
97
+ wsUrl: 'ws://127.0.0.1:18888',
98
+ wsConnected: false,
99
+ httpCompatible: null,
100
+ authProvided: true,
101
+ model: null,
102
+ models: [],
103
+ modelsStatus: null,
104
+ chatStatus: null,
105
+ message: 'ignored',
106
+ error: 'Token rejected.',
107
+ }, 2_000)
108
+ assert.ok(degraded)
109
+ let saved = loadGatewayProfiles()['gateway-health-failure-test']
110
+ assert.equal(saved.status, 'degraded')
111
+ assert.equal(saved.lastError, 'Token rejected.')
112
+ assert.equal(saved.deployment?.lastVerifiedOk, false)
113
+ assert.equal(saved.deployment?.lastVerifiedMessage, 'Token rejected.')
114
+
115
+ const offline = persistGatewayHealthResult('gateway-health-failure-test', {
116
+ ok: false,
117
+ endpoint: 'http://127.0.0.1:18888/v1',
118
+ wsUrl: 'ws://127.0.0.1:18888',
119
+ wsConnected: false,
120
+ httpCompatible: null,
121
+ authProvided: false,
122
+ model: null,
123
+ models: [],
124
+ modelsStatus: null,
125
+ chatStatus: null,
126
+ message: 'ignored',
127
+ hint: 'Gateway unreachable.',
128
+ }, 3_000)
129
+ assert.ok(offline)
130
+ saved = loadGatewayProfiles()['gateway-health-failure-test']
131
+ assert.equal(saved.status, 'offline')
132
+ assert.equal(saved.lastError, 'Gateway unreachable.')
133
+ assert.equal(saved.deployment?.lastVerifiedOk, false)
134
+ assert.equal(saved.deployment?.lastVerifiedMessage, 'Gateway unreachable.')
135
+ })
@@ -32,6 +32,8 @@ function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
32
32
  useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
33
33
  exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
34
34
  managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
35
+ localInstanceId: normalizeText(deployment.localInstanceId),
36
+ localPort: normalizeNullableNumber(deployment.localPort),
35
37
  targetHost: normalizeText(deployment.targetHost),
36
38
  sshHost: normalizeText(deployment.sshHost),
37
39
  sshUser: normalizeText(deployment.sshUser),
@@ -0,0 +1,130 @@
1
+ import assert from 'node:assert/strict'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import test, { afterEach } from 'node:test'
5
+
6
+ import { GET as getMcpServer, PUT as updateMcpServer, DELETE as deleteMcpServer } from './[id]/route'
7
+ import { POST as runMcpConformance } from './[id]/conformance/route'
8
+ import { POST as invokeMcpTool } from './[id]/invoke/route'
9
+ import { POST as testMcpServer } from './[id]/test/route'
10
+ import { GET as listMcpTools } from './[id]/tools/route'
11
+ import { GET as listMcpServers, POST as createMcpServer } from './route'
12
+ import { loadMcpServers, saveMcpServers } from '@/lib/server/storage'
13
+
14
+ const originalMcpServers = loadMcpServers()
15
+ const fixturePath = path.resolve(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '../../../lib/server/__fixtures__/fake-mcp-stdio-server.mjs',
18
+ )
19
+
20
+ function routeParams(id: string) {
21
+ return { params: Promise.resolve({ id }) }
22
+ }
23
+
24
+ afterEach(() => {
25
+ saveMcpServers(originalMcpServers)
26
+ })
27
+
28
+ test('MCP server routes exercise a live stdio server end to end', async () => {
29
+ const createResponse = await createMcpServer(new Request('http://local/api/mcp-servers', {
30
+ method: 'POST',
31
+ headers: { 'content-type': 'application/json' },
32
+ body: JSON.stringify({
33
+ name: 'smoke',
34
+ transport: 'stdio',
35
+ command: process.execPath,
36
+ args: [fixturePath],
37
+ }),
38
+ }))
39
+ assert.equal(createResponse.status, 200)
40
+ const created = await createResponse.json() as Record<string, unknown>
41
+ const serverId = String(created.id)
42
+ assert.equal(created.name, 'smoke')
43
+
44
+ const listResponse = await listMcpServers(new Request('http://local/api/mcp-servers'))
45
+ assert.equal(listResponse.status, 200)
46
+ const listed = await listResponse.json() as Record<string, Record<string, unknown>>
47
+ assert.equal(listed[serverId]?.name, 'smoke')
48
+
49
+ const detailResponse = await getMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`), routeParams(serverId))
50
+ assert.equal(detailResponse.status, 200)
51
+ const detail = await detailResponse.json() as Record<string, unknown>
52
+ assert.equal(detail.command, process.execPath)
53
+
54
+ const healthResponse = await testMcpServer(new Request(`http://local/api/mcp-servers/${serverId}/test`, {
55
+ method: 'POST',
56
+ }), routeParams(serverId))
57
+ assert.equal(healthResponse.status, 200)
58
+ const health = await healthResponse.json() as Record<string, unknown>
59
+ assert.equal(health.ok, true)
60
+ assert.deepEqual(health.tools, ['mcp_smoke_ping', 'mcp_smoke_echo'])
61
+
62
+ const toolsResponse = await listMcpTools(new Request(`http://local/api/mcp-servers/${serverId}/tools`), routeParams(serverId))
63
+ assert.equal(toolsResponse.status, 200)
64
+ const tools = await toolsResponse.json() as Array<Record<string, unknown>>
65
+ assert.deepEqual(tools.map((tool) => tool.name), ['ping', 'echo'])
66
+
67
+ const invokeResponse = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: JSON.stringify({
71
+ toolName: 'echo',
72
+ args: JSON.stringify({ message: 'hello from route test' }),
73
+ }),
74
+ }), routeParams(serverId))
75
+ assert.equal(invokeResponse.status, 200)
76
+ const invokePayload = await invokeResponse.json() as Record<string, unknown>
77
+ assert.equal(invokePayload.ok, true)
78
+ assert.equal(invokePayload.text, 'echo: hello from route test')
79
+
80
+ const conformanceResponse = await runMcpConformance(new Request(`http://local/api/mcp-servers/${serverId}/conformance`, {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/json' },
83
+ body: JSON.stringify({ smokeToolName: 'ping' }),
84
+ }), routeParams(serverId))
85
+ assert.equal(conformanceResponse.status, 200)
86
+ const conformance = await conformanceResponse.json() as Record<string, unknown>
87
+ assert.equal(conformance.ok, true)
88
+ assert.equal(conformance.toolsCount, 2)
89
+ assert.equal(conformance.smokeToolName, 'ping')
90
+
91
+ const updateResponse = await updateMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
92
+ method: 'PUT',
93
+ headers: { 'content-type': 'application/json' },
94
+ body: JSON.stringify({ name: 'smoke-renamed' }),
95
+ }), routeParams(serverId))
96
+ assert.equal(updateResponse.status, 200)
97
+ const updated = await updateResponse.json() as Record<string, unknown>
98
+ assert.equal(updated.name, 'smoke-renamed')
99
+
100
+ const deleteResponse = await deleteMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
101
+ method: 'DELETE',
102
+ }), routeParams(serverId))
103
+ assert.equal(deleteResponse.status, 200)
104
+ assert.equal(loadMcpServers()[serverId], undefined)
105
+ })
106
+
107
+ test('MCP invoke route validates required fields before connecting', async () => {
108
+ const serverId = 'mcp-validate-smoke'
109
+ const servers = loadMcpServers()
110
+ servers[serverId] = {
111
+ id: serverId,
112
+ name: 'smoke',
113
+ transport: 'stdio',
114
+ command: process.execPath,
115
+ args: [fixturePath],
116
+ createdAt: 1,
117
+ updatedAt: 1,
118
+ }
119
+ saveMcpServers(servers)
120
+
121
+ const response = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
122
+ method: 'POST',
123
+ headers: { 'content-type': 'application/json' },
124
+ body: JSON.stringify({ args: {} }),
125
+ }), routeParams(serverId))
126
+
127
+ assert.equal(response.status, 400)
128
+ const payload = await response.json() as Record<string, unknown>
129
+ assert.equal(payload.error, 'toolName is required')
130
+ })
@@ -1,6 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import {
3
3
  buildOpenClawDeployBundle,
4
+ getOpenClawLocalDeployCollectionStatus,
5
+ getOpenClawRemoteDeployCollectionStatus,
4
6
  deployOpenClawOverSsh,
5
7
  getOpenClawLocalDeployStatus,
6
8
  getOpenClawRemoteDeployStatus,
@@ -96,9 +98,15 @@ function parseSsh(value: unknown): Partial<OpenClawSshConfig> | null {
96
98
  }
97
99
 
98
100
  export async function GET() {
101
+ const locals = getOpenClawLocalDeployCollectionStatus()
102
+ const remotes = getOpenClawRemoteDeployCollectionStatus()
99
103
  return NextResponse.json({
100
- local: getOpenClawLocalDeployStatus(),
101
- remote: getOpenClawRemoteDeployStatus(),
104
+ local: getOpenClawLocalDeployStatus(locals.primaryId),
105
+ locals: locals.items,
106
+ localPrimaryId: locals.primaryId,
107
+ remote: remotes.primaryId ? getOpenClawRemoteDeployStatus(remotes.primaryId) : null,
108
+ remotes: remotes.items,
109
+ remotePrimaryId: remotes.primaryId,
102
110
  })
103
111
  }
104
112
 
@@ -109,31 +117,44 @@ export async function POST(req: Request) {
109
117
  try {
110
118
  if (action === 'start-local') {
111
119
  const result = await startOpenClawLocalDeploy({
120
+ localId: typeof body.localId === 'string' ? body.localId : null,
121
+ name: typeof body.name === 'string' ? body.name : null,
112
122
  port: parsePort(body.port),
113
123
  token: typeof body.token === 'string' ? body.token : null,
124
+ makePrimary: body.makePrimary !== false,
114
125
  })
115
126
  return NextResponse.json({
116
127
  ok: true,
117
128
  local: result.local,
129
+ locals: result.locals,
130
+ localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
118
131
  token: result.token,
119
132
  })
120
133
  }
121
134
 
122
135
  if (action === 'stop-local') {
136
+ const result = stopOpenClawLocalDeploy(typeof body.localId === 'string' ? body.localId : null)
123
137
  return NextResponse.json({
124
138
  ok: true,
125
- local: stopOpenClawLocalDeploy(),
139
+ local: result.local,
140
+ locals: result.locals,
141
+ localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
126
142
  })
127
143
  }
128
144
 
129
145
  if (action === 'restart-local') {
130
146
  const result = await restartOpenClawLocalDeploy({
147
+ localId: typeof body.localId === 'string' ? body.localId : null,
148
+ name: typeof body.name === 'string' ? body.name : null,
131
149
  port: parsePort(body.port),
132
150
  token: typeof body.token === 'string' ? body.token : null,
151
+ makePrimary: body.makePrimary !== false,
133
152
  })
134
153
  return NextResponse.json({
135
154
  ok: true,
136
155
  local: result.local,
156
+ locals: result.locals,
157
+ localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
137
158
  token: result.token,
138
159
  })
139
160
  }
@@ -157,6 +178,8 @@ export async function POST(req: Request) {
157
178
 
158
179
  if (action === 'ssh-deploy') {
159
180
  const result = await deployOpenClawOverSsh({
181
+ remoteId: typeof body.remoteId === 'string' ? body.remoteId : null,
182
+ name: typeof body.name === 'string' ? body.name : null,
160
183
  template: parseTemplate(body.template),
161
184
  target: typeof body.target === 'string' ? body.target : null,
162
185
  token: typeof body.token === 'string' ? body.token : null,
@@ -166,10 +189,14 @@ export async function POST(req: Request) {
166
189
  useCase: parseUseCase(body.useCase),
167
190
  exposure: parseExposure(body.exposure),
168
191
  ssh: parseSsh(body.ssh),
192
+ makePrimary: body.makePrimary !== false,
169
193
  })
194
+ const remotes = getOpenClawRemoteDeployCollectionStatus()
170
195
  return NextResponse.json({
171
196
  ok: result.ok,
172
- remote: getOpenClawRemoteDeployStatus(),
197
+ remote: getOpenClawRemoteDeployStatus(result.remoteId || remotes.primaryId),
198
+ remotes: remotes.items,
199
+ remotePrimaryId: remotes.primaryId,
173
200
  processId: result.processId || null,
174
201
  token: result.token,
175
202
  bundle: result.bundle,
@@ -198,14 +225,20 @@ export async function POST(req: Request) {
198
225
  } as const
199
226
  const lifecycleAction = action as keyof typeof actionMap
200
227
  const result = await runOpenClawRemoteLifecycle({
228
+ remoteId: typeof body.remoteId === 'string' ? body.remoteId : null,
229
+ name: typeof body.name === 'string' ? body.name : null,
201
230
  action: actionMap[lifecycleAction],
202
231
  ssh: parseSsh(body.ssh),
203
232
  token: typeof body.token === 'string' ? body.token : null,
204
233
  backupPath: typeof body.backupPath === 'string' ? body.backupPath : null,
234
+ makePrimary: body.makePrimary !== false,
205
235
  })
236
+ const remotes = getOpenClawRemoteDeployCollectionStatus()
206
237
  return NextResponse.json({
207
238
  ok: result.ok,
208
- remote: getOpenClawRemoteDeployStatus(),
239
+ remote: getOpenClawRemoteDeployStatus(result.remoteId || remotes.primaryId),
240
+ remotes: remotes.items,
241
+ remotePrimaryId: remotes.primaryId,
209
242
  processId: result.processId || null,
210
243
  token: result.token,
211
244
  summary: result.summary,
@@ -1,22 +1,61 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager, sanitizePluginFilename } from '@/lib/server/plugins'
3
+ import {
4
+ inferPluginInstallSourceFromUrl,
5
+ inferPluginPublisherSourceFromUrl,
6
+ normalizePluginInstallSource,
7
+ normalizePluginPublisherSource,
8
+ } from '@/lib/plugin-sources'
9
+ import {
10
+ buildPluginInstallCorsHeaders,
11
+ resolvePluginInstallCorsOrigin,
12
+ } from '@/lib/plugin-install-cors'
13
+
14
+ function json(body: Record<string, unknown>, status: number, origin: string | null) {
15
+ return NextResponse.json(body, {
16
+ status,
17
+ headers: buildPluginInstallCorsHeaders(origin),
18
+ })
19
+ }
20
+
21
+ export async function OPTIONS(req: Request) {
22
+ const origin = resolvePluginInstallCorsOrigin(req.headers.get('origin'))
23
+ if (!origin) return NextResponse.json({ error: 'Origin not allowed' }, { status: 403 })
24
+ return new NextResponse(null, {
25
+ status: 204,
26
+ headers: buildPluginInstallCorsHeaders(origin),
27
+ })
28
+ }
3
29
 
4
30
  export async function POST(req: Request) {
31
+ const origin = resolvePluginInstallCorsOrigin(req.headers.get('origin'))
5
32
  const body = await req.json()
6
33
  const url = typeof body?.url === 'string' ? body.url : ''
7
34
  const filename = typeof body?.filename === 'string' ? body.filename : ''
35
+ const installMethod = body?.installMethod === 'marketplace' ? 'marketplace' : 'manual'
36
+ const sourceLabel = normalizePluginPublisherSource(body?.sourceLabel)
37
+ || inferPluginPublisherSourceFromUrl(url)
38
+ || 'manual'
39
+ const installSource = normalizePluginInstallSource(body?.installSource)
40
+ || inferPluginInstallSourceFromUrl(url)
41
+ || 'manual'
8
42
 
9
43
  if (!url || !url.startsWith('https://')) {
10
- return NextResponse.json(
44
+ return json(
11
45
  { error: 'URL must be a valid HTTPS URL' },
12
- { status: 400 },
46
+ 400,
47
+ origin,
13
48
  )
14
49
  }
15
50
 
16
51
  try {
17
52
  const sanitizedFilename = sanitizePluginFilename(filename)
18
- const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename)
19
- return NextResponse.json({ ok: true, filename: installed.filename, hash: installed.sourceHash })
53
+ const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename, {
54
+ source: installMethod,
55
+ sourceLabel,
56
+ installSource,
57
+ })
58
+ return json({ ok: true, filename: installed.filename, hash: installed.sourceHash }, 200, origin)
20
59
  } catch (err: unknown) {
21
60
  const msg = err instanceof Error ? err.message : String(err)
22
61
  const isTimeout = /abort|timeout/i.test(msg)
@@ -25,9 +64,10 @@ export async function POST(req: Request) {
25
64
  : isTimeout
26
65
  ? 504
27
66
  : 500
28
- return NextResponse.json(
67
+ return json(
29
68
  { error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : msg },
30
- { status },
69
+ status,
70
+ origin,
31
71
  )
32
72
  }
33
73
  }
@@ -1,11 +1,25 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { inferPluginPublisherSourceFromUrl } from '@/lib/plugin-sources'
2
3
  import { searchClawHub } from '@/lib/server/clawhub-client'
4
+ import type { PluginCatalogSource } from '@/types'
3
5
 
4
6
  export const dynamic = 'force-dynamic'
5
7
 
6
- const REGISTRY_URLS = [
7
- 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json',
8
- 'https://swarmclaw.ai/registry/plugins.json',
8
+ interface RegistryPluginEntry {
9
+ id?: string
10
+ name?: string
11
+ description?: string
12
+ url?: string
13
+ author?: string
14
+ version?: string
15
+ tags?: string[]
16
+ openclaw?: boolean
17
+ downloads?: number
18
+ }
19
+
20
+ const REGISTRY_URLS: Array<{ url: string; catalogSource: PluginCatalogSource }> = [
21
+ { url: 'https://swarmclaw.ai/registry/plugins.json', catalogSource: 'swarmclaw-site' },
22
+ { url: 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json', catalogSource: 'swarmforge' },
9
23
  ]
10
24
  const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
11
25
 
@@ -33,34 +47,43 @@ export async function GET(req: Request) {
33
47
  }
34
48
 
35
49
  const allPlugins: Record<string, unknown>[] = []
50
+ const registryPlugins = new Map<string, Record<string, unknown>>()
36
51
 
37
52
  // 1. Fetch SwarmClaw Registry
38
- for (const registryUrl of REGISTRY_URLS) {
53
+ for (const registry of REGISTRY_URLS) {
39
54
  try {
40
- const res = await fetch(registryUrl, { cache: 'no-store' })
55
+ const res = await fetch(registry.url, { cache: 'no-store' })
41
56
  if (!res.ok) continue
42
57
 
43
58
  const data = await res.json()
44
- const filtered = (data as Array<{ name: string; description: string; url?: string }>).filter((p) => {
59
+ const entries = Array.isArray(data) ? data as RegistryPluginEntry[] : []
60
+ const filtered = entries.filter((p) => {
45
61
  if (!p || typeof p.name !== 'string' || typeof p.description !== 'string') return false
46
62
  return !query || p.name.toLowerCase().includes(query.toLowerCase()) || p.description.toLowerCase().includes(query.toLowerCase())
47
63
  })
48
64
 
49
- allPlugins.push(...filtered.map((p: { id?: string; name?: string; url?: string }) => ({
50
- ...p,
51
- id: p.id || (p.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_'),
52
- url: normalizeRegistryPluginUrl(p.url) || p.url,
53
- source: 'swarmclaw',
54
- })))
55
- break
65
+ for (const p of filtered) {
66
+ const normalizedUrl = normalizeRegistryPluginUrl(p.url) || p.url
67
+ const id = p.id || (p.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_')
68
+ if (registryPlugins.has(id)) continue
69
+ registryPlugins.set(id, {
70
+ ...p,
71
+ id,
72
+ url: normalizedUrl,
73
+ source: inferPluginPublisherSourceFromUrl(normalizedUrl) || 'swarmforge',
74
+ catalogSource: registry.catalogSource,
75
+ })
76
+ }
56
77
  } catch (err: unknown) {
57
78
  console.warn('[marketplace] SC Registry failed:', {
58
- registryUrl,
79
+ registryUrl: registry.url,
59
80
  error: err instanceof Error ? err.message : String(err),
60
81
  })
61
82
  }
62
83
  }
63
84
 
85
+ allPlugins.push(...registryPlugins.values())
86
+
64
87
  // 2. Fetch ClawHub Skills/Plugins
65
88
  try {
66
89
  const hubResults = await searchClawHub(query)
@@ -71,12 +94,22 @@ export async function GET(req: Request) {
71
94
  author: s.author,
72
95
  version: s.version || '1.0.0',
73
96
  url: s.url,
74
- source: 'clawhub'
97
+ source: 'clawhub',
98
+ catalogSource: 'clawhub',
75
99
  })))
76
100
  } catch (err: unknown) {
77
101
  console.warn('[marketplace] ClawHub failed:', err instanceof Error ? err.message : String(err))
78
102
  }
79
103
 
104
+ allPlugins.sort((a, b) => {
105
+ const catalogA = typeof a.catalogSource === 'string' ? a.catalogSource : ''
106
+ const catalogB = typeof b.catalogSource === 'string' ? b.catalogSource : ''
107
+ if (catalogA !== catalogB) return catalogA.localeCompare(catalogB)
108
+ const nameA = typeof a.name === 'string' ? a.name : ''
109
+ const nameB = typeof b.name === 'string' ? b.name : ''
110
+ return nameA.localeCompare(nameB)
111
+ })
112
+
80
113
  // Update cache only for empty queries
81
114
  if (!query) {
82
115
  cache = { data: allPlugins, fetchedAt: now }