@swarmclawai/swarmclaw 0.6.8 → 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 (166) hide show
  1. package/README.md +70 -45
  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 +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -1,251 +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, PluginToolDef } 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
- // Hook registrar: maps event names to functions that register a handler
12
- type HookRegistrar = Record<string, (fn: (...args: unknown[]) => unknown) => void>
40
+ type HookContext<K extends keyof PluginHooks> =
41
+ PluginHooks[K] extends ((ctx: infer C) => unknown) | undefined ? C : never
13
42
 
14
- // OpenClaw plugin format: { name, version, activate(ctx), deactivate() }
15
- interface OpenClawPlugin {
43
+ /** Legacy OpenClaw format: activate(ctx)/deactivate() */
44
+ interface OpenClawLegacyPlugin {
16
45
  name: string
17
46
  version?: string
18
- activate: (ctx: HookRegistrar) => void
47
+ activate: (ctx: HookRegistrar & { registerTool: (def: PluginToolDef) => void; log: PluginLogger }) => void
19
48
  deactivate?: () => void
20
49
  }
21
50
 
22
51
  /**
23
- * Normalize a module export into SwarmClaw's Plugin interface.
24
- * Supports both SwarmClaw format ({ name, hooks }) and OpenClaw format
25
- * ({ 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().
26
54
  */
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
+ }
69
+
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
90
+ }
91
+
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
+
27
120
  function normalizePlugin(mod: unknown): Plugin | null {
28
121
  const modObj = mod as Record<string, unknown>
29
122
  const raw: Record<string, unknown> = (modObj?.default as Record<string, unknown>) || modObj
30
123
 
31
- // SwarmClaw native format
32
- if (raw.name && raw.hooks) {
124
+ if (raw.name && (raw.hooks || raw.tools || raw.ui || raw.providers || raw.connectors)) {
125
+ const hooks = isRecord(raw.hooks) ? (raw.hooks as PluginHooks) : {}
33
126
  return {
34
- name: raw.name,
35
- description: raw.description,
36
- hooks: raw.hooks,
37
- tools: raw.tools,
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,
38
135
  } as Plugin
39
136
  }
40
137
 
41
- // OpenClaw format: { name, activate(ctx), deactivate() }
42
- if (raw.name && typeof raw.activate === 'function') {
43
- const oc = raw as unknown as OpenClawPlugin
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
44
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
+ }
164
+
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
+ }
45
170
 
46
- // OpenClaw's activate receives an object of hook registrars.
47
- // Map OpenClaw lifecycle names to SwarmClaw hook names.
48
- const registrar: HookRegistrar = {
49
- onAgentStart: (fn) => { hooks.beforeAgentStart = fn as PluginHooks['beforeAgentStart'] },
50
- onAgentComplete: (fn) => { hooks.afterAgentComplete = fn as PluginHooks['afterAgentComplete'] },
51
- onToolCall: (fn) => { hooks.beforeToolExec = fn as PluginHooks['beforeToolExec'] },
52
- onToolResult: (fn) => { hooks.afterToolExec = fn as PluginHooks['afterToolExec'] },
53
- onMessage: (fn) => { hooks.onMessage = fn as PluginHooks['onMessage'] },
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
+ }
54
238
  }
55
239
 
56
240
  try {
57
241
  oc.activate(registrar)
58
242
  } catch (err: unknown) {
59
- console.error(`[plugins] OpenClaw activate() failed for ${oc.name}:`, err instanceof Error ? err.message : String(err))
243
+ log.error('plugins', 'OpenClaw activate() failed', {
244
+ pluginName: oc.name,
245
+ error: err instanceof Error ? err.message : String(err),
246
+ })
60
247
  return null
61
248
  }
62
249
 
63
250
  return {
64
251
  name: oc.name,
252
+ version: oc.version,
65
253
  description: `OpenClaw plugin (v${oc.version || '0.0.0'})`,
66
254
  hooks,
255
+ tools,
67
256
  }
68
257
  }
69
-
70
258
  return null
71
259
  }
72
260
 
