@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -0,0 +1,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,13 +117,123 @@ 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().
111
233
  */
112
234
  interface OpenClawPluginApi {
113
235
  registerHook: (event: string, handler: (...args: unknown[]) => unknown, meta?: { name?: string; description?: string }) => void
114
- registerTool: (def: PluginToolDef | { name: string; description?: string; parameters?: Record<string, unknown>; execute: (...args: unknown[]) => unknown }) => void
236
+ registerTool: (def: PluginToolDef | { name: string; description?: string; parameters?: Record<string, unknown>; planning?: PluginToolDef['planning']; execute: (...args: unknown[]) => unknown }) => void
115
237
  registerCommand: (def: { name: string; description?: string; handler: (...args: unknown[]) => unknown }) => void
116
238
  registerService: (def: { id: string; start: () => void; stop?: () => void }) => void
117
239
  registerProvider: (def: Record<string, unknown>) => void
@@ -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
@@ -295,6 +441,7 @@ function coerceTools(rawTools: unknown): PluginToolDef[] {
295
441
  name,
296
442
  description: typeof rawTool.description === 'string' ? rawTool.description : `Plugin tool: ${name}`,
297
443
  parameters: isRecord(rawTool.parameters) ? rawTool.parameters : { type: 'object', properties: {} },
444
+ planning: isRecord(rawTool.planning) ? rawTool.planning as PluginToolDef['planning'] : undefined,
298
445
  execute: execute as PluginToolDef['execute'],
299
446
  })
300
447
  }
@@ -320,6 +467,7 @@ function coerceTools(rawTools: unknown): PluginToolDef[] {
320
467
  name,
321
468
  description: typeof rawTool.description === 'string' ? rawTool.description : `Plugin tool: ${name}`,
322
469
  parameters: isRecord(rawTool.parameters) ? rawTool.parameters : { type: 'object', properties: {} },
470
+ planning: isRecord(rawTool.planning) ? rawTool.planning as PluginToolDef['planning'] : undefined,
323
471
  execute: rawTool.execute as PluginToolDef['execute'],
324
472
  })
325
473
  }
@@ -397,6 +545,9 @@ function normalizePlugin(mod: unknown): Plugin | null {
397
545
  name: def.name,
398
546
  description: def.description || `Plugin tool: ${def.name}`,
399
547
  parameters: (def.parameters || { type: 'object', properties: {} }) as Record<string, unknown>,
548
+ planning: isRecord((def as Record<string, unknown>).planning)
549
+ ? (def as PluginToolDef).planning
550
+ : undefined,
400
551
  execute: def.execute as PluginToolDef['execute'],
401
552
  })
402
553
  }
@@ -830,9 +981,11 @@ class PluginManager {
830
981
  author: p.author || 'SwarmClaw',
831
982
  version: p.version || '1.0.0',
832
983
  source: 'local',
984
+ sourceLabel: 'builtin',
985
+ installSource: 'builtin',
833
986
  openclaw: p.openclaw === true,
834
987
  },
835
- hooks: p.hooks || {},
988
+ hooks: buildPluginHooks(id, p.name, p.hooks, p.tools),
836
989
  tools: p.tools || [],
837
990
  ui: p.ui,
838
991
  providers: p.providers,
@@ -850,7 +1003,7 @@ class PluginManager {
850
1003
 
851
1004
  let dynamicRequire: NodeRequire | null = null
852
1005
  try {
853
- dynamicRequire = createRequire(import.meta.url || __filename)
1006
+ dynamicRequire = createRequire(path.join(process.cwd(), 'package.json'))
854
1007
  } catch (err: unknown) {
855
1008
  log.warn('plugins', 'createRequire failed; external plugins disabled', {
856
1009
  error: err instanceof Error ? err.message : String(err),
@@ -881,10 +1034,13 @@ class PluginManager {
881
1034
  enabled: true,
882
1035
  author: plugin.author,
883
1036
  version: plugin.version || '0.0.1',
884
- source: explicitConfig?.sourceUrl ? 'marketplace' : 'local',
1037
+ source: inferStoredPluginSource(explicitConfig),
1038
+ sourceLabel: inferStoredPublisherSource(explicitConfig),
1039
+ installSource: inferStoredInstallSource(explicitConfig),
1040
+ sourceUrl: explicitConfig?.sourceUrl,
885
1041
  openclaw: plugin.openclaw === true,
886
1042
  },
887
- hooks: plugin.hooks || {},
1043
+ hooks: buildPluginHooks(file, plugin.name, plugin.hooks, plugin.tools),
888
1044
  tools: plugin.tools || [],
889
1045
  ui: plugin.ui,
890
1046
  providers: plugin.providers,
@@ -1141,6 +1297,44 @@ class PluginManager {
1141
1297
  return lines
1142
1298
  }
1143
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
+
1144
1338
  /** Collect all settings fields declared by enabled plugins */
1145
1339
  collectSettingsFields(enabledPlugins: string[]): Array<{ pluginId: string; pluginName: string; fields: import('@/types').PluginSettingsField[] }> {
1146
1340
  this.load()
@@ -1318,6 +1512,11 @@ class PluginManager {
1318
1512
  return true
1319
1513
  }
1320
1514
 
1515
+ isExplicitlyDisabled(filename: string): boolean {
1516
+ const explicit = this.readConfigEntry(filename)
1517
+ return explicit?.enabled === false
1518
+ }
1519
+
1321
1520
  listPlugins(): PluginMeta[] {
1322
1521
  try {
1323
1522
  this.load()
@@ -1358,6 +1557,9 @@ class PluginManager {
1358
1557
  author: p.author || 'SwarmClaw',
1359
1558
  version: (p as { version?: string }).version || loaded?.meta.version || '1.0.0',
1360
1559
  source: loaded?.meta.source || 'local',
1560
+ sourceLabel: 'builtin',
1561
+ installSource: 'builtin',
1562
+ sourceUrl: loaded?.meta.sourceUrl,
1361
1563
  openclaw: p.openclaw === true,
1362
1564
  failureCount: failure?.count,
1363
1565
  lastFailureAt: failure?.lastFailedAt,
@@ -1386,7 +1588,10 @@ class PluginManager {
1386
1588
  isBuiltin: false,
1387
1589
  author: loaded?.meta.author,
1388
1590
  version: loaded?.meta.version || '0.0.1',
1389
- 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,
1390
1595
  openclaw: loaded?.meta.openclaw,
1391
1596
  createdByAgentId: explicitCfg?.createdByAgentId || null,
1392
1597
  failureCount: failure?.count,