@elizaos/plugin-registry 2.0.3-beta.2
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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/api/app-plugins-routes.d.ts +103 -0
- package/dist/api/app-plugins-routes.d.ts.map +1 -0
- package/dist/api/app-plugins-routes.js +1185 -0
- package/dist/api/app-plugins-routes.js.map +1 -0
- package/dist/api/plugin-routes.d.ts +140 -0
- package/dist/api/plugin-routes.d.ts.map +1 -0
- package/dist/api/plugin-routes.js +1391 -0
- package/dist/api/plugin-routes.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/services/plugin-installer.d.ts +22 -0
- package/dist/services/plugin-installer.d.ts.map +1 -0
- package/dist/services/plugin-installer.js +34 -0
- package/dist/services/plugin-installer.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyAdvancedCapabilitiesConfig,
|
|
3
|
+
applyPluginRuntimeMutation,
|
|
4
|
+
CORE_PLUGINS,
|
|
5
|
+
getPluginWidgets,
|
|
6
|
+
isAdvancedCapabilityPluginId,
|
|
7
|
+
loadElizaConfig,
|
|
8
|
+
OPTIONAL_CORE_PLUGINS,
|
|
9
|
+
resolveAdvancedCapabilitiesEnabled,
|
|
10
|
+
resolveDefaultAgentWorkspaceDir,
|
|
11
|
+
saveElizaConfig,
|
|
12
|
+
validatePluginConfig
|
|
13
|
+
} from "@elizaos/agent";
|
|
14
|
+
import { logger } from "@elizaos/core";
|
|
15
|
+
import {
|
|
16
|
+
asRecord,
|
|
17
|
+
isElizaSettingsDebugEnabled,
|
|
18
|
+
PostPluginCoreToggleRequestSchema,
|
|
19
|
+
PostPluginInstallRequestSchema,
|
|
20
|
+
PostPluginUninstallRequestSchema,
|
|
21
|
+
PostPluginUpdateRequestSchema,
|
|
22
|
+
PutPluginRequestSchema,
|
|
23
|
+
PutSecretsRequestSchema,
|
|
24
|
+
sanitizeForSettingsDebug,
|
|
25
|
+
settingsDebugCloudSummary
|
|
26
|
+
} from "@elizaos/shared";
|
|
27
|
+
function optionalPluginListId(npmName) {
|
|
28
|
+
if (npmName.startsWith("@elizaos/app-")) {
|
|
29
|
+
return npmName.slice("@elizaos/".length);
|
|
30
|
+
}
|
|
31
|
+
return npmName.replace("@elizaos/plugin-", "");
|
|
32
|
+
}
|
|
33
|
+
const ADVANCED_CAPABILITY_SERVICE_BY_PLUGIN_ID = {
|
|
34
|
+
experience: "EXPERIENCE",
|
|
35
|
+
personality: "CHARACTER_MANAGEMENT"
|
|
36
|
+
};
|
|
37
|
+
function getPluginHealthProbe(plugin) {
|
|
38
|
+
if (!plugin || typeof plugin !== "object") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const record = plugin;
|
|
42
|
+
for (const key of ["health", "healthCheck", "testConnection", "test"]) {
|
|
43
|
+
const candidate = record[key];
|
|
44
|
+
if (typeof candidate === "function") {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function normalizeRegistryLookupKey(value) {
|
|
51
|
+
const trimmed = value?.trim();
|
|
52
|
+
return trimmed ? trimmed.toLowerCase() : null;
|
|
53
|
+
}
|
|
54
|
+
function registryLookupCandidates(plugin) {
|
|
55
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
56
|
+
const add = (value) => {
|
|
57
|
+
const normalized = normalizeRegistryLookupKey(value);
|
|
58
|
+
if (normalized) candidates.add(normalized);
|
|
59
|
+
};
|
|
60
|
+
add(plugin.npmName);
|
|
61
|
+
add(plugin.name);
|
|
62
|
+
add(plugin.id);
|
|
63
|
+
add(`@elizaos/plugin-${plugin.id}`);
|
|
64
|
+
add(`@elizaos/app-${plugin.id}`);
|
|
65
|
+
return Array.from(candidates);
|
|
66
|
+
}
|
|
67
|
+
function applyRegistryMetadata(plugin, registryMetadataByName) {
|
|
68
|
+
const registryInfo = registryLookupCandidates(plugin).map((candidate) => registryMetadataByName.get(candidate)).find((candidate) => Boolean(candidate));
|
|
69
|
+
if (!registryInfo) {
|
|
70
|
+
if (plugin.npmName?.startsWith("@elizaos/")) {
|
|
71
|
+
plugin.origin ??= "builtin";
|
|
72
|
+
plugin.registrySource ??= "builtin";
|
|
73
|
+
plugin.support ??= "first-party";
|
|
74
|
+
plugin.builtIn ??= true;
|
|
75
|
+
plugin.firstParty ??= true;
|
|
76
|
+
plugin.thirdParty ??= false;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
plugin.directory = registryInfo.directory ?? plugin.directory ?? null;
|
|
81
|
+
plugin.registryKind = registryInfo.registryKind ?? registryInfo.kind ?? plugin.registryKind;
|
|
82
|
+
plugin.origin = registryInfo.origin ?? plugin.origin;
|
|
83
|
+
plugin.registrySource = registryInfo.source ?? plugin.registrySource;
|
|
84
|
+
plugin.support = registryInfo.support ?? plugin.support;
|
|
85
|
+
plugin.builtIn = registryInfo.builtIn ?? plugin.builtIn;
|
|
86
|
+
plugin.firstParty = registryInfo.firstParty ?? plugin.firstParty;
|
|
87
|
+
plugin.thirdParty = registryInfo.thirdParty ?? plugin.thirdParty;
|
|
88
|
+
plugin.status = registryInfo.status ?? plugin.status;
|
|
89
|
+
plugin.homepage = plugin.homepage ?? registryInfo.homepage ?? void 0;
|
|
90
|
+
plugin.repository = plugin.repository ?? `https://github.com/${registryInfo.gitRepo}`;
|
|
91
|
+
plugin.latestVersion = plugin.latestVersion ?? registryInfo.npm.v2Version ?? registryInfo.npm.v1Version ?? registryInfo.npm.v0Version ?? null;
|
|
92
|
+
}
|
|
93
|
+
const pluginsListInFlight = /* @__PURE__ */ new WeakMap();
|
|
94
|
+
function readCompatEnabledFromConfig(config, pluginId) {
|
|
95
|
+
const container = asRecord(config.connectors)?.[pluginId] ?? asRecord(config.streaming)?.[pluginId];
|
|
96
|
+
const value = asRecord(container)?.enabled;
|
|
97
|
+
return typeof value === "boolean" ? value : null;
|
|
98
|
+
}
|
|
99
|
+
function buildCoreToggleDiagnostics(config, npmName) {
|
|
100
|
+
const pluginId = optionalPluginListId(npmName);
|
|
101
|
+
const isOptional = OPTIONAL_CORE_PLUGINS.includes(
|
|
102
|
+
npmName
|
|
103
|
+
);
|
|
104
|
+
if (!isOptional) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const allowList = new Set(config.plugins?.allow ?? []);
|
|
108
|
+
const enabledAllowList = allowList.has(npmName) || allowList.has(pluginId);
|
|
109
|
+
const entryEnabledRaw = config.plugins?.entries?.[pluginId]?.enabled;
|
|
110
|
+
const enabledEntries = typeof entryEnabledRaw === "boolean" ? entryEnabledRaw : null;
|
|
111
|
+
const enabledCompat = readCompatEnabledFromConfig(config, pluginId);
|
|
112
|
+
const driftFlags = [];
|
|
113
|
+
if (enabledEntries !== null && enabledEntries !== enabledAllowList) {
|
|
114
|
+
driftFlags.push("entries_vs_allowlist");
|
|
115
|
+
}
|
|
116
|
+
if (enabledEntries !== null && enabledCompat !== null && enabledEntries !== enabledCompat) {
|
|
117
|
+
driftFlags.push("entries_vs_compat");
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
pluginId,
|
|
121
|
+
npmName,
|
|
122
|
+
enabled_allowlist: enabledAllowList,
|
|
123
|
+
enabled_entries: enabledEntries,
|
|
124
|
+
enabled_compat: enabledCompat,
|
|
125
|
+
drift_flags: driftFlags
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function handlePluginRoutes(ctx) {
|
|
129
|
+
const {
|
|
130
|
+
req,
|
|
131
|
+
res,
|
|
132
|
+
method,
|
|
133
|
+
pathname,
|
|
134
|
+
state,
|
|
135
|
+
json,
|
|
136
|
+
error,
|
|
137
|
+
readJsonBody,
|
|
138
|
+
scheduleRuntimeRestart,
|
|
139
|
+
restartRuntime,
|
|
140
|
+
BLOCKED_ENV_KEYS,
|
|
141
|
+
discoverInstalledPlugins,
|
|
142
|
+
maskValue,
|
|
143
|
+
aggregateSecrets,
|
|
144
|
+
readProviderCache,
|
|
145
|
+
paramKeyToCategory,
|
|
146
|
+
buildPluginEvmDiagnosticEntry,
|
|
147
|
+
EVM_PLUGIN_PACKAGE,
|
|
148
|
+
applyWhatsAppQrOverride,
|
|
149
|
+
applySignalQrOverride,
|
|
150
|
+
resolvePluginConfigMutationRejections,
|
|
151
|
+
requirePluginManager,
|
|
152
|
+
requireCoreManager
|
|
153
|
+
} = ctx;
|
|
154
|
+
const buildPluginsListSnapshot = async () => {
|
|
155
|
+
let freshConfig;
|
|
156
|
+
try {
|
|
157
|
+
freshConfig = loadElizaConfig();
|
|
158
|
+
} catch {
|
|
159
|
+
freshConfig = state.config;
|
|
160
|
+
}
|
|
161
|
+
const bundledIds = new Set(state.plugins.map((p) => p.id));
|
|
162
|
+
const installedEntries = discoverInstalledPlugins(freshConfig, bundledIds);
|
|
163
|
+
const allPlugins = [...state.plugins, ...installedEntries];
|
|
164
|
+
let installedMetadataByName = /* @__PURE__ */ new Map();
|
|
165
|
+
let registryMetadataByName = /* @__PURE__ */ new Map();
|
|
166
|
+
try {
|
|
167
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
168
|
+
const installed = await pluginManager.listInstalledPlugins();
|
|
169
|
+
installedMetadataByName = new Map(
|
|
170
|
+
installed.map((plugin) => [
|
|
171
|
+
plugin.name,
|
|
172
|
+
{
|
|
173
|
+
version: plugin.version,
|
|
174
|
+
releaseStream: plugin.releaseStream,
|
|
175
|
+
requestedVersion: plugin.requestedVersion,
|
|
176
|
+
latestVersion: plugin.latestVersion,
|
|
177
|
+
betaVersion: plugin.betaVersion
|
|
178
|
+
}
|
|
179
|
+
])
|
|
180
|
+
);
|
|
181
|
+
const registry = await pluginManager.refreshRegistry();
|
|
182
|
+
registryMetadataByName = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const [key, info] of registry) {
|
|
184
|
+
for (const candidate of [key, info.name, info.npm.package]) {
|
|
185
|
+
const normalized = normalizeRegistryLookupKey(candidate);
|
|
186
|
+
if (normalized) registryMetadataByName.set(normalized, info);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
const evmDiagnostic = buildPluginEvmDiagnosticEntry({
|
|
192
|
+
config: state.config,
|
|
193
|
+
runtime: state.runtime
|
|
194
|
+
});
|
|
195
|
+
const existingEvmPlugin = allPlugins.find(
|
|
196
|
+
(plugin) => plugin.id === "evm" || plugin.id === "wallet" || plugin.npmName === EVM_PLUGIN_PACKAGE
|
|
197
|
+
);
|
|
198
|
+
if (existingEvmPlugin) {
|
|
199
|
+
existingEvmPlugin.autoEnabled = evmDiagnostic.autoEnabled;
|
|
200
|
+
existingEvmPlugin.managementMode = "core-optional";
|
|
201
|
+
existingEvmPlugin.capabilityStatus = evmDiagnostic.capabilityStatus;
|
|
202
|
+
existingEvmPlugin.capabilityReason = evmDiagnostic.capabilityReason;
|
|
203
|
+
existingEvmPlugin.prerequisites = evmDiagnostic.prerequisites;
|
|
204
|
+
existingEvmPlugin.setupGuideUrl = existingEvmPlugin.setupGuideUrl ?? evmDiagnostic.setupGuideUrl;
|
|
205
|
+
existingEvmPlugin.tags = Array.from(
|
|
206
|
+
/* @__PURE__ */ new Set([...existingEvmPlugin.tags, ...evmDiagnostic.tags])
|
|
207
|
+
);
|
|
208
|
+
} else {
|
|
209
|
+
allPlugins.push(evmDiagnostic);
|
|
210
|
+
}
|
|
211
|
+
const configEntries = freshConfig.plugins?.entries;
|
|
212
|
+
const advancedCapabilitiesEnabled = resolveAdvancedCapabilitiesEnabled(freshConfig);
|
|
213
|
+
const loadedNames = state.runtime ? state.runtime.plugins.map((p) => p.name) : [];
|
|
214
|
+
for (const plugin of allPlugins) {
|
|
215
|
+
applyRegistryMetadata(plugin, registryMetadataByName);
|
|
216
|
+
const installedMetadata = (plugin.npmName ? installedMetadataByName.get(plugin.npmName) : null) ?? installedMetadataByName.get(plugin.name);
|
|
217
|
+
if (installedMetadata) {
|
|
218
|
+
plugin.version = installedMetadata.version ?? plugin.version;
|
|
219
|
+
plugin.releaseStream = installedMetadata.releaseStream ?? plugin.releaseStream;
|
|
220
|
+
plugin.requestedVersion = installedMetadata.requestedVersion ?? plugin.requestedVersion;
|
|
221
|
+
plugin.latestVersion = installedMetadata.latestVersion ?? plugin.latestVersion ?? null;
|
|
222
|
+
plugin.betaVersion = installedMetadata.betaVersion ?? plugin.betaVersion ?? null;
|
|
223
|
+
}
|
|
224
|
+
if (isAdvancedCapabilityPluginId(plugin.id)) {
|
|
225
|
+
const serviceType = ADVANCED_CAPABILITY_SERVICE_BY_PLUGIN_ID[plugin.id];
|
|
226
|
+
plugin.enabled = advancedCapabilitiesEnabled;
|
|
227
|
+
plugin.isActive = advancedCapabilitiesEnabled ? serviceType ? Boolean(state.runtime?.getService(serviceType)) : Boolean(state.runtime) : false;
|
|
228
|
+
plugin.autoEnabled = advancedCapabilitiesEnabled;
|
|
229
|
+
plugin.loadError = void 0;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const suffix = `plugin-${plugin.id}`;
|
|
233
|
+
const packageName = `@elizaos/plugin-${plugin.id}`;
|
|
234
|
+
const npmPkgName = plugin.npmName;
|
|
235
|
+
const isLoaded = loadedNames.length > 0 && loadedNames.some((name) => {
|
|
236
|
+
return name === plugin.id || name === suffix || name === packageName || npmPkgName != null && name === npmPkgName || name.endsWith(`/${suffix}`);
|
|
237
|
+
});
|
|
238
|
+
plugin.isActive = isLoaded;
|
|
239
|
+
const configEntry = configEntries?.[plugin.id];
|
|
240
|
+
if (configEntry && typeof configEntry.enabled === "boolean") {
|
|
241
|
+
plugin.enabled = configEntry.enabled;
|
|
242
|
+
} else {
|
|
243
|
+
plugin.enabled = isLoaded;
|
|
244
|
+
}
|
|
245
|
+
plugin.loadError = void 0;
|
|
246
|
+
if (plugin.enabled && !isLoaded && state.runtime) {
|
|
247
|
+
const installs = freshConfig.plugins?.installs;
|
|
248
|
+
const packageName2 = `@elizaos/plugin-${plugin.id}`;
|
|
249
|
+
const hasInstallRecord = installs?.[packageName2] || installs?.[plugin.id];
|
|
250
|
+
if (hasInstallRecord) {
|
|
251
|
+
plugin.loadError = "Plugin installed but failed to load \u2014 the package may be missing compiled files.";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (plugin.id === "evm" || plugin.id === "wallet" || plugin.npmName === EVM_PLUGIN_PACKAGE) {
|
|
255
|
+
plugin.enabled = evmDiagnostic.enabled;
|
|
256
|
+
plugin.isActive = evmDiagnostic.isActive;
|
|
257
|
+
plugin.autoEnabled = evmDiagnostic.autoEnabled;
|
|
258
|
+
plugin.managementMode = "core-optional";
|
|
259
|
+
plugin.capabilityStatus = evmDiagnostic.capabilityStatus;
|
|
260
|
+
plugin.capabilityReason = evmDiagnostic.capabilityReason;
|
|
261
|
+
plugin.prerequisites = evmDiagnostic.prerequisites;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const plugin of allPlugins) {
|
|
265
|
+
for (const param of plugin.parameters) {
|
|
266
|
+
const envValue = process.env[param.key];
|
|
267
|
+
param.isSet = Boolean(envValue?.trim());
|
|
268
|
+
param.currentValue = param.isSet ? param.sensitive ? maskValue(envValue ?? "") : envValue ?? "" : null;
|
|
269
|
+
}
|
|
270
|
+
const paramInfos = plugin.parameters.map((p) => ({
|
|
271
|
+
key: p.key,
|
|
272
|
+
required: p.required,
|
|
273
|
+
sensitive: p.sensitive,
|
|
274
|
+
type: p.type,
|
|
275
|
+
description: p.description,
|
|
276
|
+
default: p.default
|
|
277
|
+
}));
|
|
278
|
+
const validation = validatePluginConfig(
|
|
279
|
+
plugin.id,
|
|
280
|
+
plugin.category,
|
|
281
|
+
plugin.envKey,
|
|
282
|
+
plugin.configKeys,
|
|
283
|
+
void 0,
|
|
284
|
+
paramInfos
|
|
285
|
+
);
|
|
286
|
+
plugin.validationErrors = validation.errors;
|
|
287
|
+
plugin.validationWarnings = validation.warnings;
|
|
288
|
+
}
|
|
289
|
+
applyWhatsAppQrOverride(allPlugins, resolveDefaultAgentWorkspaceDir());
|
|
290
|
+
applySignalQrOverride(allPlugins, resolveDefaultAgentWorkspaceDir());
|
|
291
|
+
for (const plugin of allPlugins) {
|
|
292
|
+
const providerModels = readProviderCache(plugin.id)?.models ?? [];
|
|
293
|
+
for (const param of plugin.parameters) {
|
|
294
|
+
if (!param.key.toUpperCase().includes("MODEL")) continue;
|
|
295
|
+
const expectedCat = paramKeyToCategory(param.key);
|
|
296
|
+
const filtered = providerModels.filter(
|
|
297
|
+
(m) => m.category === expectedCat
|
|
298
|
+
);
|
|
299
|
+
if (!plugin.configUiHints) plugin.configUiHints = {};
|
|
300
|
+
plugin.configUiHints[param.key] = {
|
|
301
|
+
...plugin.configUiHints[param.key],
|
|
302
|
+
type: "select",
|
|
303
|
+
options: filtered.map((m) => ({
|
|
304
|
+
value: m.id,
|
|
305
|
+
label: m.name !== m.id ? `${m.name} (${m.id})` : m.id
|
|
306
|
+
}))
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const runtimePlugins = state.runtime?.plugins ?? [];
|
|
311
|
+
const normalizeId = (value) => {
|
|
312
|
+
let v = value.trim();
|
|
313
|
+
if (v.startsWith("@")) {
|
|
314
|
+
const slash = v.indexOf("/");
|
|
315
|
+
if (slash > 0) v = v.slice(slash + 1);
|
|
316
|
+
}
|
|
317
|
+
if (v.startsWith("plugin-")) v = v.slice("plugin-".length);
|
|
318
|
+
if (v.startsWith("app-")) v = v.slice("app-".length);
|
|
319
|
+
return v;
|
|
320
|
+
};
|
|
321
|
+
for (const plugin of allPlugins) {
|
|
322
|
+
const widgets = getPluginWidgets(plugin.id, runtimePlugins);
|
|
323
|
+
if (widgets.length > 0) {
|
|
324
|
+
plugin.widgets = widgets;
|
|
325
|
+
}
|
|
326
|
+
const normalizedPluginId = normalizeId(plugin.id);
|
|
327
|
+
const matchedRuntime = runtimePlugins.find(
|
|
328
|
+
(p) => normalizeId(p.name) === normalizedPluginId
|
|
329
|
+
);
|
|
330
|
+
if (matchedRuntime?.app) {
|
|
331
|
+
const a = matchedRuntime.app;
|
|
332
|
+
plugin.app = {
|
|
333
|
+
displayName: a.displayName,
|
|
334
|
+
category: a.category,
|
|
335
|
+
icon: a.icon,
|
|
336
|
+
developerOnly: a.developerOnly,
|
|
337
|
+
visibleInAppStore: a.visibleInAppStore,
|
|
338
|
+
navTabs: a.navTabs?.map((t) => ({
|
|
339
|
+
id: t.id,
|
|
340
|
+
label: t.label,
|
|
341
|
+
icon: t.icon,
|
|
342
|
+
path: t.path,
|
|
343
|
+
order: t.order,
|
|
344
|
+
developerOnly: t.developerOnly,
|
|
345
|
+
group: t.group,
|
|
346
|
+
componentExport: t.componentExport
|
|
347
|
+
}))
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return allPlugins;
|
|
352
|
+
};
|
|
353
|
+
const resolvePluginsSnapshot = async (config) => {
|
|
354
|
+
const { resolvePlugins } = await import("@elizaos/agent");
|
|
355
|
+
return resolvePlugins(config, { quiet: true });
|
|
356
|
+
};
|
|
357
|
+
const resolvePluginsSnapshotSafe = async (config, reason) => {
|
|
358
|
+
try {
|
|
359
|
+
return await resolvePluginsSnapshot(config);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
logger.warn(
|
|
362
|
+
`[plugin-routes] Failed to resolve plugin snapshot for ${reason}: ${err instanceof Error ? err.message : String(err)}`
|
|
363
|
+
);
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const npmNamePattern = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
368
|
+
const validateRegistryPluginPackageName = (pluginName) => {
|
|
369
|
+
const trimmedName = pluginName.trim();
|
|
370
|
+
if (!trimmedName) {
|
|
371
|
+
return "Request body must include 'name' (plugin package name)";
|
|
372
|
+
}
|
|
373
|
+
if (!npmNamePattern.test(trimmedName)) {
|
|
374
|
+
return "Invalid plugin name format";
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
};
|
|
378
|
+
if (method === "GET" && pathname === "/api/plugins") {
|
|
379
|
+
let inFlight = pluginsListInFlight.get(state);
|
|
380
|
+
if (!inFlight) {
|
|
381
|
+
inFlight = buildPluginsListSnapshot();
|
|
382
|
+
pluginsListInFlight.set(state, inFlight);
|
|
383
|
+
}
|
|
384
|
+
const allPlugins = await inFlight.finally(() => {
|
|
385
|
+
pluginsListInFlight.delete(state);
|
|
386
|
+
});
|
|
387
|
+
json(res, { plugins: allPlugins });
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
if (method === "PUT" && pathname.startsWith("/api/plugins/")) {
|
|
391
|
+
const pluginId = pathname.slice("/api/plugins/".length);
|
|
392
|
+
const rawPlugin = await readJsonBody(req, res);
|
|
393
|
+
if (rawPlugin === null) return true;
|
|
394
|
+
const parsedPlugin = PutPluginRequestSchema.safeParse(rawPlugin);
|
|
395
|
+
if (!parsedPlugin.success) {
|
|
396
|
+
error(
|
|
397
|
+
res,
|
|
398
|
+
parsedPlugin.error.issues[0]?.message ?? "Invalid request body",
|
|
399
|
+
400
|
|
400
|
+
);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
const body = parsedPlugin.data;
|
|
404
|
+
if (isElizaSettingsDebugEnabled()) {
|
|
405
|
+
logger.debug(
|
|
406
|
+
`[eliza][settings][api] PUT /api/plugins/${pluginId} body=${JSON.stringify(
|
|
407
|
+
sanitizeForSettingsDebug({
|
|
408
|
+
enabled: body.enabled,
|
|
409
|
+
configKeys: body.config ? Object.keys(body.config).sort() : [],
|
|
410
|
+
config: body.config ?? {}
|
|
411
|
+
})
|
|
412
|
+
)}`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
let plugin = state.plugins.find((p) => p.id === pluginId);
|
|
416
|
+
if (!plugin) {
|
|
417
|
+
let freshCfg;
|
|
418
|
+
try {
|
|
419
|
+
freshCfg = loadElizaConfig();
|
|
420
|
+
} catch {
|
|
421
|
+
freshCfg = state.config;
|
|
422
|
+
}
|
|
423
|
+
const bundledIds = new Set(state.plugins.map((p) => p.id));
|
|
424
|
+
const installed = discoverInstalledPlugins(freshCfg, bundledIds);
|
|
425
|
+
const found = installed.find((p) => p.id === pluginId);
|
|
426
|
+
if (found) {
|
|
427
|
+
state.plugins.push(found);
|
|
428
|
+
plugin = found;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!plugin) {
|
|
432
|
+
error(res, `Plugin "${pluginId}" not found`, 404);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
const previousConfig = structuredClone(state.config);
|
|
436
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin update") : void 0;
|
|
437
|
+
if (body.enabled !== void 0) {
|
|
438
|
+
plugin.enabled = body.enabled;
|
|
439
|
+
}
|
|
440
|
+
if (body.config) {
|
|
441
|
+
const configRejections = resolvePluginConfigMutationRejections(
|
|
442
|
+
plugin.parameters,
|
|
443
|
+
body.config
|
|
444
|
+
);
|
|
445
|
+
if (configRejections.length > 0) {
|
|
446
|
+
json(
|
|
447
|
+
res,
|
|
448
|
+
{ ok: false, plugin, validationErrors: configRejections },
|
|
449
|
+
422
|
|
450
|
+
);
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
const configObj = body.config;
|
|
454
|
+
const submittedParamInfos = plugin.parameters.filter((p) => p.key in configObj).map((p) => ({
|
|
455
|
+
key: p.key,
|
|
456
|
+
required: p.required,
|
|
457
|
+
sensitive: p.sensitive,
|
|
458
|
+
type: p.type,
|
|
459
|
+
description: p.description,
|
|
460
|
+
default: p.default
|
|
461
|
+
}));
|
|
462
|
+
const configValidation = validatePluginConfig(
|
|
463
|
+
pluginId,
|
|
464
|
+
plugin.category,
|
|
465
|
+
plugin.envKey,
|
|
466
|
+
plugin.configKeys,
|
|
467
|
+
body.config,
|
|
468
|
+
submittedParamInfos
|
|
469
|
+
);
|
|
470
|
+
if (!configValidation.valid) {
|
|
471
|
+
json(
|
|
472
|
+
res,
|
|
473
|
+
{ ok: false, plugin, validationErrors: configValidation.errors },
|
|
474
|
+
422
|
|
475
|
+
);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
const allowedParamKeys = new Set(plugin.parameters.map((p) => p.key));
|
|
479
|
+
const allowedParamsByKey = new Map(
|
|
480
|
+
plugin.parameters.map((p) => [p.key, p])
|
|
481
|
+
);
|
|
482
|
+
if (!state.config.env) {
|
|
483
|
+
state.config.env = {};
|
|
484
|
+
}
|
|
485
|
+
if (!state.config.plugins) {
|
|
486
|
+
state.config.plugins = {};
|
|
487
|
+
}
|
|
488
|
+
if (!state.config.plugins.entries) {
|
|
489
|
+
state.config.plugins.entries = {};
|
|
490
|
+
}
|
|
491
|
+
const entries = state.config.plugins.entries;
|
|
492
|
+
const pluginEntry = entries[pluginId] ?? {};
|
|
493
|
+
const nextPluginConfig = asRecord(pluginEntry.config) ? { ...pluginEntry.config } : {};
|
|
494
|
+
let touchedPluginConfig = false;
|
|
495
|
+
for (const [key, value] of Object.entries(body.config)) {
|
|
496
|
+
if (allowedParamKeys.has(key) && !BLOCKED_ENV_KEYS.has(key.toUpperCase()) && typeof value === "string") {
|
|
497
|
+
touchedPluginConfig = true;
|
|
498
|
+
if (value.trim()) {
|
|
499
|
+
process.env[key] = value;
|
|
500
|
+
state.config.env[key] = value;
|
|
501
|
+
nextPluginConfig[key] = value;
|
|
502
|
+
} else if (!allowedParamsByKey.get(key)?.required) {
|
|
503
|
+
delete process.env[key];
|
|
504
|
+
delete state.config.env[key];
|
|
505
|
+
delete nextPluginConfig[key];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (touchedPluginConfig) {
|
|
510
|
+
pluginEntry.config = nextPluginConfig;
|
|
511
|
+
entries[pluginId] = pluginEntry;
|
|
512
|
+
}
|
|
513
|
+
plugin.configured = true;
|
|
514
|
+
if (body.enabled === void 0) {
|
|
515
|
+
try {
|
|
516
|
+
saveElizaConfig(state.config);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
logger.warn(
|
|
519
|
+
`[eliza-api] Failed to save config: ${err instanceof Error ? err.message : err}`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const refreshParamInfos = plugin.parameters.map((p) => ({
|
|
525
|
+
key: p.key,
|
|
526
|
+
required: p.required,
|
|
527
|
+
sensitive: p.sensitive,
|
|
528
|
+
type: p.type,
|
|
529
|
+
description: p.description,
|
|
530
|
+
default: p.default
|
|
531
|
+
}));
|
|
532
|
+
const updated = validatePluginConfig(
|
|
533
|
+
pluginId,
|
|
534
|
+
plugin.category,
|
|
535
|
+
plugin.envKey,
|
|
536
|
+
plugin.configKeys,
|
|
537
|
+
void 0,
|
|
538
|
+
refreshParamInfos
|
|
539
|
+
);
|
|
540
|
+
plugin.validationErrors = updated.errors;
|
|
541
|
+
plugin.validationWarnings = updated.warnings;
|
|
542
|
+
if (body.enabled !== void 0) {
|
|
543
|
+
if (isAdvancedCapabilityPluginId(pluginId)) {
|
|
544
|
+
applyAdvancedCapabilitiesConfig(state.config, body.enabled);
|
|
545
|
+
for (const candidate of state.plugins) {
|
|
546
|
+
if (isAdvancedCapabilityPluginId(candidate.id)) {
|
|
547
|
+
candidate.enabled = body.enabled;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
logger.info(
|
|
551
|
+
`[eliza-api] ${body.enabled ? "Enabled" : "Disabled"} advanced capabilities via plugin alias: ${pluginId}`
|
|
552
|
+
);
|
|
553
|
+
} else {
|
|
554
|
+
const packageName = `@elizaos/plugin-${pluginId}`;
|
|
555
|
+
if (!state.config.plugins) {
|
|
556
|
+
state.config.plugins = {};
|
|
557
|
+
}
|
|
558
|
+
if (!state.config.plugins.entries) {
|
|
559
|
+
state.config.plugins.entries = {};
|
|
560
|
+
}
|
|
561
|
+
const entries = state.config.plugins.entries;
|
|
562
|
+
entries[pluginId] = { enabled: body.enabled };
|
|
563
|
+
state.config.plugins.allow = state.config.plugins.allow ?? [];
|
|
564
|
+
const allow = state.config.plugins.allow;
|
|
565
|
+
if (body.enabled) {
|
|
566
|
+
if (!allow.includes(pluginId) && !allow.includes(packageName)) {
|
|
567
|
+
allow.push(pluginId);
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
state.config.plugins.allow = allow.filter(
|
|
571
|
+
(p) => p !== pluginId && p !== packageName
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
logger.info(
|
|
575
|
+
`[eliza-api] ${body.enabled ? "Enabled" : "Disabled"} plugin: ${packageName}`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const CAPABILITY_FEATURE_IDS = /* @__PURE__ */ new Set([
|
|
579
|
+
"vision",
|
|
580
|
+
"browser",
|
|
581
|
+
"computeruse",
|
|
582
|
+
"coding-agent"
|
|
583
|
+
]);
|
|
584
|
+
if (CAPABILITY_FEATURE_IDS.has(pluginId)) {
|
|
585
|
+
if (!state.config.features) {
|
|
586
|
+
state.config.features = {};
|
|
587
|
+
}
|
|
588
|
+
state.config.features[pluginId] = body.enabled;
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
saveElizaConfig(state.config);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
logger.warn(
|
|
594
|
+
`[eliza-api] Failed to save config: ${err instanceof Error ? err.message : err}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
599
|
+
runtime: state.runtime,
|
|
600
|
+
previousConfig,
|
|
601
|
+
nextConfig: state.config,
|
|
602
|
+
previousResolvedPlugins,
|
|
603
|
+
changedPluginId: pluginId,
|
|
604
|
+
changedPluginPackage: plugin.npmName,
|
|
605
|
+
config: body.config,
|
|
606
|
+
expectRuntimeGraphChange: body.enabled !== void 0,
|
|
607
|
+
reason: body.enabled !== void 0 ? `Plugin toggle: ${pluginId}` : `Plugin config updated: ${pluginId}`,
|
|
608
|
+
restartRuntime
|
|
609
|
+
});
|
|
610
|
+
if (runtimeApply.requiresRestart) {
|
|
611
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
612
|
+
}
|
|
613
|
+
if (isElizaSettingsDebugEnabled()) {
|
|
614
|
+
const cloud = state.config.cloud;
|
|
615
|
+
logger.debug(
|
|
616
|
+
`[eliza][settings][api] PUT /api/plugins/${pluginId} \u2192 done configured=${plugin.configured} enabled=${plugin.enabled} cloud=${JSON.stringify(settingsDebugCloudSummary(cloud))}`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
json(res, {
|
|
620
|
+
ok: true,
|
|
621
|
+
plugin,
|
|
622
|
+
applied: runtimeApply.mode,
|
|
623
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
624
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
625
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
626
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
627
|
+
reloadedPackages: runtimeApply.reloadedPackages
|
|
628
|
+
});
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
if (method === "GET" && pathname === "/api/secrets") {
|
|
632
|
+
const bundledIds = new Set(state.plugins.map((p) => p.id));
|
|
633
|
+
const installedEntries = discoverInstalledPlugins(state.config, bundledIds);
|
|
634
|
+
const allPlugins = [...state.plugins, ...installedEntries];
|
|
635
|
+
if (state.runtime) {
|
|
636
|
+
const loadedNames = state.runtime.plugins.map((p) => p.name);
|
|
637
|
+
for (const plugin of allPlugins) {
|
|
638
|
+
const suffix = `plugin-${plugin.id}`;
|
|
639
|
+
const packageName = `@elizaos/plugin-${plugin.id}`;
|
|
640
|
+
plugin.enabled = loadedNames.some(
|
|
641
|
+
(name) => name === plugin.id || name === suffix || name === packageName || name.endsWith(`/${suffix}`)
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const secrets = aggregateSecrets(allPlugins);
|
|
646
|
+
json(res, { secrets });
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
if (method === "PUT" && pathname === "/api/secrets") {
|
|
650
|
+
const rawSecrets = await readJsonBody(req, res);
|
|
651
|
+
if (rawSecrets === null) return true;
|
|
652
|
+
const parsedSecrets = PutSecretsRequestSchema.safeParse(rawSecrets);
|
|
653
|
+
if (!parsedSecrets.success) {
|
|
654
|
+
error(
|
|
655
|
+
res,
|
|
656
|
+
parsedSecrets.error.issues[0]?.message ?? "Invalid request body",
|
|
657
|
+
400
|
|
658
|
+
);
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
const body = parsedSecrets.data;
|
|
662
|
+
const bundledIds = new Set(state.plugins.map((p) => p.id));
|
|
663
|
+
const installedEntries = discoverInstalledPlugins(state.config, bundledIds);
|
|
664
|
+
const allPlugins = [...state.plugins, ...installedEntries];
|
|
665
|
+
const allowedKeys = /* @__PURE__ */ new Set();
|
|
666
|
+
for (const plugin of allPlugins) {
|
|
667
|
+
for (const param of plugin.parameters) {
|
|
668
|
+
if (param.sensitive) allowedKeys.add(param.key);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const updatedKeys = [];
|
|
672
|
+
for (const [key, value] of Object.entries(body.secrets)) {
|
|
673
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
674
|
+
if (!allowedKeys.has(key)) continue;
|
|
675
|
+
if (BLOCKED_ENV_KEYS.has(key.toUpperCase())) continue;
|
|
676
|
+
process.env[key] = value;
|
|
677
|
+
updatedKeys.push(key);
|
|
678
|
+
}
|
|
679
|
+
for (const plugin of allPlugins) {
|
|
680
|
+
const pluginKeys = new Set(plugin.parameters.map((p) => p.key));
|
|
681
|
+
if (updatedKeys.some((k) => pluginKeys.has(k))) {
|
|
682
|
+
plugin.configured = true;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
json(res, { ok: true, updated: updatedKeys });
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
const pluginTestMatch = method === "POST" && pathname.match(/^\/api\/plugins\/([^/]+)\/test$/);
|
|
689
|
+
if (pluginTestMatch) {
|
|
690
|
+
const pluginId = decodeURIComponent(pluginTestMatch[1]);
|
|
691
|
+
const startMs = Date.now();
|
|
692
|
+
try {
|
|
693
|
+
const allPlugins = state.runtime?.plugins ?? [];
|
|
694
|
+
const normalizePluginId = (value) => {
|
|
695
|
+
const scopedPackage = value.match(/^@[^/]+\/plugin-(.+)$/);
|
|
696
|
+
if (scopedPackage) {
|
|
697
|
+
return scopedPackage[1] ?? value;
|
|
698
|
+
}
|
|
699
|
+
return value.replace(/^@[^/]+\//, "").replace(/^plugin-/, "");
|
|
700
|
+
};
|
|
701
|
+
const normalizedPluginId = normalizePluginId(pluginId);
|
|
702
|
+
const plugin = allPlugins.find((p) => {
|
|
703
|
+
const runtimeName = p.name ?? "";
|
|
704
|
+
const runtimeId = normalizePluginId(runtimeName);
|
|
705
|
+
return p.id === pluginId || p.name === pluginId || runtimeId === pluginId || runtimeId === normalizedPluginId;
|
|
706
|
+
});
|
|
707
|
+
if (!plugin) {
|
|
708
|
+
json(
|
|
709
|
+
res,
|
|
710
|
+
{
|
|
711
|
+
success: false,
|
|
712
|
+
pluginId,
|
|
713
|
+
error: "Plugin not found or not loaded",
|
|
714
|
+
durationMs: Date.now() - startMs
|
|
715
|
+
},
|
|
716
|
+
404
|
|
717
|
+
);
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
const testFn = getPluginHealthProbe(plugin);
|
|
721
|
+
if (typeof testFn === "function") {
|
|
722
|
+
const result = await testFn();
|
|
723
|
+
json(res, {
|
|
724
|
+
success: result.ok !== false,
|
|
725
|
+
pluginId,
|
|
726
|
+
message: result.message ?? (result.ok !== false ? "Connection successful" : "Connection failed"),
|
|
727
|
+
durationMs: Date.now() - startMs
|
|
728
|
+
});
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
json(res, {
|
|
732
|
+
success: true,
|
|
733
|
+
pluginId,
|
|
734
|
+
message: "Plugin is loaded and active (no custom test available)",
|
|
735
|
+
durationMs: Date.now() - startMs
|
|
736
|
+
});
|
|
737
|
+
} catch (err) {
|
|
738
|
+
json(
|
|
739
|
+
res,
|
|
740
|
+
{
|
|
741
|
+
success: false,
|
|
742
|
+
pluginId,
|
|
743
|
+
error: err instanceof Error ? err.message : String(err),
|
|
744
|
+
durationMs: Date.now() - startMs
|
|
745
|
+
},
|
|
746
|
+
500
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
if (method === "POST" && pathname === "/api/plugins/install") {
|
|
752
|
+
const rawInstall = await readJsonBody(req, res);
|
|
753
|
+
if (rawInstall === null) return true;
|
|
754
|
+
const parsedInstall = PostPluginInstallRequestSchema.safeParse(rawInstall);
|
|
755
|
+
if (!parsedInstall.success) {
|
|
756
|
+
error(
|
|
757
|
+
res,
|
|
758
|
+
parsedInstall.error.issues[0]?.message ?? "Invalid request body",
|
|
759
|
+
400
|
|
760
|
+
);
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
const body = parsedInstall.data;
|
|
764
|
+
const pluginName = body.name;
|
|
765
|
+
const installValidationError = validateRegistryPluginPackageName(pluginName);
|
|
766
|
+
if (installValidationError) {
|
|
767
|
+
error(res, installValidationError, 400);
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const previousConfig = structuredClone(state.config);
|
|
772
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin install") : void 0;
|
|
773
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
774
|
+
const result = await pluginManager.installPlugin(
|
|
775
|
+
pluginName,
|
|
776
|
+
(progress) => {
|
|
777
|
+
logger.info(`[install] ${progress.phase}: ${progress.message}`);
|
|
778
|
+
state.broadcastWs?.({
|
|
779
|
+
type: "install-progress",
|
|
780
|
+
pluginName: progress.pluginName,
|
|
781
|
+
phase: progress.phase,
|
|
782
|
+
message: progress.message
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
releaseStream: body.stream,
|
|
787
|
+
version: body.version
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
if (!result.success) {
|
|
791
|
+
json(res, { ok: false, error: result.error }, 422);
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
const installedId = result.pluginName.replace(/^@[^/]+\/plugin-/, "").replace(/^@[^/]+\//, "").replace(/^plugin-/, "");
|
|
795
|
+
if (!state.config.plugins) {
|
|
796
|
+
state.config.plugins = {};
|
|
797
|
+
}
|
|
798
|
+
if (!state.config.plugins.entries) {
|
|
799
|
+
state.config.plugins.entries = {};
|
|
800
|
+
}
|
|
801
|
+
const pluginEntries = state.config.plugins.entries;
|
|
802
|
+
pluginEntries[installedId] = { enabled: true };
|
|
803
|
+
if (result.installPath) {
|
|
804
|
+
if (!state.config.plugins.installs || typeof state.config.plugins.installs !== "object") {
|
|
805
|
+
state.config.plugins.installs = {};
|
|
806
|
+
}
|
|
807
|
+
const installs = state.config.plugins.installs;
|
|
808
|
+
installs[result.pluginName] = {
|
|
809
|
+
source: "npm",
|
|
810
|
+
requestedVersion: result.requestedVersion,
|
|
811
|
+
releaseStream: result.releaseStream,
|
|
812
|
+
installPath: result.installPath,
|
|
813
|
+
version: result.version,
|
|
814
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
saveElizaConfig(state.config);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
logger.warn(
|
|
821
|
+
`[eliza-api] Failed to save config after install: ${err instanceof Error ? err.message : err}`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
825
|
+
runtime: state.runtime,
|
|
826
|
+
previousConfig,
|
|
827
|
+
nextConfig: state.config,
|
|
828
|
+
previousResolvedPlugins,
|
|
829
|
+
changedPluginId: installedId,
|
|
830
|
+
changedPluginPackage: result.pluginName,
|
|
831
|
+
expectRuntimeGraphChange: true,
|
|
832
|
+
reason: `Plugin ${result.pluginName} installed`,
|
|
833
|
+
restartRuntime
|
|
834
|
+
});
|
|
835
|
+
if (runtimeApply.requiresRestart && body.autoRestart !== false) {
|
|
836
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
837
|
+
}
|
|
838
|
+
json(res, {
|
|
839
|
+
ok: true,
|
|
840
|
+
pluginName: result.pluginName,
|
|
841
|
+
plugin: {
|
|
842
|
+
name: result.pluginName,
|
|
843
|
+
version: result.version,
|
|
844
|
+
installPath: result.installPath
|
|
845
|
+
},
|
|
846
|
+
applied: runtimeApply.mode,
|
|
847
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
848
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
849
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
850
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
851
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
852
|
+
releaseStream: result.releaseStream,
|
|
853
|
+
requestedVersion: result.requestedVersion,
|
|
854
|
+
latestVersion: result.latestVersion,
|
|
855
|
+
betaVersion: result.betaVersion,
|
|
856
|
+
message: runtimeApply.requiresRestart ? `${result.pluginName} installed. Restart required to activate.` : `${result.pluginName} installed.`
|
|
857
|
+
});
|
|
858
|
+
} catch (err) {
|
|
859
|
+
error(
|
|
860
|
+
res,
|
|
861
|
+
`Install failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
862
|
+
500
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
if (method === "POST" && pathname === "/api/plugins/update") {
|
|
868
|
+
const rawUpdate = await readJsonBody(req, res);
|
|
869
|
+
if (rawUpdate === null) return true;
|
|
870
|
+
const parsedUpdate = PostPluginUpdateRequestSchema.safeParse(rawUpdate);
|
|
871
|
+
if (!parsedUpdate.success) {
|
|
872
|
+
error(
|
|
873
|
+
res,
|
|
874
|
+
parsedUpdate.error.issues[0]?.message ?? "Invalid request body",
|
|
875
|
+
400
|
|
876
|
+
);
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
const body = parsedUpdate.data;
|
|
880
|
+
const pluginName = body.name;
|
|
881
|
+
const updateValidationError = validateRegistryPluginPackageName(pluginName);
|
|
882
|
+
if (updateValidationError) {
|
|
883
|
+
error(res, updateValidationError, 400);
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const previousConfig = structuredClone(state.config);
|
|
888
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin update") : void 0;
|
|
889
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
890
|
+
const updatePlugin = typeof pluginManager.updatePlugin === "function" ? pluginManager.updatePlugin.bind(pluginManager) : pluginManager.installPlugin.bind(pluginManager);
|
|
891
|
+
const result = await updatePlugin(
|
|
892
|
+
pluginName,
|
|
893
|
+
(progress) => {
|
|
894
|
+
logger.info(`[update] ${progress.phase}: ${progress.message}`);
|
|
895
|
+
state.broadcastWs?.({
|
|
896
|
+
type: "install-progress",
|
|
897
|
+
pluginName: progress.pluginName,
|
|
898
|
+
phase: progress.phase,
|
|
899
|
+
message: progress.message
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
releaseStream: body.stream,
|
|
904
|
+
version: body.version
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
if (!result.success) {
|
|
908
|
+
json(res, { ok: false, error: result.error }, 422);
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
if (!state.config.plugins) {
|
|
912
|
+
state.config.plugins = {};
|
|
913
|
+
}
|
|
914
|
+
if (!state.config.plugins.entries) {
|
|
915
|
+
state.config.plugins.entries = {};
|
|
916
|
+
}
|
|
917
|
+
const updatedId = result.pluginName.replace(/^@[^/]+\/plugin-/, "").replace(/^@[^/]+\//, "").replace(/^plugin-/, "");
|
|
918
|
+
state.config.plugins.entries[updatedId] = { enabled: true };
|
|
919
|
+
state.config.plugins.installs = state.config.plugins.installs ?? {};
|
|
920
|
+
state.config.plugins.installs[result.pluginName] = {
|
|
921
|
+
source: "npm",
|
|
922
|
+
requestedVersion: result.requestedVersion,
|
|
923
|
+
releaseStream: result.releaseStream,
|
|
924
|
+
installPath: result.installPath,
|
|
925
|
+
version: result.version,
|
|
926
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
927
|
+
};
|
|
928
|
+
try {
|
|
929
|
+
saveElizaConfig(state.config);
|
|
930
|
+
} catch (err) {
|
|
931
|
+
logger.warn(
|
|
932
|
+
`[eliza-api] Failed to save config after update: ${err instanceof Error ? err.message : err}`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
936
|
+
runtime: state.runtime,
|
|
937
|
+
previousConfig,
|
|
938
|
+
nextConfig: state.config,
|
|
939
|
+
previousResolvedPlugins,
|
|
940
|
+
changedPluginId: updatedId,
|
|
941
|
+
changedPluginPackage: result.pluginName,
|
|
942
|
+
forceReloadPackages: [result.pluginName],
|
|
943
|
+
expectRuntimeGraphChange: true,
|
|
944
|
+
reason: `Plugin ${result.pluginName} updated`,
|
|
945
|
+
restartRuntime
|
|
946
|
+
});
|
|
947
|
+
if (runtimeApply.requiresRestart && body.autoRestart !== false) {
|
|
948
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
949
|
+
}
|
|
950
|
+
json(res, {
|
|
951
|
+
ok: true,
|
|
952
|
+
pluginName: result.pluginName,
|
|
953
|
+
plugin: {
|
|
954
|
+
name: result.pluginName,
|
|
955
|
+
version: result.version,
|
|
956
|
+
installPath: result.installPath
|
|
957
|
+
},
|
|
958
|
+
applied: runtimeApply.mode,
|
|
959
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
960
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
961
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
962
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
963
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
964
|
+
releaseStream: result.releaseStream,
|
|
965
|
+
requestedVersion: result.requestedVersion,
|
|
966
|
+
latestVersion: result.latestVersion,
|
|
967
|
+
betaVersion: result.betaVersion,
|
|
968
|
+
message: runtimeApply.requiresRestart ? `${result.pluginName} updated. Restart required to activate.` : `${result.pluginName} updated.`
|
|
969
|
+
});
|
|
970
|
+
} catch (err) {
|
|
971
|
+
error(
|
|
972
|
+
res,
|
|
973
|
+
`Update failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
974
|
+
500
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
if (method === "POST" && pathname === "/api/plugins/uninstall") {
|
|
980
|
+
const rawUninstall = await readJsonBody(req, res);
|
|
981
|
+
if (rawUninstall === null) return true;
|
|
982
|
+
const parsedUninstall = PostPluginUninstallRequestSchema.safeParse(rawUninstall);
|
|
983
|
+
if (!parsedUninstall.success) {
|
|
984
|
+
error(
|
|
985
|
+
res,
|
|
986
|
+
parsedUninstall.error.issues[0]?.message ?? "Invalid request body",
|
|
987
|
+
400
|
|
988
|
+
);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
const body = parsedUninstall.data;
|
|
992
|
+
const pluginName = body.name;
|
|
993
|
+
const uninstallValidationError = validateRegistryPluginPackageName(pluginName);
|
|
994
|
+
if (uninstallValidationError) {
|
|
995
|
+
error(res, uninstallValidationError, 400);
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
const previousConfig = structuredClone(state.config);
|
|
1000
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin uninstall") : void 0;
|
|
1001
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
1002
|
+
const result = await pluginManager.uninstallPlugin(pluginName);
|
|
1003
|
+
if (!result.success) {
|
|
1004
|
+
json(res, { ok: false, error: result.error }, 422);
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
const removedId = result.pluginName.replace(/^@[^/]+\/plugin-/, "").replace(/^@[^/]+\//, "").replace(/^plugin-/, "");
|
|
1008
|
+
const installs = state.config.plugins?.installs;
|
|
1009
|
+
if (installs && typeof installs === "object") {
|
|
1010
|
+
delete installs[result.pluginName];
|
|
1011
|
+
}
|
|
1012
|
+
const entries = state.config.plugins?.entries;
|
|
1013
|
+
if (entries && typeof entries === "object") {
|
|
1014
|
+
delete entries[removedId];
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
saveElizaConfig(state.config);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
logger.warn(
|
|
1020
|
+
`[eliza-api] Failed to save config after uninstall: ${err instanceof Error ? err.message : err}`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
1024
|
+
runtime: state.runtime,
|
|
1025
|
+
previousConfig,
|
|
1026
|
+
nextConfig: state.config,
|
|
1027
|
+
previousResolvedPlugins,
|
|
1028
|
+
changedPluginId: removedId,
|
|
1029
|
+
changedPluginPackage: result.pluginName,
|
|
1030
|
+
expectRuntimeGraphChange: true,
|
|
1031
|
+
reason: `Plugin ${pluginName} uninstalled`,
|
|
1032
|
+
restartRuntime
|
|
1033
|
+
});
|
|
1034
|
+
if (runtimeApply.requiresRestart && body.autoRestart !== false) {
|
|
1035
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
1036
|
+
}
|
|
1037
|
+
json(res, {
|
|
1038
|
+
ok: true,
|
|
1039
|
+
pluginName: result.pluginName,
|
|
1040
|
+
applied: runtimeApply.mode,
|
|
1041
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
1042
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
1043
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
1044
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
1045
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
1046
|
+
message: runtimeApply.requiresRestart ? `${pluginName} uninstalled. Restart required.` : `${pluginName} uninstalled.`
|
|
1047
|
+
});
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
error(
|
|
1050
|
+
res,
|
|
1051
|
+
`Uninstall failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1052
|
+
500
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
if (method === "POST" && pathname.match(/^\/api\/plugins\/[^/]+\/eject$/)) {
|
|
1058
|
+
const pluginName = decodeURIComponent(
|
|
1059
|
+
pathname.slice("/api/plugins/".length, pathname.length - "/eject".length)
|
|
1060
|
+
);
|
|
1061
|
+
try {
|
|
1062
|
+
const previousConfig = structuredClone(state.config);
|
|
1063
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin eject") : void 0;
|
|
1064
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
1065
|
+
if (typeof pluginManager.ejectPlugin !== "function") {
|
|
1066
|
+
throw new Error("Plugin manager does not support ejecting plugins");
|
|
1067
|
+
}
|
|
1068
|
+
const result = await pluginManager.ejectPlugin(pluginName);
|
|
1069
|
+
if (!result.success) {
|
|
1070
|
+
json(res, { ok: false, error: result.error }, 422);
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
1074
|
+
runtime: state.runtime,
|
|
1075
|
+
previousConfig,
|
|
1076
|
+
nextConfig: state.config,
|
|
1077
|
+
previousResolvedPlugins,
|
|
1078
|
+
changedPluginId: pluginName,
|
|
1079
|
+
changedPluginPackage: result.pluginName,
|
|
1080
|
+
forceReloadPackages: [result.pluginName],
|
|
1081
|
+
expectRuntimeGraphChange: true,
|
|
1082
|
+
reason: `Plugin ${pluginName} ejected`,
|
|
1083
|
+
restartRuntime
|
|
1084
|
+
});
|
|
1085
|
+
if (runtimeApply.requiresRestart) {
|
|
1086
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
1087
|
+
}
|
|
1088
|
+
json(res, {
|
|
1089
|
+
ok: true,
|
|
1090
|
+
pluginName: result.pluginName,
|
|
1091
|
+
applied: runtimeApply.mode,
|
|
1092
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
1093
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
1094
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
1095
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
1096
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
1097
|
+
message: `${pluginName} ejected to local source.`
|
|
1098
|
+
});
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
error(
|
|
1101
|
+
res,
|
|
1102
|
+
`Eject failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1103
|
+
500
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
if (method === "POST" && pathname.match(/^\/api\/plugins\/[^/]+\/sync$/)) {
|
|
1109
|
+
const pluginName = decodeURIComponent(
|
|
1110
|
+
pathname.slice("/api/plugins/".length, pathname.length - "/sync".length)
|
|
1111
|
+
);
|
|
1112
|
+
try {
|
|
1113
|
+
const previousConfig = structuredClone(state.config);
|
|
1114
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin sync") : void 0;
|
|
1115
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
1116
|
+
if (typeof pluginManager.syncPlugin !== "function") {
|
|
1117
|
+
throw new Error("Plugin manager does not support syncing plugins");
|
|
1118
|
+
}
|
|
1119
|
+
const result = await pluginManager.syncPlugin(pluginName);
|
|
1120
|
+
if (!result.success) {
|
|
1121
|
+
json(res, { ok: false, error: result.error }, 422);
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
1125
|
+
runtime: state.runtime,
|
|
1126
|
+
previousConfig,
|
|
1127
|
+
nextConfig: state.config,
|
|
1128
|
+
previousResolvedPlugins,
|
|
1129
|
+
changedPluginId: pluginName,
|
|
1130
|
+
changedPluginPackage: result.pluginName,
|
|
1131
|
+
forceReloadPackages: result.requiresRestart ? [result.pluginName] : [],
|
|
1132
|
+
expectRuntimeGraphChange: true,
|
|
1133
|
+
reason: `Plugin ${pluginName} synced`,
|
|
1134
|
+
restartRuntime
|
|
1135
|
+
});
|
|
1136
|
+
if (runtimeApply.requiresRestart) {
|
|
1137
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
1138
|
+
}
|
|
1139
|
+
json(res, {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
pluginName: result.pluginName,
|
|
1142
|
+
applied: runtimeApply.mode,
|
|
1143
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
1144
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
1145
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
1146
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
1147
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
1148
|
+
message: `${pluginName} synced with upstream.`
|
|
1149
|
+
});
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
error(
|
|
1152
|
+
res,
|
|
1153
|
+
`Sync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1154
|
+
500
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
if (method === "POST" && pathname.match(/^\/api\/plugins\/[^/]+\/reinject$/)) {
|
|
1160
|
+
const pluginName = decodeURIComponent(
|
|
1161
|
+
pathname.slice(
|
|
1162
|
+
"/api/plugins/".length,
|
|
1163
|
+
pathname.length - "/reinject".length
|
|
1164
|
+
)
|
|
1165
|
+
);
|
|
1166
|
+
try {
|
|
1167
|
+
const previousConfig = structuredClone(state.config);
|
|
1168
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "plugin reinject") : void 0;
|
|
1169
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
1170
|
+
if (typeof pluginManager.reinjectPlugin !== "function") {
|
|
1171
|
+
throw new Error("Plugin manager does not support reinjecting plugins");
|
|
1172
|
+
}
|
|
1173
|
+
const result = await pluginManager.reinjectPlugin(pluginName);
|
|
1174
|
+
if (!result.success) {
|
|
1175
|
+
json(res, { ok: false, error: result.error }, 422);
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
1179
|
+
runtime: state.runtime,
|
|
1180
|
+
previousConfig,
|
|
1181
|
+
nextConfig: state.config,
|
|
1182
|
+
previousResolvedPlugins,
|
|
1183
|
+
changedPluginId: pluginName,
|
|
1184
|
+
changedPluginPackage: result.pluginName,
|
|
1185
|
+
forceReloadPackages: [result.pluginName],
|
|
1186
|
+
expectRuntimeGraphChange: true,
|
|
1187
|
+
reason: `Plugin ${pluginName} reinjected`,
|
|
1188
|
+
restartRuntime
|
|
1189
|
+
});
|
|
1190
|
+
if (runtimeApply.requiresRestart) {
|
|
1191
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
1192
|
+
}
|
|
1193
|
+
json(res, {
|
|
1194
|
+
ok: true,
|
|
1195
|
+
pluginName: result.pluginName,
|
|
1196
|
+
applied: runtimeApply.mode,
|
|
1197
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
1198
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
1199
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
1200
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
1201
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
1202
|
+
message: `${pluginName} restored to registry version.`
|
|
1203
|
+
});
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
error(
|
|
1206
|
+
res,
|
|
1207
|
+
`Reinject failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1208
|
+
500
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (method === "GET" && pathname === "/api/plugins/installed") {
|
|
1214
|
+
try {
|
|
1215
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
1216
|
+
const installed = await pluginManager.listInstalledPlugins();
|
|
1217
|
+
json(res, { count: installed.length, plugins: installed });
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
error(
|
|
1220
|
+
res,
|
|
1221
|
+
`Failed to list installed plugins: ${err instanceof Error ? err.message : String(err)}`,
|
|
1222
|
+
500
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
if (method === "GET" && pathname === "/api/plugins/ejected") {
|
|
1228
|
+
try {
|
|
1229
|
+
const pluginManager = requirePluginManager(state.runtime);
|
|
1230
|
+
if (typeof pluginManager.listEjectedPlugins !== "function") {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
"Plugin manager does not support listing ejected plugins"
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
const plugins = await pluginManager.listEjectedPlugins();
|
|
1236
|
+
json(res, { count: plugins.length, plugins });
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
error(
|
|
1239
|
+
res,
|
|
1240
|
+
`Failed to list ejected plugins: ${err instanceof Error ? err.message : String(err)}`,
|
|
1241
|
+
500
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
if (method === "GET" && pathname === "/api/core/status") {
|
|
1247
|
+
try {
|
|
1248
|
+
const coreManager = requireCoreManager(state.runtime);
|
|
1249
|
+
const coreStatus = await coreManager.getCoreStatus();
|
|
1250
|
+
json(res, coreStatus);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
error(
|
|
1253
|
+
res,
|
|
1254
|
+
`Failed to get core status: ${err instanceof Error ? err.message : String(err)}`,
|
|
1255
|
+
500
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
if (method === "GET" && pathname === "/api/plugins/core") {
|
|
1261
|
+
const loadedNames = state.runtime ? new Set(state.runtime.plugins.map((p) => p.name)) : /* @__PURE__ */ new Set();
|
|
1262
|
+
const isLoaded = (npmName) => {
|
|
1263
|
+
if (loadedNames.has(npmName)) return true;
|
|
1264
|
+
const withoutScope = npmName.replace("@elizaos/", "");
|
|
1265
|
+
if (loadedNames.has(withoutScope)) return true;
|
|
1266
|
+
const shortId = withoutScope.replace("plugin-", "");
|
|
1267
|
+
if (loadedNames.has(shortId)) return true;
|
|
1268
|
+
for (const n of loadedNames) {
|
|
1269
|
+
if (n.includes(shortId) || shortId.includes(n)) return true;
|
|
1270
|
+
}
|
|
1271
|
+
return false;
|
|
1272
|
+
};
|
|
1273
|
+
const allowList = new Set(state.config.plugins?.allow ?? []);
|
|
1274
|
+
const makeEntry = (npm, isCore) => {
|
|
1275
|
+
const id = optionalPluginListId(npm);
|
|
1276
|
+
return {
|
|
1277
|
+
npmName: npm,
|
|
1278
|
+
id,
|
|
1279
|
+
name: id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1280
|
+
isCore,
|
|
1281
|
+
loaded: isLoaded(npm),
|
|
1282
|
+
enabled: isCore || allowList.has(npm) || allowList.has(id)
|
|
1283
|
+
};
|
|
1284
|
+
};
|
|
1285
|
+
const coreList = CORE_PLUGINS.map((npm) => makeEntry(npm, true));
|
|
1286
|
+
const optionalList = OPTIONAL_CORE_PLUGINS.map(
|
|
1287
|
+
(npm) => makeEntry(npm, false)
|
|
1288
|
+
);
|
|
1289
|
+
json(res, { core: coreList, optional: optionalList });
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
if (method === "POST" && pathname === "/api/plugins/core/toggle") {
|
|
1293
|
+
const rawToggle = await readJsonBody(req, res);
|
|
1294
|
+
if (rawToggle === null) return true;
|
|
1295
|
+
const parsedToggle = PostPluginCoreToggleRequestSchema.safeParse(rawToggle);
|
|
1296
|
+
if (!parsedToggle.success) {
|
|
1297
|
+
error(
|
|
1298
|
+
res,
|
|
1299
|
+
parsedToggle.error.issues[0]?.message ?? "Invalid request body",
|
|
1300
|
+
400
|
|
1301
|
+
);
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
const body = parsedToggle.data;
|
|
1305
|
+
const isCorePlugin = CORE_PLUGINS.includes(
|
|
1306
|
+
body.npmName
|
|
1307
|
+
);
|
|
1308
|
+
if (isCorePlugin) {
|
|
1309
|
+
error(res, "Core plugins cannot be disabled");
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
const isOptional = OPTIONAL_CORE_PLUGINS.includes(
|
|
1313
|
+
body.npmName
|
|
1314
|
+
);
|
|
1315
|
+
if (!isOptional) {
|
|
1316
|
+
error(res, "Unknown optional plugin");
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
const previousConfig = structuredClone(state.config);
|
|
1320
|
+
const previousResolvedPlugins = state.runtime ? await resolvePluginsSnapshotSafe(previousConfig, "core plugin toggle") : void 0;
|
|
1321
|
+
state.config.plugins = state.config.plugins ?? {};
|
|
1322
|
+
state.config.plugins.allow = state.config.plugins.allow ?? [];
|
|
1323
|
+
const allow = state.config.plugins.allow;
|
|
1324
|
+
const shortId = optionalPluginListId(body.npmName);
|
|
1325
|
+
if (body.enabled) {
|
|
1326
|
+
if (!allow.includes(body.npmName) && !allow.includes(shortId)) {
|
|
1327
|
+
allow.push(body.npmName);
|
|
1328
|
+
}
|
|
1329
|
+
} else {
|
|
1330
|
+
state.config.plugins.allow = allow.filter(
|
|
1331
|
+
(p) => p !== body.npmName && p !== shortId
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
const pluginsRoot = state.config.plugins;
|
|
1335
|
+
const prevEntries = pluginsRoot.entries ?? {};
|
|
1336
|
+
pluginsRoot.entries = {
|
|
1337
|
+
...prevEntries,
|
|
1338
|
+
[shortId]: {
|
|
1339
|
+
...prevEntries[shortId],
|
|
1340
|
+
enabled: body.enabled
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
try {
|
|
1344
|
+
saveElizaConfig(state.config);
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
logger.warn(
|
|
1347
|
+
`[api] Config save failed: ${err instanceof Error ? err.message : err}`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
const runtimeApply = await applyPluginRuntimeMutation({
|
|
1351
|
+
runtime: state.runtime,
|
|
1352
|
+
previousConfig,
|
|
1353
|
+
nextConfig: state.config,
|
|
1354
|
+
previousResolvedPlugins,
|
|
1355
|
+
changedPluginId: shortId,
|
|
1356
|
+
changedPluginPackage: body.npmName,
|
|
1357
|
+
expectRuntimeGraphChange: true,
|
|
1358
|
+
reason: `Plugin ${shortId} ${body.enabled ? "enabled" : "disabled"}`,
|
|
1359
|
+
restartRuntime
|
|
1360
|
+
});
|
|
1361
|
+
if (runtimeApply.requiresRestart) {
|
|
1362
|
+
scheduleRuntimeRestart(runtimeApply.reason);
|
|
1363
|
+
}
|
|
1364
|
+
json(res, {
|
|
1365
|
+
ok: true,
|
|
1366
|
+
applied: runtimeApply.mode,
|
|
1367
|
+
requiresRestart: runtimeApply.requiresRestart,
|
|
1368
|
+
restartedRuntime: runtimeApply.restartedRuntime,
|
|
1369
|
+
loadedPackages: runtimeApply.loadedPackages,
|
|
1370
|
+
unloadedPackages: runtimeApply.unloadedPackages,
|
|
1371
|
+
reloadedPackages: runtimeApply.reloadedPackages,
|
|
1372
|
+
diagnostics: (() => {
|
|
1373
|
+
const diagnostic = buildCoreToggleDiagnostics(
|
|
1374
|
+
state.config,
|
|
1375
|
+
body.npmName
|
|
1376
|
+
);
|
|
1377
|
+
return diagnostic && diagnostic.drift_flags.length > 0 ? {
|
|
1378
|
+
withDrift: true,
|
|
1379
|
+
plugin: diagnostic
|
|
1380
|
+
} : void 0;
|
|
1381
|
+
})(),
|
|
1382
|
+
message: runtimeApply.requiresRestart ? `${shortId} ${body.enabled ? "enabled" : "disabled"}. Restart required.` : `${shortId} ${body.enabled ? "enabled" : "disabled"}.`
|
|
1383
|
+
});
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
export {
|
|
1389
|
+
handlePluginRoutes
|
|
1390
|
+
};
|
|
1391
|
+
//# sourceMappingURL=plugin-routes.js.map
|