@swarmclawai/swarmclaw 0.7.8 → 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 (251) hide show
  1. package/README.md +12 -15
  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 +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -0,0 +1,351 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ canonicalizePluginId,
5
+ expandPluginIds,
6
+ getPluginAliases,
7
+ normalizePluginId,
8
+ pluginIdMatches,
9
+ } from './tool-aliases'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // normalizePluginId
13
+ // ---------------------------------------------------------------------------
14
+ describe('normalizePluginId', () => {
15
+ it('converts uppercase to lowercase', () => {
16
+ assert.equal(normalizePluginId('WEB_SEARCH'), 'web_search')
17
+ })
18
+
19
+ it('trims leading and trailing whitespace', () => {
20
+ assert.equal(normalizePluginId(' shell '), 'shell')
21
+ })
22
+
23
+ it('handles combined upper + whitespace', () => {
24
+ assert.equal(normalizePluginId(' WEB_SEARCH '), 'web_search')
25
+ })
26
+
27
+ it('returns empty string for empty input', () => {
28
+ assert.equal(normalizePluginId(''), '')
29
+ })
30
+
31
+ it('returns already normalized value unchanged', () => {
32
+ assert.equal(normalizePluginId('files'), 'files')
33
+ })
34
+
35
+ it('returns empty string for non-string input (number)', () => {
36
+ assert.equal(normalizePluginId(42), '')
37
+ })
38
+
39
+ it('returns empty string for null', () => {
40
+ assert.equal(normalizePluginId(null), '')
41
+ })
42
+
43
+ it('returns empty string for undefined', () => {
44
+ assert.equal(normalizePluginId(undefined), '')
45
+ })
46
+ })
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // canonicalizePluginId
50
+ // ---------------------------------------------------------------------------
51
+ describe('canonicalizePluginId', () => {
52
+ it('resolves web_search → web', () => {
53
+ assert.equal(canonicalizePluginId('web_search'), 'web')
54
+ })
55
+
56
+ it('resolves web_fetch → web', () => {
57
+ assert.equal(canonicalizePluginId('web_fetch'), 'web')
58
+ })
59
+
60
+ it('keeps web (already canonical)', () => {
61
+ assert.equal(canonicalizePluginId('web'), 'web')
62
+ })
63
+
64
+ it('resolves execute_command → shell', () => {
65
+ assert.equal(canonicalizePluginId('execute_command'), 'shell')
66
+ })
67
+
68
+ it('resolves memory_tool → memory', () => {
69
+ assert.equal(canonicalizePluginId('memory_tool'), 'memory')
70
+ })
71
+
72
+ it('keeps files (already canonical)', () => {
73
+ assert.equal(canonicalizePluginId('files'), 'files')
74
+ })
75
+
76
+ it('returns unknown plugin as-is', () => {
77
+ assert.equal(canonicalizePluginId('totally_unknown'), 'totally_unknown')
78
+ })
79
+
80
+ it('resolves delegate_to_claude_code → delegate', () => {
81
+ assert.equal(canonicalizePluginId('delegate_to_claude_code'), 'delegate')
82
+ })
83
+
84
+ it('resolves claude_code → delegate', () => {
85
+ assert.equal(canonicalizePluginId('claude_code'), 'delegate')
86
+ })
87
+
88
+ it('resolves process_tool → shell', () => {
89
+ assert.equal(canonicalizePluginId('process_tool'), 'shell')
90
+ })
91
+
92
+ it('resolves openclaw_browser → browser', () => {
93
+ assert.equal(canonicalizePluginId('openclaw_browser'), 'browser')
94
+ })
95
+
96
+ it('returns raw string (preserving case) for empty normalized result', () => {
97
+ // non-string input → normalizePluginId returns ''
98
+ assert.equal(canonicalizePluginId(123), '')
99
+ })
100
+ })
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // expandPluginIds
104
+ // ---------------------------------------------------------------------------
105
+ describe('expandPluginIds', () => {
106
+ it('shell implies process', () => {
107
+ const result = expandPluginIds(['shell'])
108
+ assert.ok(result.includes('shell'))
109
+ assert.ok(result.includes('process'))
110
+ })
111
+
112
+ it('manage_platform expands to 10 sub-plugins', () => {
113
+ const result = expandPluginIds(['manage_platform'])
114
+ const expected = [
115
+ 'manage_platform',
116
+ 'manage_agents',
117
+ 'manage_projects',
118
+ 'manage_tasks',
119
+ 'manage_schedules',
120
+ 'manage_skills',
121
+ 'manage_documents',
122
+ 'manage_webhooks',
123
+ 'manage_connectors',
124
+ 'manage_sessions',
125
+ 'manage_secrets',
126
+ ]
127
+ for (const e of expected) {
128
+ assert.ok(result.includes(e), `expected ${e} in expansion`)
129
+ }
130
+ })
131
+
132
+ it('web expands to include web_search and web_fetch', () => {
133
+ const result = expandPluginIds(['web'])
134
+ assert.ok(result.includes('web'))
135
+ assert.ok(result.includes('web_search'))
136
+ assert.ok(result.includes('web_fetch'))
137
+ })
138
+
139
+ it('removes duplicates after expansion', () => {
140
+ const result = expandPluginIds(['web', 'web_search', 'web_fetch'])
141
+ const unique = new Set(result)
142
+ assert.equal(result.length, unique.size)
143
+ })
144
+
145
+ it('returns empty array for empty input', () => {
146
+ assert.deepEqual(expandPluginIds([]), [])
147
+ })
148
+
149
+ it('keeps unknown plugin as-is', () => {
150
+ const result = expandPluginIds(['my_custom_plugin'])
151
+ assert.ok(result.includes('my_custom_plugin'))
152
+ })
153
+
154
+ it('deduplicates overlapping expansions from multiple inputs', () => {
155
+ const result = expandPluginIds(['web', 'web_search'])
156
+ const counts = result.reduce<Record<string, number>>((acc, id) => {
157
+ acc[id] = (acc[id] || 0) + 1
158
+ return acc
159
+ }, {})
160
+ for (const [id, count] of Object.entries(counts)) {
161
+ assert.equal(count, 1, `${id} appears ${count} times`)
162
+ }
163
+ })
164
+
165
+ it('returns empty array for null', () => {
166
+ assert.deepEqual(expandPluginIds(null), [])
167
+ })
168
+
169
+ it('returns empty array for undefined', () => {
170
+ assert.deepEqual(expandPluginIds(undefined), [])
171
+ })
172
+
173
+ it('shell also expands aliases (execute_command, process_tool)', () => {
174
+ const result = expandPluginIds(['shell'])
175
+ assert.ok(result.includes('execute_command'))
176
+ assert.ok(result.includes('process_tool'))
177
+ })
178
+
179
+ it('manage_platform + shell has no duplicates', () => {
180
+ const result = expandPluginIds(['manage_platform', 'shell'])
181
+ const unique = new Set(result)
182
+ assert.equal(result.length, unique.size)
183
+ })
184
+
185
+ it('handles same plugin requested multiple times', () => {
186
+ const result = expandPluginIds(['web', 'web', 'web'])
187
+ const webCount = result.filter((id) => id === 'web').length
188
+ assert.equal(webCount, 1)
189
+ })
190
+ })
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // getPluginAliases
194
+ // ---------------------------------------------------------------------------
195
+ describe('getPluginAliases', () => {
196
+ it('web returns [web, web_search, web_fetch]', () => {
197
+ const result = getPluginAliases('web')
198
+ assert.ok(result.includes('web'))
199
+ assert.ok(result.includes('web_search'))
200
+ assert.ok(result.includes('web_fetch'))
201
+ assert.equal(result.length, 3)
202
+ })
203
+
204
+ it('web_search returns the same group as web', () => {
205
+ const fromWeb = getPluginAliases('web').sort()
206
+ const fromAlias = getPluginAliases('web_search').sort()
207
+ assert.deepEqual(fromWeb, fromAlias)
208
+ })
209
+
210
+ it('unknown plugin returns array with just the input', () => {
211
+ assert.deepEqual(getPluginAliases('unknown_thing'), ['unknown_thing'])
212
+ })
213
+
214
+ it('shell includes execute_command and process_tool', () => {
215
+ const result = getPluginAliases('shell')
216
+ assert.ok(result.includes('shell'))
217
+ assert.ok(result.includes('execute_command'))
218
+ assert.ok(result.includes('process_tool'))
219
+ })
220
+
221
+ it('returns empty array for empty string', () => {
222
+ assert.deepEqual(getPluginAliases(''), [])
223
+ })
224
+
225
+ it('returns empty array for null', () => {
226
+ assert.deepEqual(getPluginAliases(null), [])
227
+ })
228
+
229
+ it('delegate group includes all delegate variants', () => {
230
+ const result = getPluginAliases('delegate')
231
+ assert.ok(result.includes('claude_code'))
232
+ assert.ok(result.includes('delegate_to_claude_code'))
233
+ assert.ok(result.includes('codex_cli'))
234
+ assert.ok(result.includes('delegate_to_codex_cli'))
235
+ })
236
+ })
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // pluginIdMatches
240
+ // ---------------------------------------------------------------------------
241
+ describe('pluginIdMatches', () => {
242
+ it('web enabled, web_search matches (alias)', () => {
243
+ assert.equal(pluginIdMatches(['web'], 'web_search'), true)
244
+ })
245
+
246
+ it('web_search enabled, web matches (reverse alias)', () => {
247
+ assert.equal(pluginIdMatches(['web_search'], 'web'), true)
248
+ })
249
+
250
+ it('files enabled, shell does not match (different families)', () => {
251
+ assert.equal(pluginIdMatches(['files'], 'shell'), false)
252
+ })
253
+
254
+ it('manage_platform enabled, manage_tasks matches (implication)', () => {
255
+ assert.equal(pluginIdMatches(['manage_platform'], 'manage_tasks'), true)
256
+ })
257
+
258
+ it('empty enabled list, nothing matches', () => {
259
+ assert.equal(pluginIdMatches([], 'web'), false)
260
+ })
261
+
262
+ it('case insensitive match', () => {
263
+ assert.equal(pluginIdMatches(['WEB'], 'web_search'), true)
264
+ })
265
+
266
+ it('shell enabled, process matches (implication)', () => {
267
+ assert.equal(pluginIdMatches(['shell'], 'process'), true)
268
+ })
269
+
270
+ it('manage_platform enabled, manage_secrets matches', () => {
271
+ assert.equal(pluginIdMatches(['manage_platform'], 'manage_secrets'), true)
272
+ })
273
+
274
+ it('null enabled list returns false', () => {
275
+ assert.equal(pluginIdMatches(null, 'web'), false)
276
+ })
277
+
278
+ it('undefined enabled list returns false', () => {
279
+ assert.equal(pluginIdMatches(undefined, 'web'), false)
280
+ })
281
+ })
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Complex expansion scenarios
285
+ // ---------------------------------------------------------------------------
286
+ describe('complex expansion scenarios', () => {
287
+ it('shell + web + memory fully expands', () => {
288
+ const result = expandPluginIds(['shell', 'web', 'memory'])
289
+ // shell family
290
+ assert.ok(result.includes('shell'))
291
+ assert.ok(result.includes('execute_command'))
292
+ assert.ok(result.includes('process_tool'))
293
+ assert.ok(result.includes('process'))
294
+ // web family
295
+ assert.ok(result.includes('web'))
296
+ assert.ok(result.includes('web_search'))
297
+ assert.ok(result.includes('web_fetch'))
298
+ // memory family
299
+ assert.ok(result.includes('memory'))
300
+ assert.ok(result.includes('memory_tool'))
301
+ })
302
+
303
+ it('large plugin list (50+ items) all expanded correctly', () => {
304
+ const ids = Array.from({ length: 50 }, (_, i) => `custom_plugin_${i}`)
305
+ ids.push('shell', 'web')
306
+ const result = expandPluginIds(ids)
307
+ // All custom ones present
308
+ for (let i = 0; i < 50; i++) {
309
+ assert.ok(result.includes(`custom_plugin_${i}`))
310
+ }
311
+ // Shell expansion present
312
+ assert.ok(result.includes('process'))
313
+ // Web expansion present
314
+ assert.ok(result.includes('web_fetch'))
315
+ })
316
+
317
+ it('alias chains do not cause infinite loops', () => {
318
+ // delegate has many aliases; expansion should terminate
319
+ const result = expandPluginIds(['delegate'])
320
+ assert.ok(result.includes('delegate'))
321
+ assert.ok(result.includes('claude_code'))
322
+ assert.ok(result.includes('delegate_to_claude_code'))
323
+ // Just confirm it returned without hanging
324
+ assert.ok(result.length > 0)
325
+ })
326
+
327
+ it('connector aliases expand correctly', () => {
328
+ const result = expandPluginIds(['manage_connectors'])
329
+ assert.ok(result.includes('manage_connectors'))
330
+ assert.ok(result.includes('connectors'))
331
+ assert.ok(result.includes('connector_message_tool'))
332
+ })
333
+
334
+ it('sandbox aliases expand', () => {
335
+ const result = expandPluginIds(['sandbox'])
336
+ assert.ok(result.includes('sandbox'))
337
+ assert.ok(result.includes('sandbox_exec'))
338
+ assert.ok(result.includes('sandbox_list_runtimes'))
339
+ })
340
+
341
+ it('files expands to include read_file, write_file, etc.', () => {
342
+ const result = expandPluginIds(['files'])
343
+ assert.ok(result.includes('read_file'))
344
+ assert.ok(result.includes('write_file'))
345
+ assert.ok(result.includes('list_files'))
346
+ assert.ok(result.includes('copy_file'))
347
+ assert.ok(result.includes('move_file'))
348
+ assert.ok(result.includes('delete_file'))
349
+ assert.ok(result.includes('send_file'))
350
+ })
351
+ })
@@ -4,6 +4,13 @@ import crypto from 'crypto'
4
4
  import { createRequire } from 'module'
