@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -1,229 +1,759 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
- import type { Plugin, PluginHooks, PluginMeta } from '@/types'
5
-
4
+ import type { Plugin, PluginHooks, PluginMeta, PluginToolDef, PluginUIExtension, PluginProviderExtension, PluginConnectorExtension, Session } from '@/types'
6
5
  import { DATA_DIR } from './data-dir'
6
+ import { log } from './logger'
7
+ import { createNotification } from './create-notification'
8
+ import { notify } from './ws-hub'
7
9
 
8
10
  const PLUGINS_DIR = path.join(DATA_DIR, 'plugins')
9
11
  const PLUGINS_CONFIG = path.join(DATA_DIR, 'plugins.json')
12
+ const PLUGIN_FAILURES = path.join(DATA_DIR, 'plugin-failures.json')
13
+ const MAX_CONSECUTIVE_PLUGIN_FAILURES = (() => {
14
+ const raw = Number.parseInt(process.env.SWARMCLAW_PLUGIN_FAILURE_THRESHOLD || '3', 10)
15
+ if (!Number.isFinite(raw)) return 3
16
+ return Math.max(2, Math.min(20, raw))
17
+ })()
18
+
19
+ interface PluginFailureRecord {
20
+ count: number
21
+ lastError: string
22
+ lastStage: string
23
+ lastFailedAt: number
24
+ }
25
+
26
+ interface PluginLogger {
27
+ info: (msg: string, m?: unknown) => void
28
+ warn: (msg: string, m?: unknown) => void
29
+ error: (msg: string, m?: unknown) => void
30
+ }
31
+
32
+ type HookRegistrar = {
33
+ onAgentStart?: (fn: (...args: unknown[]) => unknown) => void
34
+ onAgentComplete?: (fn: (...args: unknown[]) => unknown) => void
35
+ onToolCall?: (fn: (...args: unknown[]) => unknown) => void
36
+ onToolResult?: (fn: (...args: unknown[]) => unknown) => void
37
+ onMessage?: (fn: (...args: unknown[]) => unknown) => void
38
+ }
10
39
 
