@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.
- package/README.md +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- 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
|
-
|
|
12
|
-
|
|
40
|
+
type HookContext<K extends keyof PluginHooks> =
|
|
41
|
+
PluginHooks[K] extends ((ctx: infer C) => unknown) | undefined ? C : never
|
|
13
42
|
|
|
14
|
-
|
|
15
|
-
interface
|
|
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
|
-
*
|
|
24
|
-
* Supports
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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:
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
+
getTools(enabledIds: string[]): Array<{ pluginId: string; tool: PluginToolDef }> {
|
|
141
462
|
this.load()
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
152
|
-
hookName: K,
|
|
153
|
-
ctx: Parameters<NonNullable<PluginHooks[K]>>[0],
|
|
154
|
-
): Promise<void> {
|
|
517
|
+
getUIExtensions(): PluginUIExtension[] {
|
|
155
518
|
this.load()
|
|
156
|
-
|
|
157
|
-
|
|
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 (
|
|
536
|
+
await (hook as (hookCtx: HookContext<K>) => Promise<unknown> | unknown)(ctx)
|
|
537
|
+
this.markPluginSuccess(id)
|
|
161
538
|
} catch (err: unknown) {
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
557
|
+
const filterIds = enabledIds.length > 0 ? new Set(enabledIds) : null
|
|
558
|
+
let currentText = params.text
|
|
171
559
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
}
|
|
578
|
+
}
|
|
579
|
+
return currentText
|
|
580
|
+
}
|
|
189
581
|
|
|
190
|
-
|
|
582
|
+
recordExternalToolFailure(pluginId: string, toolName: string, err: unknown): void {
|
|
583
|
+
this.markPluginFailure(pluginId, `tool.${toolName}`, err, true)
|
|
191
584
|
}
|
|
192
585
|
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
668
|
+
log.error('plugins', 'listPlugins failed', { error: err instanceof Error ? err.message : String(err) })
|
|
669
|
+
return []
|
|
225
670
|
}
|
|
226
671
|
}
|
|
227
672
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
}
|