@nextclaw/openclaw-compat 0.1.1
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/dist/index.d.ts +462 -0
- package/dist/index.js +1835 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
// src/plugin-sdk/index.ts
|
|
2
|
+
function emptyPluginConfigSchema() {
|
|
3
|
+
return {
|
|
4
|
+
type: "object",
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
properties: {}
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function buildChannelConfigSchema(schema) {
|
|
10
|
+
return schema;
|
|
11
|
+
}
|
|
12
|
+
function buildOauthProviderAuthResult(params) {
|
|
13
|
+
const profileId = params.email ? `${params.providerId}:${params.email}` : params.providerId;
|
|
14
|
+
return {
|
|
15
|
+
profiles: [
|
|
16
|
+
{
|
|
17
|
+
profileId,
|
|
18
|
+
credential: {
|
|
19
|
+
providerId: params.providerId,
|
|
20
|
+
accessToken: params.access,
|
|
21
|
+
refreshToken: params.refresh,
|
|
22
|
+
expiresAt: params.expires,
|
|
23
|
+
email: params.email,
|
|
24
|
+
extra: params.credentialExtra ?? {}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
defaultModel: params.defaultModel,
|
|
29
|
+
notes: params.notes
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function sleep(ms) {
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
34
|
+
}
|
|
35
|
+
function normalizePluginHttpPath(rawPath) {
|
|
36
|
+
const trimmed = rawPath.trim();
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
41
|
+
}
|
|
42
|
+
var DEFAULT_ACCOUNT_ID = "default";
|
|
43
|
+
function normalizeAccountId(accountId) {
|
|
44
|
+
const trimmed = accountId?.trim();
|
|
45
|
+
return trimmed || DEFAULT_ACCOUNT_ID;
|
|
46
|
+
}
|
|
47
|
+
var __nextclawPluginSdkCompat = true;
|
|
48
|
+
|
|
49
|
+
// src/plugins/config-state.ts
|
|
50
|
+
function normalizeList(value) {
|
|
51
|
+
if (!Array.isArray(value)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
function normalizeEntries(entries) {
|
|
57
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
const normalized = {};
|
|
61
|
+
for (const [idRaw, value] of Object.entries(entries)) {
|
|
62
|
+
const id = idRaw.trim();
|
|
63
|
+
if (!id) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
67
|
+
normalized[id] = {};
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const entry = value;
|
|
71
|
+
normalized[id] = {
|
|
72
|
+
enabled: typeof entry.enabled === "boolean" ? entry.enabled : void 0,
|
|
73
|
+
config: Object.prototype.hasOwnProperty.call(entry, "config") ? entry.config : void 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
function normalizePluginsConfig(plugins) {
|
|
79
|
+
return {
|
|
80
|
+
enabled: plugins?.enabled !== false,
|
|
81
|
+
allow: normalizeList(plugins?.allow),
|
|
82
|
+
deny: normalizeList(plugins?.deny),
|
|
83
|
+
loadPaths: normalizeList(plugins?.load?.paths),
|
|
84
|
+
entries: normalizeEntries(plugins?.entries)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function resolveEnableState(id, config) {
|
|
88
|
+
if (!config.enabled) {
|
|
89
|
+
return { enabled: false, reason: "plugins disabled" };
|
|
90
|
+
}
|
|
91
|
+
if (config.deny.includes(id)) {
|
|
92
|
+
return { enabled: false, reason: "blocked by denylist" };
|
|
93
|
+
}
|
|
94
|
+
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
|
95
|
+
return { enabled: false, reason: "not in allowlist" };
|
|
96
|
+
}
|
|
97
|
+
const entry = config.entries[id];
|
|
98
|
+
if (entry?.enabled === true) {
|
|
99
|
+
return { enabled: true };
|
|
100
|
+
}
|
|
101
|
+
if (entry?.enabled === false) {
|
|
102
|
+
return { enabled: false, reason: "disabled in config" };
|
|
103
|
+
}
|
|
104
|
+
return { enabled: true };
|
|
105
|
+
}
|
|
106
|
+
function recordPluginInstall(config, update) {
|
|
107
|
+
const { pluginId, ...record } = update;
|
|
108
|
+
const installs = {
|
|
109
|
+
...config.plugins.installs ?? {},
|
|
110
|
+
[pluginId]: {
|
|
111
|
+
...config.plugins.installs?.[pluginId] ?? {},
|
|
112
|
+
...record,
|
|
113
|
+
installedAt: record.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
...config,
|
|
118
|
+
plugins: {
|
|
119
|
+
...config.plugins,
|
|
120
|
+
installs
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function enablePluginInConfig(config, pluginId) {
|
|
125
|
+
const nextEntries = {
|
|
126
|
+
...config.plugins.entries ?? {},
|
|
127
|
+
[pluginId]: {
|
|
128
|
+
...config.plugins.entries?.[pluginId] ?? {},
|
|
129
|
+
enabled: true
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const allow = config.plugins.allow;
|
|
133
|
+
const nextAllow = Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId) ? [...allow, pluginId] : allow;
|
|
134
|
+
return {
|
|
135
|
+
...config,
|
|
136
|
+
plugins: {
|
|
137
|
+
...config.plugins,
|
|
138
|
+
entries: nextEntries,
|
|
139
|
+
...nextAllow ? { allow: nextAllow } : {}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function disablePluginInConfig(config, pluginId) {
|
|
144
|
+
return {
|
|
145
|
+
...config,
|
|
146
|
+
plugins: {
|
|
147
|
+
...config.plugins,
|
|
148
|
+
entries: {
|
|
149
|
+
...config.plugins.entries ?? {},
|
|
150
|
+
[pluginId]: {
|
|
151
|
+
...config.plugins.entries?.[pluginId] ?? {},
|
|
152
|
+
enabled: false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function addPluginLoadPath(config, loadPath) {
|
|
159
|
+
const paths = Array.from(/* @__PURE__ */ new Set([...config.plugins.load?.paths ?? [], loadPath]));
|
|
160
|
+
return {
|
|
161
|
+
...config,
|
|
162
|
+
plugins: {
|
|
163
|
+
...config.plugins,
|
|
164
|
+
load: {
|
|
165
|
+
...config.plugins.load ?? {},
|
|
166
|
+
paths
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/plugins/discovery.ts
|
|
173
|
+
import fs2 from "fs";
|
|
174
|
+
import path2 from "path";
|
|
175
|
+
import { homedir } from "os";
|
|
176
|
+
import { expandHome, getDataPath } from "@nextclaw/core";
|
|
177
|
+
|
|
178
|
+
// src/plugins/manifest.ts
|
|
179
|
+
import fs from "fs";
|
|
180
|
+
import path from "path";
|
|
181
|
+
var PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
|
182
|
+
var PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME];
|
|
183
|
+
function isRecord(value) {
|
|
184
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
185
|
+
}
|
|
186
|
+
function normalizeStringList(value) {
|
|
187
|
+
if (!Array.isArray(value)) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
function resolvePluginManifestPath(rootDir) {
|
|
193
|
+
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
|
194
|
+
const candidate = path.join(rootDir, filename);
|
|
195
|
+
if (fs.existsSync(candidate)) {
|
|
196
|
+
return candidate;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return path.join(rootDir, PLUGIN_MANIFEST_FILENAME);
|
|
200
|
+
}
|
|
201
|
+
function loadPluginManifest(rootDir) {
|
|
202
|
+
const manifestPath = resolvePluginManifestPath(rootDir);
|
|
203
|
+
if (!fs.existsSync(manifestPath)) {
|
|
204
|
+
return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath };
|
|
205
|
+
}
|
|
206
|
+
let raw;
|
|
207
|
+
try {
|
|
208
|
+
raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
error: `failed to parse plugin manifest: ${String(err)}`,
|
|
213
|
+
manifestPath
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(raw)) {
|
|
217
|
+
return { ok: false, error: "plugin manifest must be an object", manifestPath };
|
|
218
|
+
}
|
|
219
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
220
|
+
if (!id) {
|
|
221
|
+
return { ok: false, error: "plugin manifest requires id", manifestPath };
|
|
222
|
+
}
|
|
223
|
+
const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null;
|
|
224
|
+
if (!configSchema) {
|
|
225
|
+
return { ok: false, error: "plugin manifest requires configSchema", manifestPath };
|
|
226
|
+
}
|
|
227
|
+
const manifest = {
|
|
228
|
+
id,
|
|
229
|
+
configSchema,
|
|
230
|
+
kind: typeof raw.kind === "string" ? raw.kind : void 0,
|
|
231
|
+
channels: normalizeStringList(raw.channels),
|
|
232
|
+
providers: normalizeStringList(raw.providers),
|
|
233
|
+
skills: normalizeStringList(raw.skills),
|
|
234
|
+
name: typeof raw.name === "string" ? raw.name.trim() : void 0,
|
|
235
|
+
description: typeof raw.description === "string" ? raw.description.trim() : void 0,
|
|
236
|
+
version: typeof raw.version === "string" ? raw.version.trim() : void 0,
|
|
237
|
+
uiHints: isRecord(raw.uiHints) ? raw.uiHints : void 0
|
|
238
|
+
};
|
|
239
|
+
return { ok: true, manifest, manifestPath };
|
|
240
|
+
}
|
|
241
|
+
function getPackageManifestMetadata(manifest) {
|
|
242
|
+
if (!manifest) {
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
return manifest.openclaw;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/plugins/discovery.ts
|
|
249
|
+
var EXTENSION_EXTS = /* @__PURE__ */ new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
|
|
250
|
+
function resolveUserPath(input) {
|
|
251
|
+
return path2.resolve(expandHome(input));
|
|
252
|
+
}
|
|
253
|
+
function isExtensionFile(filePath) {
|
|
254
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
255
|
+
if (!EXTENSION_EXTS.has(ext)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return !filePath.endsWith(".d.ts");
|
|
259
|
+
}
|
|
260
|
+
function readPackageManifest(dir) {
|
|
261
|
+
const manifestPath = path2.join(dir, "package.json");
|
|
262
|
+
if (!fs2.existsSync(manifestPath)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function resolvePackageExtensions(manifest) {
|
|
272
|
+
const raw = getPackageManifestMetadata(manifest)?.extensions;
|
|
273
|
+
if (!Array.isArray(raw)) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
return raw.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
277
|
+
}
|
|
278
|
+
function deriveIdHint(params) {
|
|
279
|
+
const base = path2.basename(params.filePath, path2.extname(params.filePath));
|
|
280
|
+
const packageName = params.packageName?.trim();
|
|
281
|
+
if (!packageName) {
|
|
282
|
+
return base;
|
|
283
|
+
}
|
|
284
|
+
const unscoped = packageName.includes("/") ? packageName.split("/").pop() ?? packageName : packageName;
|
|
285
|
+
if (!params.hasMultipleExtensions) {
|
|
286
|
+
return unscoped;
|
|
287
|
+
}
|
|
288
|
+
return `${unscoped}/${base}`;
|
|
289
|
+
}
|
|
290
|
+
function addCandidate(params) {
|
|
291
|
+
const resolvedSource = path2.resolve(params.source);
|
|
292
|
+
if (params.seen.has(resolvedSource)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
params.seen.add(resolvedSource);
|
|
296
|
+
const manifest = params.manifest ?? null;
|
|
297
|
+
params.candidates.push({
|
|
298
|
+
idHint: params.idHint,
|
|
299
|
+
source: resolvedSource,
|
|
300
|
+
rootDir: path2.resolve(params.rootDir),
|
|
301
|
+
origin: params.origin,
|
|
302
|
+
workspaceDir: params.workspaceDir,
|
|
303
|
+
packageName: manifest?.name?.trim() || void 0,
|
|
304
|
+
packageVersion: manifest?.version?.trim() || void 0,
|
|
305
|
+
packageDescription: manifest?.description?.trim() || void 0,
|
|
306
|
+
packageDir: params.packageDir
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function discoverInDirectory(params) {
|
|
310
|
+
if (!fs2.existsSync(params.dir)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let entries = [];
|
|
314
|
+
try {
|
|
315
|
+
entries = fs2.readdirSync(params.dir, { withFileTypes: true });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
params.diagnostics.push({
|
|
318
|
+
level: "warn",
|
|
319
|
+
message: `failed to read extensions dir: ${params.dir} (${String(err)})`,
|
|
320
|
+
source: params.dir
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
const fullPath = path2.join(params.dir, entry.name);
|
|
326
|
+
if (entry.isFile()) {
|
|
327
|
+
if (!isExtensionFile(fullPath)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
addCandidate({
|
|
331
|
+
candidates: params.candidates,
|
|
332
|
+
seen: params.seen,
|
|
333
|
+
idHint: path2.basename(entry.name, path2.extname(entry.name)),
|
|
334
|
+
source: fullPath,
|
|
335
|
+
rootDir: path2.dirname(fullPath),
|
|
336
|
+
origin: params.origin,
|
|
337
|
+
workspaceDir: params.workspaceDir
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (!entry.isDirectory()) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const manifest = readPackageManifest(fullPath);
|
|
345
|
+
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
|
|
346
|
+
if (extensions.length > 0) {
|
|
347
|
+
for (const extPath of extensions) {
|
|
348
|
+
const resolved = path2.resolve(fullPath, extPath);
|
|
349
|
+
addCandidate({
|
|
350
|
+
candidates: params.candidates,
|
|
351
|
+
seen: params.seen,
|
|
352
|
+
idHint: deriveIdHint({
|
|
353
|
+
filePath: resolved,
|
|
354
|
+
packageName: manifest?.name,
|
|
355
|
+
hasMultipleExtensions: extensions.length > 1
|
|
356
|
+
}),
|
|
357
|
+
source: resolved,
|
|
358
|
+
rootDir: fullPath,
|
|
359
|
+
origin: params.origin,
|
|
360
|
+
workspaceDir: params.workspaceDir,
|
|
361
|
+
manifest,
|
|
362
|
+
packageDir: fullPath
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
368
|
+
const indexFile = indexCandidates.map((candidate) => path2.join(fullPath, candidate)).find((candidate) => fs2.existsSync(candidate));
|
|
369
|
+
if (indexFile && isExtensionFile(indexFile)) {
|
|
370
|
+
addCandidate({
|
|
371
|
+
candidates: params.candidates,
|
|
372
|
+
seen: params.seen,
|
|
373
|
+
idHint: entry.name,
|
|
374
|
+
source: indexFile,
|
|
375
|
+
rootDir: fullPath,
|
|
376
|
+
origin: params.origin,
|
|
377
|
+
workspaceDir: params.workspaceDir,
|
|
378
|
+
manifest,
|
|
379
|
+
packageDir: fullPath
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function discoverFromPath(params) {
|
|
385
|
+
const resolved = resolveUserPath(params.rawPath);
|
|
386
|
+
if (!fs2.existsSync(resolved)) {
|
|
387
|
+
params.diagnostics.push({
|
|
388
|
+
level: "error",
|
|
389
|
+
message: `plugin path not found: ${resolved}`,
|
|
390
|
+
source: resolved
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const stat = fs2.statSync(resolved);
|
|
395
|
+
if (stat.isFile()) {
|
|
396
|
+
if (!isExtensionFile(resolved)) {
|
|
397
|
+
params.diagnostics.push({
|
|
398
|
+
level: "error",
|
|
399
|
+
message: `plugin path is not a supported file: ${resolved}`,
|
|
400
|
+
source: resolved
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
addCandidate({
|
|
405
|
+
candidates: params.candidates,
|
|
406
|
+
seen: params.seen,
|
|
407
|
+
idHint: path2.basename(resolved, path2.extname(resolved)),
|
|
408
|
+
source: resolved,
|
|
409
|
+
rootDir: path2.dirname(resolved),
|
|
410
|
+
origin: params.origin,
|
|
411
|
+
workspaceDir: params.workspaceDir
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (stat.isDirectory()) {
|
|
416
|
+
const manifest = readPackageManifest(resolved);
|
|
417
|
+
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
|
|
418
|
+
if (extensions.length > 0) {
|
|
419
|
+
for (const extPath of extensions) {
|
|
420
|
+
const source = path2.resolve(resolved, extPath);
|
|
421
|
+
addCandidate({
|
|
422
|
+
candidates: params.candidates,
|
|
423
|
+
seen: params.seen,
|
|
424
|
+
idHint: deriveIdHint({
|
|
425
|
+
filePath: source,
|
|
426
|
+
packageName: manifest?.name,
|
|
427
|
+
hasMultipleExtensions: extensions.length > 1
|
|
428
|
+
}),
|
|
429
|
+
source,
|
|
430
|
+
rootDir: resolved,
|
|
431
|
+
origin: params.origin,
|
|
432
|
+
workspaceDir: params.workspaceDir,
|
|
433
|
+
manifest,
|
|
434
|
+
packageDir: resolved
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
440
|
+
const indexFile = indexCandidates.map((candidate) => path2.join(resolved, candidate)).find((candidate) => fs2.existsSync(candidate));
|
|
441
|
+
if (indexFile && isExtensionFile(indexFile)) {
|
|
442
|
+
addCandidate({
|
|
443
|
+
candidates: params.candidates,
|
|
444
|
+
seen: params.seen,
|
|
445
|
+
idHint: path2.basename(resolved),
|
|
446
|
+
source: indexFile,
|
|
447
|
+
rootDir: resolved,
|
|
448
|
+
origin: params.origin,
|
|
449
|
+
workspaceDir: params.workspaceDir,
|
|
450
|
+
manifest,
|
|
451
|
+
packageDir: resolved
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
discoverInDirectory({
|
|
456
|
+
dir: resolved,
|
|
457
|
+
origin: params.origin,
|
|
458
|
+
workspaceDir: params.workspaceDir,
|
|
459
|
+
candidates: params.candidates,
|
|
460
|
+
diagnostics: params.diagnostics,
|
|
461
|
+
seen: params.seen
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function discoverOpenClawPlugins(params) {
|
|
466
|
+
const candidates = [];
|
|
467
|
+
const diagnostics = [];
|
|
468
|
+
const seen = /* @__PURE__ */ new Set();
|
|
469
|
+
const workspaceDir = params.workspaceDir?.trim();
|
|
470
|
+
const loadPaths = params.extraPaths ?? params.config?.plugins?.load?.paths ?? [];
|
|
471
|
+
for (const rawPath of loadPaths) {
|
|
472
|
+
if (typeof rawPath !== "string") {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const trimmed = rawPath.trim();
|
|
476
|
+
if (!trimmed) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
discoverFromPath({
|
|
480
|
+
rawPath: trimmed,
|
|
481
|
+
origin: "config",
|
|
482
|
+
workspaceDir,
|
|
483
|
+
candidates,
|
|
484
|
+
diagnostics,
|
|
485
|
+
seen
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (workspaceDir) {
|
|
489
|
+
discoverInDirectory({
|
|
490
|
+
dir: path2.join(workspaceDir, ".openclaw", "extensions"),
|
|
491
|
+
origin: "workspace",
|
|
492
|
+
workspaceDir,
|
|
493
|
+
candidates,
|
|
494
|
+
diagnostics,
|
|
495
|
+
seen
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
discoverInDirectory({
|
|
499
|
+
dir: path2.join(getDataPath(), "extensions"),
|
|
500
|
+
origin: "global",
|
|
501
|
+
candidates,
|
|
502
|
+
diagnostics,
|
|
503
|
+
seen
|
|
504
|
+
});
|
|
505
|
+
discoverInDirectory({
|
|
506
|
+
dir: path2.join(homedir(), ".openclaw", "extensions"),
|
|
507
|
+
origin: "global",
|
|
508
|
+
candidates,
|
|
509
|
+
diagnostics,
|
|
510
|
+
seen
|
|
511
|
+
});
|
|
512
|
+
return { candidates, diagnostics };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/plugins/install.ts
|
|
516
|
+
import fs3 from "fs/promises";
|
|
517
|
+
import os from "os";
|
|
518
|
+
import path3 from "path";
|
|
519
|
+
import { spawn } from "child_process";
|
|
520
|
+
import JSZip from "jszip";
|
|
521
|
+
import * as tar from "tar";
|
|
522
|
+
import { getDataPath as getDataPath2 } from "@nextclaw/core";
|
|
523
|
+
var defaultLogger = {};
|
|
524
|
+
function resolveUserPath2(input) {
|
|
525
|
+
if (input.startsWith("~/")) {
|
|
526
|
+
return path3.resolve(os.homedir(), input.slice(2));
|
|
527
|
+
}
|
|
528
|
+
return path3.resolve(input);
|
|
529
|
+
}
|
|
530
|
+
function safeDirName(input) {
|
|
531
|
+
return input.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-/, "").replace(/-$/, "").trim();
|
|
532
|
+
}
|
|
533
|
+
function validatePluginId(pluginId) {
|
|
534
|
+
if (!pluginId) {
|
|
535
|
+
return "invalid plugin name: missing";
|
|
536
|
+
}
|
|
537
|
+
if (pluginId === "." || pluginId === "..") {
|
|
538
|
+
return "invalid plugin name: reserved path segment";
|
|
539
|
+
}
|
|
540
|
+
if (pluginId.includes("/") || pluginId.includes("\\")) {
|
|
541
|
+
return "invalid plugin name: path separators not allowed";
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
function resolveExtensionsDir(extensionsDir) {
|
|
546
|
+
return extensionsDir ? resolveUserPath2(extensionsDir) : path3.join(getDataPath2(), "extensions");
|
|
547
|
+
}
|
|
548
|
+
async function exists(filePath) {
|
|
549
|
+
try {
|
|
550
|
+
await fs3.access(filePath);
|
|
551
|
+
return true;
|
|
552
|
+
} catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function readJsonFile(filePath) {
|
|
557
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
558
|
+
return JSON.parse(raw);
|
|
559
|
+
}
|
|
560
|
+
function resolveArchiveKind(filePath) {
|
|
561
|
+
const lower = filePath.toLowerCase();
|
|
562
|
+
if (lower.endsWith(".zip")) {
|
|
563
|
+
return "zip";
|
|
564
|
+
}
|
|
565
|
+
if (lower.endsWith(".tgz") || lower.endsWith(".tar.gz")) {
|
|
566
|
+
return "tgz";
|
|
567
|
+
}
|
|
568
|
+
if (lower.endsWith(".tar")) {
|
|
569
|
+
return "tar";
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
async function extractArchive(params) {
|
|
574
|
+
if (params.kind === "zip") {
|
|
575
|
+
const raw = await fs3.readFile(params.archivePath);
|
|
576
|
+
const zip = await JSZip.loadAsync(raw);
|
|
577
|
+
await Promise.all(
|
|
578
|
+
Object.values(zip.files).map(async (entry) => {
|
|
579
|
+
const fullPath = path3.resolve(params.destDir, entry.name);
|
|
580
|
+
if (!fullPath.startsWith(path3.resolve(params.destDir) + path3.sep) && fullPath !== path3.resolve(params.destDir)) {
|
|
581
|
+
throw new Error(`zip entry escapes destination: ${entry.name}`);
|
|
582
|
+
}
|
|
583
|
+
if (entry.dir) {
|
|
584
|
+
await fs3.mkdir(fullPath, { recursive: true });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const parent = path3.dirname(fullPath);
|
|
588
|
+
await fs3.mkdir(parent, { recursive: true });
|
|
589
|
+
const content = await entry.async("nodebuffer");
|
|
590
|
+
await fs3.writeFile(fullPath, content);
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await tar.x({
|
|
596
|
+
file: params.archivePath,
|
|
597
|
+
cwd: params.destDir,
|
|
598
|
+
strict: true,
|
|
599
|
+
preservePaths: false
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
async function resolvePackedRootDir(extractDir) {
|
|
603
|
+
const packageDir = path3.join(extractDir, "package");
|
|
604
|
+
if (await exists(path3.join(packageDir, "package.json"))) {
|
|
605
|
+
return packageDir;
|
|
606
|
+
}
|
|
607
|
+
if (await exists(path3.join(extractDir, "package.json"))) {
|
|
608
|
+
return extractDir;
|
|
609
|
+
}
|
|
610
|
+
const entries = await fs3.readdir(extractDir, { withFileTypes: true });
|
|
611
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path3.join(extractDir, entry.name));
|
|
612
|
+
if (dirs.length === 1 && await exists(path3.join(dirs[0], "package.json"))) {
|
|
613
|
+
return dirs[0];
|
|
614
|
+
}
|
|
615
|
+
for (const dir of dirs) {
|
|
616
|
+
if (await exists(path3.join(dir, "package.json"))) {
|
|
617
|
+
return dir;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
throw new Error("archive missing package root");
|
|
621
|
+
}
|
|
622
|
+
async function ensureOpenClawExtensions(manifest) {
|
|
623
|
+
const extensions = manifest.openclaw?.extensions;
|
|
624
|
+
if (!Array.isArray(extensions)) {
|
|
625
|
+
throw new Error("package.json missing openclaw.extensions");
|
|
626
|
+
}
|
|
627
|
+
const list = extensions.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
628
|
+
if (list.length === 0) {
|
|
629
|
+
throw new Error("package.json openclaw.extensions is empty");
|
|
630
|
+
}
|
|
631
|
+
return list;
|
|
632
|
+
}
|
|
633
|
+
async function runCommand(command, args, cwd) {
|
|
634
|
+
return new Promise((resolve) => {
|
|
635
|
+
const child = spawn(command, args, {
|
|
636
|
+
cwd,
|
|
637
|
+
env: {
|
|
638
|
+
...process.env,
|
|
639
|
+
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
|
640
|
+
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
|
641
|
+
},
|
|
642
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
643
|
+
});
|
|
644
|
+
let stdout = "";
|
|
645
|
+
let stderr = "";
|
|
646
|
+
child.stdout.on("data", (chunk) => {
|
|
647
|
+
stdout += String(chunk);
|
|
648
|
+
});
|
|
649
|
+
child.stderr.on("data", (chunk) => {
|
|
650
|
+
stderr += String(chunk);
|
|
651
|
+
});
|
|
652
|
+
child.on("close", (code) => {
|
|
653
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
654
|
+
});
|
|
655
|
+
child.on("error", (error) => {
|
|
656
|
+
resolve({ code: 1, stdout, stderr: `${stderr}
|
|
657
|
+
${String(error)}` });
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
async function installDependenciesIfNeeded(packageDir, manifest, logger) {
|
|
662
|
+
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
logger.info?.("Installing plugin dependencies...");
|
|
666
|
+
const result = await runCommand("npm", ["install", "--ignore-scripts"], packageDir);
|
|
667
|
+
if (result.code !== 0) {
|
|
668
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || "npm install failed");
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function resolvePluginInstallDir(pluginId, extensionsDir) {
|
|
672
|
+
const err = validatePluginId(pluginId);
|
|
673
|
+
if (err) {
|
|
674
|
+
throw new Error(err);
|
|
675
|
+
}
|
|
676
|
+
const baseDir = resolveExtensionsDir(extensionsDir);
|
|
677
|
+
const target = path3.resolve(baseDir, pluginId);
|
|
678
|
+
if (!target.startsWith(path3.resolve(baseDir) + path3.sep) && target !== path3.resolve(baseDir)) {
|
|
679
|
+
throw new Error("invalid plugin name: path traversal detected");
|
|
680
|
+
}
|
|
681
|
+
return target;
|
|
682
|
+
}
|
|
683
|
+
async function installPluginFromPackageDir(params) {
|
|
684
|
+
const logger = params.logger ?? defaultLogger;
|
|
685
|
+
const packageDir = resolveUserPath2(params.packageDir);
|
|
686
|
+
const mode = params.mode ?? "install";
|
|
687
|
+
const dryRun = params.dryRun ?? false;
|
|
688
|
+
const packageJsonPath = path3.join(packageDir, "package.json");
|
|
689
|
+
if (!await exists(packageJsonPath)) {
|
|
690
|
+
return { ok: false, error: "plugin package missing package.json" };
|
|
691
|
+
}
|
|
692
|
+
let packageManifest;
|
|
693
|
+
try {
|
|
694
|
+
packageManifest = await readJsonFile(packageJsonPath);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return { ok: false, error: `invalid package.json: ${String(err)}` };
|
|
697
|
+
}
|
|
698
|
+
let extensions;
|
|
699
|
+
try {
|
|
700
|
+
extensions = await ensureOpenClawExtensions(packageManifest);
|
|
701
|
+
} catch (err) {
|
|
702
|
+
return { ok: false, error: String(err) };
|
|
703
|
+
}
|
|
704
|
+
const manifestRes = loadPluginManifest(packageDir);
|
|
705
|
+
if (!manifestRes.ok) {
|
|
706
|
+
return { ok: false, error: manifestRes.error };
|
|
707
|
+
}
|
|
708
|
+
const pluginId = manifestRes.manifest.id;
|
|
709
|
+
const pluginErr = validatePluginId(pluginId);
|
|
710
|
+
if (pluginErr) {
|
|
711
|
+
return { ok: false, error: pluginErr };
|
|
712
|
+
}
|
|
713
|
+
if (params.expectedPluginId && params.expectedPluginId !== pluginId) {
|
|
714
|
+
return {
|
|
715
|
+
ok: false,
|
|
716
|
+
error: `plugin id mismatch: expected ${params.expectedPluginId}, got ${pluginId}`
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const targetDir = resolvePluginInstallDir(pluginId, params.extensionsDir);
|
|
720
|
+
if (mode === "install" && await exists(targetDir)) {
|
|
721
|
+
return { ok: false, error: `plugin already exists: ${targetDir} (delete it first)` };
|
|
722
|
+
}
|
|
723
|
+
if (dryRun) {
|
|
724
|
+
return {
|
|
725
|
+
ok: true,
|
|
726
|
+
pluginId,
|
|
727
|
+
targetDir,
|
|
728
|
+
manifestName: packageManifest.name,
|
|
729
|
+
version: packageManifest.version,
|
|
730
|
+
extensions
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
await fs3.mkdir(path3.dirname(targetDir), { recursive: true });
|
|
734
|
+
if (mode === "update" && await exists(targetDir)) {
|
|
735
|
+
await fs3.rm(targetDir, { recursive: true, force: true });
|
|
736
|
+
}
|
|
737
|
+
logger.info?.(`Installing to ${targetDir}...`);
|
|
738
|
+
await fs3.cp(packageDir, targetDir, { recursive: true, force: true });
|
|
739
|
+
for (const extensionPath of extensions) {
|
|
740
|
+
const resolved = path3.resolve(targetDir, extensionPath);
|
|
741
|
+
if (!resolved.startsWith(path3.resolve(targetDir) + path3.sep) && resolved !== path3.resolve(targetDir)) {
|
|
742
|
+
return { ok: false, error: `extension entry escapes plugin directory: ${extensionPath}` };
|
|
743
|
+
}
|
|
744
|
+
if (!await exists(resolved)) {
|
|
745
|
+
logger.warn?.(`extension entry not found: ${extensionPath}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
await installDependenciesIfNeeded(targetDir, packageManifest, logger);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
return { ok: false, error: `failed to install dependencies: ${String(err)}` };
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
ok: true,
|
|
755
|
+
pluginId,
|
|
756
|
+
targetDir,
|
|
757
|
+
manifestName: packageManifest.name,
|
|
758
|
+
version: packageManifest.version,
|
|
759
|
+
extensions
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function installPluginFromArchive(params) {
|
|
763
|
+
const logger = params.logger ?? defaultLogger;
|
|
764
|
+
const archivePath = resolveUserPath2(params.archivePath);
|
|
765
|
+
if (!await exists(archivePath)) {
|
|
766
|
+
return { ok: false, error: `archive not found: ${archivePath}` };
|
|
767
|
+
}
|
|
768
|
+
const kind = resolveArchiveKind(archivePath);
|
|
769
|
+
if (!kind) {
|
|
770
|
+
return { ok: false, error: `unsupported archive: ${archivePath}` };
|
|
771
|
+
}
|
|
772
|
+
const tempDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "nextclaw-plugin-"));
|
|
773
|
+
try {
|
|
774
|
+
const extractDir = path3.join(tempDir, "extract");
|
|
775
|
+
await fs3.mkdir(extractDir, { recursive: true });
|
|
776
|
+
logger.info?.(`Extracting ${archivePath}...`);
|
|
777
|
+
try {
|
|
778
|
+
await extractArchive({ archivePath, destDir: extractDir, kind });
|
|
779
|
+
} catch (err) {
|
|
780
|
+
return { ok: false, error: `failed to extract archive: ${String(err)}` };
|
|
781
|
+
}
|
|
782
|
+
let packageDir = "";
|
|
783
|
+
try {
|
|
784
|
+
packageDir = await resolvePackedRootDir(extractDir);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
return { ok: false, error: String(err) };
|
|
787
|
+
}
|
|
788
|
+
return installPluginFromPackageDir({
|
|
789
|
+
packageDir,
|
|
790
|
+
extensionsDir: params.extensionsDir,
|
|
791
|
+
logger,
|
|
792
|
+
mode: params.mode,
|
|
793
|
+
dryRun: params.dryRun,
|
|
794
|
+
expectedPluginId: params.expectedPluginId
|
|
795
|
+
});
|
|
796
|
+
} finally {
|
|
797
|
+
await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function installPluginFromDir(params) {
|
|
801
|
+
const dirPath = resolveUserPath2(params.dirPath);
|
|
802
|
+
if (!await exists(dirPath)) {
|
|
803
|
+
return { ok: false, error: `directory not found: ${dirPath}` };
|
|
804
|
+
}
|
|
805
|
+
const stat = await fs3.stat(dirPath);
|
|
806
|
+
if (!stat.isDirectory()) {
|
|
807
|
+
return { ok: false, error: `not a directory: ${dirPath}` };
|
|
808
|
+
}
|
|
809
|
+
return installPluginFromPackageDir({
|
|
810
|
+
packageDir: dirPath,
|
|
811
|
+
extensionsDir: params.extensionsDir,
|
|
812
|
+
logger: params.logger,
|
|
813
|
+
mode: params.mode,
|
|
814
|
+
dryRun: params.dryRun,
|
|
815
|
+
expectedPluginId: params.expectedPluginId
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
async function installPluginFromFile(params) {
|
|
819
|
+
const filePath = resolveUserPath2(params.filePath);
|
|
820
|
+
const logger = params.logger ?? defaultLogger;
|
|
821
|
+
const mode = params.mode ?? "install";
|
|
822
|
+
const dryRun = params.dryRun ?? false;
|
|
823
|
+
if (!await exists(filePath)) {
|
|
824
|
+
return { ok: false, error: `file not found: ${filePath}` };
|
|
825
|
+
}
|
|
826
|
+
const stat = await fs3.stat(filePath);
|
|
827
|
+
if (!stat.isFile()) {
|
|
828
|
+
return { ok: false, error: `not a file: ${filePath}` };
|
|
829
|
+
}
|
|
830
|
+
const ext = path3.extname(filePath);
|
|
831
|
+
const pluginId = safeDirName(path3.basename(filePath, ext) || "plugin");
|
|
832
|
+
const pluginErr = validatePluginId(pluginId);
|
|
833
|
+
if (pluginErr) {
|
|
834
|
+
return { ok: false, error: pluginErr };
|
|
835
|
+
}
|
|
836
|
+
const targetDir = resolveExtensionsDir(params.extensionsDir);
|
|
837
|
+
const targetFile = path3.join(targetDir, `${pluginId}${ext}`);
|
|
838
|
+
if (mode === "install" && await exists(targetFile)) {
|
|
839
|
+
return { ok: false, error: `plugin already exists: ${targetFile} (delete it first)` };
|
|
840
|
+
}
|
|
841
|
+
if (dryRun) {
|
|
842
|
+
return {
|
|
843
|
+
ok: true,
|
|
844
|
+
pluginId,
|
|
845
|
+
targetDir: targetFile,
|
|
846
|
+
extensions: [path3.basename(targetFile)]
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
await fs3.mkdir(targetDir, { recursive: true });
|
|
850
|
+
logger.info?.(`Installing to ${targetFile}...`);
|
|
851
|
+
await fs3.copyFile(filePath, targetFile);
|
|
852
|
+
return {
|
|
853
|
+
ok: true,
|
|
854
|
+
pluginId,
|
|
855
|
+
targetDir: targetFile,
|
|
856
|
+
extensions: [path3.basename(targetFile)]
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function validateRegistryNpmSpec(spec) {
|
|
860
|
+
const trimmed = spec.trim();
|
|
861
|
+
if (!trimmed) {
|
|
862
|
+
return "npm spec is required";
|
|
863
|
+
}
|
|
864
|
+
const lower = trimmed.toLowerCase();
|
|
865
|
+
if (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("git+") || lower.startsWith("github:") || lower.startsWith("file:")) {
|
|
866
|
+
return "only registry npm specs are supported";
|
|
867
|
+
}
|
|
868
|
+
if (trimmed.includes("/") && !trimmed.startsWith("@")) {
|
|
869
|
+
return "only registry npm specs are supported";
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
async function installPluginFromNpmSpec(params) {
|
|
874
|
+
const logger = params.logger ?? defaultLogger;
|
|
875
|
+
const spec = params.spec.trim();
|
|
876
|
+
const specError = validateRegistryNpmSpec(spec);
|
|
877
|
+
if (specError) {
|
|
878
|
+
return { ok: false, error: specError };
|
|
879
|
+
}
|
|
880
|
+
const tempDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "nextclaw-npm-pack-"));
|
|
881
|
+
try {
|
|
882
|
+
logger.info?.(`Downloading ${spec}...`);
|
|
883
|
+
const packed = await runCommand("npm", ["pack", spec, "--ignore-scripts"], tempDir);
|
|
884
|
+
if (packed.code !== 0) {
|
|
885
|
+
return {
|
|
886
|
+
ok: false,
|
|
887
|
+
error: `npm pack failed: ${packed.stderr.trim() || packed.stdout.trim()}`
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const archiveName = packed.stdout.split("\n").map((line) => line.trim()).filter(Boolean).pop();
|
|
891
|
+
if (!archiveName) {
|
|
892
|
+
return { ok: false, error: "npm pack produced no archive" };
|
|
893
|
+
}
|
|
894
|
+
const archivePath = path3.join(tempDir, archiveName);
|
|
895
|
+
return installPluginFromArchive({
|
|
896
|
+
archivePath,
|
|
897
|
+
extensionsDir: params.extensionsDir,
|
|
898
|
+
logger,
|
|
899
|
+
mode: params.mode,
|
|
900
|
+
dryRun: params.dryRun,
|
|
901
|
+
expectedPluginId: params.expectedPluginId
|
|
902
|
+
});
|
|
903
|
+
} finally {
|
|
904
|
+
await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async function installPluginFromPath(params) {
|
|
908
|
+
const resolvedPath = resolveUserPath2(params.path);
|
|
909
|
+
if (!await exists(resolvedPath)) {
|
|
910
|
+
return { ok: false, error: `path not found: ${resolvedPath}` };
|
|
911
|
+
}
|
|
912
|
+
const stat = await fs3.stat(resolvedPath);
|
|
913
|
+
if (stat.isDirectory()) {
|
|
914
|
+
return installPluginFromDir({
|
|
915
|
+
dirPath: resolvedPath,
|
|
916
|
+
extensionsDir: params.extensionsDir,
|
|
917
|
+
logger: params.logger,
|
|
918
|
+
mode: params.mode,
|
|
919
|
+
dryRun: params.dryRun,
|
|
920
|
+
expectedPluginId: params.expectedPluginId
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const archiveKind = resolveArchiveKind(resolvedPath);
|
|
924
|
+
if (archiveKind) {
|
|
925
|
+
return installPluginFromArchive({
|
|
926
|
+
archivePath: resolvedPath,
|
|
927
|
+
extensionsDir: params.extensionsDir,
|
|
928
|
+
logger: params.logger,
|
|
929
|
+
mode: params.mode,
|
|
930
|
+
dryRun: params.dryRun,
|
|
931
|
+
expectedPluginId: params.expectedPluginId
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return installPluginFromFile({
|
|
935
|
+
filePath: resolvedPath,
|
|
936
|
+
extensionsDir: params.extensionsDir,
|
|
937
|
+
logger: params.logger,
|
|
938
|
+
mode: params.mode,
|
|
939
|
+
dryRun: params.dryRun
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/plugins/loader.ts
|
|
944
|
+
import fs5 from "fs";
|
|
945
|
+
import path4 from "path";
|
|
946
|
+
import { fileURLToPath } from "url";
|
|
947
|
+
import createJitiImport from "jiti";
|
|
948
|
+
import { getWorkspacePathFromConfig } from "@nextclaw/core";
|
|
949
|
+
import { expandHome as expandHome2 } from "@nextclaw/core";
|
|
950
|
+
|
|
951
|
+
// src/plugins/manifest-registry.ts
|
|
952
|
+
import fs4 from "fs";
|
|
953
|
+
var PLUGIN_ORIGIN_RANK = {
|
|
954
|
+
config: 0,
|
|
955
|
+
workspace: 1,
|
|
956
|
+
global: 2,
|
|
957
|
+
bundled: 3
|
|
958
|
+
};
|
|
959
|
+
function safeRealpathSync(rootDir, cache) {
|
|
960
|
+
const cached = cache.get(rootDir);
|
|
961
|
+
if (cached) {
|
|
962
|
+
return cached;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const resolved = fs4.realpathSync(rootDir);
|
|
966
|
+
cache.set(rootDir, resolved);
|
|
967
|
+
return resolved;
|
|
968
|
+
} catch {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function safeStatMtimeMs(filePath) {
|
|
973
|
+
try {
|
|
974
|
+
return fs4.statSync(filePath).mtimeMs;
|
|
975
|
+
} catch {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function normalizeManifestLabel(raw) {
|
|
980
|
+
const trimmed = raw?.trim();
|
|
981
|
+
return trimmed ? trimmed : void 0;
|
|
982
|
+
}
|
|
983
|
+
function buildRecord(params) {
|
|
984
|
+
return {
|
|
985
|
+
id: params.manifest.id,
|
|
986
|
+
name: normalizeManifestLabel(params.manifest.name) ?? params.candidate.packageName,
|
|
987
|
+
description: normalizeManifestLabel(params.manifest.description) ?? params.candidate.packageDescription,
|
|
988
|
+
version: normalizeManifestLabel(params.manifest.version) ?? params.candidate.packageVersion,
|
|
989
|
+
kind: params.manifest.kind,
|
|
990
|
+
channels: params.manifest.channels ?? [],
|
|
991
|
+
providers: params.manifest.providers ?? [],
|
|
992
|
+
skills: params.manifest.skills ?? [],
|
|
993
|
+
origin: params.candidate.origin,
|
|
994
|
+
workspaceDir: params.candidate.workspaceDir,
|
|
995
|
+
rootDir: params.candidate.rootDir,
|
|
996
|
+
source: params.candidate.source,
|
|
997
|
+
manifestPath: params.manifestPath,
|
|
998
|
+
schemaCacheKey: params.schemaCacheKey,
|
|
999
|
+
configSchema: params.configSchema,
|
|
1000
|
+
configUiHints: params.manifest.uiHints
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function loadPluginManifestRegistry(params) {
|
|
1004
|
+
const normalized = normalizePluginsConfig(params.config?.plugins);
|
|
1005
|
+
const discovery = params.candidates ? {
|
|
1006
|
+
candidates: params.candidates,
|
|
1007
|
+
diagnostics: params.diagnostics ?? []
|
|
1008
|
+
} : discoverOpenClawPlugins({
|
|
1009
|
+
config: params.config,
|
|
1010
|
+
workspaceDir: params.workspaceDir,
|
|
1011
|
+
extraPaths: normalized.loadPaths
|
|
1012
|
+
});
|
|
1013
|
+
const diagnostics = [...discovery.diagnostics];
|
|
1014
|
+
const records = [];
|
|
1015
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
1016
|
+
const realpathCache = /* @__PURE__ */ new Map();
|
|
1017
|
+
for (const candidate of discovery.candidates) {
|
|
1018
|
+
const manifestRes = loadPluginManifest(candidate.rootDir);
|
|
1019
|
+
if (!manifestRes.ok) {
|
|
1020
|
+
diagnostics.push({
|
|
1021
|
+
level: "error",
|
|
1022
|
+
message: manifestRes.error,
|
|
1023
|
+
source: manifestRes.manifestPath
|
|
1024
|
+
});
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const manifest = manifestRes.manifest;
|
|
1028
|
+
const configSchema = manifest.configSchema;
|
|
1029
|
+
if (candidate.idHint && candidate.idHint !== manifest.id) {
|
|
1030
|
+
diagnostics.push({
|
|
1031
|
+
level: "warn",
|
|
1032
|
+
pluginId: manifest.id,
|
|
1033
|
+
source: candidate.source,
|
|
1034
|
+
message: `plugin id mismatch (manifest uses "${manifest.id}", entry hints "${candidate.idHint}")`
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
const manifestMtime = safeStatMtimeMs(manifestRes.manifestPath);
|
|
1038
|
+
const schemaCacheKey = manifestMtime ? `${manifestRes.manifestPath}:${manifestMtime}` : manifestRes.manifestPath;
|
|
1039
|
+
const existing = seenIds.get(manifest.id);
|
|
1040
|
+
if (existing) {
|
|
1041
|
+
const existingReal = safeRealpathSync(existing.candidate.rootDir, realpathCache);
|
|
1042
|
+
const candidateReal = safeRealpathSync(candidate.rootDir, realpathCache);
|
|
1043
|
+
const samePlugin = Boolean(existingReal && candidateReal && existingReal === candidateReal);
|
|
1044
|
+
if (samePlugin) {
|
|
1045
|
+
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) {
|
|
1046
|
+
records[existing.recordIndex] = buildRecord({
|
|
1047
|
+
manifest,
|
|
1048
|
+
candidate,
|
|
1049
|
+
manifestPath: manifestRes.manifestPath,
|
|
1050
|
+
schemaCacheKey,
|
|
1051
|
+
configSchema
|
|
1052
|
+
});
|
|
1053
|
+
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
|
|
1054
|
+
}
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
diagnostics.push({
|
|
1058
|
+
level: "warn",
|
|
1059
|
+
pluginId: manifest.id,
|
|
1060
|
+
source: candidate.source,
|
|
1061
|
+
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`
|
|
1062
|
+
});
|
|
1063
|
+
} else {
|
|
1064
|
+
seenIds.set(manifest.id, { candidate, recordIndex: records.length });
|
|
1065
|
+
}
|
|
1066
|
+
records.push(
|
|
1067
|
+
buildRecord({
|
|
1068
|
+
manifest,
|
|
1069
|
+
candidate,
|
|
1070
|
+
manifestPath: manifestRes.manifestPath,
|
|
1071
|
+
schemaCacheKey,
|
|
1072
|
+
configSchema
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
plugins: records,
|
|
1078
|
+
diagnostics
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function toPluginUiMetadata(records) {
|
|
1082
|
+
return records.map((record) => ({
|
|
1083
|
+
id: record.id,
|
|
1084
|
+
configSchema: record.configSchema,
|
|
1085
|
+
configUiHints: record.configUiHints
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
function loadPluginUiMetadata(params) {
|
|
1089
|
+
const registry = loadPluginManifestRegistry({
|
|
1090
|
+
config: params.config,
|
|
1091
|
+
workspaceDir: params.workspaceDir
|
|
1092
|
+
});
|
|
1093
|
+
return toPluginUiMetadata(registry.plugins);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/plugins/runtime.ts
|
|
1097
|
+
import { getPackageVersion } from "@nextclaw/core";
|
|
1098
|
+
import { MemoryGetTool, MemorySearchTool } from "@nextclaw/core";
|
|
1099
|
+
function toPluginTool(tool) {
|
|
1100
|
+
return {
|
|
1101
|
+
name: tool.name,
|
|
1102
|
+
description: tool.description,
|
|
1103
|
+
parameters: tool.parameters,
|
|
1104
|
+
execute: (params) => tool.execute(params)
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function createPluginRuntime(params) {
|
|
1108
|
+
return {
|
|
1109
|
+
version: getPackageVersion(),
|
|
1110
|
+
tools: {
|
|
1111
|
+
createMemorySearchTool: () => toPluginTool(new MemorySearchTool(params.workspace)),
|
|
1112
|
+
createMemoryGetTool: () => toPluginTool(new MemoryGetTool(params.workspace))
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/plugins/schema-validator.ts
|
|
1118
|
+
import AjvPkg from "ajv";
|
|
1119
|
+
var AjvCtor = AjvPkg;
|
|
1120
|
+
var ajv = new AjvCtor({
|
|
1121
|
+
allErrors: true,
|
|
1122
|
+
strict: false,
|
|
1123
|
+
removeAdditional: false
|
|
1124
|
+
});
|
|
1125
|
+
var schemaCache = /* @__PURE__ */ new Map();
|
|
1126
|
+
function formatAjvErrors(errors) {
|
|
1127
|
+
if (!errors || errors.length === 0) {
|
|
1128
|
+
return ["invalid config"];
|
|
1129
|
+
}
|
|
1130
|
+
return errors.map((error) => {
|
|
1131
|
+
const path6 = error.instancePath?.replace(/^\//, "").replace(/\//g, ".") || "<root>";
|
|
1132
|
+
const message = error.message ?? "invalid";
|
|
1133
|
+
return `${path6}: ${message}`;
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
function validateJsonSchemaValue(params) {
|
|
1137
|
+
let cached = schemaCache.get(params.cacheKey);
|
|
1138
|
+
if (!cached || cached.schema !== params.schema) {
|
|
1139
|
+
const validate = ajv.compile(params.schema);
|
|
1140
|
+
cached = { validate, schema: params.schema };
|
|
1141
|
+
schemaCache.set(params.cacheKey, cached);
|
|
1142
|
+
}
|
|
1143
|
+
const ok = cached.validate(params.value);
|
|
1144
|
+
if (ok) {
|
|
1145
|
+
return { ok: true };
|
|
1146
|
+
}
|
|
1147
|
+
return { ok: false, errors: formatAjvErrors(cached.validate.errors) };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/plugins/loader.ts
|
|
1151
|
+
var createJiti = createJitiImport;
|
|
1152
|
+
var defaultLogger2 = {
|
|
1153
|
+
info: (message) => console.log(message),
|
|
1154
|
+
warn: (message) => console.warn(message),
|
|
1155
|
+
error: (message) => console.error(message),
|
|
1156
|
+
debug: (message) => console.debug(message)
|
|
1157
|
+
};
|
|
1158
|
+
function resolvePluginSdkAliasFile(params) {
|
|
1159
|
+
try {
|
|
1160
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
1161
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1162
|
+
let cursor = path4.dirname(modulePath);
|
|
1163
|
+
for (let i = 0; i < 6; i += 1) {
|
|
1164
|
+
const srcCandidate = path4.join(cursor, "src", "plugin-sdk", params.srcFile);
|
|
1165
|
+
const distCandidate = path4.join(cursor, "dist", "plugin-sdk", params.distFile);
|
|
1166
|
+
const candidates = isProduction ? [distCandidate, srcCandidate] : [srcCandidate, distCandidate];
|
|
1167
|
+
for (const candidate of candidates) {
|
|
1168
|
+
if (fs5.existsSync(candidate)) {
|
|
1169
|
+
return candidate;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const parent = path4.dirname(cursor);
|
|
1173
|
+
if (parent === cursor) {
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
cursor = parent;
|
|
1177
|
+
}
|
|
1178
|
+
} catch {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
function resolvePluginSdkAlias() {
|
|
1184
|
+
return resolvePluginSdkAliasFile({ srcFile: "index.ts", distFile: "index.js" });
|
|
1185
|
+
}
|
|
1186
|
+
function resolvePluginModuleExport(moduleExport) {
|
|
1187
|
+
const resolved = moduleExport && typeof moduleExport === "object" && "default" in moduleExport ? moduleExport.default : moduleExport;
|
|
1188
|
+
if (typeof resolved === "function") {
|
|
1189
|
+
return {
|
|
1190
|
+
register: resolved
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (resolved && typeof resolved === "object") {
|
|
1194
|
+
const definition = resolved;
|
|
1195
|
+
return {
|
|
1196
|
+
definition,
|
|
1197
|
+
register: definition.register ?? definition.activate
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
return {};
|
|
1201
|
+
}
|
|
1202
|
+
function normalizeToolList(value) {
|
|
1203
|
+
if (!value) {
|
|
1204
|
+
return [];
|
|
1205
|
+
}
|
|
1206
|
+
const list = Array.isArray(value) ? value : [value];
|
|
1207
|
+
return list.filter((entry) => {
|
|
1208
|
+
if (!entry || typeof entry !== "object") {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
const candidate = entry;
|
|
1212
|
+
return typeof candidate.name === "string" && candidate.name.trim().length > 0 && candidate.parameters !== void 0 && typeof candidate.execute === "function";
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
function createPluginRecord(params) {
|
|
1216
|
+
return {
|
|
1217
|
+
id: params.id,
|
|
1218
|
+
name: params.name ?? params.id,
|
|
1219
|
+
description: params.description,
|
|
1220
|
+
version: params.version,
|
|
1221
|
+
kind: params.kind,
|
|
1222
|
+
source: params.source,
|
|
1223
|
+
origin: params.origin,
|
|
1224
|
+
workspaceDir: params.workspaceDir,
|
|
1225
|
+
enabled: params.enabled,
|
|
1226
|
+
status: params.enabled ? "loaded" : "disabled",
|
|
1227
|
+
toolNames: [],
|
|
1228
|
+
channelIds: [],
|
|
1229
|
+
providerIds: [],
|
|
1230
|
+
configSchema: params.configSchema,
|
|
1231
|
+
configUiHints: params.configUiHints,
|
|
1232
|
+
configJsonSchema: params.configJsonSchema
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function buildPluginLogger(base, pluginId) {
|
|
1236
|
+
const withPrefix = (message) => `[plugins:${pluginId}] ${message}`;
|
|
1237
|
+
return {
|
|
1238
|
+
info: (message) => base.info(withPrefix(message)),
|
|
1239
|
+
warn: (message) => base.warn(withPrefix(message)),
|
|
1240
|
+
error: (message) => base.error(withPrefix(message)),
|
|
1241
|
+
debug: base.debug ? (message) => base.debug?.(withPrefix(message)) : void 0
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function ensureUniqueNames(params) {
|
|
1245
|
+
const accepted = [];
|
|
1246
|
+
for (const rawName of params.names) {
|
|
1247
|
+
const name = rawName.trim();
|
|
1248
|
+
if (!name) {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (params.reserved.has(name)) {
|
|
1252
|
+
params.diagnostics.push({
|
|
1253
|
+
level: "error",
|
|
1254
|
+
pluginId: params.pluginId,
|
|
1255
|
+
source: params.source,
|
|
1256
|
+
message: `${params.kind} already registered by core: ${name}`
|
|
1257
|
+
});
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const owner = params.owners.get(name);
|
|
1261
|
+
if (owner && owner !== params.pluginId) {
|
|
1262
|
+
params.diagnostics.push({
|
|
1263
|
+
level: "error",
|
|
1264
|
+
pluginId: params.pluginId,
|
|
1265
|
+
source: params.source,
|
|
1266
|
+
message: `${params.kind} already registered: ${name} (${owner})`
|
|
1267
|
+
});
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
params.owners.set(name, params.pluginId);
|
|
1271
|
+
accepted.push(name);
|
|
1272
|
+
}
|
|
1273
|
+
return accepted;
|
|
1274
|
+
}
|
|
1275
|
+
function validatePluginConfig(params) {
|
|
1276
|
+
if (!params.schema) {
|
|
1277
|
+
return { ok: true, value: params.value };
|
|
1278
|
+
}
|
|
1279
|
+
const cacheKey = params.cacheKey ?? JSON.stringify(params.schema);
|
|
1280
|
+
const result = validateJsonSchemaValue({
|
|
1281
|
+
schema: params.schema,
|
|
1282
|
+
cacheKey,
|
|
1283
|
+
value: params.value ?? {}
|
|
1284
|
+
});
|
|
1285
|
+
if (result.ok) {
|
|
1286
|
+
return { ok: true, value: params.value };
|
|
1287
|
+
}
|
|
1288
|
+
return { ok: false, errors: result.errors };
|
|
1289
|
+
}
|
|
1290
|
+
function loadOpenClawPlugins(options) {
|
|
1291
|
+
const logger = options.logger ?? defaultLogger2;
|
|
1292
|
+
const workspaceDir = options.workspaceDir?.trim() || getWorkspacePathFromConfig(options.config);
|
|
1293
|
+
const normalized = normalizePluginsConfig(options.config.plugins);
|
|
1294
|
+
const mode = options.mode ?? "full";
|
|
1295
|
+
const registry = {
|
|
1296
|
+
plugins: [],
|
|
1297
|
+
tools: [],
|
|
1298
|
+
channels: [],
|
|
1299
|
+
providers: [],
|
|
1300
|
+
diagnostics: [],
|
|
1301
|
+
resolvedTools: []
|
|
1302
|
+
};
|
|
1303
|
+
const toolNameOwners = /* @__PURE__ */ new Map();
|
|
1304
|
+
const channelIdOwners = /* @__PURE__ */ new Map();
|
|
1305
|
+
const providerIdOwners = /* @__PURE__ */ new Map();
|
|
1306
|
+
const resolvedToolNames = /* @__PURE__ */ new Set();
|
|
1307
|
+
const reservedToolNames = new Set(options.reservedToolNames ?? []);
|
|
1308
|
+
const reservedChannelIds = new Set(options.reservedChannelIds ?? []);
|
|
1309
|
+
const reservedProviderIds = new Set(options.reservedProviderIds ?? []);
|
|
1310
|
+
const discovery = discoverOpenClawPlugins({
|
|
1311
|
+
config: options.config,
|
|
1312
|
+
workspaceDir,
|
|
1313
|
+
extraPaths: normalized.loadPaths
|
|
1314
|
+
});
|
|
1315
|
+
const manifestRegistry = loadPluginManifestRegistry({
|
|
1316
|
+
config: options.config,
|
|
1317
|
+
workspaceDir,
|
|
1318
|
+
candidates: discovery.candidates,
|
|
1319
|
+
diagnostics: discovery.diagnostics
|
|
1320
|
+
});
|
|
1321
|
+
registry.diagnostics.push(...manifestRegistry.diagnostics);
|
|
1322
|
+
const manifestByRoot = new Map(manifestRegistry.plugins.map((entry) => [entry.rootDir, entry]));
|
|
1323
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
1324
|
+
const pluginSdkAlias = resolvePluginSdkAlias();
|
|
1325
|
+
const jiti = createJiti(import.meta.url, {
|
|
1326
|
+
interopDefault: true,
|
|
1327
|
+
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
|
|
1328
|
+
...pluginSdkAlias ? {
|
|
1329
|
+
alias: {
|
|
1330
|
+
"openclaw/plugin-sdk": pluginSdkAlias
|
|
1331
|
+
}
|
|
1332
|
+
} : {}
|
|
1333
|
+
});
|
|
1334
|
+
for (const candidate of discovery.candidates) {
|
|
1335
|
+
const manifest = manifestByRoot.get(candidate.rootDir);
|
|
1336
|
+
if (!manifest) {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const pluginId = manifest.id;
|
|
1340
|
+
const existingOrigin = seenIds.get(pluginId);
|
|
1341
|
+
if (existingOrigin) {
|
|
1342
|
+
const record2 = createPluginRecord({
|
|
1343
|
+
id: pluginId,
|
|
1344
|
+
name: manifest.name ?? pluginId,
|
|
1345
|
+
description: manifest.description,
|
|
1346
|
+
version: manifest.version,
|
|
1347
|
+
kind: manifest.kind,
|
|
1348
|
+
source: candidate.source,
|
|
1349
|
+
origin: candidate.origin,
|
|
1350
|
+
workspaceDir: candidate.workspaceDir,
|
|
1351
|
+
enabled: false,
|
|
1352
|
+
configSchema: Boolean(manifest.configSchema),
|
|
1353
|
+
configUiHints: manifest.configUiHints,
|
|
1354
|
+
configJsonSchema: manifest.configSchema
|
|
1355
|
+
});
|
|
1356
|
+
record2.status = "disabled";
|
|
1357
|
+
record2.error = `overridden by ${existingOrigin} plugin`;
|
|
1358
|
+
registry.plugins.push(record2);
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const enableState = resolveEnableState(pluginId, normalized);
|
|
1362
|
+
const entry = normalized.entries[pluginId];
|
|
1363
|
+
const record = createPluginRecord({
|
|
1364
|
+
id: pluginId,
|
|
1365
|
+
name: manifest.name ?? pluginId,
|
|
1366
|
+
description: manifest.description,
|
|
1367
|
+
version: manifest.version,
|
|
1368
|
+
kind: manifest.kind,
|
|
1369
|
+
source: candidate.source,
|
|
1370
|
+
origin: candidate.origin,
|
|
1371
|
+
workspaceDir: candidate.workspaceDir,
|
|
1372
|
+
enabled: enableState.enabled,
|
|
1373
|
+
configSchema: Boolean(manifest.configSchema),
|
|
1374
|
+
configUiHints: manifest.configUiHints,
|
|
1375
|
+
configJsonSchema: manifest.configSchema
|
|
1376
|
+
});
|
|
1377
|
+
if (!enableState.enabled) {
|
|
1378
|
+
record.status = "disabled";
|
|
1379
|
+
record.error = enableState.reason;
|
|
1380
|
+
registry.plugins.push(record);
|
|
1381
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (!manifest.configSchema) {
|
|
1385
|
+
record.status = "error";
|
|
1386
|
+
record.error = "missing config schema";
|
|
1387
|
+
registry.plugins.push(record);
|
|
1388
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1389
|
+
registry.diagnostics.push({
|
|
1390
|
+
level: "error",
|
|
1391
|
+
pluginId,
|
|
1392
|
+
source: candidate.source,
|
|
1393
|
+
message: record.error
|
|
1394
|
+
});
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
const validatedConfig = validatePluginConfig({
|
|
1398
|
+
schema: manifest.configSchema,
|
|
1399
|
+
cacheKey: manifest.schemaCacheKey,
|
|
1400
|
+
value: entry?.config
|
|
1401
|
+
});
|
|
1402
|
+
if (!validatedConfig.ok) {
|
|
1403
|
+
record.status = "error";
|
|
1404
|
+
record.error = `invalid config: ${validatedConfig.errors.join(", ")}`;
|
|
1405
|
+
registry.plugins.push(record);
|
|
1406
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1407
|
+
registry.diagnostics.push({
|
|
1408
|
+
level: "error",
|
|
1409
|
+
pluginId,
|
|
1410
|
+
source: candidate.source,
|
|
1411
|
+
message: record.error
|
|
1412
|
+
});
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (mode === "validate") {
|
|
1416
|
+
registry.plugins.push(record);
|
|
1417
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
let moduleExport = null;
|
|
1421
|
+
try {
|
|
1422
|
+
moduleExport = jiti(candidate.source);
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
record.status = "error";
|
|
1425
|
+
record.error = `failed to load plugin: ${String(err)}`;
|
|
1426
|
+
registry.plugins.push(record);
|
|
1427
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1428
|
+
registry.diagnostics.push({
|
|
1429
|
+
level: "error",
|
|
1430
|
+
pluginId,
|
|
1431
|
+
source: candidate.source,
|
|
1432
|
+
message: record.error
|
|
1433
|
+
});
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const resolved = resolvePluginModuleExport(moduleExport);
|
|
1437
|
+
const definition = resolved.definition;
|
|
1438
|
+
const register = resolved.register;
|
|
1439
|
+
if (definition?.id && definition.id !== pluginId) {
|
|
1440
|
+
registry.diagnostics.push({
|
|
1441
|
+
level: "warn",
|
|
1442
|
+
pluginId,
|
|
1443
|
+
source: candidate.source,
|
|
1444
|
+
message: `plugin id mismatch (manifest uses "${pluginId}", export uses "${definition.id}")`
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
record.name = definition?.name ?? record.name;
|
|
1448
|
+
record.description = definition?.description ?? record.description;
|
|
1449
|
+
record.version = definition?.version ?? record.version;
|
|
1450
|
+
record.kind = definition?.kind ?? record.kind;
|
|
1451
|
+
if (typeof register !== "function") {
|
|
1452
|
+
record.status = "error";
|
|
1453
|
+
record.error = "plugin export missing register/activate";
|
|
1454
|
+
registry.plugins.push(record);
|
|
1455
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1456
|
+
registry.diagnostics.push({
|
|
1457
|
+
level: "error",
|
|
1458
|
+
pluginId,
|
|
1459
|
+
source: candidate.source,
|
|
1460
|
+
message: record.error
|
|
1461
|
+
});
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const pluginRuntime = createPluginRuntime({ workspace: workspaceDir, config: options.config });
|
|
1465
|
+
const pluginLogger = buildPluginLogger(logger, pluginId);
|
|
1466
|
+
const pushUnsupported = (capability) => {
|
|
1467
|
+
registry.diagnostics.push({
|
|
1468
|
+
level: "warn",
|
|
1469
|
+
pluginId,
|
|
1470
|
+
source: candidate.source,
|
|
1471
|
+
message: `${capability} is not supported by nextclaw compat layer yet`
|
|
1472
|
+
});
|
|
1473
|
+
pluginLogger.warn(`${capability} is ignored (not supported yet)`);
|
|
1474
|
+
};
|
|
1475
|
+
const api = {
|
|
1476
|
+
id: pluginId,
|
|
1477
|
+
name: record.name,
|
|
1478
|
+
version: record.version,
|
|
1479
|
+
description: record.description,
|
|
1480
|
+
source: candidate.source,
|
|
1481
|
+
config: options.config,
|
|
1482
|
+
pluginConfig: validatedConfig.value,
|
|
1483
|
+
runtime: pluginRuntime,
|
|
1484
|
+
logger: pluginLogger,
|
|
1485
|
+
registerTool: (tool, opts) => {
|
|
1486
|
+
const declaredNames = opts && Array.isArray(opts.names) ? opts.names : [];
|
|
1487
|
+
const names = [...declaredNames, ...opts && opts.name ? [opts.name] : []];
|
|
1488
|
+
if (typeof tool !== "function" && typeof tool.name === "string") {
|
|
1489
|
+
names.push(tool.name);
|
|
1490
|
+
}
|
|
1491
|
+
const uniqueNames = Array.from(new Set(names.map((name) => name.trim()).filter(Boolean)));
|
|
1492
|
+
const acceptedNames = ensureUniqueNames({
|
|
1493
|
+
names: uniqueNames,
|
|
1494
|
+
pluginId,
|
|
1495
|
+
diagnostics: registry.diagnostics,
|
|
1496
|
+
source: candidate.source,
|
|
1497
|
+
owners: toolNameOwners,
|
|
1498
|
+
reserved: reservedToolNames,
|
|
1499
|
+
kind: "tool"
|
|
1500
|
+
});
|
|
1501
|
+
if (acceptedNames.length === 0) {
|
|
1502
|
+
registry.diagnostics.push({
|
|
1503
|
+
level: "warn",
|
|
1504
|
+
pluginId,
|
|
1505
|
+
source: candidate.source,
|
|
1506
|
+
message: "tool registration skipped: no available tool names"
|
|
1507
|
+
});
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const factory = typeof tool === "function" ? tool : (_ctx) => tool;
|
|
1511
|
+
registry.tools.push({
|
|
1512
|
+
pluginId,
|
|
1513
|
+
factory,
|
|
1514
|
+
names: acceptedNames,
|
|
1515
|
+
optional: opts?.optional === true,
|
|
1516
|
+
source: candidate.source
|
|
1517
|
+
});
|
|
1518
|
+
record.toolNames.push(...acceptedNames);
|
|
1519
|
+
try {
|
|
1520
|
+
const previewTools = normalizeToolList(
|
|
1521
|
+
factory({
|
|
1522
|
+
config: options.config,
|
|
1523
|
+
workspaceDir,
|
|
1524
|
+
sandboxed: false
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
const byName = new Map(previewTools.map((entry2) => [entry2.name, entry2]));
|
|
1528
|
+
for (const name of acceptedNames) {
|
|
1529
|
+
const resolvedTool = byName.get(name);
|
|
1530
|
+
if (!resolvedTool || resolvedToolNames.has(resolvedTool.name)) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
resolvedToolNames.add(resolvedTool.name);
|
|
1534
|
+
registry.resolvedTools.push(resolvedTool);
|
|
1535
|
+
}
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
registry.diagnostics.push({
|
|
1538
|
+
level: "warn",
|
|
1539
|
+
pluginId,
|
|
1540
|
+
source: candidate.source,
|
|
1541
|
+
message: `tool preview failed: ${String(err)}`
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
registerChannel: (registration) => {
|
|
1546
|
+
const normalizedChannel = registration && typeof registration === "object" && "plugin" in registration ? registration.plugin : registration;
|
|
1547
|
+
if (!normalizedChannel || typeof normalizedChannel !== "object") {
|
|
1548
|
+
registry.diagnostics.push({
|
|
1549
|
+
level: "error",
|
|
1550
|
+
pluginId,
|
|
1551
|
+
source: candidate.source,
|
|
1552
|
+
message: "channel registration missing plugin object"
|
|
1553
|
+
});
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const channelObj = normalizedChannel;
|
|
1557
|
+
const rawId = typeof channelObj.id === "string" ? channelObj.id : String(channelObj.id ?? "");
|
|
1558
|
+
const accepted = ensureUniqueNames({
|
|
1559
|
+
names: [rawId],
|
|
1560
|
+
pluginId,
|
|
1561
|
+
diagnostics: registry.diagnostics,
|
|
1562
|
+
source: candidate.source,
|
|
1563
|
+
owners: channelIdOwners,
|
|
1564
|
+
reserved: reservedChannelIds,
|
|
1565
|
+
kind: "channel"
|
|
1566
|
+
});
|
|
1567
|
+
if (accepted.length === 0) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
registry.channels.push({
|
|
1571
|
+
pluginId,
|
|
1572
|
+
channel: normalizedChannel,
|
|
1573
|
+
source: candidate.source
|
|
1574
|
+
});
|
|
1575
|
+
record.channelIds.push(accepted[0]);
|
|
1576
|
+
},
|
|
1577
|
+
registerProvider: (provider) => {
|
|
1578
|
+
const accepted = ensureUniqueNames({
|
|
1579
|
+
names: [provider.id],
|
|
1580
|
+
pluginId,
|
|
1581
|
+
diagnostics: registry.diagnostics,
|
|
1582
|
+
source: candidate.source,
|
|
1583
|
+
owners: providerIdOwners,
|
|
1584
|
+
reserved: reservedProviderIds,
|
|
1585
|
+
kind: "provider"
|
|
1586
|
+
});
|
|
1587
|
+
if (accepted.length === 0) {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
registry.providers.push({
|
|
1591
|
+
pluginId,
|
|
1592
|
+
provider,
|
|
1593
|
+
source: candidate.source
|
|
1594
|
+
});
|
|
1595
|
+
record.providerIds.push(accepted[0]);
|
|
1596
|
+
},
|
|
1597
|
+
registerHook: () => pushUnsupported("registerHook"),
|
|
1598
|
+
registerGatewayMethod: () => pushUnsupported("registerGatewayMethod"),
|
|
1599
|
+
registerCli: () => pushUnsupported("registerCli"),
|
|
1600
|
+
registerService: () => pushUnsupported("registerService"),
|
|
1601
|
+
registerCommand: () => pushUnsupported("registerCommand"),
|
|
1602
|
+
registerHttpHandler: () => pushUnsupported("registerHttpHandler"),
|
|
1603
|
+
registerHttpRoute: () => pushUnsupported("registerHttpRoute"),
|
|
1604
|
+
resolvePath: (input) => {
|
|
1605
|
+
const trimmed = input.trim();
|
|
1606
|
+
if (!trimmed) {
|
|
1607
|
+
return candidate.rootDir;
|
|
1608
|
+
}
|
|
1609
|
+
if (path4.isAbsolute(trimmed)) {
|
|
1610
|
+
return path4.resolve(expandHome2(trimmed));
|
|
1611
|
+
}
|
|
1612
|
+
return path4.resolve(candidate.rootDir, trimmed);
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
try {
|
|
1616
|
+
const result = register(api);
|
|
1617
|
+
if (result && typeof result === "object" && "then" in result) {
|
|
1618
|
+
registry.diagnostics.push({
|
|
1619
|
+
level: "warn",
|
|
1620
|
+
pluginId,
|
|
1621
|
+
source: candidate.source,
|
|
1622
|
+
message: "plugin register returned a promise; async registration is ignored"
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
registry.plugins.push(record);
|
|
1626
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
record.status = "error";
|
|
1629
|
+
record.error = `plugin failed during register: ${String(err)}`;
|
|
1630
|
+
registry.plugins.push(record);
|
|
1631
|
+
seenIds.set(pluginId, candidate.origin);
|
|
1632
|
+
registry.diagnostics.push({
|
|
1633
|
+
level: "error",
|
|
1634
|
+
pluginId,
|
|
1635
|
+
source: candidate.source,
|
|
1636
|
+
message: record.error
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return registry;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/plugins/status.ts
|
|
1644
|
+
import { getWorkspacePathFromConfig as getWorkspacePathFromConfig2 } from "@nextclaw/core";
|
|
1645
|
+
function buildPluginStatusReport(params) {
|
|
1646
|
+
const workspaceDir = params.workspaceDir?.trim() || getWorkspacePathFromConfig2(params.config);
|
|
1647
|
+
const registry = loadOpenClawPlugins({
|
|
1648
|
+
config: params.config,
|
|
1649
|
+
workspaceDir,
|
|
1650
|
+
logger: params.logger,
|
|
1651
|
+
reservedToolNames: params.reservedToolNames,
|
|
1652
|
+
reservedChannelIds: params.reservedChannelIds,
|
|
1653
|
+
reservedProviderIds: params.reservedProviderIds
|
|
1654
|
+
});
|
|
1655
|
+
return {
|
|
1656
|
+
workspaceDir,
|
|
1657
|
+
...registry
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/plugins/uninstall.ts
|
|
1662
|
+
import fs6 from "fs/promises";
|
|
1663
|
+
import path5 from "path";
|
|
1664
|
+
function resolveUninstallDirectoryTarget(params) {
|
|
1665
|
+
if (!params.hasInstall) {
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
if (params.installRecord?.source === "path") {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
let defaultPath;
|
|
1672
|
+
try {
|
|
1673
|
+
defaultPath = resolvePluginInstallDir(params.pluginId, params.extensionsDir);
|
|
1674
|
+
} catch {
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
const configuredPath = params.installRecord?.installPath;
|
|
1678
|
+
if (!configuredPath) {
|
|
1679
|
+
return defaultPath;
|
|
1680
|
+
}
|
|
1681
|
+
if (path5.resolve(configuredPath) === path5.resolve(defaultPath)) {
|
|
1682
|
+
return configuredPath;
|
|
1683
|
+
}
|
|
1684
|
+
return defaultPath;
|
|
1685
|
+
}
|
|
1686
|
+
function removePluginFromConfig(config, pluginId) {
|
|
1687
|
+
const actions = {
|
|
1688
|
+
entry: false,
|
|
1689
|
+
install: false,
|
|
1690
|
+
allowlist: false,
|
|
1691
|
+
loadPath: false
|
|
1692
|
+
};
|
|
1693
|
+
const pluginsConfig = config.plugins ?? {};
|
|
1694
|
+
let entries = pluginsConfig.entries;
|
|
1695
|
+
if (entries && pluginId in entries) {
|
|
1696
|
+
const rest = { ...entries };
|
|
1697
|
+
delete rest[pluginId];
|
|
1698
|
+
entries = Object.keys(rest).length > 0 ? rest : void 0;
|
|
1699
|
+
actions.entry = true;
|
|
1700
|
+
}
|
|
1701
|
+
let installs = pluginsConfig.installs;
|
|
1702
|
+
const installRecord = installs?.[pluginId];
|
|
1703
|
+
if (installs && pluginId in installs) {
|
|
1704
|
+
const rest = { ...installs };
|
|
1705
|
+
delete rest[pluginId];
|
|
1706
|
+
installs = Object.keys(rest).length > 0 ? rest : void 0;
|
|
1707
|
+
actions.install = true;
|
|
1708
|
+
}
|
|
1709
|
+
let allow = pluginsConfig.allow;
|
|
1710
|
+
if (Array.isArray(allow) && allow.includes(pluginId)) {
|
|
1711
|
+
allow = allow.filter((id) => id !== pluginId);
|
|
1712
|
+
if (allow.length === 0) {
|
|
1713
|
+
allow = void 0;
|
|
1714
|
+
}
|
|
1715
|
+
actions.allowlist = true;
|
|
1716
|
+
}
|
|
1717
|
+
let load = pluginsConfig.load;
|
|
1718
|
+
if (installRecord?.source === "path" && installRecord.sourcePath) {
|
|
1719
|
+
const sourcePath = installRecord.sourcePath;
|
|
1720
|
+
const loadPaths = load?.paths;
|
|
1721
|
+
if (Array.isArray(loadPaths) && loadPaths.includes(sourcePath)) {
|
|
1722
|
+
const nextLoadPaths = loadPaths.filter((entry) => entry !== sourcePath);
|
|
1723
|
+
load = nextLoadPaths.length > 0 ? { ...load, paths: nextLoadPaths } : void 0;
|
|
1724
|
+
actions.loadPath = true;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const nextPlugins = {
|
|
1728
|
+
...pluginsConfig
|
|
1729
|
+
};
|
|
1730
|
+
if (entries === void 0) {
|
|
1731
|
+
delete nextPlugins.entries;
|
|
1732
|
+
} else {
|
|
1733
|
+
nextPlugins.entries = entries;
|
|
1734
|
+
}
|
|
1735
|
+
if (installs === void 0) {
|
|
1736
|
+
delete nextPlugins.installs;
|
|
1737
|
+
} else {
|
|
1738
|
+
nextPlugins.installs = installs;
|
|
1739
|
+
}
|
|
1740
|
+
if (allow === void 0) {
|
|
1741
|
+
delete nextPlugins.allow;
|
|
1742
|
+
} else {
|
|
1743
|
+
nextPlugins.allow = allow;
|
|
1744
|
+
}
|
|
1745
|
+
if (load === void 0) {
|
|
1746
|
+
delete nextPlugins.load;
|
|
1747
|
+
} else {
|
|
1748
|
+
nextPlugins.load = load;
|
|
1749
|
+
}
|
|
1750
|
+
return {
|
|
1751
|
+
config: {
|
|
1752
|
+
...config,
|
|
1753
|
+
plugins: nextPlugins
|
|
1754
|
+
},
|
|
1755
|
+
actions
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
async function uninstallPlugin(params) {
|
|
1759
|
+
const { config, pluginId, deleteFiles = true, extensionsDir } = params;
|
|
1760
|
+
const hasEntry = pluginId in (config.plugins.entries ?? {});
|
|
1761
|
+
const hasInstall = pluginId in (config.plugins.installs ?? {});
|
|
1762
|
+
if (!hasEntry && !hasInstall) {
|
|
1763
|
+
return { ok: false, error: `Plugin not found: ${pluginId}` };
|
|
1764
|
+
}
|
|
1765
|
+
const installRecord = config.plugins.installs?.[pluginId];
|
|
1766
|
+
const isLinked = installRecord?.source === "path";
|
|
1767
|
+
const { config: nextConfig, actions: configActions } = removePluginFromConfig(config, pluginId);
|
|
1768
|
+
const actions = {
|
|
1769
|
+
...configActions,
|
|
1770
|
+
directory: false
|
|
1771
|
+
};
|
|
1772
|
+
const warnings = [];
|
|
1773
|
+
const deleteTarget = deleteFiles && !isLinked ? resolveUninstallDirectoryTarget({
|
|
1774
|
+
pluginId,
|
|
1775
|
+
hasInstall,
|
|
1776
|
+
installRecord,
|
|
1777
|
+
extensionsDir
|
|
1778
|
+
}) : null;
|
|
1779
|
+
if (deleteTarget) {
|
|
1780
|
+
const existed = await fs6.access(deleteTarget).then(() => true).catch(() => false) ?? false;
|
|
1781
|
+
try {
|
|
1782
|
+
await fs6.rm(deleteTarget, { recursive: true, force: true });
|
|
1783
|
+
actions.directory = existed;
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
warnings.push(
|
|
1786
|
+
`Failed to remove plugin directory ${deleteTarget}: ${error instanceof Error ? error.message : String(error)}`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
ok: true,
|
|
1792
|
+
config: nextConfig,
|
|
1793
|
+
pluginId,
|
|
1794
|
+
actions,
|
|
1795
|
+
warnings
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
export {
|
|
1799
|
+
DEFAULT_ACCOUNT_ID,
|
|
1800
|
+
PLUGIN_MANIFEST_FILENAME,
|
|
1801
|
+
PLUGIN_MANIFEST_FILENAMES,
|
|
1802
|
+
__nextclawPluginSdkCompat,
|
|
1803
|
+
addPluginLoadPath,
|
|
1804
|
+
buildChannelConfigSchema,
|
|
1805
|
+
buildOauthProviderAuthResult,
|
|
1806
|
+
buildPluginStatusReport,
|
|
1807
|
+
createPluginRuntime,
|
|
1808
|
+
disablePluginInConfig,
|
|
1809
|
+
discoverOpenClawPlugins,
|
|
1810
|
+
emptyPluginConfigSchema,
|
|
1811
|
+
enablePluginInConfig,
|
|
1812
|
+
getPackageManifestMetadata,
|
|
1813
|
+
installPluginFromArchive,
|
|
1814
|
+
installPluginFromDir,
|
|
1815
|
+
installPluginFromFile,
|
|
1816
|
+
installPluginFromNpmSpec,
|
|
1817
|
+
installPluginFromPath,
|
|
1818
|
+
loadOpenClawPlugins,
|
|
1819
|
+
loadPluginManifest,
|
|
1820
|
+
loadPluginManifestRegistry,
|
|
1821
|
+
loadPluginUiMetadata,
|
|
1822
|
+
normalizeAccountId,
|
|
1823
|
+
normalizePluginHttpPath,
|
|
1824
|
+
normalizePluginsConfig,
|
|
1825
|
+
recordPluginInstall,
|
|
1826
|
+
removePluginFromConfig,
|
|
1827
|
+
resolveEnableState,
|
|
1828
|
+
resolvePluginInstallDir,
|
|
1829
|
+
resolvePluginManifestPath,
|
|
1830
|
+
resolveUninstallDirectoryTarget,
|
|
1831
|
+
sleep,
|
|
1832
|
+
toPluginUiMetadata,
|
|
1833
|
+
uninstallPlugin,
|
|
1834
|
+
validateJsonSchemaValue
|
|
1835
|
+
};
|