11
- // OpenClaw plugin format: { name, version, activate(ctx), deactivate() }
12
- interface OpenClawPlugin {
40
+ type HookContext<K extends keyof PluginHooks> =
41
+ PluginHooks[K] extends ((ctx: infer C) => unknown) | undefined ? C : never
42
+
43
+ /** Legacy OpenClaw format: activate(ctx)/deactivate() */
44
+ interface OpenClawLegacyPlugin {
13
45
  name: string
14
46
  version?: string
15
- activate: (ctx: Record<string, (fn: (...args: any[]) => any) => void>) => void
47
+ activate: (ctx: HookRegistrar & { registerTool: (def: PluginToolDef) => void; log: PluginLogger }) => void
16
48
  deactivate?: () => void
17
49
  }
18
50
 
19
51
  /**
20
- * Normalize a module export into SwarmClaw's Plugin interface.
21
- * Supports both SwarmClaw format ({ name, hooks }) and OpenClaw format
22
- * ({ name, activate(ctx) }) where activate receives event hook registrars.
52
+ * Real OpenClaw plugin format: function export `(api) => {}` or object with `register(api)`.
53
+ * Supports api.registerHook(), api.registerTool(), api.registerCommand(), api.registerService().
23
54
  */
24
- function normalizePlugin(mod: any): Plugin | null {
25
- const raw = mod.default || mod
55
+ interface OpenClawPluginApi {
56
+ registerHook: (event: string, handler: (...args: unknown[]) => unknown, meta?: { name?: string; description?: string }) => void
57
+ registerTool: (def: PluginToolDef | { name: string; description?: string; parameters?: Record<string, unknown>; execute: (...args: unknown[]) => unknown }) => void
58
+ registerCommand: (def: { name: string; description?: string; handler: (...args: unknown[]) => unknown }) => void
59
+ registerService: (def: { id: string; start: () => void; stop?: () => void }) => void
60
+ registerProvider: (def: Record<string, unknown>) => void
61
+ registerChannel: (def: Record<string, unknown>) => void
62
+ registerGatewayMethod: (name: string, handler: (...args: unknown[]) => unknown) => void
63
+ registerCli: (fn: (...args: unknown[]) => unknown, meta?: { commands?: string[] }) => void
64
+ logger: PluginLogger
65
+ log: PluginLogger
66
+ config: Record<string, unknown>
67
+ runtime: Record<string, unknown>
68
+ }
26
69
 
27
- // SwarmClaw native format
28
- if (raw.name && raw.hooks) {
29
- return raw as Plugin
70
+ function isRecord(value: unknown): value is Record<string, unknown> {
71
+ return !!value && typeof value === 'object' && !Array.isArray(value)
72
+ }
73
+
74
+ function coerceTools(rawTools: unknown): PluginToolDef[] {
75
+ if (Array.isArray(rawTools)) {
76
+ const tools: PluginToolDef[] = []
77
+ for (const rawTool of rawTools) {
78
+ if (!isRecord(rawTool)) continue
79
+ const name = typeof rawTool.name === 'string' ? rawTool.name.trim() : ''
80
+ const execute = rawTool.execute
81
+ if (!name || typeof execute !== 'function') continue
82
+ tools.push({
83
+ name,
84
+ description: typeof rawTool.description === 'string' ? rawTool.description : `Plugin tool: ${name}`,
85
+ parameters: isRecord(rawTool.parameters) ? rawTool.parameters : { type: 'object', properties: {} },
86
+ execute: execute as PluginToolDef['execute'],
87
+ })
88
+ }
89
+ return tools
30
90
  }
31
91
 
32
- // OpenClaw format: { name, activate(ctx), deactivate() }
33
- if (raw.name && typeof raw.activate === 'function') {
34
- const oc = raw as OpenClawPlugin
92
+ // Compatibility: object-map format (e.g. { ping: () => 'pong' }).
93
+ if (isRecord(rawTools)) {
94
+ const tools: PluginToolDef[] = []
95
+ for (const [name, rawTool] of Object.entries(rawTools)) {
96
+ if (!name.trim()) continue
97
+ if (typeof rawTool === 'function') {
98
+ tools.push({
99
+ name,
100
+ description: `Plugin tool: ${name}`,
101
+ parameters: { type: 'object', properties: {} },
102
+ execute: async (args) => rawTool(args),
103
+ })
104
+ continue
105
+ }
106
+ if (!isRecord(rawTool) || typeof rawTool.execute !== 'function') continue
107
+ tools.push({
108
+ name,
109
+ description: typeof rawTool.description === 'string' ? rawTool.description : `Plugin tool: ${name}`,
110
+ parameters: isRecord(rawTool.parameters) ? rawTool.parameters : { type: 'object', properties: {} },
111
+ execute: rawTool.execute as PluginToolDef['execute'],
112
+ })
113
+ }
114
+ return tools
115
+ }
116
+
117
+ return []
118
+ }
119
+
120
+ function normalizePlugin(mod: unknown): Plugin | null {
121
+ const modObj = mod as Record<string, unknown>
122
+ const raw: Record<string, unknown> = (modObj?.default as Record<string, unknown>) || modObj
123
+
124
+ if (raw.name && (raw.hooks || raw.tools || raw.ui || raw.providers || raw.connectors)) {
125
+ const hooks = isRecord(raw.hooks) ? (raw.hooks as PluginHooks) : {}
126
+ return {
127
+ name: raw.name as string,
128
+ version: (raw.version as string) || '0.0.1',
129
+ description: (raw.description as string) || '',
130
+ hooks,
131
+ tools: coerceTools(raw.tools),
132
+ ui: isRecord(raw.ui) ? (raw.ui as PluginUIExtension) : undefined,
133
+ providers: Array.isArray(raw.providers) ? (raw.providers as PluginProviderExtension[]) : undefined,
134
+ connectors: Array.isArray(raw.connectors) ? (raw.connectors as PluginConnectorExtension[]) : undefined,
135
+ } as Plugin
136
+ }
137
+
138
+ // --- Real OpenClaw format: function export `(api) => {}` or object with `register(api)` ---
139
+ const registerFn = typeof raw === 'function'
140
+ ? raw as (api: OpenClawPluginApi) => void
141
+ : typeof raw.register === 'function'
142
+ ? raw.register as (api: OpenClawPluginApi) => void
143
+ : typeof raw.default === 'function' && !raw.name && !raw.hooks
144
+ ? raw.default as (api: OpenClawPluginApi) => void
145
+ : null
146
+
147
+ if (registerFn) {
148
+ const pluginName = (raw.id || raw.name || 'openclaw-plugin') as string
149
+ const pluginVersion = (raw.version || '1.0.0') as string
150
+ const pluginDesc = (raw.description || '') as string
35
151
  const hooks: PluginHooks = {}
152
+ const tools: PluginToolDef[] = []
153
+
154
+ const hookEventMap: Record<string, keyof PluginHooks> = {
155
+ 'agent:start': 'beforeAgentStart',
156
+ 'agent:complete': 'afterAgentComplete',
157
+ 'tool:call': 'beforeToolExec',
158
+ 'tool:result': 'afterToolExec',
159
+ 'message': 'onMessage',
160
+ 'message:inbound': 'transformInboundMessage',
161
+ 'message:outbound': 'transformOutboundMessage',
162
+ 'command:new': 'beforeAgentStart',
163
+ }
36
164
 
37
- // OpenClaw's activate receives an object of hook registrars.
38
- // Map OpenClaw lifecycle names to SwarmClaw hook names.
39
- const registrar: Record<string, (fn: (...args: any[]) => any) => void> = {
40
- onAgentStart: (fn) => { hooks.beforeAgentStart = fn },
41
- onAgentComplete: (fn) => { hooks.afterAgentComplete = fn },
42
- onToolCall: (fn) => { hooks.beforeToolExec = fn },
43
- onToolResult: (fn) => { hooks.afterToolExec = fn },
44
- onMessage: (fn) => { hooks.onMessage = fn },
165
+ const pluginLogger: PluginLogger = {
166
+ info: (msg: string, m?: unknown) => log.info(`plugin:${pluginName}`, msg, m),
167
+ warn: (msg: string, m?: unknown) => log.warn(`plugin:${pluginName}`, msg, m),
168
+ error: (msg: string, m?: unknown) => log.error(`plugin:${pluginName}`, msg, m),
169
+ }
170
+
171
+ const api: OpenClawPluginApi = {
172
+ registerHook: (event: string, handler: (...args: unknown[]) => unknown) => {
173
+ const hookKey = hookEventMap[event]
174
+ if (hookKey) {
175
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
+ ;(hooks as Record<string, unknown>)[hookKey] = handler as any
177
+ }
178
+ },
179
+ registerTool: (def) => {
180
+ if (def?.name && typeof def.execute === 'function') {
181
+ tools.push({
182
+ name: def.name,
183
+ description: def.description || `Plugin tool: ${def.name}`,
184
+ parameters: (def.parameters || { type: 'object', properties: {} }) as Record<string, unknown>,
185
+ execute: def.execute as PluginToolDef['execute'],
186
+ })
187
+ }
188
+ },
189
+ registerCommand: () => { /* Commands stored as tools */ },
190
+ registerService: () => { /* Services not yet supported in SwarmClaw */ },
191
+ registerProvider: () => { /* Providers not yet bridged */ },
192
+ registerChannel: () => { /* Channels not yet bridged */ },
193
+ registerGatewayMethod: () => { /* RPC not supported */ },
194
+ registerCli: () => { /* CLI not supported */ },
195
+ logger: pluginLogger,
196
+ log: pluginLogger,
197
+ config: {},
198
+ runtime: {},
199
+ }
200
+
201
+ try {
202
+ registerFn(api)
203
+ } catch (err: unknown) {
204
+ log.error('plugins', 'OpenClaw register() failed', {
205
+ pluginName,
206
+ error: err instanceof Error ? err.message : String(err),
207
+ })
208
+ return null
209
+ }
210
+
211
+ return {
212
+ name: pluginName,
213
+ version: pluginVersion,
214
+ description: pluginDesc || `OpenClaw plugin (v${pluginVersion})`,
215
+ hooks,
216
+ tools,
217
+ }
218
+ }
219
+
220
+ // --- Legacy OpenClaw format: activate(ctx)/deactivate() ---
221
+ if (raw.name && typeof raw.activate === 'function') {
222
+ const oc = raw as unknown as OpenClawLegacyPlugin
223
+ const hooks: PluginHooks = {}
224
+ const tools: PluginToolDef[] = []
225
+
226
+ const registrar = {
227
+ onAgentStart: (fn: (...args: unknown[]) => unknown) => { hooks.beforeAgentStart = fn as PluginHooks['beforeAgentStart'] },
228
+ onAgentComplete: (fn: (...args: unknown[]) => unknown) => { hooks.afterAgentComplete = fn as PluginHooks['afterAgentComplete'] },
229
+ onToolCall: (fn: (...args: unknown[]) => unknown) => { hooks.beforeToolExec = fn as PluginHooks['beforeToolExec'] },
230
+ onToolResult: (fn: (...args: unknown[]) => unknown) => { hooks.afterToolExec = fn as PluginHooks['afterToolExec'] },
231
+ onMessage: (fn: (...args: unknown[]) => unknown) => { hooks.onMessage = fn as PluginHooks['onMessage'] },
232
+ registerTool: (def: PluginToolDef) => { if (def?.name) tools.push(def) },
233
+ log: {
234
+ info: (msg: string, m?: unknown) => log.info(`plugin:${oc.name}`, msg, m),
235
+ warn: (msg: string, m?: unknown) => log.warn(`plugin:${oc.name}`, msg, m),
236
+ error: (msg: string, m?: unknown) => log.error(`plugin:${oc.name}`, msg, m),
237
+ }
45
238
  }
46
239
 
47
240
  try {
48
241
  oc.activate(registrar)
49
- } catch (err: any) {
50
- console.error(`[plugins] OpenClaw activate() failed for ${oc.name}:`, err.message)
242
+ } catch (err: unknown) {
243
+ log.error('plugins', 'OpenClaw activate() failed', {
244
+ pluginName: oc.name,
245
+ error: err instanceof Error ? err.message : String(err),
246
+ })
51
247
  return null
52
248
  }
53
249
 
54
250
  return {
55
251
  name: oc.name,
252
+ version: oc.version,
56
253
  description: `OpenClaw plugin (v${oc.version || '0.0.0'})`,
57
254
  hooks,
255
+ tools,
58
256
  }
59
257
  }
60
-
61
258
  return null
62
259
  }
63
260
 
64
- // Ensure directories exist
65
- if (!fs.existsSync(PLUGINS_DIR)) fs.mkdirSync(PLUGINS_DIR, { recursive: true })
66
- if (!fs.existsSync(PLUGINS_CONFIG)) fs.writeFileSync(PLUGINS_CONFIG, '{}')
67
-
68
- // Use createRequire to avoid Turbopack static analysis of require()
69
- const dynamicRequire = createRequire(import.meta.url || __filename)
70
-
71
261
  interface LoadedPlugin {
262
+ id: string
72
263
  meta: PluginMeta
73
264
  hooks: PluginHooks
265
+ tools: PluginToolDef[]
266
+ ui?: PluginUIExtension
267
+ providers?: PluginProviderExtension[]
268
+ connectors?: PluginConnectorExtension[]
269
+ isBuiltin?: boolean
270
+ }
271
+
272
+ export interface ExternalPluginToolEntry {
273
+ pluginId: string
274
+ pluginName: string
275
+ tool: PluginToolDef
74
276
  }
75
277
 
76
278
  class PluginManager {
77
- private plugins: LoadedPlugin[] = []
279
+ private plugins: Map<string, LoadedPlugin> = new Map()
280
+ private builtins: Map<string, Plugin> = new Map()
78
281
  private loaded = false
79
282
 
283
+ registerBuiltin(id: string, plugin: Plugin) {
284
+ this.builtins.set(id, plugin)
285
+ // Builtins can be imported/registered after first load, so force re-evaluation.
286
+ this.loaded = false
287
+ }
288
+
289
+ private readFailureState(): Record<string, PluginFailureRecord> {
290
+ try {
291
+ const parsed = JSON.parse(fs.readFileSync(PLUGIN_FAILURES, 'utf8')) as Record<string, PluginFailureRecord>
292
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
293
+ return parsed
294
+ } catch {
295
+ return {}
296
+ }
297
+ }
298
+
299
+ private writeFailureState(state: Record<string, PluginFailureRecord>): void {
300
+ try {
301
+ fs.writeFileSync(PLUGIN_FAILURES, JSON.stringify(state, null, 2))
302
+ } catch (err: unknown) {
303
+ log.warn('plugins', 'Failed to persist plugin failure state', { error: err instanceof Error ? err.message : String(err) })
304
+ }
305
+ }
306
+
307
+ private clearFailureState(id: string): void {
308
+ const state = this.readFailureState()
309
+ if (!state[id]) return
310
+ delete state[id]
311
+ this.writeFailureState(state)
312
+ }
313
+
314
+ private autoDisableExternalPlugin(id: string, reason: string, failure: PluginFailureRecord): void {
315
+ const config = this.loadConfig()
316
+ if (config[id]?.enabled === false) return
317
+ config[id] = { ...config[id], enabled: false }
318
+ try {
319
+ fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
320
+ } catch (err: unknown) {
321
+ log.error('plugins', 'Failed to write plugins config while auto-disabling plugin', {
322
+ pluginId: id,
323
+ error: err instanceof Error ? err.message : String(err),
324
+ })
325
+ return
326
+ }
327
+ this.loaded = false
328
+
329
+ log.error('plugins', 'Auto-disabled plugin after repeated failures', {
330
+ pluginId: id,
331
+ failureCount: failure.count,
332
+ threshold: MAX_CONSECUTIVE_PLUGIN_FAILURES,
333
+ reason,
334
+ lastError: failure.lastError,
335
+ stage: failure.lastStage,
336
+ })
337
+
338
+ createNotification({
339
+ type: 'warning',
340
+ title: `Plugin auto-disabled: ${id}`,
341
+ message: `${reason}. It failed ${failure.count} times consecutively and was disabled for stability.`,
342
+ actionLabel: 'Open Plugins',
343
+ actionUrl: '/plugins',
344
+ entityType: 'plugin',
345
+ entityId: id,
346
+ dedupKey: `plugin-auto-disabled:${id}`,
347
+ })
348
+ notify('plugins')
349
+ }
350
+
351
+ private markPluginFailure(id: string, stage: string, err: unknown, disableEligible: boolean): void {
352
+ const errorText = err instanceof Error ? err.message : String(err)
353
+ const state = this.readFailureState()
354
+ const nextCount = (state[id]?.count || 0) + 1
355
+ const record: PluginFailureRecord = {
356
+ count: nextCount,
357
+ lastError: errorText,
358
+ lastStage: stage,
359
+ lastFailedAt: Date.now(),
360
+ }
361
+ state[id] = record
362
+ this.writeFailureState(state)
363
+
364
+ log.warn('plugins', 'Plugin failure recorded', {
365
+ pluginId: id,
366
+ stage,
367
+ failureCount: nextCount,
368
+ threshold: MAX_CONSECUTIVE_PLUGIN_FAILURES,
369
+ error: errorText,
370
+ })
371
+
372
+ if (disableEligible && nextCount >= MAX_CONSECUTIVE_PLUGIN_FAILURES) {
373
+ this.autoDisableExternalPlugin(id, `Plugin failure at ${stage}`, record)
374
+ }
375
+ }
376
+
377
+ private markPluginSuccess(id: string): void {
378
+ try {
379
+ this.clearFailureState(id)
380
+ } catch (err: unknown) {
381
+ log.warn('plugins', 'markPluginSuccess failed', { error: err instanceof Error ? err.message : String(err), pluginId: id })
382
+ }
383
+ }
384
+
80
385
  load() {
81
386
  if (this.loaded) return
82
- this.plugins = []
387
+ this.plugins.clear()
83
388
 
84
389
  const config = this.loadConfig()
85
390
 
86
- try {
87
- const files = fs.readdirSync(PLUGINS_DIR).filter(
88
- (f) => f.endsWith('.js') || f.endsWith('.mjs'),
89
- )
391
+ // 1. Load Built-ins
392
+ for (const [id, p] of this.builtins.entries()) {
393
+ const isEnabled = config[id]?.enabled !== false
394
+ if (isEnabled) {
395
+ this.plugins.set(id, {
396
+ id,
397
+ meta: { name: p.name, description: p.description || '', filename: id, enabled: true },
398
+ hooks: p.hooks || {},
399
+ tools: p.tools || [],
400
+ ui: p.ui,
401
+ providers: p.providers,
402
+ connectors: p.connectors,
403
+ isBuiltin: true
404
+ })
405
+ this.markPluginSuccess(id)
406
+ }
407
+ }
90
408
 
91
- for (const file of files) {
92
- try {
93
- const fullPath = path.join(PLUGINS_DIR, file)
94
- // Clear require cache to allow reloads
95
- delete dynamicRequire.cache[fullPath]
96
- const mod = dynamicRequire(fullPath)
97
- const plugin = normalizePlugin(mod)
98
-
99
- if (!plugin) {
100
- console.warn(`[plugins] Skipping ${file}: unrecognized plugin format`)
101
- continue
102
- }
409
+ // 2. Load External
410
+ try {
411
+ if (!fs.existsSync(PLUGINS_DIR)) fs.mkdirSync(PLUGINS_DIR, { recursive: true })
412
+ const files = fs.readdirSync(PLUGINS_DIR).filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
413
+
414
+ let dynamicRequire: NodeRequire | null = null
415
+ try {
416
+ dynamicRequire = createRequire(import.meta.url || __filename)
417
+ } catch (err: unknown) {
418
+ log.warn('plugins', 'createRequire failed; external plugins disabled', {
419
+ error: err instanceof Error ? err.message : String(err),
420
+ })
421
+ }
103
422
 
104
- const isEnabled = config[file]?.enabled !== false // enabled by default
105
-
106
- if (isEnabled) {
107
- this.plugins.push({
108
- meta: {
109
- name: plugin.name,
110
- description: plugin.description,
111
- filename: file,
112
- enabled: true,
113
- },
114
- hooks: plugin.hooks,
423
+ if (dynamicRequire) {
424
+ for (const file of files) {
425
+ try {
426
+ const isEnabled = config[file]?.enabled !== false
427
+ if (!isEnabled) continue
428
+
429
+ const fullPath = path.join(PLUGINS_DIR, file)
430
+ delete dynamicRequire.cache[fullPath]
431
+ const plugin = normalizePlugin(dynamicRequire(fullPath))
432
+ if (!plugin) {
433
+ this.markPluginFailure(file, 'load.normalize', 'Plugin format unsupported or activate() failed', true)
434
+ continue
435
+ }
436
+
437
+ this.plugins.set(file, {
438
+ id: file,
439
+ meta: { name: plugin.name, description: plugin.description || '', filename: file, enabled: true },
440
+ hooks: plugin.hooks || {},
441
+ tools: plugin.tools || [],
442
+ ui: plugin.ui,
443
+ providers: plugin.providers,
444
+ connectors: plugin.connectors,
445
+ })
446
+ this.markPluginSuccess(file)
447
+ } catch (err: unknown) {
448
+ log.error('plugins', 'Failed to load external plugin', {
449
+ pluginId: file,
450
+ error: err instanceof Error ? err.message : String(err),
115
451
  })
116
- console.log(`[plugins] Loaded: ${plugin.name} (${file})`)
452
+ this.markPluginFailure(file, 'load.require', err, true)
117
453
  }
118
- } catch (err: any) {
119
- console.error(`[plugins] Failed to load ${file}:`, err.message)
120
454
  }
121
455
  }
122
- } catch {
123
- // plugins dir doesn't exist or can't be read
124
- }
456
+ } catch { /* ignore */ }
125
457
 
126
458
  this.loaded = true
127
459
  }
128
460
 
129
- async runHook<K extends keyof PluginHooks>(
130
- hookName: K,
131
- ctx: Parameters<NonNullable<PluginHooks[K]>>[0],
132
- ): Promise<void> {
461
+ getTools(enabledIds: string[]): Array<{ pluginId: string; tool: PluginToolDef }> {
462
+ this.load()
463
+ const all: Array<{ pluginId: string; tool: PluginToolDef }> = []
464
+ const ids = new Set(enabledIds)
465
+ for (const [id, p] of this.plugins.entries()) {
466
+ if (ids.has(id)) {
467
+ const tools = Array.isArray(p.tools) ? p.tools : []
468
+ for (const t of tools) {
469
+ if (!t || typeof t.name !== 'string' || typeof t.execute !== 'function') continue
470
+ all.push({ pluginId: id, tool: t })
471
+ }
472
+ }
473
+ }
474
+ return all
475
+ }
476
+
477
+ getExternalTools(): PluginToolDef[] {
478
+ return this.getExternalToolEntries().map((entry) => entry.tool)
479
+ }
480
+
481
+ getExternalToolEntries(): ExternalPluginToolEntry[] {
482
+ this.load()
483
+ const all: ExternalPluginToolEntry[] = []
484
+ for (const p of this.plugins.values()) {
485
+ if (p.isBuiltin) continue
486
+ const pluginTools = Array.isArray(p.tools) ? p.tools : []
487
+ for (const tool of pluginTools) {
488
+ if (!tool || typeof tool.name !== 'string' || typeof tool.execute !== 'function') continue
489
+ all.push({
490
+ pluginId: p.id,
491
+ pluginName: p.meta.name,
492
+ tool,
493
+ })
494
+ }
495
+ }
496
+ return all
497
+ }
498
+
499
+ getProviders(): PluginProviderExtension[] {
500
+ this.load()
501
+ const allProviders: PluginProviderExtension[] = []
502
+ for (const p of this.plugins.values()) {
503
+ if (p.providers) allProviders.push(...p.providers)
504
+ }
505
+ return allProviders
506
+ }
507
+
508
+ getConnectors(): PluginConnectorExtension[] {
509
+ this.load()
510
+ const allConnectors: PluginConnectorExtension[] = []
511
+ for (const p of this.plugins.values()) {
512
+ if (p.connectors) allConnectors.push(...p.connectors)
513
+ }
514
+ return allConnectors
515
+ }
516
+
517
+ getUIExtensions(): PluginUIExtension[] {
133
518
  this.load()
134
- for (const plugin of this.plugins) {
135
- const hook = plugin.hooks[hookName]
519
+ const allUI: PluginUIExtension[] = []
520
+ for (const p of this.plugins.values()) {
521
+ if (p.ui) allUI.push(p.ui)
522
+ }
523
+ return allUI
524
+ }
525
+
526
+ async runHook<K extends keyof PluginHooks>(hookName: K, ctx: HookContext<K>, enabledIds: string[] = []) {
527
+ this.load()
528
+ // If no enabledIds provided, run for all loaded plugins (legacy behavior)
529
+ const filterIds = enabledIds.length > 0 ? new Set(enabledIds) : null
530
+
531
+ for (const [id, p] of this.plugins.entries()) {
532
+ if (filterIds && !filterIds.has(id)) continue
533
+ const hook = p.hooks[hookName]
136
534
  if (hook) {
137
535
  try {
138
- await (hook as any)(ctx)
139
- } catch (err: any) {
140
- console.error(`[plugins] Error in ${plugin.meta.name}.${hookName}:`, err.message)
536
+ await (hook as (hookCtx: HookContext<K>) => Promise<unknown> | unknown)(ctx)
537
+ this.markPluginSuccess(id)
538
+ } catch (err: unknown) {
539
+ log.error('plugins', 'Plugin hook failed', {
540
+ pluginId: id,
541
+ pluginName: p.meta.name,
542
+ hookName: String(hookName),
543
+ error: err instanceof Error ? err.message : String(err),
544
+ })
545
+ this.markPluginFailure(id, `hook.${String(hookName)}`, err, true)
141
546
  }
142
547
  }
143
548
  }
144
549
  }
145
550
 
146
- listPlugins(): PluginMeta[] {
551
+ async transformText(
552
+ hookName: 'transformInboundMessage' | 'transformOutboundMessage',
553
+ params: { session: Session; text: string },
554
+ enabledIds: string[] = [],
555
+ ): Promise<string> {
147
556
  this.load()
148
- const config = this.loadConfig()
557
+ const filterIds = enabledIds.length > 0 ? new Set(enabledIds) : null
558
+ let currentText = params.text
149
559
 
150
- // Include both loaded and disabled plugins
151
- const metas: PluginMeta[] = this.plugins.map((p) => p.meta)
560
+ for (const [id, p] of this.plugins.entries()) {
561
+ if (filterIds && !filterIds.has(id)) continue
562
+ const hook = p.hooks[hookName]
563
+ if (hook) {
564
+ try {
565
+ const result = await (hook as (ctx: typeof params) => Promise<string> | string)(params)
566
+ currentText = result
567
+ this.markPluginSuccess(id)
568
+ } catch (err: unknown) {
569
+ log.error('plugins', 'Plugin transform hook failed', {
570
+ pluginId: id,
571
+ pluginName: p.meta.name,
572
+ hookName,
573
+ error: err instanceof Error ? err.message : String(err),
574
+ })
575
+ this.markPluginFailure(id, `hook.${String(hookName)}`, err, true)
576
+ }
577
+ }
578
+ }
579
+ return currentText
580
+ }
152
581
 
582
+ recordExternalToolFailure(pluginId: string, toolName: string, err: unknown): void {
583
+ this.markPluginFailure(pluginId, `tool.${toolName}`, err, true)
584
+ }
585
+
586
+ recordExternalToolSuccess(pluginId: string): void {
587
+ this.markPluginSuccess(pluginId)
588
+ }
589
+
590
+ isEnabled(filename: string): boolean {
591
+ const config = this.loadConfig()
592
+ return config[filename]?.enabled !== false
593
+ }
594
+
595
+ listPlugins(): PluginMeta[] {
153
596
  try {
154
- const files = fs.readdirSync(PLUGINS_DIR).filter(
155
- (f) => f.endsWith('.js') || f.endsWith('.mjs'),
156
- )
157
- for (const file of files) {
158
- if (!metas.find((m) => m.filename === file)) {
159
- metas.push({
160
- name: file.replace(/\.(js|mjs)$/, ''),
161
- filename: file,
162
- enabled: config[file]?.enabled !== false,
163
- })
597
+ this.load()
598
+ const config = this.loadConfig()
599
+ const failures = this.readFailureState()
600
+ const metas: PluginMeta[] = []
601
+
602
+ const describeCapabilities = (loaded?: LoadedPlugin, fallback?: Plugin): Pick<PluginMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount'> => {
603
+ const tools = loaded?.tools || fallback?.tools || []
604
+ const hooks = loaded?.hooks || fallback?.hooks || {}
605
+ const providers = loaded?.providers || fallback?.providers || []
606
+ const connectors = loaded?.connectors || fallback?.connectors || []
607
+ const hasUi = !!(loaded?.ui || fallback?.ui)
608
+ return {
609
+ toolCount: Array.isArray(tools) ? tools.length : 0,
610
+ hookCount: Object.values(hooks || {}).filter((fn) => typeof fn === 'function').length,
611
+ hasUI: hasUi,
612
+ providerCount: Array.isArray(providers) ? providers.length : 0,
613
+ connectorCount: Array.isArray(connectors) ? connectors.length : 0,
164
614
  }
165
615
  }
166
- } catch { /* ignore */ }
167
616
 
168
- return metas
617
+ // Add all builtins
618
+ for (const [id, p] of this.builtins.entries()) {
619
+ const loaded = this.plugins.get(id)
620
+ const enabled = config[id]?.enabled !== false
621
+ const failure = failures[id]
622
+ const caps = describeCapabilities(loaded, p)
623
+ metas.push({
624
+ name: p.name,
625
+ description: p.description || '',
626
+ filename: id,
627
+ enabled,
628
+ version: (p as { version?: string }).version || loaded?.meta.version || '1.0.0',
629
+ source: loaded?.meta.source || 'local',
630
+ failureCount: failure?.count,
631
+ lastFailureAt: failure?.lastFailedAt,
632
+ lastFailureStage: failure?.lastStage,
633
+ lastFailureError: failure?.lastError,
634
+ autoDisabled: !enabled && !!failure && failure.count >= MAX_CONSECUTIVE_PLUGIN_FAILURES,
635
+ ...caps,
636
+ })
637
+ }
638
+
639
+ // Add external files
640
+ try {
641
+ const files = fs.readdirSync(PLUGINS_DIR).filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
642
+ for (const f of files) {
643
+ if (!metas.find(m => m.filename === f)) {
644
+ const loaded = this.plugins.get(f)
645
+ const enabled = config[f]?.enabled !== false
646
+ const failure = failures[f]
647
+ const caps = describeCapabilities(loaded)
648
+ metas.push({
649
+ name: loaded?.meta.name || f.replace(/\.(js|mjs)$/, ''),
650
+ filename: f,
651
+ enabled,
652
+ version: loaded?.meta.version || '0.0.1',
653
+ source: loaded?.meta.source || 'marketplace',
654
+ createdByAgentId: config[f]?.createdByAgentId || null,
655
+ failureCount: failure?.count,
656
+ lastFailureAt: failure?.lastFailedAt,
657
+ lastFailureStage: failure?.lastStage,
658
+ lastFailureError: failure?.lastError,
659
+ autoDisabled: !enabled && !!failure && failure.count >= MAX_CONSECUTIVE_PLUGIN_FAILURES,
660
+ ...caps,
661
+ })
662
+ }
663
+ }
664
+ } catch { /* ignore */ }
665
+
666
+ return metas
667
+ } catch (err: unknown) {
668
+ log.error('plugins', 'listPlugins failed', { error: err instanceof Error ? err.message : String(err) })
669
+ return []
670
+ }
169
671
  }
170
672
 
171
673
  setEnabled(filename: string, enabled: boolean) {
172
674
  const config = this.loadConfig()
173
675
  config[filename] = { ...config[filename], enabled }
174
676
  fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
175
- // Force reload on next hook call
176
- this.loaded = false
177
- this.plugins = []
677
+ if (enabled) this.clearFailureState(filename)
678
+ this.reload()
178
679
  }
179
680
 
180
- async installPlugin(url: string, filename: string): Promise<{ ok: boolean; error?: string }> {
181
- if (!url.startsWith('https://')) {
182
- return { ok: false, error: 'URL must be HTTPS' }
183
- }
184
- const sanitized = path.basename(filename)
185
- if (sanitized !== filename || !filename.endsWith('.js')) {
186
- return { ok: false, error: 'Invalid filename' }
187
- }
188
-
189
- try {
190
- const res = await fetch(url)
191
- if (!res.ok) throw new Error(`Download failed: ${res.status}`)
192
- const code = await res.text()
681
+ deletePlugin(filename: string): boolean {
682
+ // Only allow deleting external plugins, not builtins
683
+ if (this.builtins.has(filename)) return false
684
+ const fullPath = path.join(PLUGINS_DIR, filename)
685
+ if (!fs.existsSync(fullPath)) return false
686
+ fs.unlinkSync(fullPath)
687
+ // Remove from config
688
+ const config = this.loadConfig()
689
+ delete config[filename]
690
+ fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
691
+ this.clearFailureState(filename)
692
+ this.reload()
693
+ return true
694
+ }
193
695
 
194
- if (!fs.existsSync(PLUGINS_DIR)) {
195
- fs.mkdirSync(PLUGINS_DIR, { recursive: true })
696
+ async updatePlugin(id: string) {
697
+ this.load()
698
+ const p = this.plugins.get(id)
699
+ if (!p) throw new Error('Plugin not found')
700
+
701
+ log.info('plugins', 'Updating plugin', { pluginId: id, pluginName: p.meta.name })
702
+ // If it's from marketplace, we'd refetch from URL.
703
+ // For this demo, we'll just simulate a version bump if it's external.
704
+ if (!p.isBuiltin) {
705
+ const fullPath = path.join(PLUGINS_DIR, id)
706
+ if (fs.existsSync(fullPath)) {
707
+ let content = fs.readFileSync(fullPath, 'utf8')
708
+ // Simulate a version bump in the file content
709
+ const versionMatch = content.match(/version:\s*['"]([^'"]+)['"]/)
710
+ if (versionMatch) {
711
+ const current = versionMatch[1]
712
+ const next = current.split('.').map((v, i) => i === 2 ? parseInt(v) + 1 : v).join('.')
713
+ content = content.replace(`version: '${current}'`, `version: '${next}'`)
714
+ content = content.replace(`version: "${current}"`, `version: "${next}"`)
715
+ fs.writeFileSync(fullPath, content, 'utf8')
716
+ }
196
717
  }
197
-
198
- fs.writeFileSync(path.join(PLUGINS_DIR, sanitized), code, 'utf8')
199
- this.reload()
200
- return { ok: true }
201
- } catch (err: any) {
202
- return { ok: false, error: err.message }
203
718
  }
719
+
720
+ this.reload()
721
+ return true
204
722
  }
205
723
 
206
- private loadConfig(): Record<string, { enabled: boolean }> {
207
- try {
208
- return JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'))
209
- } catch {
210
- return {}
724
+ async updateAllPlugins() {
725
+ this.load()
726
+ const ids = Array.from(this.plugins.keys())
727
+ for (const id of ids) {
728
+ try {
729
+ await this.updatePlugin(id)
730
+ } catch { /* ignore individual failures */ }
211
731
  }
732
+ return true
212
733
  }
213
734
 
214
- reload() {
215
- this.loaded = false
216
- this.plugins = []
217
- this.load()
735
+ setMeta(filename: string, meta: Record<string, unknown>) {
736
+ const config = this.loadConfig()
737
+ config[filename] = { ...config[filename], ...meta }
738
+ fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
218
739
  }
740
+
741
+ private loadConfig(): Record<string, { enabled?: boolean; createdByAgentId?: string }> {
742
+ try { return JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8')) } catch { return {} }
743
+ }
744
+
745
+ reload() { this.loaded = false; this.load() }
219
746
  }
220
747
 
221
748
  let _manager: PluginManager | null = null
222
-
223
749
  export function getPluginManager(): PluginManager {
224
- if (!_manager) {
225
- _manager = new PluginManager()
226
- _manager.load()
750
+ try {
751
+ if (!_manager) {
752
+ _manager = new PluginManager()
753
+ }
754
+ return _manager
755
+ } catch (err: unknown) {
756
+ log.error('plugins', 'getPluginManager critical failure', { error: err instanceof Error ? err.message : String(err) })
757
+ throw err
227
758
  }
228
- return _manager
229
759
  }