5
5
  import { spawn } from 'child_process'
6
6
  import type { Plugin, PluginHooks, PluginMeta, PluginToolDef, PluginUIExtension, PluginProviderExtension, PluginConnectorExtension, Session, PluginPackageManager, PluginDependencyInstallStatus } from '@/types'
7
+ import {
8
+ inferPluginInstallSourceFromUrl,
9
+ inferPluginPublisherSourceFromUrl,
10
+ isMarketplaceInstallSource,
11
+ normalizePluginInstallSource,
12
+ normalizePluginPublisherSource,
13
+ } from '@/lib/plugin-sources'
7
14
  import { DATA_DIR } from './data-dir'
8
15
  import { canonicalizePluginId, expandPluginIds, getPluginAliases } from './tool-aliases'
9
16
  import { log } from './logger'
@@ -34,6 +41,9 @@ interface PluginFailureRecord {
34
41
  interface PluginConfigEntry {
35
42
  enabled?: boolean
36
43
  createdByAgentId?: string
44
+ source?: PluginMeta['source']
45
+ sourceLabel?: PluginMeta['sourceLabel']
46
+ installSource?: PluginMeta['installSource']
37
47
  sourceUrl?: string
38
48
  sourceHash?: string
39
49
  installedAt?: number
@@ -97,6 +107,8 @@ type HookRegistrar = {
97
107
  type HookContext<K extends keyof PluginHooks> =
98
108
  PluginHooks[K] extends ((ctx: infer C) => unknown) | undefined ? C : never
99
109
 
110
+ type ApprovalGuidanceHook = NonNullable<PluginHooks['getApprovalGuidance']>
111
+
100
112
  /** Legacy OpenClaw format: activate(ctx)/deactivate() */
101
113
  interface OpenClawLegacyPlugin {
102
114
  name: string
@@ -105,6 +117,116 @@ interface OpenClawLegacyPlugin {
105
117
  deactivate?: () => void
106
118
  }
107
119
 
120
+ function trimString(value: unknown): string {
121
+ return typeof value === 'string' ? value.trim() : ''
122
+ }
123
+
124
+ function normalizeApprovalGuidanceLines(
125
+ value: string | string[] | null | undefined,
126
+ ): string[] {
127
+ if (typeof value === 'string') {
128
+ const trimmed = value.trim()
129
+ return trimmed ? [trimmed] : []
130
+ }
131
+ if (!Array.isArray(value)) return []
132
+ return value
133
+ .map((line) => (typeof line === 'string' ? line.trim() : ''))
134
+ .filter(Boolean)
135
+ }
136
+
137
+ function dedupeApprovalGuidanceLines(lines: string[]): string[] {
138
+ return Array.from(new Set(lines.map((line) => line.trim()).filter(Boolean)))
139
+ }
140
+
141
+ function formatApprovalToolLabel(toolNames: string[]): string {
142
+ const uniqueNames = Array.from(new Set(toolNames.map((name) => name.trim()).filter(Boolean)))
143
+ if (uniqueNames.length === 0) return 'its tools'
144
+ if (uniqueNames.length === 1) return `\`${uniqueNames[0]}\``
145
+ if (uniqueNames.length === 2) return `\`${uniqueNames[0]}\` and \`${uniqueNames[1]}\``
146
+ return `${uniqueNames.slice(0, -1).map((name) => `\`${name}\``).join(', ')}, and \`${uniqueNames.at(-1)}\``
147
+ }
148
+
149
+ function buildDefaultPluginApprovalGuidance(params: {
150
+ pluginId: string
151
+ pluginName: string
152
+ tools: PluginToolDef[]
153
+ }): ApprovalGuidanceHook {
154
+ const toolNames = params.tools
155
+ .map((tool) => (typeof tool?.name === 'string' ? tool.name.trim() : ''))
156
+ .filter(Boolean)
157
+ const toolLabel = formatApprovalToolLabel(toolNames)
158
+ const matchIds = new Set(
159
+ dedupeApprovalGuidanceLines([
160
+ params.pluginId,
161
+ ...toolNames,
162
+ ...expandPluginIds([params.pluginId]),
163
+ ...toolNames.flatMap((toolName) => expandPluginIds([toolName])),
164
+ ]).map((value) => canonicalizePluginId(value) || value.toLowerCase()),
165
+ )
166
+
167
+ return ({ approval, phase, approved }) => {
168
+ if (approval.category !== 'tool_access') return null
169
+ const requestedIds = [
170
+ trimString(approval.data.pluginId),
171
+ trimString(approval.data.toolId),
172
+ trimString(approval.data.toolName),
173
+ ].filter(Boolean)
174
+ const matchesPlugin = requestedIds.some((value) => {
175
+ const candidates = [value, ...expandPluginIds([value])]
176
+ return candidates.some((candidate) => matchIds.has(canonicalizePluginId(candidate) || candidate.toLowerCase()))
177
+ })
178
+ if (!matchesPlugin) return null
179
+
180
+ if (phase === 'connector_reminder') {
181
+ return `Approving this lets the agent use ${toolLabel} from ${params.pluginName}.`
182
+ }
183
+ if (approved === true) {
184
+ return [
185
+ `Access to ${params.pluginName} is approved. Continue with ${toolLabel} on the next turn.`,
186
+ 'Do not request the same access again in prose once it has been approved.',
187
+ ]
188
+ }
189
+ if (approved === false) {
190
+ return `Do not request access to ${params.pluginName} again unless the task or required capability materially changes.`
191
+ }
192
+ return [
193
+ `If access to ${params.pluginName} is granted, continue with ${toolLabel} on the next turn.`,
194
+ 'Do not ask for the same access again in prose while this approval is pending.',
195
+ ]
196
+ }
197
+ }
198
+
199
+ function composeApprovalGuidance(
200
+ defaultHook: ApprovalGuidanceHook,
201
+ customHook?: PluginHooks['getApprovalGuidance'],
202
+ ): ApprovalGuidanceHook {
203
+ return (ctx) => {
204
+ const combined = dedupeApprovalGuidanceLines([
205
+ ...normalizeApprovalGuidanceLines(defaultHook(ctx)),
206
+ ...normalizeApprovalGuidanceLines(customHook?.(ctx)),
207
+ ])
208
+ return combined.length > 0 ? combined : null
209
+ }
210
+ }
211
+
212
+ function buildPluginHooks(
213
+ pluginId: string,
214
+ pluginName: string,
215
+ hooks: PluginHooks | undefined,
216
+ tools: PluginToolDef[] | undefined,
217
+ ): PluginHooks {
218
+ const nextHooks: PluginHooks = { ...(hooks || {}) }
219
+ nextHooks.getApprovalGuidance = composeApprovalGuidance(
220
+ buildDefaultPluginApprovalGuidance({
221
+ pluginId,
222
+ pluginName,
223
+ tools: tools || [],
224
+ }),
225
+ hooks?.getApprovalGuidance,
226
+ )
227
+ return nextHooks
228
+ }
229
+
108
230
  /**
109
231
  * Real OpenClaw plugin format: function export `(api) => {}` or object with `register(api)`.
110
232
  * Supports api.registerHook(), api.registerTool(), api.registerCommand(), api.registerService().
@@ -215,6 +337,30 @@ function toRawPluginUrl(url: string): string {
215
337
  return url
216
338
  }
217
339
 
340
+ function inferStoredPluginSource(config: PluginConfigEntry | null | undefined): PluginMeta['source'] {
341
+ if (config?.source === 'local' || config?.source === 'manual' || config?.source === 'marketplace') {
342
+ return config.source
343
+ }
344
+ if (config?.sourceUrl) {
345
+ const installSource = normalizePluginInstallSource(config?.installSource)
346
+ || inferPluginInstallSourceFromUrl(config.sourceUrl)
347
+ return isMarketplaceInstallSource(installSource) ? 'marketplace' : 'manual'
348
+ }
349
+ return 'local'
350
+ }
351
+
352
+ function inferStoredPublisherSource(config: PluginConfigEntry | null | undefined): NonNullable<PluginMeta['sourceLabel']> {
353
+ return normalizePluginPublisherSource(config?.sourceLabel)
354
+ || inferPluginPublisherSourceFromUrl(config?.sourceUrl)
355
+ || (config?.sourceUrl ? 'manual' : 'local')
356
+ }
357
+
358
+ function inferStoredInstallSource(config: PluginConfigEntry | null | undefined): NonNullable<PluginMeta['installSource']> {
359
+ return normalizePluginInstallSource(config?.installSource)
360
+ || inferPluginInstallSourceFromUrl(config?.sourceUrl)
361
+ || (config?.sourceUrl ? 'manual' : 'local')
362
+ }
363
+
218
364
  export function normalizeMarketplacePluginUrl(url: string): string {
219
365
  const trimmed = typeof url === 'string' ? url.trim() : ''
220
366
  if (!trimmed) return trimmed
@@ -835,9 +981,11 @@ class PluginManager {
835
981
  author: p.author || 'SwarmClaw',
836
982
  version: p.version || '1.0.0',
837
983
  source: 'local',
984
+ sourceLabel: 'builtin',
985
+ installSource: 'builtin',
838
986
  openclaw: p.openclaw === true,
839
987
  },
840
- hooks: p.hooks || {},
988
+ hooks: buildPluginHooks(id, p.name, p.hooks, p.tools),
841
989
  tools: p.tools || [],
842
990
  ui: p.ui,
843
991
  providers: p.providers,
@@ -855,7 +1003,7 @@ class PluginManager {
855
1003
 
856
1004
  let dynamicRequire: NodeRequire | null = null
857
1005
  try {
858
- dynamicRequire = createRequire(import.meta.url || __filename)
1006
+ dynamicRequire = createRequire(path.join(process.cwd(), 'package.json'))
859
1007
  } catch (err: unknown) {
860
1008
  log.warn('plugins', 'createRequire failed; external plugins disabled', {
861
1009
  error: err instanceof Error ? err.message : String(err),
@@ -886,10 +1034,13 @@ class PluginManager {
886
1034
  enabled: true,
887
1035
  author: plugin.author,
888
1036
  version: plugin.version || '0.0.1',
889
- source: explicitConfig?.sourceUrl ? 'marketplace' : 'local',
1037
+ source: inferStoredPluginSource(explicitConfig),
1038
+ sourceLabel: inferStoredPublisherSource(explicitConfig),
1039
+ installSource: inferStoredInstallSource(explicitConfig),
1040
+ sourceUrl: explicitConfig?.sourceUrl,
890
1041
  openclaw: plugin.openclaw === true,
891
1042
  },
892
- hooks: plugin.hooks || {},
1043
+ hooks: buildPluginHooks(file, plugin.name, plugin.hooks, plugin.tools),
893
1044
  tools: plugin.tools || [],
894
1045
  ui: plugin.ui,
895
1046
  providers: plugin.providers,
@@ -1146,6 +1297,44 @@ class PluginManager {
1146
1297
  return lines
1147
1298
  }
1148
1299
 
1300
+ /** Collect approval guidance from all enabled plugins for a specific approval event */
1301
+ collectApprovalGuidance(
1302
+ enabledPlugins: string[],
1303
+ ctx: {
1304
+ approval: import('@/types').ApprovalRequest
1305
+ phase: 'request' | 'resume' | 'connector_reminder'
1306
+ approved?: boolean
1307
+ },
1308
+ ): string[] {
1309
+ this.load()
1310
+ const enabledSet = new Set(expandPluginIds(enabledPlugins))
1311
+ const lines: string[] = []
1312
+
1313
+ for (const [id, p] of this.plugins.entries()) {
1314
+ if (!enabledSet.has(id)) continue
1315
+ const hook = p.hooks.getApprovalGuidance
1316
+ if (!hook) continue
1317
+ try {
1318
+ const result = hook(ctx)
1319
+ if (result === null || result === undefined) continue
1320
+ if (typeof result === 'string' && result.trim()) {
1321
+ lines.push(result)
1322
+ } else if (Array.isArray(result)) {
1323
+ for (const line of result) {
1324
+ if (typeof line === 'string' && line.trim()) lines.push(line)
1325
+ }
1326
+ }
1327
+ } catch (err: unknown) {
1328
+ log.error('plugins', 'getApprovalGuidance hook failed', {
1329
+ pluginId: id,
1330
+ error: err instanceof Error ? err.message : String(err),
1331
+ })
1332
+ }
1333
+ }
1334
+
1335
+ return lines
1336
+ }
1337
+
1149
1338
  /** Collect all settings fields declared by enabled plugins */
1150
1339
  collectSettingsFields(enabledPlugins: string[]): Array<{ pluginId: string; pluginName: string; fields: import('@/types').PluginSettingsField[] }> {
1151
1340
  this.load()
@@ -1323,6 +1512,11 @@ class PluginManager {
1323
1512
  return true
1324
1513
  }
1325
1514
 
1515
+ isExplicitlyDisabled(filename: string): boolean {
1516
+ const explicit = this.readConfigEntry(filename)
1517
+ return explicit?.enabled === false
1518
+ }
1519
+
1326
1520
  listPlugins(): PluginMeta[] {
1327
1521
  try {
1328
1522
  this.load()
@@ -1363,6 +1557,9 @@ class PluginManager {
1363
1557
  author: p.author || 'SwarmClaw',
1364
1558
  version: (p as { version?: string }).version || loaded?.meta.version || '1.0.0',
1365
1559
  source: loaded?.meta.source || 'local',
1560
+ sourceLabel: 'builtin',
1561
+ installSource: 'builtin',
1562
+ sourceUrl: loaded?.meta.sourceUrl,
1366
1563
  openclaw: p.openclaw === true,
1367
1564
  failureCount: failure?.count,
1368
1565
  lastFailureAt: failure?.lastFailedAt,
@@ -1391,7 +1588,10 @@ class PluginManager {
1391
1588
  isBuiltin: false,
1392
1589
  author: loaded?.meta.author,
1393
1590
  version: loaded?.meta.version || '0.0.1',
1394
- source: loaded?.meta.source || (explicitCfg?.sourceUrl ? 'marketplace' : 'local'),
1591
+ source: loaded?.meta.source || inferStoredPluginSource(explicitCfg),
1592
+ sourceLabel: loaded?.meta.sourceLabel || inferStoredPublisherSource(explicitCfg),
1593
+ installSource: loaded?.meta.installSource || inferStoredInstallSource(explicitCfg),
1594
+ sourceUrl: loaded?.meta.sourceUrl || explicitCfg?.sourceUrl,
1395
1595
  openclaw: loaded?.meta.openclaw,
1396
1596
  createdByAgentId: explicitCfg?.createdByAgentId || null,
1397
1597
  failureCount: failure?.count,