@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.
- package/README.md +82 -39
- 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 +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- 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/graph/route.ts +46 -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/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -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 +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- 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/checkpoint-timeline.tsx +112 -0
- 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 +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- 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 +37 -7
- package/src/components/home/home-view.tsx +54 -24
- 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 +87 -19
- 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-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- 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 +28 -9
- 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/hint-tip.tsx +31 -0
- 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 +149 -4
- 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 +224 -0
- 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 +72 -48
- 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 +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -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 +115 -16
- 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 +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -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 +32 -2
- 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 +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- 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 +78 -0
- 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 +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- 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 +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- 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
|
-
|
|
12
|
-
|
|
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:
|
|
47
|
+
activate: (ctx: HookRegistrar & { registerTool: (def: PluginToolDef) => void; log: PluginLogger }) => void
|
|
16
48
|
deactivate?: () => void
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
/**
|
|
20
|
-
*
|
|
21
|
-
* Supports
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
50
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
557
|
+
const filterIds = enabledIds.length > 0 ? new Set(enabledIds) : null
|
|
558
|
+
let currentText = params.text
|
|
149
559
|
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
this.
|
|
177
|
-
this.plugins = []
|
|
677
|
+
if (enabled) this.clearFailureState(filename)
|
|
678
|
+
this.reload()
|
|
178
679
|
}
|
|
179
680
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
}
|