73
- // Ensure directories exist
74
- if (!fs.existsSync(PLUGINS_DIR)) fs.mkdirSync(PLUGINS_DIR, { recursive: true })
75
- if (!fs.existsSync(PLUGINS_CONFIG)) fs.writeFileSync(PLUGINS_CONFIG, '{}')
76
-
77
- // Use createRequire to avoid Turbopack static analysis of require()
78
- const dynamicRequire = createRequire(import.meta.url || __filename)
79
-
80
- interface LoadedPlugin {
261
+ interface LoadedPlugin {
262
+ id: string
81
263
  meta: PluginMeta
82
264
  hooks: PluginHooks
83
- tools?: PluginToolDef[]
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
84
276
  }
85
277
 
86
278
  class PluginManager {
87
- private plugins: LoadedPlugin[] = []
279
+ private plugins: Map<string, LoadedPlugin> = new Map()
280
+ private builtins: Map<string, Plugin> = new Map()
88
281
  private loaded = false
89
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
+
90
385
  load() {
91
386
  if (this.loaded) return
92
- this.plugins = []
387
+ this.plugins.clear()
93
388
 
94
389
  const config = this.loadConfig()
95
390
 
96
- try {
97
- const files = fs.readdirSync(PLUGINS_DIR).filter(
98
- (f) => f.endsWith('.js') || f.endsWith('.mjs'),
99
- )
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
+ }
100
408
 
101
- for (const file of files) {
102
- try {
103
- const fullPath = path.join(PLUGINS_DIR, file)
104
- // Clear require cache to allow reloads
105
- delete dynamicRequire.cache[fullPath]
106
- const mod = dynamicRequire(fullPath)
107
- const plugin = normalizePlugin(mod)
108
-
109
- if (!plugin) {
110
- console.warn(`[plugins] Skipping ${file}: unrecognized plugin format`)
111
- continue
112
- }
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
+ }
113
422
 
114
- const isEnabled = config[file]?.enabled !== false // enabled by default
115
-
116
- if (isEnabled) {
117
- this.plugins.push({
118
- meta: {
119
- name: plugin.name,
120
- description: plugin.description,
121
- filename: file,
122
- enabled: true,
123
- },
124
- hooks: plugin.hooks,
125
- tools: plugin.tools,
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,
126
445
  })
127
- console.log(`[plugins] Loaded: ${plugin.name} (${file})`)
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),
451
+ })
452
+ this.markPluginFailure(file, 'load.require', err, true)
128
453
  }
129
- } catch (err: unknown) {
130
- console.error(`[plugins] Failed to load ${file}:`, err instanceof Error ? err.message : String(err))
131
454
  }
132
455
  }
133
- } catch {
134
- // plugins dir doesn't exist or can't be read
135
- }
456
+ } catch { /* ignore */ }
136
457
 
137
458
  this.loaded = true
138
459
  }
139
460
 
140
- getPluginTools(): PluginToolDef[] {
461
+ getTools(enabledIds: string[]): Array<{ pluginId: string; tool: PluginToolDef }> {
141
462
  this.load()
142
- const allTools: PluginToolDef[] = []
143
- for (const plugin of this.plugins) {
144
- if (plugin.tools && Array.isArray(plugin.tools)) {
145
- allTools.push(...plugin.tools)
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
+ }
146
472
  }
147
473
  }
148
- return allTools
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
149
515
  }
150
516
 
151
- async runHook<K extends keyof PluginHooks>(
152
- hookName: K,
153
- ctx: Parameters<NonNullable<PluginHooks[K]>>[0],
154
- ): Promise<void> {
517
+ getUIExtensions(): PluginUIExtension[] {
155
518
  this.load()
156
- for (const plugin of this.plugins) {
157
- 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]
158
534
  if (hook) {
159
535
  try {
160
- await (hook as (ctx: Parameters<NonNullable<PluginHooks[K]>>[0]) => Promise<void> | void)(ctx)
536
+ await (hook as (hookCtx: HookContext<K>) => Promise<unknown> | unknown)(ctx)
537
+ this.markPluginSuccess(id)
161
538
  } catch (err: unknown) {
162
- console.error(`[plugins] Error in ${plugin.meta.name}.${hookName}:`, err instanceof Error ? err.message : String(err))
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)
163
546
  }
164
547
  }
165
548
  }
166
549
  }
167
550
 
