@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -14,6 +14,7 @@ import {
14
14
  traverseLinkedMemoryGraph,
15
15
  type MemoryLookupLimits,
16
16
  } from './memory-graph'
17
+ import { isWorkingMemoryCategory } from './memory-tiers'
17
18
 
18
19
  import { DATA_DIR } from './data-dir'
19
20
 
@@ -1203,8 +1204,7 @@ function initDb() {
1203
1204
  if (seenCanonical.has(keyCanonical)) canonicalDuplicateCandidates++
1204
1205
  else seenCanonical.add(keyCanonical)
1205
1206
 
1206
- const category = String(row.category || '').toLowerCase()
1207
- const isWorkingLike = category === 'execution' || category === 'working' || category === 'scratch'
1207
+ const isWorkingLike = isWorkingMemoryCategory(row.category)
1208
1208
  if (isWorkingLike && (row.updatedAt || row.createdAt || 0) < cutoff) staleWorkingCandidates++
1209
1209
  }
1210
1210
 
@@ -1303,8 +1303,7 @@ function initDb() {
1303
1303
  if (pruneWorking && toDelete.size < deleteBudget) {
1304
1304
  for (const row of rows) {
1305
1305
  if (toDelete.has(row.id)) continue
1306
- const category = String(row.category || '').toLowerCase()
1307
- const isWorkingLike = category === 'execution' || category === 'working' || category === 'scratch'
1306
+ const isWorkingLike = isWorkingMemoryCategory(row.category)
1308
1307
  const updatedAt = row.updatedAt || row.createdAt || 0
1309
1308
  if (isWorkingLike && updatedAt < cutoff) toDelete.add(row.id)
1310
1309
  if (toDelete.size >= deleteBudget) break
@@ -1323,8 +1322,7 @@ function initDb() {
1323
1322
  const deletedSet = new Set(deleteIds)
1324
1323
  for (const row of rows) {
1325
1324
  if (!deletedSet.has(row.id)) continue
1326
- const category = String(row.category || '').toLowerCase()
1327
- const isWorkingLike = category === 'execution' || category === 'working' || category === 'scratch'
1325
+ const isWorkingLike = isWorkingMemoryCategory(row.category)
1328
1326
  if (isWorkingLike) pruned++
1329
1327
  else deduped++
1330
1328
  }
@@ -0,0 +1,40 @@
1
+ import type { MemoryEntry } from '@/types'
2
+
3
+ export type MemoryTier = 'working' | 'durable' | 'archive'
4
+
5
+ const WORKING_CATEGORIES = new Set(['execution', 'working', 'scratch', 'breadcrumb'])
6
+ const ARCHIVE_CATEGORIES = new Set(['session_archive'])
7
+
8
+ export function getMemoryTierForCategory(category: unknown): MemoryTier {
9
+ const normalized = typeof category === 'string' ? category.trim().toLowerCase() : ''
10
+ if (ARCHIVE_CATEGORIES.has(normalized)) return 'archive'
11
+ if (WORKING_CATEGORIES.has(normalized)) return 'working'
12
+ return 'durable'
13
+ }
14
+
15
+ export function getMemoryTier(entry: Pick<MemoryEntry, 'category' | 'metadata'>): MemoryTier {
16
+ const metadataTier = typeof entry.metadata?.tier === 'string' ? entry.metadata.tier.trim().toLowerCase() : ''
17
+ if (metadataTier === 'archive' || metadataTier === 'session_archive') return 'archive'
18
+ if (metadataTier === 'working') return 'working'
19
+ if (metadataTier === 'durable') return 'durable'
20
+ return getMemoryTierForCategory(entry.category)
21
+ }
22
+
23
+ export function partitionMemoriesByTier<T extends Pick<MemoryEntry, 'category' | 'metadata'>>(entries: T[]) {
24
+ const working: T[] = []
25
+ const durable: T[] = []
26
+ const archive: T[] = []
27
+
28
+ for (const entry of entries) {
29
+ const tier = getMemoryTier(entry)
30
+ if (tier === 'working') working.push(entry)
31
+ else if (tier === 'archive') archive.push(entry)
32
+ else durable.push(entry)
33
+ }
34
+
35
+ return { working, durable, archive }
36
+ }
37
+
38
+ export function isWorkingMemoryCategory(category: unknown): boolean {
39
+ return getMemoryTierForCategory(category) === 'working'
40
+ }
@@ -0,0 +1,70 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import type { Agent } from '@/types'
4
+ import {
5
+ resolveOpenClawGatewayAgentIdFromList,
6
+ type OpenClawGatewayAgentSummary,
7
+ } from './openclaw-agent-resolver'
8
+
9
+ function makeOpenClawAgent(overrides: Partial<Agent> = {}): Agent {
10
+ const now = Date.now()
11
+ return {
12
+ id: 'f4535f26',
13
+ name: 'OpenClaw Ops',
14
+ description: '',
15
+ systemPrompt: '',
16
+ provider: 'openclaw',
17
+ model: 'openclaw-default',
18
+ createdAt: now,
19
+ updatedAt: now,
20
+ ...overrides,
21
+ }
22
+ }
23
+
24
+ test('resolveOpenClawGatewayAgentIdFromList matches a local OpenClaw agent by normalized name', () => {
25
+ const gatewayAgents: OpenClawGatewayAgentSummary[] = [
26
+ { id: 'main', name: 'Main' },
27
+ { id: 'openclaw-ops', name: 'OpenClaw Ops' },
28
+ ]
29
+ const resolved = resolveOpenClawGatewayAgentIdFromList({
30
+ agentRef: 'f4535f26',
31
+ gatewayAgents,
32
+ localAgent: makeOpenClawAgent(),
33
+ })
34
+ assert.equal(resolved, 'openclaw-ops')
35
+ })
36
+
37
+ test('resolveOpenClawGatewayAgentIdFromList preserves direct gateway ids', () => {
38
+ const gatewayAgents: OpenClawGatewayAgentSummary[] = [
39
+ { id: 'main', name: 'Main' },
40
+ ]
41
+ const resolved = resolveOpenClawGatewayAgentIdFromList({
42
+ agentRef: 'main',
43
+ gatewayAgents,
44
+ })
45
+ assert.equal(resolved, 'main')
46
+ })
47
+
48
+ test('resolveOpenClawGatewayAgentIdFromList can match identity names when display names differ', () => {
49
+ const gatewayAgents: OpenClawGatewayAgentSummary[] = [
50
+ { id: 'research-ops', identity: { name: 'Research Ops' } },
51
+ ]
52
+ const resolved = resolveOpenClawGatewayAgentIdFromList({
53
+ agentRef: 'agent-123',
54
+ gatewayAgents,
55
+ localAgent: makeOpenClawAgent({ id: 'agent-123', name: 'Research Ops' }),
56
+ })
57
+ assert.equal(resolved, 'research-ops')
58
+ })
59
+
60
+ test('single-agent gateway can back a local OpenClaw provider agent without an explicit name match', async () => {
61
+ const gatewayAgents: OpenClawGatewayAgentSummary[] = [
62
+ { id: 'main', name: 'Main' },
63
+ ]
64
+ const resolved = resolveOpenClawGatewayAgentIdFromList({
65
+ agentRef: 'f4535f26',
66
+ gatewayAgents,
67
+ localAgent: makeOpenClawAgent({ name: 'OpenClaw-2' }),
68
+ })
69
+ assert.equal(resolved, 'main')
70
+ })
@@ -0,0 +1,128 @@
1
+ import type { Agent } from '@/types'
2
+ import { normalizeOpenClawAgentId } from '@/lib/openclaw-agent-id'
3
+ import { ensureGatewayConnected, type OpenClawGateway } from './openclaw-gateway'
4
+ import { loadAgents } from './storage'
5
+
6
+ export interface OpenClawGatewayAgentSummary {
7
+ id: string
8
+ name?: string
9
+ identity?: {
10
+ name?: string
11
+ } | null
12
+ }
13
+
14
+ interface OpenClawGatewayAgentsList {
15
+ defaultId?: string
16
+ agents?: OpenClawGatewayAgentSummary[]
17
+ }
18
+
19
+ function addTextCandidate(target: Set<string>, value: string | undefined | null) {
20
+ const trimmed = (value ?? '').trim()
21
+ if (trimmed) {
22
+ target.add(trimmed.toLowerCase())
23
+ }
24
+ }
25
+
26
+ function addNormalizedCandidate(target: Set<string>, value: string | undefined | null) {
27
+ const trimmed = (value ?? '').trim()
28
+ if (trimmed) {
29
+ target.add(normalizeOpenClawAgentId(trimmed))
30
+ }
31
+ }
32
+
33
+ export function resolveOpenClawGatewayAgentIdFromList(params: {
34
+ agentRef: string
35
+ gatewayAgents: OpenClawGatewayAgentSummary[]
36
+ localAgent?: Agent | null
37
+ }): string | null {
38
+ const rawRef = params.agentRef.trim()
39
+ if (!rawRef) {
40
+ return null
41
+ }
42
+
43
+ const exactTextCandidates = new Set<string>()
44
+ const normalizedCandidates = new Set<string>()
45
+
46
+ addTextCandidate(exactTextCandidates, rawRef)
47
+ addNormalizedCandidate(normalizedCandidates, rawRef)
48
+
49
+ if (params.localAgent) {
50
+ addTextCandidate(exactTextCandidates, params.localAgent.id)
51
+ addTextCandidate(exactTextCandidates, params.localAgent.name)
52
+ addNormalizedCandidate(normalizedCandidates, params.localAgent.id)
53
+ addNormalizedCandidate(normalizedCandidates, params.localAgent.name)
54
+ }
55
+
56
+ for (const gatewayAgent of params.gatewayAgents) {
57
+ if (exactTextCandidates.has(gatewayAgent.id.trim().toLowerCase())) {
58
+ return gatewayAgent.id
59
+ }
60
+ }
61
+
62
+ for (const gatewayAgent of params.gatewayAgents) {
63
+ if (normalizedCandidates.has(normalizeOpenClawAgentId(gatewayAgent.id))) {
64
+ return gatewayAgent.id
65
+ }
66
+ }
67
+
68
+ for (const gatewayAgent of params.gatewayAgents) {
69
+ const labels = [gatewayAgent.name, gatewayAgent.identity?.name]
70
+ for (const label of labels) {
71
+ if (!label?.trim()) continue
72
+ if (exactTextCandidates.has(label.trim().toLowerCase())) {
73
+ return gatewayAgent.id
74
+ }
75
+ }
76
+ }
77
+
78
+ for (const gatewayAgent of params.gatewayAgents) {
79
+ const labels = [gatewayAgent.name, gatewayAgent.identity?.name]
80
+ for (const label of labels) {
81
+ if (!label?.trim()) continue
82
+ if (normalizedCandidates.has(normalizeOpenClawAgentId(label))) {
83
+ return gatewayAgent.id
84
+ }
85
+ }
86
+ }
87
+
88
+ if (params.localAgent && params.gatewayAgents.length === 1) {
89
+ return params.gatewayAgents[0].id
90
+ }
91
+
92
+ return null
93
+ }
94
+
95
+ export async function resolveOpenClawGatewayAgentId(
96
+ agentRef: string,
97
+ gatewayArg?: OpenClawGateway | null,
98
+ ): Promise<string> {
99
+ const trimmedRef = agentRef.trim()
100
+ if (!trimmedRef) {
101
+ throw new Error('Missing agentId')
102
+ }
103
+
104
+ const localAgents = loadAgents({ includeTrashed: true }) as Record<string, Agent>
105
+ const localAgent = localAgents[trimmedRef] || null
106
+ if (localAgent && localAgent.provider !== 'openclaw') {
107
+ throw new Error(`Agent "${localAgent.name}" is not an OpenClaw agent`)
108
+ }
109
+
110
+ const gateway = gatewayArg ?? await ensureGatewayConnected()
111
+ if (!gateway) {
112
+ throw new Error('OpenClaw gateway not connected')
113
+ }
114
+
115
+ const result = await gateway.rpc('agents.list', {}) as OpenClawGatewayAgentsList | undefined
116
+ const gatewayAgents = Array.isArray(result?.agents) ? result.agents : []
117
+ const resolved = resolveOpenClawGatewayAgentIdFromList({
118
+ agentRef: trimmedRef,
119
+ gatewayAgents,
120
+ localAgent,
121
+ })
122
+ if (resolved) {
123
+ return resolved
124
+ }
125
+
126
+ const label = localAgent?.name?.trim() || trimmedRef
127
+ throw new Error(`OpenClaw gateway agent not found for "${label}"`)
128
+ }
@@ -7,12 +7,13 @@ const DEFAULT_CONFIG: ExecApprovalConfig = {
7
7
  patterns: [],
8
8
  }
9
9
 
10
- /** Fetch exec approval config from gateway for a given agent */
11
- export async function getExecConfig(agentId: string): Promise<ExecApprovalSnapshot> {
10
+ /** Fetch the gateway's global exec approval config. */
11
+ export async function getExecConfig(agentId?: string): Promise<ExecApprovalSnapshot> {
12
+ void agentId
12
13
  const gw = await ensureGatewayConnected()
13
14
  if (!gw) throw new Error('Gateway not connected')
14
15
 
15
- const result = await gw.rpc('exec.approvals.get', { agentId }) as ExecApprovalSnapshot | undefined
16
+ const result = await gw.rpc('exec.approvals.get', {}) as ExecApprovalSnapshot | undefined
16
17
  if (!result) {
17
18
  return { path: '', exists: false, hash: '', file: { ...DEFAULT_CONFIG } }
18
19
  }
@@ -25,6 +26,7 @@ export async function setExecConfig(
25
26
  config: ExecApprovalConfig,
26
27
  baseHash: string,
27
28
  ): Promise<{ ok: boolean; hash: string }> {
29
+ void agentId
28
30
  const gw = await ensureGatewayConnected()
29
31
  if (!gw) throw new Error('Gateway not connected')
30
32
 
@@ -32,7 +34,6 @@ export async function setExecConfig(
32
34
  for (let attempt = 0; attempt < 3; attempt++) {
33
35
  try {
34
36
  const result = await gw.rpc('exec.approvals.set', {
35
- agentId,
36
37
  file: config,
37
38
  baseHash: currentHash,
38
39
  }) as { hash?: string } | undefined
@@ -41,7 +42,7 @@ export async function setExecConfig(
41
42
  const msg = err instanceof Error ? err.message : String(err)
42
43
  if (msg.includes('conflict') && attempt < 2) {
43
44
  // Re-fetch to get fresh hash
44
- const fresh = await getExecConfig(agentId)
45
+ const fresh = await getExecConfig()
45
46
  currentHash = fresh.hash
46
47
  continue
47
48
  }
@@ -3,6 +3,7 @@ import { randomUUID } from 'crypto'
3
3
  import { wsConnect, buildOpenClawConnectParams } from '../providers/openclaw'
4
4
  import { loadAgents, loadCredentials, decryptKey } from './storage'
5
5
  import { notify, notifyWithPayload } from './ws-hub'
6
+ import { getGatewayProfile, getGatewayProfiles, resolvePrimaryAgentRoute } from './agent-runtime-config'
6
7
 
7
8
  // --- Types ---
8
9
 
@@ -19,19 +20,22 @@ type EventHandler = (payload: unknown) => void
19
20
  const GK = '__swarmclaw_ocgateway__' as const
20
21
 
21
22
  interface GatewayState {
22
- instance: OpenClawGateway | null
23
+ instances: Map<string, OpenClawGateway>
24
+ activeKey: string | null
23
25
  }
24
26
 
25
27
  function getState(): GatewayState {
26
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
29
  const g = globalThis as any
28
- if (!g[GK]) g[GK] = { instance: null }
30
+ if (!g[GK]) g[GK] = { instances: new Map<string, OpenClawGateway>(), activeKey: null }
29
31
  return g[GK] as GatewayState
30
32
  }
31
33
 
32
34
  // --- Helper: resolve gateway config from first OpenClaw agent ---
33
35
 
34
36
  interface GatewayConfig {
37
+ key: string
38
+ profileId?: string | null
35
39
  wsUrl: string
36
40
  token: string | undefined
37
41
  }
@@ -43,27 +47,79 @@ function normalizeWsUrl(raw: string): string {
43
47
  return url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
44
48
  }
45
49
 
46
- export function resolveGatewayConfig(): GatewayConfig | null {
47
- const agents = loadAgents({ includeTrashed: true })
50
+ function resolveTokenForCredential(credentialId?: string | null): string | undefined {
51
+ const id = typeof credentialId === 'string' && credentialId.trim() ? credentialId.trim() : ''
52
+ if (!id) return undefined
48
53
  const creds = loadCredentials()
54
+ const cred = creds[id]
55
+ if (!cred?.encryptedKey) return undefined
56
+ try {
57
+ return decryptKey(cred.encryptedKey)
58
+ } catch {
59
+ return undefined
60
+ }
61
+ }
62
+
63
+ export function resolveGatewayConfig(target?: {
64
+ profileId?: string | null
65
+ agentId?: string | null
66
+ }): GatewayConfig | null {
67
+ const profileId = typeof target?.profileId === 'string' ? target.profileId.trim() : ''
68
+ if (profileId) {
69
+ const profile = getGatewayProfile(profileId)
70
+ if (!profile) return null
71
+ return {
72
+ key: `profile:${profile.id}`,
73
+ profileId: profile.id,
74
+ wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : normalizeWsUrl(profile.endpoint),
75
+ token: resolveTokenForCredential(profile.credentialId),
76
+ }
77
+ }
78
+
79
+ const agentId = typeof target?.agentId === 'string' ? target.agentId.trim() : ''
80
+ if (agentId) {
81
+ const agents = loadAgents({ includeTrashed: true })
82
+ const agent = agents[agentId]
83
+ const route = resolvePrimaryAgentRoute(agent)
84
+ if (route?.provider === 'openclaw') {
85
+ return {
86
+ key: route.gatewayProfileId ? `profile:${route.gatewayProfileId}` : `agent:${agentId}`,
87
+ profileId: route.gatewayProfileId ?? null,
88
+ wsUrl: normalizeWsUrl(route.apiEndpoint || 'ws://127.0.0.1:18789'),
89
+ token: resolveTokenForCredential(route.credentialId),
90
+ }
91
+ }
92
+ }
93
+
94
+ const gatewayProfiles = getGatewayProfiles('openclaw')
95
+ if (gatewayProfiles[0]) {
96
+ const profile = gatewayProfiles[0]
97
+ return {
98
+ key: `profile:${profile.id}`,
99
+ profileId: profile.id,
100
+ wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : normalizeWsUrl(profile.endpoint),
101
+ token: resolveTokenForCredential(profile.credentialId),
102
+ }
103
+ }
104
+
105
+ const agents = loadAgents({ includeTrashed: true })
49
106
  for (const agent of Object.values(agents)) {
50
107
  if (agent?.provider !== 'openclaw') continue
51
108
  const wsUrl = agent.apiEndpoint
52
109
  ? normalizeWsUrl(agent.apiEndpoint)
53
110
  : 'ws://127.0.0.1:18789'
54
- let token: string | undefined
55
- if (agent.credentialId) {
56
- const cred = creds[agent.credentialId]
57
- if (cred?.encryptedKey) {
58
- try { token = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
59
- }
111
+ return {
112
+ key: `agent:${agent.id}`,
113
+ profileId: agent.gatewayProfileId ?? null,
114
+ wsUrl,
115
+ token: resolveTokenForCredential(agent.credentialId),
60
116
  }
61
- return { wsUrl, token }
62
117
  }
63
118
  return null
64
119
  }
65
120
 
66
121
  export function hasOpenClawAgents(): boolean {
122
+ if (getGatewayProfiles('openclaw').length > 0) return true
67
123
  const agents = loadAgents({ includeTrashed: true })
68
124
  return Object.values(agents).some((a) => a?.provider === 'openclaw' && !a.trashedAt)
69
125
  }
@@ -253,47 +309,78 @@ export class OpenClawGateway {
253
309
 
254
310
  // --- Singleton access ---
255
311
 
256
- export function getGateway(): OpenClawGateway | null {
257
- return getState().instance
312
+ export function getGateway(profileId?: string | null): OpenClawGateway | null {
313
+ const state = getState()
314
+ const key = typeof profileId === 'string' && profileId.trim() ? `profile:${profileId.trim()}` : null
315
+ if (key) {
316
+ return state.instances.get(key) || null
317
+ }
318
+ if (state.activeKey) {
319
+ return state.instances.get(state.activeKey) || null
320
+ }
321
+ for (const instance of state.instances.values()) {
322
+ if (instance.connected) return instance
323
+ }
324
+ return null
258
325
  }
259
326
 
260
- export async function ensureGatewayConnected(): Promise<OpenClawGateway | null> {
327
+ export async function ensureGatewayConnected(target?: {
328
+ profileId?: string | null
329
+ agentId?: string | null
330
+ }): Promise<OpenClawGateway | null> {
261
331
  const state = getState()
262
- if (state.instance?.connected) return state.instance
263
-
264
- const config = resolveGatewayConfig()
332
+ const config = resolveGatewayConfig(target)
265
333
  if (!config) return null
266
-
267
- if (!state.instance) {
268
- state.instance = new OpenClawGateway()
334
+ const existing = state.instances.get(config.key)
335
+ if (existing?.connected) {
336
+ state.activeKey = config.key
337
+ return existing
269
338
  }
270
339
 
271
- const ok = await state.instance.connect(config.wsUrl, config.token)
272
- return ok ? state.instance : null
340
+ const instance = existing || new OpenClawGateway()
341
+ state.instances.set(config.key, instance)
342
+ const ok = await instance.connect(config.wsUrl, config.token)
343
+ if (ok) {
344
+ state.activeKey = config.key
345
+ return instance
346
+ }
347
+ return null
273
348
  }
274
349
 
275
- export function disconnectGateway() {
350
+ export function disconnectGateway(profileId?: string | null) {
276
351
  const state = getState()
277
- if (state.instance) {
278
- state.instance.disconnect()
279
- state.instance = null
352
+ const key = typeof profileId === 'string' && profileId.trim() ? `profile:${profileId.trim()}` : null
353
+ if (key) {
354
+ const instance = state.instances.get(key)
355
+ if (instance) {
356
+ instance.disconnect()
357
+ state.instances.delete(key)
358
+ if (state.activeKey === key) state.activeKey = null
359
+ }
360
+ return
361
+ }
362
+ for (const [instanceKey, instance] of state.instances.entries()) {
363
+ instance.disconnect()
364
+ state.instances.delete(instanceKey)
280
365
  }
366
+ state.activeKey = null
281
367
  }
282
368
 
283
369
  /** Manual connect with explicit URL/token (used by gateway connection panel) */
284
- export async function manualConnect(url?: string, token?: string): Promise<boolean> {
370
+ export async function manualConnect(url?: string, token?: string, profileId?: string | null): Promise<boolean> {
285
371
  const state = getState()
286
- if (state.instance?.connected) {
287
- state.instance.disconnect()
372
+ const config = resolveGatewayConfig({ profileId: profileId || null })
373
+ const key = profileId ? `profile:${profileId}` : '__manual__'
374
+ const instance = state.instances.get(key) || new OpenClawGateway()
375
+ if (instance.connected) {
376
+ instance.disconnect()
288
377
  }
289
-
290
- const config = resolveGatewayConfig()
378
+ state.instances.set(key, instance)
291
379
  const wsUrl = url ? normalizeWsUrl(url) : config?.wsUrl ?? 'ws://127.0.0.1:18789'
292
380
  const resolvedToken = token ?? config?.token
293
-
294
- if (!state.instance) {
295
- state.instance = new OpenClawGateway()
381
+ const ok = await instance.connect(wsUrl, resolvedToken)
382
+ if (ok) {
383
+ state.activeKey = key
296
384
  }
297
-
298
- return state.instance.connect(wsUrl, resolvedToken)
385
+ return ok
299
386
  }
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { normalizeOpenClawSkillsPayload } from './openclaw-skills-normalize'
4
+
5
+ test('normalizeOpenClawSkillsPayload maps gateway skill reports into UI entries', () => {
6
+ const normalized = normalizeOpenClawSkillsPayload({
7
+ workspaceDir: '/tmp/workspace',
8
+ skills: [
9
+ {
10
+ name: 'github',
11
+ description: 'GitHub operations',
12
+ source: 'openclaw-bundled',
13
+ eligible: true,
14
+ requirements: {
15
+ bins: ['gh'],
16
+ anyBins: [['git', 'jj']],
17
+ env: ['GH_TOKEN'],
18
+ },
19
+ missing: {
20
+ config: ['channels.github'],
21
+ },
22
+ install: [
23
+ { kind: 'brew', label: 'Install GitHub CLI', bins: ['gh'] },
24
+ ],
25
+ configChecks: [
26
+ { path: 'channels.github', satisfied: false },
27
+ ],
28
+ skillKey: 'github',
29
+ baseDir: '/tmp/github',
30
+ },
31
+ ],
32
+ })
33
+
34
+ assert.equal(normalized.length, 1)
35
+ assert.deepEqual(normalized[0], {
36
+ name: 'github',
37
+ description: 'GitHub operations',
38
+ source: 'bundled',
39
+ eligible: true,
40
+ missing: ['config channels.github'],
41
+ disabled: false,
42
+ installOptions: [
43
+ { kind: 'brew', label: 'Install GitHub CLI', bins: ['gh'] },
44
+ ],
45
+ skillRequirements: {
46
+ bins: ['gh'],
47
+ anyBins: [['git', 'jj']],
48
+ env: ['GH_TOKEN'],
49
+ config: undefined,
50
+ os: undefined,
51
+ },
52
+ configChecks: [{ key: 'channels.github', ok: false }],
53
+ skillKey: 'github',
54
+ baseDir: '/tmp/github',
55
+ })
56
+ })