@swarmclawai/swarmclaw 0.7.2 → 0.7.3

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 (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. 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,12 @@ 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
12
  const gw = await ensureGatewayConnected()
13
13
  if (!gw) throw new Error('Gateway not connected')
14
14
 
15
- const result = await gw.rpc('exec.approvals.get', { agentId }) as ExecApprovalSnapshot | undefined
15
+ const result = await gw.rpc('exec.approvals.get', {}) as ExecApprovalSnapshot | undefined
16
16
  if (!result) {
17
17
  return { path: '', exists: false, hash: '', file: { ...DEFAULT_CONFIG } }
18
18
  }
@@ -21,7 +21,7 @@ export async function getExecConfig(agentId: string): Promise<ExecApprovalSnapsh
21
21
 
22
22
  /** Save exec approval config with hash-based conflict retry (up to 3 attempts) */
23
23
  export async function setExecConfig(
24
- agentId: string,
24
+ _agentId: string,
25
25
  config: ExecApprovalConfig,
26
26
  baseHash: string,
27
27
  ): Promise<{ ok: boolean; hash: string }> {
@@ -32,7 +32,6 @@ export async function setExecConfig(
32
32
  for (let attempt = 0; attempt < 3; attempt++) {
33
33
  try {
34
34
  const result = await gw.rpc('exec.approvals.set', {
35
- agentId,
36
35
  file: config,
37
36
  baseHash: currentHash,
38
37
  }) as { hash?: string } | undefined
@@ -41,7 +40,7 @@ export async function setExecConfig(
41
40
  const msg = err instanceof Error ? err.message : String(err)
42
41
  if (msg.includes('conflict') && attempt < 2) {
43
42
  // Re-fetch to get fresh hash
44
- const fresh = await getExecConfig(agentId)
43
+ const fresh = await getExecConfig()
45
44
  currentHash = fresh.hash
46
45
  continue
47
46
  }
@@ -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
+ })
@@ -0,0 +1,136 @@
1
+ import type { OpenClawSkillEntry, SkillInstallOption, SkillRequirements } from '@/types'
2
+
3
+ interface GatewayConfigCheck {
4
+ path?: string
5
+ satisfied?: boolean
6
+ }
7
+
8
+ interface GatewayInstallOption {
9
+ kind?: string
10
+ label?: string
11
+ bins?: string[]
12
+ }
13
+
14
+ interface GatewaySkillRequirements {
15
+ bins?: string[]
16
+ anyBins?: string[][]
17
+ env?: string[]
18
+ config?: string[]
19
+ os?: string[]
20
+ }
21
+
22
+ interface GatewaySkillEntry {
23
+ name?: string
24
+ description?: string
25
+ source?: string
26
+ eligible?: boolean
27
+ requirements?: GatewaySkillRequirements
28
+ missing?: GatewaySkillRequirements
29
+ disabled?: boolean
30
+ install?: GatewayInstallOption[]
31
+ configChecks?: GatewayConfigCheck[]
32
+ skillKey?: string
33
+ baseDir?: string
34
+ }
35
+
36
+ interface GatewaySkillsStatusPayload {
37
+ skills?: GatewaySkillEntry[]
38
+ }
39
+
40
+ function uniq(values: Array<string | undefined | null>): string[] {
41
+ return [...new Set(values.map((value) => (value ?? '').trim()).filter(Boolean))]
42
+ }
43
+
44
+ function normalizeSource(source: string | undefined): OpenClawSkillEntry['source'] {
45
+ switch ((source ?? '').trim()) {
46
+ case 'openclaw-bundled':
47
+ case 'bundled':
48
+ return 'bundled'
49
+ case 'managed':
50
+ return 'managed'
51
+ case 'personal':
52
+ return 'personal'
53
+ case 'workspace':
54
+ return 'workspace'
55
+ default:
56
+ return 'workspace'
57
+ }
58
+ }
59
+
60
+ function normalizeInstallOptions(install: GatewayInstallOption[] | undefined): SkillInstallOption[] | undefined {
61
+ if (!Array.isArray(install) || !install.length) return undefined
62
+ const normalized = install
63
+ .map((entry) => {
64
+ const kind = (entry.kind ?? '').trim()
65
+ if (!kind || !entry.label?.trim()) return null
66
+ if (!['brew', 'node', 'go', 'uv', 'download'].includes(kind)) return null
67
+ return {
68
+ kind: kind as SkillInstallOption['kind'],
69
+ label: entry.label.trim(),
70
+ bins: Array.isArray(entry.bins) ? uniq(entry.bins) : undefined,
71
+ } satisfies SkillInstallOption
72
+ })
73
+ .filter((value): value is NonNullable<typeof value> => value !== null)
74
+ return normalized.length ? normalized : undefined
75
+ }
76
+
77
+ function normalizeRequirements(input: GatewaySkillRequirements | undefined): SkillRequirements | undefined {
78
+ if (!input || typeof input !== 'object') return undefined
79
+ const bins = Array.isArray(input.bins) ? uniq(input.bins) : undefined
80
+ const anyBins = Array.isArray(input.anyBins)
81
+ ? input.anyBins
82
+ .map((group) => Array.isArray(group) ? uniq(group) : [])
83
+ .filter((group) => group.length > 0)
84
+ : undefined
85
+ const env = Array.isArray(input.env) ? uniq(input.env) : undefined
86
+ const config = Array.isArray(input.config) ? uniq(input.config) : undefined
87
+ const os = Array.isArray(input.os) ? uniq(input.os) : undefined
88
+ if (!bins && !anyBins && !env && !config && !os) return undefined
89
+ return { bins, anyBins, env, config, os }
90
+ }
91
+
92
+ function flattenMissing(input: GatewaySkillRequirements | undefined): string[] | undefined {
93
+ if (!input || typeof input !== 'object') return undefined
94
+ const out: string[] = []
95
+ for (const value of Array.isArray(input.bins) ? uniq(input.bins) : []) out.push(value)
96
+ for (const group of Array.isArray(input.anyBins) ? input.anyBins : []) {
97
+ const normalized = Array.isArray(group) ? uniq(group) : []
98
+ if (normalized.length) out.push(`one of: ${normalized.join(' | ')}`)
99
+ }
100
+ for (const value of Array.isArray(input.env) ? uniq(input.env) : []) out.push(`env ${value}`)
101
+ for (const value of Array.isArray(input.config) ? uniq(input.config) : []) out.push(`config ${value}`)
102
+ for (const value of Array.isArray(input.os) ? uniq(input.os) : []) out.push(`os ${value}`)
103
+ return out.length ? out : undefined
104
+ }
105
+
106
+ export function normalizeOpenClawSkillsPayload(payload: unknown): OpenClawSkillEntry[] {
107
+ const rawSkills = Array.isArray(payload)
108
+ ? payload as GatewaySkillEntry[]
109
+ : Array.isArray((payload as GatewaySkillsStatusPayload | null | undefined)?.skills)
110
+ ? (payload as GatewaySkillsStatusPayload).skills!
111
+ : []
112
+
113
+ return rawSkills
114
+ .map((skill) => {
115
+ const name = skill.name?.trim()
116
+ if (!name) return null
117
+ return {
118
+ name,
119
+ description: skill.description?.trim() || undefined,
120
+ source: normalizeSource(skill.source),
121
+ eligible: skill.eligible === true,
122
+ missing: flattenMissing(skill.missing),
123
+ disabled: skill.disabled === true,
124
+ installOptions: normalizeInstallOptions(skill.install),
125
+ skillRequirements: normalizeRequirements(skill.requirements),
126
+ configChecks: Array.isArray(skill.configChecks)
127
+ ? skill.configChecks
128
+ .filter((check) => check.path?.trim())
129
+ .map((check) => ({ key: check.path!.trim(), ok: check.satisfied === true }))
130
+ : undefined,
131
+ skillKey: skill.skillKey?.trim() || undefined,
132
+ baseDir: skill.baseDir?.trim() || undefined,
133
+ } satisfies OpenClawSkillEntry
134
+ })
135
+ .filter((skill): skill is NonNullable<typeof skill> => skill !== null)
136
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import crypto from 'node:crypto'
4
4
  import { DATA_DIR } from './data-dir'
5
+ import { normalizeOpenClawAgentId } from '@/lib/openclaw-agent-id'
5
6
  import { loadSettings, loadAgents, saveAgents, loadSchedules, saveSchedules, loadCredentials, decryptKey, encryptKey } from './storage'
6
7
  import { getMemoryDb } from './memory-db'
7
8
  import type { AppSettings, MemoryEntry, Schedule } from '@/types'
@@ -182,7 +183,7 @@ export function pushAgentToOpenClaw(agentId: string): { written: string[] } {
182
183
  const agent = agents[agentId]
183
184
  if (!agent) throw new Error(`Agent not found: ${agentId}`)
184
185
 
185
- const agentDir = path.join(config.workspacePath, 'agents', agent.name.toLowerCase().replace(/\s+/g, '-'))
186
+ const agentDir = path.join(config.workspacePath, 'agents', normalizeOpenClawAgentId(agent.name))
186
187
  ensureDir(agentDir)
187
188
 
188
189
  const written: string[] = []
@@ -214,7 +215,7 @@ export function pullAgentFromOpenClaw(agentId: string): { updated: string[] } {
214
215
  const agent = agents[agentId]
215
216
  if (!agent) throw new Error(`Agent not found: ${agentId}`)
216
217
 
217
- const agentDir = path.join(config.workspacePath, 'agents', agent.name.toLowerCase().replace(/\s+/g, '-'))
218
+ const agentDir = path.join(config.workspacePath, 'agents', normalizeOpenClawAgentId(agent.name))
218
219
  const updated: string[] = []
219
220
 
220
221
  const soulPath = path.join(agentDir, 'SOUL.md')
@@ -13,6 +13,7 @@ import { notify } from './ws-hub'
13
13
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
14
14
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
15
15
  import { getPluginManager } from './plugins'
16
+ import './builtin-plugins'
16
17
  import { genId } from '@/lib/id'
17
18
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
18
19
  import type { Agent, TaskComment, MessageToolEvent } from '@/types'
@@ -118,7 +119,7 @@ async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId:
118
119
  messages: [],
119
120
  createdAt: Date.now(),
120
121
  lastActiveAt: Date.now(),
121
- sessionType: 'orchestrated' as const,
122
+ sessionType: 'human' as const,
122
123
  agentId: agent.id,
123
124
  parentSessionId,
124
125
  plugins: agent.plugins || agent.tools || [],
@@ -177,7 +178,11 @@ export async function executeLangGraphOrchestrator(
177
178
  return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
178
179
  }
179
180
  console.log(`[orchestrator-lg] Delegating to ${agent.name}: ${agentTask.slice(0, 80)}`)
180
- getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
181
+ getPluginManager().runHook(
182
+ 'onAgentDelegation',
183
+ { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask },
184
+ { enabledIds: orchestrator.plugins || [] },
185
+ )
181
186
  const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
182
187
  saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
183
188
  name: 'delegate_to_agent',
@@ -393,6 +398,7 @@ export async function executeLangGraphOrchestrator(
393
398
 
394
399
  const checkpointSaver = getCheckpointSaver()
395
400
  const isStrictMode = settings.capabilityPolicyMode === 'strict'
401
+ const approvalInterruptsEnabled = isStrictMode && settings.approvalsEnabled !== false
396
402
  const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
397
403
  const llmWithTools = llm.bindTools(allTools)
398
404
  const toolNode = new ToolNode(allTools)
@@ -472,7 +478,7 @@ export async function executeLangGraphOrchestrator(
472
478
 
473
479
  const compiledGraph = graph.compile({
474
480
  checkpointer: checkpointSaver,
475
- ...(isStrictMode ? { interruptBefore: ['tools'] } : {}),
481
+ ...(approvalInterruptsEnabled ? { interruptBefore: ['tools'] } : {}),
476
482
  })
477
483
 
478
484
  // Export graph structure for introspection
@@ -534,7 +540,7 @@ export async function executeLangGraphOrchestrator(
534
540
  }
535
541
 
536
542
  // Check for interrupt (paused before tool execution in strict mode)
537
- if (isStrictMode && taskId) {
543
+ if (approvalInterruptsEnabled && taskId) {
538
544
  const state = await compiledGraph.getState({ configurable: { thread_id: threadId } })
539
545
  const nextNodes = state?.next || []
540
546
  if (nextNodes.includes('tools')) {
@@ -628,7 +634,11 @@ export async function resumeLangGraphOrchestrator(
628
634
  async ({ agentName, task: agentTask }) => {
629
635
  const agent = agents.find((a) => a.name.toLowerCase() === agentName.toLowerCase())
630
636
  if (!agent) return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
631
- getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
637
+ getPluginManager().runHook(
638
+ 'onAgentDelegation',
639
+ { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask },
640
+ { enabledIds: orchestrator.plugins || [] },
641
+ )
632
642
  const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
633
643
  saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
634
644
  name: 'delegate_to_agent',
@@ -753,6 +763,7 @@ export async function resumeLangGraphOrchestrator(
753
763
  const checkpointSaver = getCheckpointSaver()
754
764
  const settings = loadSettings()
755
765
  const isStrictMode = settings.capabilityPolicyMode === 'strict'
766
+ const approvalInterruptsEnabled = isStrictMode && settings.approvalsEnabled !== false
756
767
 
757
768
  const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
758
769
  const llmWithTools = llm.bindTools(allTools)
@@ -782,7 +793,7 @@ export async function resumeLangGraphOrchestrator(
782
793
  .addEdge('router', 'agent')
783
794
  .compile({
784
795
  checkpointer: checkpointSaver,
785
- ...(isStrictMode ? { interruptBefore: ['tools'] } : {}),
796
+ ...(approvalInterruptsEnabled ? { interruptBefore: ['tools'] } : {}),
786
797
  })
787
798
 
788
799
  let finalResult = ''
@@ -42,7 +42,7 @@ export function createOrchestratorSession(
42
42
  messages: [] as any[],
43
43
  createdAt: Date.now(),
44
44
  lastActiveAt: Date.now(),
45
- sessionType: 'orchestrated' as const,
45
+ sessionType: 'human' as const,
46
46
  agentId: orchestrator.id,
47
47
  parentSessionId: parentSessionId || null,
48
48
  plugins: Array.isArray(orchestrator.plugins) ? [...orchestrator.plugins] : (Array.isArray(orchestrator.tools) ? [...orchestrator.tools] : []),
@@ -288,7 +288,7 @@ async function executeSubTask(
288
288
  messages: [] as any[],
289
289
  createdAt: Date.now(),
290
290
  lastActiveAt: Date.now(),
291
- sessionType: 'orchestrated' as const,
291
+ sessionType: 'human' as const,
292
292
  agentId: agent.id,
293
293
  parentSessionId,
294
294
  plugins: agent.plugins || agent.tools || [],
@@ -11,9 +11,33 @@ import path from 'path'
11
11
  const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(process.env.DATA_DIR || path.join(process.cwd(), 'data'), 'uploads')
12
12
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
13
13
 
14
- const child = spawn('npx', ['@playwright/mcp@latest'], {
15
- stdio: ['pipe', 'pipe', 'pipe'],
16
- })
14
+ function resolvePlaywrightCli() {
15
+ const candidates = [
16
+ path.join(process.cwd(), 'node_modules', '@playwright', 'mcp', 'cli.js'),
17
+ path.join(process.cwd(), '[project]', 'node_modules', '@playwright', 'mcp', 'cli.js'),
18
+ ]
19
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null
20
+ }
21
+
22
+ function sanitizePlaywrightEnv(baseEnv) {
23
+ const env = { ...baseEnv }
24
+ for (const key of Object.keys(env)) {
25
+ if (!key.toUpperCase().startsWith('PLAYWRIGHT_MCP_')) continue
26
+ delete env[key]
27
+ }
28
+ return env
29
+ }
30
+
31
+ const cliPath = resolvePlaywrightCli()
32
+ const child = cliPath
33
+ ? spawn(process.execPath, [cliPath], {
34
+ stdio: ['pipe', 'pipe', 'pipe'],
35
+ env: sanitizePlaywrightEnv(process.env),
36
+ })
37
+ : spawn('npx', ['@playwright/mcp@latest'], {
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ env: sanitizePlaywrightEnv(process.env),
40
+ })
17
41
 
18
42
  // Forward stdin → child
19
43
  process.stdin.on('data', (chunk) => child.stdin.write(chunk))