168
- listPlugins(): PluginMeta[] {
551
+ async transformText(
552
+ hookName: 'transformInboundMessage' | 'transformOutboundMessage',
553
+ params: { session: Session; text: string },
554
+ enabledIds: string[] = [],
555
+ ): Promise<string> {
169
556
  this.load()
170
- const config = this.loadConfig()
557
+ const filterIds = enabledIds.length > 0 ? new Set(enabledIds) : null
558
+ let currentText = params.text
171
559
 
172
- // Include both loaded and disabled plugins
173
- const metas: PluginMeta[] = this.plugins.map((p) => p.meta)
174
-
175
- try {
176
- const files = fs.readdirSync(PLUGINS_DIR).filter(
177
- (f) => f.endsWith('.js') || f.endsWith('.mjs'),
178
- )
179
- for (const file of files) {
180
- if (!metas.find((m) => m.filename === file)) {
181
- metas.push({
182
- name: file.replace(/\.(js|mjs)$/, ''),
183
- filename: file,
184
- enabled: config[file]?.enabled !== false,
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),
185
574
  })
575
+ this.markPluginFailure(id, `hook.${String(hookName)}`, err, true)
186
576
  }
187
577
  }
188
- } catch { /* ignore */ }
578
+ }
579
+ return currentText
580
+ }
189
581
 
190
- return metas
582
+ recordExternalToolFailure(pluginId: string, toolName: string, err: unknown): void {
583
+ this.markPluginFailure(pluginId, `tool.${toolName}`, err, true)
191
584
  }
192
585
 
193
- setEnabled(filename: string, enabled: boolean) {
194
- const config = this.loadConfig()
195
- config[filename] = { ...config[filename], enabled }
196
- fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
197
- // Force reload on next hook call
198
- this.loaded = false
199
- this.plugins = []
586
+ recordExternalToolSuccess(pluginId: string): void {
587
+ this.markPluginSuccess(pluginId)
200
588
  }
201
589
 
202
- async installPlugin(url: string, filename: string): Promise<{ ok: boolean; error?: string }> {
203
- if (!url.startsWith('https://')) {
204
- return { ok: false, error: 'URL must be HTTPS' }
205
- }
206
- const sanitized = path.basename(filename)
207
- if (sanitized !== filename || !filename.endsWith('.js')) {
208
- return { ok: false, error: 'Invalid filename' }
209
- }
590
+ isEnabled(filename: string): boolean {
591
+ const config = this.loadConfig()
592
+ return config[filename]?.enabled !== false
593
+ }
210
594
 
595
+ listPlugins(): PluginMeta[] {
211
596
  try {
212
- const res = await fetch(url)
213
- if (!res.ok) throw new Error(`Download failed: ${res.status}`)
214
- const code = await res.text()
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,
614
+ }
615
+ }
215
616
 
216
- if (!fs.existsSync(PLUGINS_DIR)) {
217
- fs.mkdirSync(PLUGINS_DIR, { recursive: true })
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
+ })
218
637
  }
219
638
 
220
- fs.writeFileSync(path.join(PLUGINS_DIR, sanitized), code, 'utf8')
221
- this.reload()
222
- return { ok: true }
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
223
667
  } catch (err: unknown) {
224
- return { ok: false, error: err instanceof Error ? err.message : String(err) }
668
+ log.error('plugins', 'listPlugins failed', { error: err instanceof Error ? err.message : String(err) })
669
+ return []
225
670
  }
226
671
  }
227
672
 
228
- private loadConfig(): Record<string, { enabled: boolean }> {
229
- try {
230
- return JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'))
231
- } catch {
232
- return {}
673
+ setEnabled(filename: string, enabled: boolean) {
674
+ const config = this.loadConfig()
675
+ config[filename] = { ...config[filename], enabled }
676
+ fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
677
+ if (enabled) this.clearFailureState(filename)
678
+ this.reload()
679
+ }
680
+
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
+ }
695
+
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
+ }
717
+ }
233
718
  }
719
+
720
+ this.reload()
721
+ return true
234
722
  }
235
723
 
236
- reload() {
237
- this.loaded = false
238
- this.plugins = []
724
+ async updateAllPlugins() {
239
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 */ }
731
+ }
732
+ return true
733
+ }
734
+
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))
240
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() }
241
746
  }
242
747
 
243
748
  let _manager: PluginManager | null = null
244
-
245
749
  export function getPluginManager(): PluginManager {
246
- if (!_manager) {
247
- _manager = new PluginManager()
248
- _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
249
758
  }
250
- return _manager
251
759
  }