@nextclaw/openclaw-compat 0.3.4 → 0.3.6
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/package.json +13 -13
- package/dist/index.d.ts +0 -678
- package/dist/index.js +0 -2510
package/dist/index.js
DELETED
|
@@ -1,2510 +0,0 @@
|
|
|
1
|
-
// src/plugin-sdk/index.ts
|
|
2
|
-
import { resolveBuiltinChannelRuntime } from "@nextclaw/channel-runtime";
|
|
3
|
-
function emptyPluginConfigSchema() {
|
|
4
|
-
return {
|
|
5
|
-
type: "object",
|
|
6
|
-
additionalProperties: false,
|
|
7
|
-
properties: {}
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
function buildChannelConfigSchema(schema) {
|
|
11
|
-
return schema;
|
|
12
|
-
}
|
|
13
|
-
function buildOauthProviderAuthResult(params) {
|
|
14
|
-
const profileId = params.email ? `${params.providerId}:${params.email}` : params.providerId;
|
|
15
|
-
return {
|
|
16
|
-
profiles: [
|
|
17
|
-
{
|
|
18
|
-
profileId,
|
|
19
|
-
credential: {
|
|
20
|
-
providerId: params.providerId,
|
|
21
|
-
accessToken: params.access,
|
|
22
|
-
refreshToken: params.refresh,
|
|
23
|
-
expiresAt: params.expires,
|
|
24
|
-
email: params.email,
|
|
25
|
-
extra: params.credentialExtra ?? {}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
defaultModel: params.defaultModel,
|
|
30
|
-
notes: params.notes
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
async function sleep(ms) {
|
|
34
|
-
await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
35
|
-
}
|
|
36
|
-
function normalizePluginHttpPath(rawPath) {
|
|
37
|
-
const trimmed = rawPath.trim();
|
|
38
|
-
if (!trimmed) {
|
|
39
|
-
return "";
|
|
40
|
-
}
|
|
41
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
42
|
-
}
|
|
43
|
-
var DEFAULT_ACCOUNT_ID = "default";
|
|
44
|
-
function normalizeAccountId(accountId) {
|
|
45
|
-
const trimmed = accountId?.trim();
|
|
46
|
-
return trimmed || DEFAULT_ACCOUNT_ID;
|
|
47
|
-
}
|
|
48
|
-
function createNextclawBuiltinChannelPlugin(channelId) {
|
|
49
|
-
const runtime = resolveBuiltinChannelRuntime(channelId);
|
|
50
|
-
return {
|
|
51
|
-
id: channelId,
|
|
52
|
-
nextclaw: {
|
|
53
|
-
isEnabled: runtime.isEnabled,
|
|
54
|
-
createChannel: runtime.createChannel
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
var __nextclawPluginSdkCompat = true;
|
|
59
|
-
|
|
60
|
-
// src/plugins/config-state.ts
|
|
61
|
-
function normalizeList(value) {
|
|
62
|
-
if (!Array.isArray(value)) {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
66
|
-
}
|
|
67
|
-
function normalizeEntries(entries) {
|
|
68
|
-
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
const normalized = {};
|
|
72
|
-
for (const [idRaw, value] of Object.entries(entries)) {
|
|
73
|
-
const id = idRaw.trim();
|
|
74
|
-
if (!id) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
78
|
-
normalized[id] = {};
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const entry = value;
|
|
82
|
-
normalized[id] = {
|
|
83
|
-
enabled: typeof entry.enabled === "boolean" ? entry.enabled : void 0,
|
|
84
|
-
config: Object.prototype.hasOwnProperty.call(entry, "config") ? entry.config : void 0
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
return normalized;
|
|
88
|
-
}
|
|
89
|
-
function normalizePluginsConfig(plugins) {
|
|
90
|
-
return {
|
|
91
|
-
enabled: plugins?.enabled !== false,
|
|
92
|
-
allow: normalizeList(plugins?.allow),
|
|
93
|
-
deny: normalizeList(plugins?.deny),
|
|
94
|
-
loadPaths: normalizeList(plugins?.load?.paths),
|
|
95
|
-
entries: normalizeEntries(plugins?.entries)
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
function resolveEnableState(id, config) {
|
|
99
|
-
if (!config.enabled) {
|
|
100
|
-
return { enabled: false, reason: "plugins disabled" };
|
|
101
|
-
}
|
|
102
|
-
if (config.deny.includes(id)) {
|
|
103
|
-
return { enabled: false, reason: "blocked by denylist" };
|
|
104
|
-
}
|
|
105
|
-
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
|
106
|
-
return { enabled: false, reason: "not in allowlist" };
|
|
107
|
-
}
|
|
108
|
-
const entry = config.entries[id];
|
|
109
|
-
if (entry?.enabled === true) {
|
|
110
|
-
return { enabled: true };
|
|
111
|
-
}
|
|
112
|
-
if (entry?.enabled === false) {
|
|
113
|
-
return { enabled: false, reason: "disabled in config" };
|
|
114
|
-
}
|
|
115
|
-
return { enabled: true };
|
|
116
|
-
}
|
|
117
|
-
function recordPluginInstall(config, update) {
|
|
118
|
-
const { pluginId, ...record } = update;
|
|
119
|
-
const installs = {
|
|
120
|
-
...config.plugins.installs ?? {},
|
|
121
|
-
[pluginId]: {
|
|
122
|
-
...config.plugins.installs?.[pluginId] ?? {},
|
|
123
|
-
...record,
|
|
124
|
-
installedAt: record.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
return {
|
|
128
|
-
...config,
|
|
129
|
-
plugins: {
|
|
130
|
-
...config.plugins,
|
|
131
|
-
installs
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function enablePluginInConfig(config, pluginId) {
|
|
136
|
-
const nextEntries = {
|
|
137
|
-
...config.plugins.entries ?? {},
|
|
138
|
-
[pluginId]: {
|
|
139
|
-
...config.plugins.entries?.[pluginId] ?? {},
|
|
140
|
-
enabled: true
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
const allow = config.plugins.allow;
|
|
144
|
-
const nextAllow = Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId) ? [...allow, pluginId] : allow;
|
|
145
|
-
return {
|
|
146
|
-
...config,
|
|
147
|
-
plugins: {
|
|
148
|
-
...config.plugins,
|
|
149
|
-
entries: nextEntries,
|
|
150
|
-
...nextAllow ? { allow: nextAllow } : {}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
function disablePluginInConfig(config, pluginId) {
|
|
155
|
-
return {
|
|
156
|
-
...config,
|
|
157
|
-
plugins: {
|
|
158
|
-
...config.plugins,
|
|
159
|
-
entries: {
|
|
160
|
-
...config.plugins.entries ?? {},
|
|
161
|
-
[pluginId]: {
|
|
162
|
-
...config.plugins.entries?.[pluginId] ?? {},
|
|
163
|
-
enabled: false
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
function addPluginLoadPath(config, loadPath) {
|
|
170
|
-
const paths = Array.from(/* @__PURE__ */ new Set([...config.plugins.load?.paths ?? [], loadPath]));
|
|
171
|
-
return {
|
|
172
|
-
...config,
|
|
173
|
-
plugins: {
|
|
174
|
-
...config.plugins,
|
|
175
|
-
load: {
|
|
176
|
-
...config.plugins.load ?? {},
|
|
177
|
-
paths
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// src/plugins/channel-runtime.ts
|
|
184
|
-
function normalizeChannelId(channel) {
|
|
185
|
-
return (channel ?? "").trim().toLowerCase();
|
|
186
|
-
}
|
|
187
|
-
function toBinding(registration) {
|
|
188
|
-
const channelId = registration.channel.id?.trim();
|
|
189
|
-
if (!channelId) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
pluginId: registration.pluginId,
|
|
194
|
-
channelId,
|
|
195
|
-
channel: registration.channel
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function getPluginChannelBindings(registry) {
|
|
199
|
-
const bindings = [];
|
|
200
|
-
for (const entry of registry.channels) {
|
|
201
|
-
const binding = toBinding(entry);
|
|
202
|
-
if (!binding) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
bindings.push(binding);
|
|
206
|
-
}
|
|
207
|
-
return bindings;
|
|
208
|
-
}
|
|
209
|
-
function resolvePluginChannelMessageToolHints(params) {
|
|
210
|
-
const channelId = normalizeChannelId(params.channel);
|
|
211
|
-
if (!channelId) {
|
|
212
|
-
return [];
|
|
213
|
-
}
|
|
214
|
-
const binding = getPluginChannelBindings(params.registry).find(
|
|
215
|
-
(entry) => normalizeChannelId(entry.channelId) === channelId
|
|
216
|
-
);
|
|
217
|
-
if (!binding) {
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
const resolveHints = binding.channel.agentPrompt?.messageToolHints;
|
|
221
|
-
if (typeof resolveHints !== "function") {
|
|
222
|
-
return [];
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
const hinted = resolveHints({
|
|
226
|
-
cfg: params.cfg ?? {},
|
|
227
|
-
accountId: params.accountId
|
|
228
|
-
});
|
|
229
|
-
if (!Array.isArray(hinted)) {
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
|
-
return hinted.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
233
|
-
} catch {
|
|
234
|
-
return [];
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function getPluginUiMetadataFromRegistry(registry) {
|
|
238
|
-
return registry.plugins.map((plugin) => ({
|
|
239
|
-
id: plugin.id,
|
|
240
|
-
configSchema: plugin.configJsonSchema,
|
|
241
|
-
configUiHints: plugin.configUiHints
|
|
242
|
-
}));
|
|
243
|
-
}
|
|
244
|
-
async function startPluginChannelGateways(params) {
|
|
245
|
-
const logger = params.logger;
|
|
246
|
-
const diagnostics = [];
|
|
247
|
-
const handles = [];
|
|
248
|
-
for (const binding of getPluginChannelBindings(params.registry)) {
|
|
249
|
-
const gateway = binding.channel.gateway;
|
|
250
|
-
if (!gateway?.startAccount) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const accountIdsRaw = binding.channel.config?.listAccountIds?.() ?? [binding.channel.config?.defaultAccountId?.() ?? "default"];
|
|
254
|
-
const accountIds = Array.from(
|
|
255
|
-
new Set(accountIdsRaw.map((id) => typeof id === "string" ? id.trim() : "").filter(Boolean))
|
|
256
|
-
);
|
|
257
|
-
const finalAccountIds = accountIds.length > 0 ? accountIds : ["default"];
|
|
258
|
-
for (const accountId of finalAccountIds) {
|
|
259
|
-
try {
|
|
260
|
-
const started = await gateway.startAccount({
|
|
261
|
-
accountId,
|
|
262
|
-
log: logger
|
|
263
|
-
});
|
|
264
|
-
handles.push({
|
|
265
|
-
pluginId: binding.pluginId,
|
|
266
|
-
channelId: binding.channelId,
|
|
267
|
-
accountId,
|
|
268
|
-
stop: started && typeof started === "object" && "stop" in started && typeof started.stop === "function" ? started.stop : void 0
|
|
269
|
-
});
|
|
270
|
-
} catch (error) {
|
|
271
|
-
const raw = String(error);
|
|
272
|
-
const lower = raw.toLowerCase();
|
|
273
|
-
const level = lower.includes("required") || lower.includes("not configured") || lower.includes("missing") ? "warn" : "error";
|
|
274
|
-
const message = `failed to start channel gateway for ${binding.channelId}/${accountId}: ${raw}`;
|
|
275
|
-
diagnostics.push({
|
|
276
|
-
level,
|
|
277
|
-
pluginId: binding.pluginId,
|
|
278
|
-
message
|
|
279
|
-
});
|
|
280
|
-
if (level === "error") {
|
|
281
|
-
logger?.error(message);
|
|
282
|
-
} else {
|
|
283
|
-
logger?.warn(message);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return { handles, diagnostics };
|
|
289
|
-
}
|
|
290
|
-
async function stopPluginChannelGateways(handles) {
|
|
291
|
-
for (const handle of handles) {
|
|
292
|
-
if (!handle.stop) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
await handle.stop();
|
|
297
|
-
} catch {
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// src/plugins/discovery.ts
|
|
303
|
-
import fs2 from "fs";
|
|
304
|
-
import path2 from "path";
|
|
305
|
-
import { expandHome, getDataPath } from "@nextclaw/core";
|
|
306
|
-
|
|
307
|
-
// src/plugins/manifest.ts
|
|
308
|
-
import fs from "fs";
|
|
309
|
-
import path from "path";
|
|
310
|
-
var PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
|
311
|
-
var PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME];
|
|
312
|
-
function isRecord(value) {
|
|
313
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
314
|
-
}
|
|
315
|
-
function normalizeStringList(value) {
|
|
316
|
-
if (!Array.isArray(value)) {
|
|
317
|
-
return [];
|
|
318
|
-
}
|
|
319
|
-
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
320
|
-
}
|
|
321
|
-
function resolvePluginManifestPath(rootDir) {
|
|
322
|
-
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
|
323
|
-
const candidate = path.join(rootDir, filename);
|
|
324
|
-
if (fs.existsSync(candidate)) {
|
|
325
|
-
return candidate;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return path.join(rootDir, PLUGIN_MANIFEST_FILENAME);
|
|
329
|
-
}
|
|
330
|
-
function loadPluginManifest(rootDir) {
|
|
331
|
-
const manifestPath = resolvePluginManifestPath(rootDir);
|
|
332
|
-
if (!fs.existsSync(manifestPath)) {
|
|
333
|
-
return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath };
|
|
334
|
-
}
|
|
335
|
-
let raw;
|
|
336
|
-
try {
|
|
337
|
-
raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
338
|
-
} catch (err) {
|
|
339
|
-
return {
|
|
340
|
-
ok: false,
|
|
341
|
-
error: `failed to parse plugin manifest: ${String(err)}`,
|
|
342
|
-
manifestPath
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
if (!isRecord(raw)) {
|
|
346
|
-
return { ok: false, error: "plugin manifest must be an object", manifestPath };
|
|
347
|
-
}
|
|
348
|
-
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
349
|
-
if (!id) {
|
|
350
|
-
return { ok: false, error: "plugin manifest requires id", manifestPath };
|
|
351
|
-
}
|
|
352
|
-
const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null;
|
|
353
|
-
if (!configSchema) {
|
|
354
|
-
return { ok: false, error: "plugin manifest requires configSchema", manifestPath };
|
|
355
|
-
}
|
|
356
|
-
const manifest = {
|
|
357
|
-
id,
|
|
358
|
-
configSchema,
|
|
359
|
-
kind: typeof raw.kind === "string" ? raw.kind : void 0,
|
|
360
|
-
channels: normalizeStringList(raw.channels),
|
|
361
|
-
providers: normalizeStringList(raw.providers),
|
|
362
|
-
skills: normalizeStringList(raw.skills),
|
|
363
|
-
name: typeof raw.name === "string" ? raw.name.trim() : void 0,
|
|
364
|
-
description: typeof raw.description === "string" ? raw.description.trim() : void 0,
|
|
365
|
-
version: typeof raw.version === "string" ? raw.version.trim() : void 0,
|
|
366
|
-
uiHints: isRecord(raw.uiHints) ? raw.uiHints : void 0
|
|
367
|
-
};
|
|
368
|
-
return { ok: true, manifest, manifestPath };
|
|
369
|
-
}
|
|
370
|
-
function getPackageManifestMetadata(manifest) {
|
|
371
|
-
if (!manifest) {
|
|
372
|
-
return void 0;
|
|
373
|
-
}
|
|
374
|
-
return manifest.openclaw;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// src/plugins/discovery.ts
|
|
378
|
-
var EXTENSION_EXTS = /* @__PURE__ */ new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
|
|
379
|
-
function resolveUserPath(input) {
|
|
380
|
-
return path2.resolve(expandHome(input));
|
|
381
|
-
}
|
|
382
|
-
function isExtensionFile(filePath) {
|
|
383
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
384
|
-
if (!EXTENSION_EXTS.has(ext)) {
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
return !filePath.endsWith(".d.ts");
|
|
388
|
-
}
|
|
389
|
-
function readPackageManifest(dir) {
|
|
390
|
-
const manifestPath = path2.join(dir, "package.json");
|
|
391
|
-
if (!fs2.existsSync(manifestPath)) {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
return JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
396
|
-
} catch {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
function resolvePackageExtensions(manifest) {
|
|
401
|
-
const raw = getPackageManifestMetadata(manifest)?.extensions;
|
|
402
|
-
if (!Array.isArray(raw)) {
|
|
403
|
-
return [];
|
|
404
|
-
}
|
|
405
|
-
return raw.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
406
|
-
}
|
|
407
|
-
function deriveIdHint(params) {
|
|
408
|
-
const base = path2.basename(params.filePath, path2.extname(params.filePath));
|
|
409
|
-
const packageName = params.packageName?.trim();
|
|
410
|
-
if (!packageName) {
|
|
411
|
-
return base;
|
|
412
|
-
}
|
|
413
|
-
const unscoped = packageName.includes("/") ? packageName.split("/").pop() ?? packageName : packageName;
|
|
414
|
-
if (!params.hasMultipleExtensions) {
|
|
415
|
-
return unscoped;
|
|
416
|
-
}
|
|
417
|
-
return `${unscoped}/${base}`;
|
|
418
|
-
}
|
|
419
|
-
function addCandidate(params) {
|
|
420
|
-
const resolvedSource = path2.resolve(params.source);
|
|
421
|
-
if (params.seen.has(resolvedSource)) {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
params.seen.add(resolvedSource);
|
|
425
|
-
const manifest = params.manifest ?? null;
|
|
426
|
-
params.candidates.push({
|
|
427
|
-
idHint: params.idHint,
|
|
428
|
-
source: resolvedSource,
|
|
429
|
-
rootDir: path2.resolve(params.rootDir),
|
|
430
|
-
origin: params.origin,
|
|
431
|
-
workspaceDir: params.workspaceDir,
|
|
432
|
-
packageName: manifest?.name?.trim() || void 0,
|
|
433
|
-
packageVersion: manifest?.version?.trim() || void 0,
|
|
434
|
-
packageDescription: manifest?.description?.trim() || void 0,
|
|
435
|
-
packageDir: params.packageDir
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
function discoverInDirectory(params) {
|
|
439
|
-
if (!fs2.existsSync(params.dir)) {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
let entries = [];
|
|
443
|
-
try {
|
|
444
|
-
entries = fs2.readdirSync(params.dir, { withFileTypes: true });
|
|
445
|
-
} catch (err) {
|
|
446
|
-
params.diagnostics.push({
|
|
447
|
-
level: "warn",
|
|
448
|
-
message: `failed to read extensions dir: ${params.dir} (${String(err)})`,
|
|
449
|
-
source: params.dir
|
|
450
|
-
});
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
for (const entry of entries) {
|
|
454
|
-
const fullPath = path2.join(params.dir, entry.name);
|
|
455
|
-
if (entry.isFile()) {
|
|
456
|
-
if (!isExtensionFile(fullPath)) {
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
addCandidate({
|
|
460
|
-
candidates: params.candidates,
|
|
461
|
-
seen: params.seen,
|
|
462
|
-
idHint: path2.basename(entry.name, path2.extname(entry.name)),
|
|
463
|
-
source: fullPath,
|
|
464
|
-
rootDir: path2.dirname(fullPath),
|
|
465
|
-
origin: params.origin,
|
|
466
|
-
workspaceDir: params.workspaceDir
|
|
467
|
-
});
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (!entry.isDirectory()) {
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
const manifest = readPackageManifest(fullPath);
|
|
474
|
-
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
|
|
475
|
-
if (extensions.length > 0) {
|
|
476
|
-
for (const extPath of extensions) {
|
|
477
|
-
const resolved = path2.resolve(fullPath, extPath);
|
|
478
|
-
addCandidate({
|
|
479
|
-
candidates: params.candidates,
|
|
480
|
-
seen: params.seen,
|
|
481
|
-
idHint: deriveIdHint({
|
|
482
|
-
filePath: resolved,
|
|
483
|
-
packageName: manifest?.name,
|
|
484
|
-
hasMultipleExtensions: extensions.length > 1
|
|
485
|
-
}),
|
|
486
|
-
source: resolved,
|
|
487
|
-
rootDir: fullPath,
|
|
488
|
-
origin: params.origin,
|
|
489
|
-
workspaceDir: params.workspaceDir,
|
|
490
|
-
manifest,
|
|
491
|
-
packageDir: fullPath
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
497
|
-
const indexFile = indexCandidates.map((candidate) => path2.join(fullPath, candidate)).find((candidate) => fs2.existsSync(candidate));
|
|
498
|
-
if (indexFile && isExtensionFile(indexFile)) {
|
|
499
|
-
addCandidate({
|
|
500
|
-
candidates: params.candidates,
|
|
501
|
-
seen: params.seen,
|
|
502
|
-
idHint: entry.name,
|
|
503
|
-
source: indexFile,
|
|
504
|
-
rootDir: fullPath,
|
|
505
|
-
origin: params.origin,
|
|
506
|
-
workspaceDir: params.workspaceDir,
|
|
507
|
-
manifest,
|
|
508
|
-
packageDir: fullPath
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
function discoverFromPath(params) {
|
|
514
|
-
const resolved = resolveUserPath(params.rawPath);
|
|
515
|
-
if (!fs2.existsSync(resolved)) {
|
|
516
|
-
params.diagnostics.push({
|
|
517
|
-
level: "error",
|
|
518
|
-
message: `plugin path not found: ${resolved}`,
|
|
519
|
-
source: resolved
|
|
520
|
-
});
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
const stat = fs2.statSync(resolved);
|
|
524
|
-
if (stat.isFile()) {
|
|
525
|
-
if (!isExtensionFile(resolved)) {
|
|
526
|
-
params.diagnostics.push({
|
|
527
|
-
level: "error",
|
|
528
|
-
message: `plugin path is not a supported file: ${resolved}`,
|
|
529
|
-
source: resolved
|
|
530
|
-
});
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
addCandidate({
|
|
534
|
-
candidates: params.candidates,
|
|
535
|
-
seen: params.seen,
|
|
536
|
-
idHint: path2.basename(resolved, path2.extname(resolved)),
|
|
537
|
-
source: resolved,
|
|
538
|
-
rootDir: path2.dirname(resolved),
|
|
539
|
-
origin: params.origin,
|
|
540
|
-
workspaceDir: params.workspaceDir
|
|
541
|
-
});
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
if (stat.isDirectory()) {
|
|
545
|
-
const manifest = readPackageManifest(resolved);
|
|
546
|
-
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
|
|
547
|
-
if (extensions.length > 0) {
|
|
548
|
-
for (const extPath of extensions) {
|
|
549
|
-
const source = path2.resolve(resolved, extPath);
|
|
550
|
-
addCandidate({
|
|
551
|
-
candidates: params.candidates,
|
|
552
|
-
seen: params.seen,
|
|
553
|
-
idHint: deriveIdHint({
|
|
554
|
-
filePath: source,
|
|
555
|
-
packageName: manifest?.name,
|
|
556
|
-
hasMultipleExtensions: extensions.length > 1
|
|
557
|
-
}),
|
|
558
|
-
source,
|
|
559
|
-
rootDir: resolved,
|
|
560
|
-
origin: params.origin,
|
|
561
|
-
workspaceDir: params.workspaceDir,
|
|
562
|
-
manifest,
|
|
563
|
-
packageDir: resolved
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
569
|
-
const indexFile = indexCandidates.map((candidate) => path2.join(resolved, candidate)).find((candidate) => fs2.existsSync(candidate));
|
|
570
|
-
if (indexFile && isExtensionFile(indexFile)) {
|
|
571
|
-
addCandidate({
|
|
572
|
-
candidates: params.candidates,
|
|
573
|
-
seen: params.seen,
|
|
574
|
-
idHint: path2.basename(resolved),
|
|
575
|
-
source: indexFile,
|
|
576
|
-
rootDir: resolved,
|
|
577
|
-
origin: params.origin,
|
|
578
|
-
workspaceDir: params.workspaceDir,
|
|
579
|
-
manifest,
|
|
580
|
-
packageDir: resolved
|
|
581
|
-
});
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
discoverInDirectory({
|
|
585
|
-
dir: resolved,
|
|
586
|
-
origin: params.origin,
|
|
587
|
-
workspaceDir: params.workspaceDir,
|
|
588
|
-
candidates: params.candidates,
|
|
589
|
-
diagnostics: params.diagnostics,
|
|
590
|
-
seen: params.seen
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
function discoverOpenClawPlugins(params) {
|
|
595
|
-
const candidates = [];
|
|
596
|
-
const diagnostics = [];
|
|
597
|
-
const seen = /* @__PURE__ */ new Set();
|
|
598
|
-
const workspaceDir = params.workspaceDir?.trim();
|
|
599
|
-
const loadPaths = params.extraPaths ?? params.config?.plugins?.load?.paths ?? [];
|
|
600
|
-
for (const rawPath of loadPaths) {
|
|
601
|
-
if (typeof rawPath !== "string") {
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
const trimmed = rawPath.trim();
|
|
605
|
-
if (!trimmed) {
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
discoverFromPath({
|
|
609
|
-
rawPath: trimmed,
|
|
610
|
-
origin: "config",
|
|
611
|
-
workspaceDir,
|
|
612
|
-
candidates,
|
|
613
|
-
diagnostics,
|
|
614
|
-
seen
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
if (workspaceDir) {
|
|
618
|
-
discoverInDirectory({
|
|
619
|
-
dir: path2.join(workspaceDir, ".nextclaw", "extensions"),
|
|
620
|
-
origin: "workspace",
|
|
621
|
-
workspaceDir,
|
|
622
|
-
candidates,
|
|
623
|
-
diagnostics,
|
|
624
|
-
seen
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
discoverInDirectory({
|
|
628
|
-
dir: path2.join(getDataPath(), "extensions"),
|
|
629
|
-
origin: "global",
|
|
630
|
-
candidates,
|
|
631
|
-
diagnostics,
|
|
632
|
-
seen
|
|
633
|
-
});
|
|
634
|
-
return { candidates, diagnostics };
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/plugins/install.ts
|
|
638
|
-
import fs3 from "fs/promises";
|
|
639
|
-
import os from "os";
|
|
640
|
-
import path3 from "path";
|
|
641
|
-
import { spawn } from "child_process";
|
|
642
|
-
import JSZip from "jszip";
|
|
643
|
-
import * as tar from "tar";
|
|
644
|
-
import { getDataPath as getDataPath2 } from "@nextclaw/core";
|
|
645
|
-
var defaultLogger = {};
|
|
646
|
-
function resolveUserPath2(input) {
|
|
647
|
-
if (input.startsWith("~/")) {
|
|
648
|
-
return path3.resolve(os.homedir(), input.slice(2));
|
|
649
|
-
}
|
|
650
|
-
return path3.resolve(input);
|
|
651
|
-
}
|
|
652
|
-
function safeDirName(input) {
|
|
653
|
-
return input.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-/, "").replace(/-$/, "").trim();
|
|
654
|
-
}
|
|
655
|
-
function validatePluginId(pluginId) {
|
|
656
|
-
if (!pluginId) {
|
|
657
|
-
return "invalid plugin name: missing";
|
|
658
|
-
}
|
|
659
|
-
if (pluginId === "." || pluginId === "..") {
|
|
660
|
-
return "invalid plugin name: reserved path segment";
|
|
661
|
-
}
|
|
662
|
-
if (pluginId.includes("/") || pluginId.includes("\\")) {
|
|
663
|
-
return "invalid plugin name: path separators not allowed";
|
|
664
|
-
}
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
function resolveExtensionsDir(extensionsDir) {
|
|
668
|
-
return extensionsDir ? resolveUserPath2(extensionsDir) : path3.join(getDataPath2(), "extensions");
|
|
669
|
-
}
|
|
670
|
-
async function exists(filePath) {
|
|
671
|
-
try {
|
|
672
|
-
await fs3.access(filePath);
|
|
673
|
-
return true;
|
|
674
|
-
} catch {
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
async function readJsonFile(filePath) {
|
|
679
|
-
const raw = await fs3.readFile(filePath, "utf-8");
|
|
680
|
-
return JSON.parse(raw);
|
|
681
|
-
}
|
|
682
|
-
function resolveArchiveKind(filePath) {
|
|
683
|
-
const lower = filePath.toLowerCase();
|
|
684
|
-
if (lower.endsWith(".zip")) {
|
|
685
|
-
return "zip";
|
|
686
|
-
}
|
|
687
|
-
if (lower.endsWith(".tgz") || lower.endsWith(".tar.gz")) {
|
|
688
|
-
return "tgz";
|
|
689
|
-
}
|
|
690
|
-
if (lower.endsWith(".tar")) {
|
|
691
|
-
return "tar";
|
|
692
|
-
}
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
695
|
-
async function extractArchive(params) {
|
|
696
|
-
if (params.kind === "zip") {
|
|
697
|
-
const raw = await fs3.readFile(params.archivePath);
|
|
698
|
-
const zip = await JSZip.loadAsync(raw);
|
|
699
|
-
await Promise.all(
|
|
700
|
-
Object.values(zip.files).map(async (entry) => {
|
|
701
|
-
const fullPath = path3.resolve(params.destDir, entry.name);
|
|
702
|
-
if (!fullPath.startsWith(path3.resolve(params.destDir) + path3.sep) && fullPath !== path3.resolve(params.destDir)) {
|
|
703
|
-
throw new Error(`zip entry escapes destination: ${entry.name}`);
|
|
704
|
-
}
|
|
705
|
-
if (entry.dir) {
|
|
706
|
-
await fs3.mkdir(fullPath, { recursive: true });
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
const parent = path3.dirname(fullPath);
|
|
710
|
-
await fs3.mkdir(parent, { recursive: true });
|
|
711
|
-
const content = await entry.async("nodebuffer");
|
|
712
|
-
await fs3.writeFile(fullPath, content);
|
|
713
|
-
})
|
|
714
|
-
);
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
await tar.x({
|
|
718
|
-
file: params.archivePath,
|
|
719
|
-
cwd: params.destDir,
|
|
720
|
-
strict: true,
|
|
721
|
-
preservePaths: false
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
async function resolvePackedRootDir(extractDir) {
|
|
725
|
-
const packageDir = path3.join(extractDir, "package");
|
|
726
|
-
if (await exists(path3.join(packageDir, "package.json"))) {
|
|
727
|
-
return packageDir;
|
|
728
|
-
}
|
|
729
|
-
if (await exists(path3.join(extractDir, "package.json"))) {
|
|
730
|
-
return extractDir;
|
|
731
|
-
}
|
|
732
|
-
const entries = await fs3.readdir(extractDir, { withFileTypes: true });
|
|
733
|
-
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path3.join(extractDir, entry.name));
|
|
734
|
-
if (dirs.length === 1 && await exists(path3.join(dirs[0], "package.json"))) {
|
|
735
|
-
return dirs[0];
|
|
736
|
-
}
|
|
737
|
-
for (const dir of dirs) {
|
|
738
|
-
if (await exists(path3.join(dir, "package.json"))) {
|
|
739
|
-
return dir;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
throw new Error("archive missing package root");
|
|
743
|
-
}
|
|
744
|
-
async function ensureOpenClawExtensions(manifest) {
|
|
745
|
-
const extensions = manifest.openclaw?.extensions;
|
|
746
|
-
if (!Array.isArray(extensions)) {
|
|
747
|
-
throw new Error("package.json missing openclaw.extensions");
|
|
748
|
-
}
|
|
749
|
-
const list = extensions.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
750
|
-
if (list.length === 0) {
|
|
751
|
-
throw new Error("package.json openclaw.extensions is empty");
|
|
752
|
-
}
|
|
753
|
-
return list;
|
|
754
|
-
}
|
|
755
|
-
async function runCommand(command, args, cwd) {
|
|
756
|
-
return new Promise((resolve) => {
|
|
757
|
-
const child = spawn(command, args, {
|
|
758
|
-
cwd,
|
|
759
|
-
env: {
|
|
760
|
-
...process.env,
|
|
761
|
-
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
|
762
|
-
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
|
763
|
-
},
|
|
764
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
765
|
-
});
|
|
766
|
-
let stdout = "";
|
|
767
|
-
let stderr = "";
|
|
768
|
-
child.stdout.on("data", (chunk) => {
|
|
769
|
-
stdout += String(chunk);
|
|
770
|
-
});
|
|
771
|
-
child.stderr.on("data", (chunk) => {
|
|
772
|
-
stderr += String(chunk);
|
|
773
|
-
});
|
|
774
|
-
child.on("close", (code) => {
|
|
775
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
776
|
-
});
|
|
777
|
-
child.on("error", (error) => {
|
|
778
|
-
resolve({ code: 1, stdout, stderr: `${stderr}
|
|
779
|
-
${String(error)}` });
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
async function installDependenciesIfNeeded(packageDir, manifest, logger) {
|
|
784
|
-
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
logger.info?.("Installing plugin dependencies...");
|
|
788
|
-
const result = await runCommand("npm", ["install", "--ignore-scripts"], packageDir);
|
|
789
|
-
if (result.code !== 0) {
|
|
790
|
-
throw new Error(result.stderr.trim() || result.stdout.trim() || "npm install failed");
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
function resolvePluginInstallDir(pluginId, extensionsDir) {
|
|
794
|
-
const err = validatePluginId(pluginId);
|
|
795
|
-
if (err) {
|
|
796
|
-
throw new Error(err);
|
|
797
|
-
}
|
|
798
|
-
const baseDir = resolveExtensionsDir(extensionsDir);
|
|
799
|
-
const target = path3.resolve(baseDir, pluginId);
|
|
800
|
-
if (!target.startsWith(path3.resolve(baseDir) + path3.sep) && target !== path3.resolve(baseDir)) {
|
|
801
|
-
throw new Error("invalid plugin name: path traversal detected");
|
|
802
|
-
}
|
|
803
|
-
return target;
|
|
804
|
-
}
|
|
805
|
-
async function installPluginFromPackageDir(params) {
|
|
806
|
-
const logger = params.logger ?? defaultLogger;
|
|
807
|
-
const packageDir = resolveUserPath2(params.packageDir);
|
|
808
|
-
const mode = params.mode ?? "install";
|
|
809
|
-
const dryRun = params.dryRun ?? false;
|
|
810
|
-
const packageJsonPath = path3.join(packageDir, "package.json");
|
|
811
|
-
if (!await exists(packageJsonPath)) {
|
|
812
|
-
return { ok: false, error: "plugin package missing package.json" };
|
|
813
|
-
}
|
|
814
|
-
let packageManifest;
|
|
815
|
-
try {
|
|
816
|
-
packageManifest = await readJsonFile(packageJsonPath);
|
|
817
|
-
} catch (err) {
|
|
818
|
-
return { ok: false, error: `invalid package.json: ${String(err)}` };
|
|
819
|
-
}
|
|
820
|
-
let extensions;
|
|
821
|
-
try {
|
|
822
|
-
extensions = await ensureOpenClawExtensions(packageManifest);
|
|
823
|
-
} catch (err) {
|
|
824
|
-
return { ok: false, error: String(err) };
|
|
825
|
-
}
|
|
826
|
-
const manifestRes = loadPluginManifest(packageDir);
|
|
827
|
-
if (!manifestRes.ok) {
|
|
828
|
-
return { ok: false, error: manifestRes.error };
|
|
829
|
-
}
|
|
830
|
-
const pluginId = manifestRes.manifest.id;
|
|
831
|
-
const pluginErr = validatePluginId(pluginId);
|
|
832
|
-
if (pluginErr) {
|
|
833
|
-
return { ok: false, error: pluginErr };
|
|
834
|
-
}
|
|
835
|
-
if (params.expectedPluginId && params.expectedPluginId !== pluginId) {
|
|
836
|
-
return {
|
|
837
|
-
ok: false,
|
|
838
|
-
error: `plugin id mismatch: expected ${params.expectedPluginId}, got ${pluginId}`
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
const targetDir = resolvePluginInstallDir(pluginId, params.extensionsDir);
|
|
842
|
-
if (mode === "install" && await exists(targetDir)) {
|
|
843
|
-
return { ok: false, error: `plugin already exists: ${targetDir} (delete it first)` };
|
|
844
|
-
}
|
|
845
|
-
if (dryRun) {
|
|
846
|
-
return {
|
|
847
|
-
ok: true,
|
|
848
|
-
pluginId,
|
|
849
|
-
targetDir,
|
|
850
|
-
manifestName: packageManifest.name,
|
|
851
|
-
version: packageManifest.version,
|
|
852
|
-
extensions
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
await fs3.mkdir(path3.dirname(targetDir), { recursive: true });
|
|
856
|
-
if (mode === "update" && await exists(targetDir)) {
|
|
857
|
-
await fs3.rm(targetDir, { recursive: true, force: true });
|
|
858
|
-
}
|
|
859
|
-
logger.info?.(`Installing to ${targetDir}...`);
|
|
860
|
-
await fs3.cp(packageDir, targetDir, { recursive: true, force: true });
|
|
861
|
-
try {
|
|
862
|
-
for (const extensionPath of extensions) {
|
|
863
|
-
const resolved = path3.resolve(targetDir, extensionPath);
|
|
864
|
-
if (!resolved.startsWith(path3.resolve(targetDir) + path3.sep) && resolved !== path3.resolve(targetDir)) {
|
|
865
|
-
throw new Error(`extension entry escapes plugin directory: ${extensionPath}`);
|
|
866
|
-
}
|
|
867
|
-
if (!await exists(resolved)) {
|
|
868
|
-
throw new Error(`extension entry not found after install: ${extensionPath}`);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
await installDependenciesIfNeeded(targetDir, packageManifest, logger);
|
|
872
|
-
} catch (err) {
|
|
873
|
-
await fs3.rm(targetDir, { recursive: true, force: true }).catch(() => void 0);
|
|
874
|
-
return {
|
|
875
|
-
ok: false,
|
|
876
|
-
error: err instanceof Error ? err.message : `failed to install dependencies: ${String(err)}`
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
return {
|
|
880
|
-
ok: true,
|
|
881
|
-
pluginId,
|
|
882
|
-
targetDir,
|
|
883
|
-
manifestName: packageManifest.name,
|
|
884
|
-
version: packageManifest.version,
|
|
885
|
-
extensions
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
async function installPluginFromArchive(params) {
|
|
889
|
-
const logger = params.logger ?? defaultLogger;
|
|
890
|
-
const archivePath = resolveUserPath2(params.archivePath);
|
|
891
|
-
if (!await exists(archivePath)) {
|
|
892
|
-
return { ok: false, error: `archive not found: ${archivePath}` };
|
|
893
|
-
}
|
|
894
|
-
const kind = resolveArchiveKind(archivePath);
|
|
895
|
-
if (!kind) {
|
|
896
|
-
return { ok: false, error: `unsupported archive: ${archivePath}` };
|
|
897
|
-
}
|
|
898
|
-
const tempDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "nextclaw-plugin-"));
|
|
899
|
-
try {
|
|
900
|
-
const extractDir = path3.join(tempDir, "extract");
|
|
901
|
-
await fs3.mkdir(extractDir, { recursive: true });
|
|
902
|
-
logger.info?.(`Extracting ${archivePath}...`);
|
|
903
|
-
try {
|
|
904
|
-
await extractArchive({ archivePath, destDir: extractDir, kind });
|
|
905
|
-
} catch (err) {
|
|
906
|
-
return { ok: false, error: `failed to extract archive: ${String(err)}` };
|
|
907
|
-
}
|
|
908
|
-
let packageDir = "";
|
|
909
|
-
try {
|
|
910
|
-
packageDir = await resolvePackedRootDir(extractDir);
|
|
911
|
-
} catch (err) {
|
|
912
|
-
return { ok: false, error: String(err) };
|
|
913
|
-
}
|
|
914
|
-
return await installPluginFromPackageDir({
|
|
915
|
-
packageDir,
|
|
916
|
-
extensionsDir: params.extensionsDir,
|
|
917
|
-
logger,
|
|
918
|
-
mode: params.mode,
|
|
919
|
-
dryRun: params.dryRun,
|
|
920
|
-
expectedPluginId: params.expectedPluginId
|
|
921
|
-
});
|
|
922
|
-
} finally {
|
|
923
|
-
await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
async function installPluginFromDir(params) {
|
|
927
|
-
const dirPath = resolveUserPath2(params.dirPath);
|
|
928
|
-
if (!await exists(dirPath)) {
|
|
929
|
-
return { ok: false, error: `directory not found: ${dirPath}` };
|
|
930
|
-
}
|
|
931
|
-
const stat = await fs3.stat(dirPath);
|
|
932
|
-
if (!stat.isDirectory()) {
|
|
933
|
-
return { ok: false, error: `not a directory: ${dirPath}` };
|
|
934
|
-
}
|
|
935
|
-
return installPluginFromPackageDir({
|
|
936
|
-
packageDir: dirPath,
|
|
937
|
-
extensionsDir: params.extensionsDir,
|
|
938
|
-
logger: params.logger,
|
|
939
|
-
mode: params.mode,
|
|
940
|
-
dryRun: params.dryRun,
|
|
941
|
-
expectedPluginId: params.expectedPluginId
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
async function installPluginFromFile(params) {
|
|
945
|
-
const filePath = resolveUserPath2(params.filePath);
|
|
946
|
-
const logger = params.logger ?? defaultLogger;
|
|
947
|
-
const mode = params.mode ?? "install";
|
|
948
|
-
const dryRun = params.dryRun ?? false;
|
|
949
|
-
if (!await exists(filePath)) {
|
|
950
|
-
return { ok: false, error: `file not found: ${filePath}` };
|
|
951
|
-
}
|
|
952
|
-
const stat = await fs3.stat(filePath);
|
|
953
|
-
if (!stat.isFile()) {
|
|
954
|
-
return { ok: false, error: `not a file: ${filePath}` };
|
|
955
|
-
}
|
|
956
|
-
const ext = path3.extname(filePath);
|
|
957
|
-
const pluginId = safeDirName(path3.basename(filePath, ext) || "plugin");
|
|
958
|
-
const pluginErr = validatePluginId(pluginId);
|
|
959
|
-
if (pluginErr) {
|
|
960
|
-
return { ok: false, error: pluginErr };
|
|
961
|
-
}
|
|
962
|
-
const targetDir = resolveExtensionsDir(params.extensionsDir);
|
|
963
|
-
const targetFile = path3.join(targetDir, `${pluginId}${ext}`);
|
|
964
|
-
if (mode === "install" && await exists(targetFile)) {
|
|
965
|
-
return { ok: false, error: `plugin already exists: ${targetFile} (delete it first)` };
|
|
966
|
-
}
|
|
967
|
-
if (dryRun) {
|
|
968
|
-
return {
|
|
969
|
-
ok: true,
|
|
970
|
-
pluginId,
|
|
971
|
-
targetDir: targetFile,
|
|
972
|
-
extensions: [path3.basename(targetFile)]
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
await fs3.mkdir(targetDir, { recursive: true });
|
|
976
|
-
logger.info?.(`Installing to ${targetFile}...`);
|
|
977
|
-
await fs3.copyFile(filePath, targetFile);
|
|
978
|
-
return {
|
|
979
|
-
ok: true,
|
|
980
|
-
pluginId,
|
|
981
|
-
targetDir: targetFile,
|
|
982
|
-
extensions: [path3.basename(targetFile)]
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
function validateRegistryNpmSpec(spec) {
|
|
986
|
-
const trimmed = spec.trim();
|
|
987
|
-
if (!trimmed) {
|
|
988
|
-
return "npm spec is required";
|
|
989
|
-
}
|
|
990
|
-
const lower = trimmed.toLowerCase();
|
|
991
|
-
if (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("git+") || lower.startsWith("github:") || lower.startsWith("file:")) {
|
|
992
|
-
return "only registry npm specs are supported";
|
|
993
|
-
}
|
|
994
|
-
if (trimmed.includes("/") && !trimmed.startsWith("@")) {
|
|
995
|
-
return "only registry npm specs are supported";
|
|
996
|
-
}
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
async function installPluginFromNpmSpec(params) {
|
|
1000
|
-
const logger = params.logger ?? defaultLogger;
|
|
1001
|
-
const spec = params.spec.trim();
|
|
1002
|
-
const specError = validateRegistryNpmSpec(spec);
|
|
1003
|
-
if (specError) {
|
|
1004
|
-
return { ok: false, error: specError };
|
|
1005
|
-
}
|
|
1006
|
-
const tempDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "nextclaw-npm-pack-"));
|
|
1007
|
-
try {
|
|
1008
|
-
logger.info?.(`Downloading ${spec}...`);
|
|
1009
|
-
const packed = await runCommand("npm", ["pack", spec, "--ignore-scripts"], tempDir);
|
|
1010
|
-
if (packed.code !== 0) {
|
|
1011
|
-
return {
|
|
1012
|
-
ok: false,
|
|
1013
|
-
error: `npm pack failed: ${packed.stderr.trim() || packed.stdout.trim()}`
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
const archiveName = packed.stdout.split("\n").map((line) => line.trim()).filter(Boolean).pop();
|
|
1017
|
-
if (!archiveName) {
|
|
1018
|
-
return { ok: false, error: "npm pack produced no archive" };
|
|
1019
|
-
}
|
|
1020
|
-
const archivePath = path3.join(tempDir, archiveName);
|
|
1021
|
-
return await installPluginFromArchive({
|
|
1022
|
-
archivePath,
|
|
1023
|
-
extensionsDir: params.extensionsDir,
|
|
1024
|
-
logger,
|
|
1025
|
-
mode: params.mode,
|
|
1026
|
-
dryRun: params.dryRun,
|
|
1027
|
-
expectedPluginId: params.expectedPluginId
|
|
1028
|
-
});
|
|
1029
|
-
} finally {
|
|
1030
|
-
await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
async function installPluginFromPath(params) {
|
|
1034
|
-
const resolvedPath = resolveUserPath2(params.path);
|
|
1035
|
-
if (!await exists(resolvedPath)) {
|
|
1036
|
-
return { ok: false, error: `path not found: ${resolvedPath}` };
|
|
1037
|
-
}
|
|
1038
|
-
const stat = await fs3.stat(resolvedPath);
|
|
1039
|
-
if (stat.isDirectory()) {
|
|
1040
|
-
return installPluginFromDir({
|
|
1041
|
-
dirPath: resolvedPath,
|
|
1042
|
-
extensionsDir: params.extensionsDir,
|
|
1043
|
-
logger: params.logger,
|
|
1044
|
-
mode: params.mode,
|
|
1045
|
-
dryRun: params.dryRun,
|
|
1046
|
-
expectedPluginId: params.expectedPluginId
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
const archiveKind = resolveArchiveKind(resolvedPath);
|
|
1050
|
-
if (archiveKind) {
|
|
1051
|
-
return installPluginFromArchive({
|
|
1052
|
-
archivePath: resolvedPath,
|
|
1053
|
-
extensionsDir: params.extensionsDir,
|
|
1054
|
-
logger: params.logger,
|
|
1055
|
-
mode: params.mode,
|
|
1056
|
-
dryRun: params.dryRun,
|
|
1057
|
-
expectedPluginId: params.expectedPluginId
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
return installPluginFromFile({
|
|
1061
|
-
filePath: resolvedPath,
|
|
1062
|
-
extensionsDir: params.extensionsDir,
|
|
1063
|
-
logger: params.logger,
|
|
1064
|
-
mode: params.mode,
|
|
1065
|
-
dryRun: params.dryRun
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// src/plugins/loader.ts
|
|
1070
|
-
import fs5 from "fs";
|
|
1071
|
-
import path6 from "path";
|
|
1072
|
-
import { fileURLToPath } from "url";
|
|
1073
|
-
import { createRequire } from "module";
|
|
1074
|
-
import createJitiImport from "jiti";
|
|
1075
|
-
import { getWorkspacePathFromConfig } from "@nextclaw/core";
|
|
1076
|
-
|
|
1077
|
-
// src/plugins/candidate-filter.ts
|
|
1078
|
-
import path4 from "path";
|
|
1079
|
-
function isPathInsideRoot(candidatePath, rootPath) {
|
|
1080
|
-
const normalizedCandidate = path4.resolve(candidatePath);
|
|
1081
|
-
const normalizedRoot = path4.resolve(rootPath);
|
|
1082
|
-
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${path4.sep}`);
|
|
1083
|
-
}
|
|
1084
|
-
function filterPluginCandidatesByExcludedRoots(candidates, excludedRoots) {
|
|
1085
|
-
const normalizedRoots = excludedRoots.map((entry) => path4.resolve(entry));
|
|
1086
|
-
if (normalizedRoots.length === 0) {
|
|
1087
|
-
return [...candidates];
|
|
1088
|
-
}
|
|
1089
|
-
return candidates.filter((candidate) => {
|
|
1090
|
-
const candidatePaths = [candidate.source, candidate.rootDir, candidate.packageDir].filter(
|
|
1091
|
-
(entry) => typeof entry === "string" && entry.trim().length > 0
|
|
1092
|
-
);
|
|
1093
|
-
return !normalizedRoots.some(
|
|
1094
|
-
(rootPath) => candidatePaths.some((candidatePath) => isPathInsideRoot(candidatePath, rootPath))
|
|
1095
|
-
);
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
// src/plugins/manifest-registry.ts
|
|
1100
|
-
import fs4 from "fs";
|
|
1101
|
-
var PLUGIN_ORIGIN_RANK = {
|
|
1102
|
-
config: 0,
|
|
1103
|
-
workspace: 1,
|
|
1104
|
-
global: 2,
|
|
1105
|
-
bundled: 3
|
|
1106
|
-
};
|
|
1107
|
-
function safeRealpathSync(rootDir, cache) {
|
|
1108
|
-
const cached = cache.get(rootDir);
|
|
1109
|
-
if (cached) {
|
|
1110
|
-
return cached;
|
|
1111
|
-
}
|
|
1112
|
-
try {
|
|
1113
|
-
const resolved = fs4.realpathSync(rootDir);
|
|
1114
|
-
cache.set(rootDir, resolved);
|
|
1115
|
-
return resolved;
|
|
1116
|
-
} catch {
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
function safeStatMtimeMs(filePath) {
|
|
1121
|
-
try {
|
|
1122
|
-
return fs4.statSync(filePath).mtimeMs;
|
|
1123
|
-
} catch {
|
|
1124
|
-
return null;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
function normalizeManifestLabel(raw) {
|
|
1128
|
-
const trimmed = raw?.trim();
|
|
1129
|
-
return trimmed ? trimmed : void 0;
|
|
1130
|
-
}
|
|
1131
|
-
function buildRecord(params) {
|
|
1132
|
-
return {
|
|
1133
|
-
id: params.manifest.id,
|
|
1134
|
-
name: normalizeManifestLabel(params.manifest.name) ?? params.candidate.packageName,
|
|
1135
|
-
description: normalizeManifestLabel(params.manifest.description) ?? params.candidate.packageDescription,
|
|
1136
|
-
version: normalizeManifestLabel(params.manifest.version) ?? params.candidate.packageVersion,
|
|
1137
|
-
kind: params.manifest.kind,
|
|
1138
|
-
channels: params.manifest.channels ?? [],
|
|
1139
|
-
providers: params.manifest.providers ?? [],
|
|
1140
|
-
skills: params.manifest.skills ?? [],
|
|
1141
|
-
origin: params.candidate.origin,
|
|
1142
|
-
workspaceDir: params.candidate.workspaceDir,
|
|
1143
|
-
rootDir: params.candidate.rootDir,
|
|
1144
|
-
source: params.candidate.source,
|
|
1145
|
-
manifestPath: params.manifestPath,
|
|
1146
|
-
schemaCacheKey: params.schemaCacheKey,
|
|
1147
|
-
configSchema: params.configSchema,
|
|
1148
|
-
configUiHints: params.manifest.uiHints
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
function loadPluginManifestRegistry(params) {
|
|
1152
|
-
const normalized = normalizePluginsConfig(params.config?.plugins);
|
|
1153
|
-
const discovery = params.candidates ? {
|
|
1154
|
-
candidates: params.candidates,
|
|
1155
|
-
diagnostics: params.diagnostics ?? []
|
|
1156
|
-
} : discoverOpenClawPlugins({
|
|
1157
|
-
config: params.config,
|
|
1158
|
-
workspaceDir: params.workspaceDir,
|
|
1159
|
-
extraPaths: normalized.loadPaths
|
|
1160
|
-
});
|
|
1161
|
-
const diagnostics = [...discovery.diagnostics];
|
|
1162
|
-
const records = [];
|
|
1163
|
-
const seenIds = /* @__PURE__ */ new Map();
|
|
1164
|
-
const realpathCache = /* @__PURE__ */ new Map();
|
|
1165
|
-
for (const candidate of discovery.candidates) {
|
|
1166
|
-
const manifestRes = loadPluginManifest(candidate.rootDir);
|
|
1167
|
-
if (!manifestRes.ok) {
|
|
1168
|
-
diagnostics.push({
|
|
1169
|
-
level: "error",
|
|
1170
|
-
message: manifestRes.error,
|
|
1171
|
-
source: manifestRes.manifestPath
|
|
1172
|
-
});
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
const manifest = manifestRes.manifest;
|
|
1176
|
-
const configSchema = manifest.configSchema;
|
|
1177
|
-
if (candidate.idHint && candidate.idHint !== manifest.id) {
|
|
1178
|
-
diagnostics.push({
|
|
1179
|
-
level: "warn",
|
|
1180
|
-
pluginId: manifest.id,
|
|
1181
|
-
source: candidate.source,
|
|
1182
|
-
message: `plugin id mismatch (manifest uses "${manifest.id}", entry hints "${candidate.idHint}")`
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
const manifestMtime = safeStatMtimeMs(manifestRes.manifestPath);
|
|
1186
|
-
const schemaCacheKey = manifestMtime ? `${manifestRes.manifestPath}:${manifestMtime}` : manifestRes.manifestPath;
|
|
1187
|
-
const existing = seenIds.get(manifest.id);
|
|
1188
|
-
if (existing) {
|
|
1189
|
-
const existingReal = safeRealpathSync(existing.candidate.rootDir, realpathCache);
|
|
1190
|
-
const candidateReal = safeRealpathSync(candidate.rootDir, realpathCache);
|
|
1191
|
-
const samePlugin = Boolean(existingReal && candidateReal && existingReal === candidateReal);
|
|
1192
|
-
if (samePlugin) {
|
|
1193
|
-
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) {
|
|
1194
|
-
records[existing.recordIndex] = buildRecord({
|
|
1195
|
-
manifest,
|
|
1196
|
-
candidate,
|
|
1197
|
-
manifestPath: manifestRes.manifestPath,
|
|
1198
|
-
schemaCacheKey,
|
|
1199
|
-
configSchema
|
|
1200
|
-
});
|
|
1201
|
-
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
|
|
1202
|
-
}
|
|
1203
|
-
continue;
|
|
1204
|
-
}
|
|
1205
|
-
diagnostics.push({
|
|
1206
|
-
level: "warn",
|
|
1207
|
-
pluginId: manifest.id,
|
|
1208
|
-
source: candidate.source,
|
|
1209
|
-
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`
|
|
1210
|
-
});
|
|
1211
|
-
} else {
|
|
1212
|
-
seenIds.set(manifest.id, { candidate, recordIndex: records.length });
|
|
1213
|
-
}
|
|
1214
|
-
records.push(
|
|
1215
|
-
buildRecord({
|
|
1216
|
-
manifest,
|
|
1217
|
-
candidate,
|
|
1218
|
-
manifestPath: manifestRes.manifestPath,
|
|
1219
|
-
schemaCacheKey,
|
|
1220
|
-
configSchema
|
|
1221
|
-
})
|
|
1222
|
-
);
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
plugins: records,
|
|
1226
|
-
diagnostics
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
function toPluginUiMetadata(records) {
|
|
1230
|
-
return records.map((record) => ({
|
|
1231
|
-
id: record.id,
|
|
1232
|
-
configSchema: record.configSchema,
|
|
1233
|
-
configUiHints: record.configUiHints
|
|
1234
|
-
}));
|
|
1235
|
-
}
|
|
1236
|
-
function loadPluginUiMetadata(params) {
|
|
1237
|
-
const registry = loadPluginManifestRegistry({
|
|
1238
|
-
config: params.config,
|
|
1239
|
-
workspaceDir: params.workspaceDir
|
|
1240
|
-
});
|
|
1241
|
-
return toPluginUiMetadata(registry.plugins);
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// src/plugins/schema-validator.ts
|
|
1245
|
-
import AjvPkg from "ajv";
|
|
1246
|
-
var AjvCtor = AjvPkg;
|
|
1247
|
-
var ajv = new AjvCtor({
|
|
1248
|
-
allErrors: true,
|
|
1249
|
-
strict: false,
|
|
1250
|
-
removeAdditional: false
|
|
1251
|
-
});
|
|
1252
|
-
var schemaCache = /* @__PURE__ */ new Map();
|
|
1253
|
-
function formatAjvErrors(errors) {
|
|
1254
|
-
if (!errors || errors.length === 0) {
|
|
1255
|
-
return ["invalid config"];
|
|
1256
|
-
}
|
|
1257
|
-
return errors.map((error) => {
|
|
1258
|
-
const path8 = error.instancePath?.replace(/^\//, "").replace(/\//g, ".") || "<root>";
|
|
1259
|
-
const message = error.message ?? "invalid";
|
|
1260
|
-
return `${path8}: ${message}`;
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
function validateJsonSchemaValue(params) {
|
|
1264
|
-
let cached = schemaCache.get(params.cacheKey);
|
|
1265
|
-
if (!cached || cached.schema !== params.schema) {
|
|
1266
|
-
const validate = ajv.compile(params.schema);
|
|
1267
|
-
cached = { validate, schema: params.schema };
|
|
1268
|
-
schemaCache.set(params.cacheKey, cached);
|
|
1269
|
-
}
|
|
1270
|
-
const ok = cached.validate(params.value);
|
|
1271
|
-
if (ok) {
|
|
1272
|
-
return { ok: true };
|
|
1273
|
-
}
|
|
1274
|
-
return { ok: false, errors: formatAjvErrors(cached.validate.errors) };
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
// src/plugins/plugin-loader-utils.ts
|
|
1278
|
-
function createPluginRecord(params) {
|
|
1279
|
-
return {
|
|
1280
|
-
id: params.id,
|
|
1281
|
-
name: params.name ?? params.id,
|
|
1282
|
-
description: params.description,
|
|
1283
|
-
version: params.version,
|
|
1284
|
-
kind: params.kind,
|
|
1285
|
-
source: params.source,
|
|
1286
|
-
origin: params.origin,
|
|
1287
|
-
workspaceDir: params.workspaceDir,
|
|
1288
|
-
enabled: params.enabled,
|
|
1289
|
-
status: params.enabled ? "loaded" : "disabled",
|
|
1290
|
-
toolNames: [],
|
|
1291
|
-
channelIds: [],
|
|
1292
|
-
providerIds: [],
|
|
1293
|
-
engineKinds: [],
|
|
1294
|
-
ncpAgentRuntimeKinds: [],
|
|
1295
|
-
configSchema: params.configSchema,
|
|
1296
|
-
configUiHints: params.configUiHints,
|
|
1297
|
-
configJsonSchema: params.configJsonSchema
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
function isPlaceholderConfigSchema(schema) {
|
|
1301
|
-
if (!schema || typeof schema !== "object") {
|
|
1302
|
-
return false;
|
|
1303
|
-
}
|
|
1304
|
-
const type = schema.type;
|
|
1305
|
-
const isObjectType = type === "object" || Array.isArray(type) && type.includes("object");
|
|
1306
|
-
if (!isObjectType) {
|
|
1307
|
-
return false;
|
|
1308
|
-
}
|
|
1309
|
-
const properties = schema.properties;
|
|
1310
|
-
const noProperties = !properties || typeof properties === "object" && !Array.isArray(properties) && Object.keys(properties).length === 0;
|
|
1311
|
-
return noProperties && schema.additionalProperties === false;
|
|
1312
|
-
}
|
|
1313
|
-
function validatePluginConfig(params) {
|
|
1314
|
-
if (!params.schema || isPlaceholderConfigSchema(params.schema)) {
|
|
1315
|
-
return { ok: true, value: params.value };
|
|
1316
|
-
}
|
|
1317
|
-
const cacheKey = params.cacheKey ?? JSON.stringify(params.schema);
|
|
1318
|
-
const result = validateJsonSchemaValue({
|
|
1319
|
-
schema: params.schema,
|
|
1320
|
-
cacheKey,
|
|
1321
|
-
value: params.value ?? {}
|
|
1322
|
-
});
|
|
1323
|
-
if (result.ok) {
|
|
1324
|
-
return { ok: true, value: params.value };
|
|
1325
|
-
}
|
|
1326
|
-
return { ok: false, errors: result.errors };
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// src/plugins/registry.ts
|
|
1330
|
-
import path5 from "path";
|
|
1331
|
-
import { expandHome as expandHome2 } from "@nextclaw/core";
|
|
1332
|
-
|
|
1333
|
-
// src/plugins/plugin-capability-registration.ts
|
|
1334
|
-
function ensureUniqueNames(params) {
|
|
1335
|
-
const accepted = [];
|
|
1336
|
-
for (const rawName of params.names) {
|
|
1337
|
-
const name = rawName.trim();
|
|
1338
|
-
if (!name) {
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
1341
|
-
if (params.reserved.has(name)) {
|
|
1342
|
-
params.diagnostics.push({
|
|
1343
|
-
level: "error",
|
|
1344
|
-
pluginId: params.pluginId,
|
|
1345
|
-
source: params.source,
|
|
1346
|
-
message: `${params.kind} already registered by core: ${name}`
|
|
1347
|
-
});
|
|
1348
|
-
continue;
|
|
1349
|
-
}
|
|
1350
|
-
const owner = params.owners.get(name);
|
|
1351
|
-
if (owner && owner !== params.pluginId) {
|
|
1352
|
-
params.diagnostics.push({
|
|
1353
|
-
level: "error",
|
|
1354
|
-
pluginId: params.pluginId,
|
|
1355
|
-
source: params.source,
|
|
1356
|
-
message: `${params.kind} already registered: ${name} (${owner})`
|
|
1357
|
-
});
|
|
1358
|
-
continue;
|
|
1359
|
-
}
|
|
1360
|
-
params.owners.set(name, params.pluginId);
|
|
1361
|
-
accepted.push(name);
|
|
1362
|
-
}
|
|
1363
|
-
return accepted;
|
|
1364
|
-
}
|
|
1365
|
-
function registerPluginEngine(params) {
|
|
1366
|
-
const accepted = ensureUniqueNames({
|
|
1367
|
-
names: [params.kind],
|
|
1368
|
-
pluginId: params.pluginId,
|
|
1369
|
-
diagnostics: params.runtime.registry.diagnostics,
|
|
1370
|
-
source: params.source,
|
|
1371
|
-
owners: params.runtime.engineKindOwners,
|
|
1372
|
-
reserved: params.runtime.reservedEngineKinds,
|
|
1373
|
-
kind: "engine"
|
|
1374
|
-
});
|
|
1375
|
-
if (accepted.length === 0) {
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
params.runtime.registry.engines.push({
|
|
1379
|
-
pluginId: params.pluginId,
|
|
1380
|
-
kind: accepted[0],
|
|
1381
|
-
factory: params.factory,
|
|
1382
|
-
source: params.source
|
|
1383
|
-
});
|
|
1384
|
-
params.record.engineKinds.push(accepted[0]);
|
|
1385
|
-
}
|
|
1386
|
-
function registerPluginNcpAgentRuntime(params) {
|
|
1387
|
-
const rawKind = params.registration.kind?.trim().toLowerCase();
|
|
1388
|
-
if (!rawKind) {
|
|
1389
|
-
params.runtime.registry.diagnostics.push({
|
|
1390
|
-
level: "error",
|
|
1391
|
-
pluginId: params.pluginId,
|
|
1392
|
-
source: params.source,
|
|
1393
|
-
message: "registerNcpAgentRuntime requires registration.kind"
|
|
1394
|
-
});
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
const accepted = ensureUniqueNames({
|
|
1398
|
-
names: [rawKind],
|
|
1399
|
-
pluginId: params.pluginId,
|
|
1400
|
-
diagnostics: params.runtime.registry.diagnostics,
|
|
1401
|
-
source: params.source,
|
|
1402
|
-
owners: params.runtime.ncpAgentRuntimeKindOwners,
|
|
1403
|
-
reserved: params.runtime.reservedNcpAgentRuntimeKinds,
|
|
1404
|
-
kind: "ncp-runtime"
|
|
1405
|
-
});
|
|
1406
|
-
if (accepted.length === 0) {
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
params.runtime.registry.ncpAgentRuntimes.push({
|
|
1410
|
-
pluginId: params.pluginId,
|
|
1411
|
-
kind: accepted[0],
|
|
1412
|
-
label: params.registration.label?.trim() || accepted[0],
|
|
1413
|
-
createRuntime: params.registration.createRuntime,
|
|
1414
|
-
source: params.source
|
|
1415
|
-
});
|
|
1416
|
-
params.record.ncpAgentRuntimeKinds.push(accepted[0]);
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// src/plugins/runtime.ts
|
|
1420
|
-
import { getPackageVersion } from "@nextclaw/core";
|
|
1421
|
-
import { MemoryGetTool, MemorySearchTool } from "@nextclaw/core";
|
|
1422
|
-
var bridge = {};
|
|
1423
|
-
function setPluginRuntimeBridge(next) {
|
|
1424
|
-
bridge = next ?? {};
|
|
1425
|
-
}
|
|
1426
|
-
function toPluginTool(tool) {
|
|
1427
|
-
return {
|
|
1428
|
-
name: tool.name,
|
|
1429
|
-
description: tool.description,
|
|
1430
|
-
parameters: tool.parameters,
|
|
1431
|
-
execute: (params) => tool.execute(params)
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
function loadConfigWithFallback(config) {
|
|
1435
|
-
if (bridge.loadConfig) {
|
|
1436
|
-
return bridge.loadConfig();
|
|
1437
|
-
}
|
|
1438
|
-
return config ?? {};
|
|
1439
|
-
}
|
|
1440
|
-
async function writeConfigWithFallback(next) {
|
|
1441
|
-
if (!bridge.writeConfigFile) {
|
|
1442
|
-
throw new Error("plugin runtime config.writeConfigFile is unavailable outside gateway runtime");
|
|
1443
|
-
}
|
|
1444
|
-
await bridge.writeConfigFile(next);
|
|
1445
|
-
}
|
|
1446
|
-
async function dispatchReplyWithFallback(params) {
|
|
1447
|
-
if (!bridge.dispatchReplyWithBufferedBlockDispatcher) {
|
|
1448
|
-
throw new Error("plugin runtime channel.reply dispatcher is unavailable outside gateway runtime");
|
|
1449
|
-
}
|
|
1450
|
-
await bridge.dispatchReplyWithBufferedBlockDispatcher(params);
|
|
1451
|
-
}
|
|
1452
|
-
function createPluginRuntime(params) {
|
|
1453
|
-
return {
|
|
1454
|
-
version: getPackageVersion(),
|
|
1455
|
-
config: {
|
|
1456
|
-
loadConfig: () => loadConfigWithFallback(params.config),
|
|
1457
|
-
writeConfigFile: async (next) => writeConfigWithFallback(next)
|
|
1458
|
-
},
|
|
1459
|
-
tools: {
|
|
1460
|
-
createMemorySearchTool: () => toPluginTool(new MemorySearchTool(params.workspace)),
|
|
1461
|
-
createMemoryGetTool: () => toPluginTool(new MemoryGetTool(params.workspace))
|
|
1462
|
-
},
|
|
1463
|
-
channel: {
|
|
1464
|
-
reply: {
|
|
1465
|
-
dispatchReplyWithBufferedBlockDispatcher: async (dispatchParams) => dispatchReplyWithFallback(dispatchParams)
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// src/plugins/registry.ts
|
|
1472
|
-
function buildPluginLogger(base, pluginId) {
|
|
1473
|
-
const withPrefix = (message) => `[plugins:${pluginId}] ${message}`;
|
|
1474
|
-
return {
|
|
1475
|
-
info: (message) => base.info(withPrefix(message)),
|
|
1476
|
-
warn: (message) => base.warn(withPrefix(message)),
|
|
1477
|
-
error: (message) => base.error(withPrefix(message)),
|
|
1478
|
-
debug: base.debug ? (message) => base.debug?.(withPrefix(message)) : void 0
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
function normalizeToolList(value) {
|
|
1482
|
-
if (!value) {
|
|
1483
|
-
return [];
|
|
1484
|
-
}
|
|
1485
|
-
const list = Array.isArray(value) ? value : [value];
|
|
1486
|
-
return list.filter((entry) => {
|
|
1487
|
-
if (!entry || typeof entry !== "object") {
|
|
1488
|
-
return false;
|
|
1489
|
-
}
|
|
1490
|
-
const candidate = entry;
|
|
1491
|
-
return typeof candidate.name === "string" && candidate.name.trim().length > 0 && candidate.parameters !== void 0 && typeof candidate.execute === "function";
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
function createPluginRegisterRuntime(params) {
|
|
1495
|
-
return {
|
|
1496
|
-
config: params.config,
|
|
1497
|
-
workspaceDir: params.workspaceDir,
|
|
1498
|
-
logger: params.logger,
|
|
1499
|
-
registry: params.registry,
|
|
1500
|
-
toolNameOwners: /* @__PURE__ */ new Map(),
|
|
1501
|
-
channelIdOwners: /* @__PURE__ */ new Map(),
|
|
1502
|
-
providerIdOwners: /* @__PURE__ */ new Map(),
|
|
1503
|
-
engineKindOwners: /* @__PURE__ */ new Map(),
|
|
1504
|
-
ncpAgentRuntimeKindOwners: /* @__PURE__ */ new Map(),
|
|
1505
|
-
resolvedToolNames: /* @__PURE__ */ new Set(),
|
|
1506
|
-
reservedToolNames: params.reservedToolNames,
|
|
1507
|
-
reservedChannelIds: params.reservedChannelIds,
|
|
1508
|
-
reservedProviderIds: params.reservedProviderIds,
|
|
1509
|
-
reservedEngineKinds: params.reservedEngineKinds,
|
|
1510
|
-
reservedNcpAgentRuntimeKinds: params.reservedNcpAgentRuntimeKinds
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
function registerPluginTool(params) {
|
|
1514
|
-
const toolInput = params.tool;
|
|
1515
|
-
const normalizedNames = [];
|
|
1516
|
-
if (Array.isArray(params.opts?.names)) {
|
|
1517
|
-
for (const name of params.opts?.names ?? []) {
|
|
1518
|
-
const trimmed = String(name).trim();
|
|
1519
|
-
if (trimmed) {
|
|
1520
|
-
normalizedNames.push(trimmed);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
} else if (params.opts?.name && String(params.opts.name).trim()) {
|
|
1524
|
-
normalizedNames.push(String(params.opts.name).trim());
|
|
1525
|
-
}
|
|
1526
|
-
if (typeof toolInput !== "function") {
|
|
1527
|
-
const intrinsic = toolInput.name.trim();
|
|
1528
|
-
if (intrinsic) {
|
|
1529
|
-
normalizedNames.push(intrinsic);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
const acceptedNames = ensureUniqueNames({
|
|
1533
|
-
names: normalizedNames,
|
|
1534
|
-
pluginId: params.pluginId,
|
|
1535
|
-
diagnostics: params.runtime.registry.diagnostics,
|
|
1536
|
-
source: params.source,
|
|
1537
|
-
owners: params.runtime.toolNameOwners,
|
|
1538
|
-
reserved: params.runtime.reservedToolNames,
|
|
1539
|
-
kind: "tool"
|
|
1540
|
-
});
|
|
1541
|
-
if (acceptedNames.length === 0) {
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
const factory = typeof toolInput === "function" ? toolInput : () => toolInput;
|
|
1545
|
-
const registration = {
|
|
1546
|
-
pluginId: params.pluginId,
|
|
1547
|
-
factory,
|
|
1548
|
-
names: acceptedNames,
|
|
1549
|
-
optional: params.opts?.optional === true,
|
|
1550
|
-
source: params.source
|
|
1551
|
-
};
|
|
1552
|
-
params.runtime.registry.tools.push(registration);
|
|
1553
|
-
params.record.toolNames.push(...acceptedNames);
|
|
1554
|
-
if (typeof toolInput === "function") {
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
if (!params.runtime.resolvedToolNames.has(toolInput.name)) {
|
|
1558
|
-
params.runtime.resolvedToolNames.add(toolInput.name);
|
|
1559
|
-
params.runtime.registry.resolvedTools.push(toolInput);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
function registerPluginChannel(params) {
|
|
1563
|
-
const normalizedChannel = params.registration && typeof params.registration === "object" && "plugin" in params.registration ? params.registration.plugin : params.registration;
|
|
1564
|
-
if (!normalizedChannel || typeof normalizedChannel !== "object") {
|
|
1565
|
-
params.runtime.registry.diagnostics.push({
|
|
1566
|
-
level: "error",
|
|
1567
|
-
pluginId: params.pluginId,
|
|
1568
|
-
source: params.source,
|
|
1569
|
-
message: "channel registration missing plugin object"
|
|
1570
|
-
});
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
const channelObj = normalizedChannel;
|
|
1574
|
-
const rawId = typeof channelObj.id === "string" ? channelObj.id : String(channelObj.id ?? "");
|
|
1575
|
-
const accepted = ensureUniqueNames({
|
|
1576
|
-
names: [rawId],
|
|
1577
|
-
pluginId: params.pluginId,
|
|
1578
|
-
diagnostics: params.runtime.registry.diagnostics,
|
|
1579
|
-
source: params.source,
|
|
1580
|
-
owners: params.runtime.channelIdOwners,
|
|
1581
|
-
reserved: params.runtime.reservedChannelIds,
|
|
1582
|
-
kind: "channel"
|
|
1583
|
-
});
|
|
1584
|
-
if (accepted.length === 0) {
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
const channelPlugin = normalizedChannel;
|
|
1588
|
-
params.runtime.registry.channels.push({
|
|
1589
|
-
pluginId: params.pluginId,
|
|
1590
|
-
channel: channelPlugin,
|
|
1591
|
-
source: params.source
|
|
1592
|
-
});
|
|
1593
|
-
const channelId = accepted[0];
|
|
1594
|
-
params.record.channelIds.push(channelId);
|
|
1595
|
-
const configSchema = channelPlugin.configSchema;
|
|
1596
|
-
if (configSchema && typeof configSchema === "object") {
|
|
1597
|
-
const schema = configSchema.schema;
|
|
1598
|
-
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
|
1599
|
-
params.record.configJsonSchema = schema;
|
|
1600
|
-
params.record.configSchema = true;
|
|
1601
|
-
}
|
|
1602
|
-
const uiHints = configSchema.uiHints;
|
|
1603
|
-
if (uiHints && typeof uiHints === "object" && !Array.isArray(uiHints)) {
|
|
1604
|
-
params.record.configUiHints = {
|
|
1605
|
-
...params.record.configUiHints ?? {},
|
|
1606
|
-
...uiHints
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
const pushChannelTools = (value, optional, sourceLabel, resolveValue) => {
|
|
1611
|
-
const previewTools = normalizeToolList(value);
|
|
1612
|
-
if (previewTools.length === 0) {
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
const declaredNames = previewTools.map((tool) => tool.name);
|
|
1616
|
-
const acceptedNames = ensureUniqueNames({
|
|
1617
|
-
names: declaredNames,
|
|
1618
|
-
pluginId: params.pluginId,
|
|
1619
|
-
diagnostics: params.runtime.registry.diagnostics,
|
|
1620
|
-
source: params.source,
|
|
1621
|
-
owners: params.runtime.toolNameOwners,
|
|
1622
|
-
reserved: params.runtime.reservedToolNames,
|
|
1623
|
-
kind: "tool"
|
|
1624
|
-
});
|
|
1625
|
-
if (acceptedNames.length === 0) {
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
const factory = (ctx) => {
|
|
1629
|
-
const tools = normalizeToolList(resolveValue(ctx));
|
|
1630
|
-
if (tools.length === 0) {
|
|
1631
|
-
return [];
|
|
1632
|
-
}
|
|
1633
|
-
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
1634
|
-
return acceptedNames.map((name) => byName.get(name)).filter(Boolean);
|
|
1635
|
-
};
|
|
1636
|
-
params.runtime.registry.tools.push({
|
|
1637
|
-
pluginId: params.pluginId,
|
|
1638
|
-
factory,
|
|
1639
|
-
names: acceptedNames,
|
|
1640
|
-
optional,
|
|
1641
|
-
source: params.source
|
|
1642
|
-
});
|
|
1643
|
-
params.record.toolNames.push(...acceptedNames);
|
|
1644
|
-
const previewByName = new Map(previewTools.map((tool) => [tool.name, tool]));
|
|
1645
|
-
for (const name of acceptedNames) {
|
|
1646
|
-
const resolvedTool = previewByName.get(name);
|
|
1647
|
-
if (!resolvedTool || params.runtime.resolvedToolNames.has(resolvedTool.name)) {
|
|
1648
|
-
continue;
|
|
1649
|
-
}
|
|
1650
|
-
params.runtime.resolvedToolNames.add(resolvedTool.name);
|
|
1651
|
-
params.runtime.registry.resolvedTools.push(resolvedTool);
|
|
1652
|
-
}
|
|
1653
|
-
params.runtime.registry.diagnostics.push({
|
|
1654
|
-
level: "warn",
|
|
1655
|
-
pluginId: params.pluginId,
|
|
1656
|
-
source: params.source,
|
|
1657
|
-
message: `${sourceLabel} registered channel-owned tools: ${acceptedNames.join(", ")}`
|
|
1658
|
-
});
|
|
1659
|
-
};
|
|
1660
|
-
const agentTools = channelPlugin.agentTools;
|
|
1661
|
-
if (typeof agentTools === "function") {
|
|
1662
|
-
pushChannelTools(
|
|
1663
|
-
normalizeToolList(agentTools()),
|
|
1664
|
-
false,
|
|
1665
|
-
`channel "${channelId}"`,
|
|
1666
|
-
() => agentTools()
|
|
1667
|
-
);
|
|
1668
|
-
} else if (agentTools) {
|
|
1669
|
-
pushChannelTools(normalizeToolList(agentTools), false, `channel "${channelId}"`, () => agentTools);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
function registerPluginProvider(params) {
|
|
1673
|
-
const accepted = ensureUniqueNames({
|
|
1674
|
-
names: [params.provider.id],
|
|
1675
|
-
pluginId: params.pluginId,
|
|
1676
|
-
diagnostics: params.runtime.registry.diagnostics,
|
|
1677
|
-
source: params.source,
|
|
1678
|
-
owners: params.runtime.providerIdOwners,
|
|
1679
|
-
reserved: params.runtime.reservedProviderIds,
|
|
1680
|
-
kind: "provider"
|
|
1681
|
-
});
|
|
1682
|
-
if (accepted.length === 0) {
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
params.runtime.registry.providers.push({
|
|
1686
|
-
pluginId: params.pluginId,
|
|
1687
|
-
provider: params.provider,
|
|
1688
|
-
source: params.source
|
|
1689
|
-
});
|
|
1690
|
-
params.record.providerIds.push(accepted[0]);
|
|
1691
|
-
}
|
|
1692
|
-
function registerPluginWithApi(params) {
|
|
1693
|
-
const pluginRuntime = createPluginRuntime({ workspace: params.runtime.workspaceDir, config: params.runtime.config });
|
|
1694
|
-
const pluginLogger = buildPluginLogger(params.runtime.logger, params.pluginId);
|
|
1695
|
-
const pushUnsupported = (capability) => {
|
|
1696
|
-
params.runtime.registry.diagnostics.push({
|
|
1697
|
-
level: "warn",
|
|
1698
|
-
pluginId: params.pluginId,
|
|
1699
|
-
source: params.source,
|
|
1700
|
-
message: `${capability} is not supported by nextclaw compat layer yet`
|
|
1701
|
-
});
|
|
1702
|
-
pluginLogger.warn(`${capability} is ignored (not supported yet)`);
|
|
1703
|
-
};
|
|
1704
|
-
const api = {
|
|
1705
|
-
id: params.pluginId,
|
|
1706
|
-
name: params.record.name,
|
|
1707
|
-
version: params.record.version,
|
|
1708
|
-
description: params.record.description,
|
|
1709
|
-
source: params.source,
|
|
1710
|
-
config: params.runtime.config,
|
|
1711
|
-
pluginConfig: params.pluginConfig,
|
|
1712
|
-
runtime: pluginRuntime,
|
|
1713
|
-
logger: pluginLogger,
|
|
1714
|
-
registerTool: (tool, opts) => {
|
|
1715
|
-
registerPluginTool({
|
|
1716
|
-
runtime: params.runtime,
|
|
1717
|
-
record: params.record,
|
|
1718
|
-
pluginId: params.pluginId,
|
|
1719
|
-
source: params.source,
|
|
1720
|
-
tool,
|
|
1721
|
-
opts
|
|
1722
|
-
});
|
|
1723
|
-
},
|
|
1724
|
-
registerChannel: (registration) => {
|
|
1725
|
-
registerPluginChannel({
|
|
1726
|
-
runtime: params.runtime,
|
|
1727
|
-
record: params.record,
|
|
1728
|
-
pluginId: params.pluginId,
|
|
1729
|
-
source: params.source,
|
|
1730
|
-
registration
|
|
1731
|
-
});
|
|
1732
|
-
},
|
|
1733
|
-
registerProvider: (provider) => {
|
|
1734
|
-
registerPluginProvider({
|
|
1735
|
-
runtime: params.runtime,
|
|
1736
|
-
record: params.record,
|
|
1737
|
-
pluginId: params.pluginId,
|
|
1738
|
-
source: params.source,
|
|
1739
|
-
provider
|
|
1740
|
-
});
|
|
1741
|
-
},
|
|
1742
|
-
registerEngine: (factory, opts) => {
|
|
1743
|
-
const kind = opts?.kind?.trim().toLowerCase();
|
|
1744
|
-
if (!kind) {
|
|
1745
|
-
params.runtime.registry.diagnostics.push({
|
|
1746
|
-
level: "error",
|
|
1747
|
-
pluginId: params.pluginId,
|
|
1748
|
-
source: params.source,
|
|
1749
|
-
message: "registerEngine requires opts.kind"
|
|
1750
|
-
});
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
registerPluginEngine({
|
|
1754
|
-
runtime: params.runtime,
|
|
1755
|
-
record: params.record,
|
|
1756
|
-
pluginId: params.pluginId,
|
|
1757
|
-
source: params.source,
|
|
1758
|
-
kind,
|
|
1759
|
-
factory
|
|
1760
|
-
});
|
|
1761
|
-
},
|
|
1762
|
-
registerNcpAgentRuntime: (registration) => {
|
|
1763
|
-
registerPluginNcpAgentRuntime({
|
|
1764
|
-
runtime: params.runtime,
|
|
1765
|
-
record: params.record,
|
|
1766
|
-
pluginId: params.pluginId,
|
|
1767
|
-
source: params.source,
|
|
1768
|
-
registration
|
|
1769
|
-
});
|
|
1770
|
-
},
|
|
1771
|
-
registerHook: () => pushUnsupported("registerHook"),
|
|
1772
|
-
registerGatewayMethod: () => pushUnsupported("registerGatewayMethod"),
|
|
1773
|
-
registerCli: () => pushUnsupported("registerCli"),
|
|
1774
|
-
registerService: () => pushUnsupported("registerService"),
|
|
1775
|
-
registerCommand: () => pushUnsupported("registerCommand"),
|
|
1776
|
-
registerHttpHandler: () => pushUnsupported("registerHttpHandler"),
|
|
1777
|
-
registerHttpRoute: () => pushUnsupported("registerHttpRoute"),
|
|
1778
|
-
resolvePath: (input) => {
|
|
1779
|
-
const trimmed = input.trim();
|
|
1780
|
-
if (!trimmed) {
|
|
1781
|
-
return params.rootDir;
|
|
1782
|
-
}
|
|
1783
|
-
if (path5.isAbsolute(trimmed)) {
|
|
1784
|
-
return path5.resolve(expandHome2(trimmed));
|
|
1785
|
-
}
|
|
1786
|
-
return path5.resolve(params.rootDir, trimmed);
|
|
1787
|
-
}
|
|
1788
|
-
};
|
|
1789
|
-
try {
|
|
1790
|
-
const result = params.register(api);
|
|
1791
|
-
if (result && typeof result === "object" && "then" in result) {
|
|
1792
|
-
params.runtime.registry.diagnostics.push({
|
|
1793
|
-
level: "warn",
|
|
1794
|
-
pluginId: params.pluginId,
|
|
1795
|
-
source: params.source,
|
|
1796
|
-
message: "plugin register returned a promise; async registration is ignored"
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
return { ok: true };
|
|
1800
|
-
} catch (err) {
|
|
1801
|
-
const error = `plugin failed during register: ${String(err)}`;
|
|
1802
|
-
return { ok: false, error };
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
// src/plugins/loader.ts
|
|
1807
|
-
var createJiti = createJitiImport;
|
|
1808
|
-
var defaultLogger2 = {
|
|
1809
|
-
info: (message) => console.log(message),
|
|
1810
|
-
warn: (message) => console.warn(message),
|
|
1811
|
-
error: (message) => console.error(message),
|
|
1812
|
-
debug: (message) => console.debug(message)
|
|
1813
|
-
};
|
|
1814
|
-
var BUNDLED_CHANNEL_PLUGIN_PACKAGES = [
|
|
1815
|
-
"@nextclaw/channel-plugin-telegram",
|
|
1816
|
-
"@nextclaw/channel-plugin-whatsapp",
|
|
1817
|
-
"@nextclaw/channel-plugin-discord",
|
|
1818
|
-
"@nextclaw/channel-plugin-feishu",
|
|
1819
|
-
"@nextclaw/channel-plugin-mochat",
|
|
1820
|
-
"@nextclaw/channel-plugin-dingtalk",
|
|
1821
|
-
"@nextclaw/channel-plugin-wecom",
|
|
1822
|
-
"@nextclaw/channel-plugin-email",
|
|
1823
|
-
"@nextclaw/channel-plugin-slack",
|
|
1824
|
-
"@nextclaw/channel-plugin-qq"
|
|
1825
|
-
];
|
|
1826
|
-
function resolvePackageRootFromEntry(entryFile) {
|
|
1827
|
-
let cursor = path6.dirname(entryFile);
|
|
1828
|
-
for (let i = 0; i < 8; i += 1) {
|
|
1829
|
-
const candidate = path6.join(cursor, "package.json");
|
|
1830
|
-
if (fs5.existsSync(candidate)) {
|
|
1831
|
-
return cursor;
|
|
1832
|
-
}
|
|
1833
|
-
const parent = path6.dirname(cursor);
|
|
1834
|
-
if (parent === cursor) {
|
|
1835
|
-
break;
|
|
1836
|
-
}
|
|
1837
|
-
cursor = parent;
|
|
1838
|
-
}
|
|
1839
|
-
return path6.dirname(entryFile);
|
|
1840
|
-
}
|
|
1841
|
-
function resolvePluginSdkAliasFile(params) {
|
|
1842
|
-
try {
|
|
1843
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
1844
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
1845
|
-
let cursor = path6.dirname(modulePath);
|
|
1846
|
-
for (let i = 0; i < 6; i += 1) {
|
|
1847
|
-
const srcCandidate = path6.join(cursor, "src", "plugin-sdk", params.srcFile);
|
|
1848
|
-
const distCandidate = path6.join(cursor, "dist", "plugin-sdk", params.distFile);
|
|
1849
|
-
const candidates = isProduction ? [distCandidate, srcCandidate] : [srcCandidate, distCandidate];
|
|
1850
|
-
for (const candidate of candidates) {
|
|
1851
|
-
if (fs5.existsSync(candidate)) {
|
|
1852
|
-
return candidate;
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
const parent = path6.dirname(cursor);
|
|
1856
|
-
if (parent === cursor) {
|
|
1857
|
-
break;
|
|
1858
|
-
}
|
|
1859
|
-
cursor = parent;
|
|
1860
|
-
}
|
|
1861
|
-
} catch {
|
|
1862
|
-
return null;
|
|
1863
|
-
}
|
|
1864
|
-
return null;
|
|
1865
|
-
}
|
|
1866
|
-
function resolvePluginSdkAlias() {
|
|
1867
|
-
return resolvePluginSdkAliasFile({ srcFile: "index.ts", distFile: "index.js" });
|
|
1868
|
-
}
|
|
1869
|
-
function buildScopedPackageAliases(scope) {
|
|
1870
|
-
const aliases = {};
|
|
1871
|
-
const require2 = createRequire(import.meta.url);
|
|
1872
|
-
let cursor = path6.dirname(fileURLToPath(import.meta.url));
|
|
1873
|
-
for (let i = 0; i < 8; i += 1) {
|
|
1874
|
-
const scopeDir = path6.join(cursor, "node_modules", scope);
|
|
1875
|
-
if (fs5.existsSync(scopeDir)) {
|
|
1876
|
-
let entries = [];
|
|
1877
|
-
try {
|
|
1878
|
-
entries = fs5.readdirSync(scopeDir, { withFileTypes: true });
|
|
1879
|
-
} catch {
|
|
1880
|
-
entries = [];
|
|
1881
|
-
}
|
|
1882
|
-
for (const entry of entries) {
|
|
1883
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
1884
|
-
continue;
|
|
1885
|
-
}
|
|
1886
|
-
const packageName = `${scope}/${entry.name}`;
|
|
1887
|
-
try {
|
|
1888
|
-
aliases[packageName] = require2.resolve(packageName);
|
|
1889
|
-
} catch {
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
const parent = path6.dirname(cursor);
|
|
1894
|
-
if (parent === cursor) {
|
|
1895
|
-
break;
|
|
1896
|
-
}
|
|
1897
|
-
cursor = parent;
|
|
1898
|
-
}
|
|
1899
|
-
return aliases;
|
|
1900
|
-
}
|
|
1901
|
-
function buildPluginLoaderAliases() {
|
|
1902
|
-
const aliases = buildScopedPackageAliases("@nextclaw");
|
|
1903
|
-
const pluginSdkAlias = resolvePluginSdkAlias();
|
|
1904
|
-
if (pluginSdkAlias) {
|
|
1905
|
-
aliases["openclaw/plugin-sdk"] = pluginSdkAlias;
|
|
1906
|
-
}
|
|
1907
|
-
return aliases;
|
|
1908
|
-
}
|
|
1909
|
-
function resolvePluginModuleExport(moduleExport) {
|
|
1910
|
-
const resolved = moduleExport && typeof moduleExport === "object" && "default" in moduleExport ? moduleExport.default : moduleExport;
|
|
1911
|
-
if (typeof resolved === "function") {
|
|
1912
|
-
return {
|
|
1913
|
-
register: resolved
|
|
1914
|
-
};
|
|
1915
|
-
}
|
|
1916
|
-
if (resolved && typeof resolved === "object") {
|
|
1917
|
-
const definition = resolved;
|
|
1918
|
-
return {
|
|
1919
|
-
definition,
|
|
1920
|
-
register: definition.register ?? definition.activate
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
return {};
|
|
1924
|
-
}
|
|
1925
|
-
function appendBundledChannelPlugins(params) {
|
|
1926
|
-
const require2 = createRequire(import.meta.url);
|
|
1927
|
-
for (const packageName of BUNDLED_CHANNEL_PLUGIN_PACKAGES) {
|
|
1928
|
-
let entryFile = "";
|
|
1929
|
-
let rootDir = "";
|
|
1930
|
-
try {
|
|
1931
|
-
entryFile = require2.resolve(packageName);
|
|
1932
|
-
rootDir = resolvePackageRootFromEntry(entryFile);
|
|
1933
|
-
} catch (err) {
|
|
1934
|
-
params.registry.diagnostics.push({
|
|
1935
|
-
level: "error",
|
|
1936
|
-
source: packageName,
|
|
1937
|
-
message: `bundled plugin package not resolvable: ${String(err)}`
|
|
1938
|
-
});
|
|
1939
|
-
continue;
|
|
1940
|
-
}
|
|
1941
|
-
let moduleExport = null;
|
|
1942
|
-
try {
|
|
1943
|
-
moduleExport = params.jiti(entryFile);
|
|
1944
|
-
} catch (err) {
|
|
1945
|
-
params.registry.diagnostics.push({
|
|
1946
|
-
level: "error",
|
|
1947
|
-
source: entryFile,
|
|
1948
|
-
message: `failed to load bundled plugin: ${String(err)}`
|
|
1949
|
-
});
|
|
1950
|
-
continue;
|
|
1951
|
-
}
|
|
1952
|
-
const resolved = resolvePluginModuleExport(moduleExport);
|
|
1953
|
-
const definition = resolved.definition;
|
|
1954
|
-
const register = resolved.register;
|
|
1955
|
-
const pluginId = typeof definition?.id === "string" ? definition.id.trim() : "";
|
|
1956
|
-
const source = entryFile;
|
|
1957
|
-
if (!pluginId) {
|
|
1958
|
-
params.registry.diagnostics.push({
|
|
1959
|
-
level: "error",
|
|
1960
|
-
source,
|
|
1961
|
-
message: "bundled plugin definition missing id"
|
|
1962
|
-
});
|
|
1963
|
-
continue;
|
|
1964
|
-
}
|
|
1965
|
-
const enableState = resolveEnableState(pluginId, params.normalizedConfig);
|
|
1966
|
-
const record = createPluginRecord({
|
|
1967
|
-
id: pluginId,
|
|
1968
|
-
name: definition?.name ?? pluginId,
|
|
1969
|
-
description: definition?.description,
|
|
1970
|
-
version: definition?.version,
|
|
1971
|
-
kind: definition?.kind,
|
|
1972
|
-
source,
|
|
1973
|
-
origin: "bundled",
|
|
1974
|
-
workspaceDir: params.runtime.workspaceDir,
|
|
1975
|
-
enabled: enableState.enabled,
|
|
1976
|
-
configSchema: Boolean(definition?.configSchema),
|
|
1977
|
-
configJsonSchema: definition?.configSchema
|
|
1978
|
-
});
|
|
1979
|
-
if (!enableState.enabled) {
|
|
1980
|
-
record.status = "disabled";
|
|
1981
|
-
record.error = enableState.reason;
|
|
1982
|
-
params.registry.plugins.push(record);
|
|
1983
|
-
continue;
|
|
1984
|
-
}
|
|
1985
|
-
if (typeof register !== "function") {
|
|
1986
|
-
record.status = "error";
|
|
1987
|
-
record.error = "plugin export missing register/activate";
|
|
1988
|
-
params.registry.plugins.push(record);
|
|
1989
|
-
params.registry.diagnostics.push({
|
|
1990
|
-
level: "error",
|
|
1991
|
-
pluginId,
|
|
1992
|
-
source,
|
|
1993
|
-
message: record.error
|
|
1994
|
-
});
|
|
1995
|
-
continue;
|
|
1996
|
-
}
|
|
1997
|
-
const result = registerPluginWithApi({
|
|
1998
|
-
runtime: params.runtime,
|
|
1999
|
-
record,
|
|
2000
|
-
pluginId,
|
|
2001
|
-
source,
|
|
2002
|
-
rootDir,
|
|
2003
|
-
register,
|
|
2004
|
-
pluginConfig: void 0
|
|
2005
|
-
});
|
|
2006
|
-
if (!result.ok) {
|
|
2007
|
-
record.status = "error";
|
|
2008
|
-
record.error = result.error;
|
|
2009
|
-
params.registry.diagnostics.push({
|
|
2010
|
-
level: "error",
|
|
2011
|
-
pluginId,
|
|
2012
|
-
source,
|
|
2013
|
-
message: result.error
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
params.registry.plugins.push(record);
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
function loadOpenClawPlugins(options) {
|
|
2020
|
-
const loadExternalPlugins = process.env.NEXTCLAW_ENABLE_OPENCLAW_PLUGINS !== "0";
|
|
2021
|
-
const logger = options.logger ?? defaultLogger2;
|
|
2022
|
-
const workspaceDir = options.workspaceDir?.trim() || getWorkspacePathFromConfig(options.config);
|
|
2023
|
-
const normalized = normalizePluginsConfig(options.config.plugins);
|
|
2024
|
-
const mode = options.mode ?? "full";
|
|
2025
|
-
const registry = {
|
|
2026
|
-
plugins: [],
|
|
2027
|
-
tools: [],
|
|
2028
|
-
channels: [],
|
|
2029
|
-
providers: [],
|
|
2030
|
-
engines: [],
|
|
2031
|
-
ncpAgentRuntimes: [],
|
|
2032
|
-
diagnostics: [],
|
|
2033
|
-
resolvedTools: []
|
|
2034
|
-
};
|
|
2035
|
-
const reservedToolNames = new Set(options.reservedToolNames ?? []);
|
|
2036
|
-
const reservedChannelIds = new Set(options.reservedChannelIds ?? []);
|
|
2037
|
-
const reservedProviderIds = new Set(options.reservedProviderIds ?? []);
|
|
2038
|
-
const reservedEngineKinds = new Set((options.reservedEngineKinds ?? ["native"]).map((entry) => entry.toLowerCase()));
|
|
2039
|
-
const reservedNcpAgentRuntimeKinds = new Set(
|
|
2040
|
-
(options.reservedNcpAgentRuntimeKinds ?? ["native"]).map((entry) => entry.toLowerCase())
|
|
2041
|
-
);
|
|
2042
|
-
const registerRuntime = createPluginRegisterRuntime({
|
|
2043
|
-
config: options.config,
|
|
2044
|
-
workspaceDir,
|
|
2045
|
-
logger,
|
|
2046
|
-
registry,
|
|
2047
|
-
reservedToolNames,
|
|
2048
|
-
reservedChannelIds,
|
|
2049
|
-
reservedProviderIds,
|
|
2050
|
-
reservedEngineKinds,
|
|
2051
|
-
reservedNcpAgentRuntimeKinds
|
|
2052
|
-
});
|
|
2053
|
-
const jiti = createJiti(import.meta.url, {
|
|
2054
|
-
interopDefault: true,
|
|
2055
|
-
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
|
|
2056
|
-
alias: buildPluginLoaderAliases()
|
|
2057
|
-
});
|
|
2058
|
-
appendBundledChannelPlugins({
|
|
2059
|
-
registry,
|
|
2060
|
-
runtime: registerRuntime,
|
|
2061
|
-
jiti,
|
|
2062
|
-
normalizedConfig: normalized
|
|
2063
|
-
});
|
|
2064
|
-
if (!loadExternalPlugins) {
|
|
2065
|
-
return registry;
|
|
2066
|
-
}
|
|
2067
|
-
const discovery = discoverOpenClawPlugins({
|
|
2068
|
-
config: options.config,
|
|
2069
|
-
workspaceDir,
|
|
2070
|
-
extraPaths: normalized.loadPaths
|
|
2071
|
-
});
|
|
2072
|
-
const filteredCandidates = filterPluginCandidatesByExcludedRoots(discovery.candidates, options.excludeRoots ?? []);
|
|
2073
|
-
const manifestRegistry = loadPluginManifestRegistry({
|
|
2074
|
-
config: options.config,
|
|
2075
|
-
workspaceDir,
|
|
2076
|
-
candidates: filteredCandidates,
|
|
2077
|
-
diagnostics: discovery.diagnostics
|
|
2078
|
-
});
|
|
2079
|
-
registry.diagnostics.push(...manifestRegistry.diagnostics);
|
|
2080
|
-
const manifestByRoot = new Map(manifestRegistry.plugins.map((entry) => [entry.rootDir, entry]));
|
|
2081
|
-
const seenIds = new Map(
|
|
2082
|
-
registry.plugins.map((entry) => [entry.id, entry.origin])
|
|
2083
|
-
);
|
|
2084
|
-
for (const candidate of filteredCandidates) {
|
|
2085
|
-
const manifest = manifestByRoot.get(candidate.rootDir);
|
|
2086
|
-
if (!manifest) {
|
|
2087
|
-
continue;
|
|
2088
|
-
}
|
|
2089
|
-
const pluginId = manifest.id;
|
|
2090
|
-
const existingOrigin = seenIds.get(pluginId);
|
|
2091
|
-
if (existingOrigin) {
|
|
2092
|
-
const record2 = createPluginRecord({
|
|
2093
|
-
id: pluginId,
|
|
2094
|
-
name: manifest.name ?? pluginId,
|
|
2095
|
-
description: manifest.description,
|
|
2096
|
-
version: manifest.version,
|
|
2097
|
-
kind: manifest.kind,
|
|
2098
|
-
source: candidate.source,
|
|
2099
|
-
origin: candidate.origin,
|
|
2100
|
-
workspaceDir: candidate.workspaceDir,
|
|
2101
|
-
enabled: false,
|
|
2102
|
-
configSchema: Boolean(manifest.configSchema),
|
|
2103
|
-
configUiHints: manifest.configUiHints,
|
|
2104
|
-
configJsonSchema: manifest.configSchema
|
|
2105
|
-
});
|
|
2106
|
-
record2.status = "disabled";
|
|
2107
|
-
record2.error = `overridden by ${existingOrigin} plugin`;
|
|
2108
|
-
registry.plugins.push(record2);
|
|
2109
|
-
continue;
|
|
2110
|
-
}
|
|
2111
|
-
const enableState = resolveEnableState(pluginId, normalized);
|
|
2112
|
-
const entry = normalized.entries[pluginId];
|
|
2113
|
-
const record = createPluginRecord({
|
|
2114
|
-
id: pluginId,
|
|
2115
|
-
name: manifest.name ?? pluginId,
|
|
2116
|
-
description: manifest.description,
|
|
2117
|
-
version: manifest.version,
|
|
2118
|
-
kind: manifest.kind,
|
|
2119
|
-
source: candidate.source,
|
|
2120
|
-
origin: candidate.origin,
|
|
2121
|
-
workspaceDir: candidate.workspaceDir,
|
|
2122
|
-
enabled: enableState.enabled,
|
|
2123
|
-
configSchema: Boolean(manifest.configSchema),
|
|
2124
|
-
configUiHints: manifest.configUiHints,
|
|
2125
|
-
configJsonSchema: manifest.configSchema
|
|
2126
|
-
});
|
|
2127
|
-
if (!enableState.enabled) {
|
|
2128
|
-
record.status = "disabled";
|
|
2129
|
-
record.error = enableState.reason;
|
|
2130
|
-
registry.plugins.push(record);
|
|
2131
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
if (!manifest.configSchema) {
|
|
2135
|
-
record.status = "error";
|
|
2136
|
-
record.error = "missing config schema";
|
|
2137
|
-
registry.plugins.push(record);
|
|
2138
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2139
|
-
registry.diagnostics.push({
|
|
2140
|
-
level: "error",
|
|
2141
|
-
pluginId,
|
|
2142
|
-
source: candidate.source,
|
|
2143
|
-
message: record.error
|
|
2144
|
-
});
|
|
2145
|
-
continue;
|
|
2146
|
-
}
|
|
2147
|
-
const validatedConfig = validatePluginConfig({
|
|
2148
|
-
schema: manifest.configSchema,
|
|
2149
|
-
cacheKey: manifest.schemaCacheKey,
|
|
2150
|
-
value: entry?.config
|
|
2151
|
-
});
|
|
2152
|
-
if (!validatedConfig.ok) {
|
|
2153
|
-
record.status = "error";
|
|
2154
|
-
record.error = `invalid config: ${validatedConfig.errors.join(", ")}`;
|
|
2155
|
-
registry.plugins.push(record);
|
|
2156
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2157
|
-
registry.diagnostics.push({
|
|
2158
|
-
level: "error",
|
|
2159
|
-
pluginId,
|
|
2160
|
-
source: candidate.source,
|
|
2161
|
-
message: record.error
|
|
2162
|
-
});
|
|
2163
|
-
continue;
|
|
2164
|
-
}
|
|
2165
|
-
if (mode === "validate") {
|
|
2166
|
-
registry.plugins.push(record);
|
|
2167
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2168
|
-
continue;
|
|
2169
|
-
}
|
|
2170
|
-
let moduleExport = null;
|
|
2171
|
-
try {
|
|
2172
|
-
moduleExport = jiti(candidate.source);
|
|
2173
|
-
} catch (err) {
|
|
2174
|
-
record.status = "error";
|
|
2175
|
-
record.error = `failed to load plugin: ${String(err)}`;
|
|
2176
|
-
registry.plugins.push(record);
|
|
2177
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2178
|
-
registry.diagnostics.push({
|
|
2179
|
-
level: "error",
|
|
2180
|
-
pluginId,
|
|
2181
|
-
source: candidate.source,
|
|
2182
|
-
message: record.error
|
|
2183
|
-
});
|
|
2184
|
-
continue;
|
|
2185
|
-
}
|
|
2186
|
-
const resolved = resolvePluginModuleExport(moduleExport);
|
|
2187
|
-
const definition = resolved.definition;
|
|
2188
|
-
const register = resolved.register;
|
|
2189
|
-
if (definition?.id && definition.id !== pluginId) {
|
|
2190
|
-
registry.diagnostics.push({
|
|
2191
|
-
level: "warn",
|
|
2192
|
-
pluginId,
|
|
2193
|
-
source: candidate.source,
|
|
2194
|
-
message: `plugin id mismatch (manifest uses "${pluginId}", export uses "${definition.id}")`
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
|
-
record.name = definition?.name ?? record.name;
|
|
2198
|
-
record.description = definition?.description ?? record.description;
|
|
2199
|
-
record.version = definition?.version ?? record.version;
|
|
2200
|
-
record.kind = definition?.kind ?? record.kind;
|
|
2201
|
-
if (typeof register !== "function") {
|
|
2202
|
-
record.status = "error";
|
|
2203
|
-
record.error = "plugin export missing register/activate";
|
|
2204
|
-
registry.plugins.push(record);
|
|
2205
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2206
|
-
registry.diagnostics.push({
|
|
2207
|
-
level: "error",
|
|
2208
|
-
pluginId,
|
|
2209
|
-
source: candidate.source,
|
|
2210
|
-
message: record.error
|
|
2211
|
-
});
|
|
2212
|
-
continue;
|
|
2213
|
-
}
|
|
2214
|
-
const registerResult = registerPluginWithApi({
|
|
2215
|
-
runtime: registerRuntime,
|
|
2216
|
-
record,
|
|
2217
|
-
pluginId,
|
|
2218
|
-
source: candidate.source,
|
|
2219
|
-
rootDir: candidate.rootDir,
|
|
2220
|
-
register,
|
|
2221
|
-
pluginConfig: validatedConfig.value
|
|
2222
|
-
});
|
|
2223
|
-
if (!registerResult.ok) {
|
|
2224
|
-
record.status = "error";
|
|
2225
|
-
record.error = registerResult.error;
|
|
2226
|
-
registry.diagnostics.push({
|
|
2227
|
-
level: "error",
|
|
2228
|
-
pluginId,
|
|
2229
|
-
source: candidate.source,
|
|
2230
|
-
message: registerResult.error
|
|
2231
|
-
});
|
|
2232
|
-
registry.plugins.push(record);
|
|
2233
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2234
|
-
continue;
|
|
2235
|
-
}
|
|
2236
|
-
registry.plugins.push(record);
|
|
2237
|
-
seenIds.set(pluginId, candidate.origin);
|
|
2238
|
-
}
|
|
2239
|
-
return registry;
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
// src/plugins/status.ts
|
|
2243
|
-
import { getWorkspacePathFromConfig as getWorkspacePathFromConfig2 } from "@nextclaw/core";
|
|
2244
|
-
function buildPluginStatusReport(params) {
|
|
2245
|
-
const workspaceDir = params.workspaceDir?.trim() || getWorkspacePathFromConfig2(params.config);
|
|
2246
|
-
const registry = loadOpenClawPlugins({
|
|
2247
|
-
config: params.config,
|
|
2248
|
-
workspaceDir,
|
|
2249
|
-
logger: params.logger,
|
|
2250
|
-
reservedToolNames: params.reservedToolNames,
|
|
2251
|
-
reservedChannelIds: params.reservedChannelIds,
|
|
2252
|
-
reservedProviderIds: params.reservedProviderIds,
|
|
2253
|
-
reservedEngineKinds: params.reservedEngineKinds,
|
|
2254
|
-
reservedNcpAgentRuntimeKinds: params.reservedNcpAgentRuntimeKinds
|
|
2255
|
-
});
|
|
2256
|
-
return {
|
|
2257
|
-
workspaceDir,
|
|
2258
|
-
...registry
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
// src/plugins/uninstall.ts
|
|
2263
|
-
import fs6 from "fs/promises";
|
|
2264
|
-
import { existsSync, statSync } from "fs";
|
|
2265
|
-
import path7 from "path";
|
|
2266
|
-
import { getWorkspacePathFromConfig as getWorkspacePathFromConfig3 } from "@nextclaw/core";
|
|
2267
|
-
function isLinkedPathInstall(record) {
|
|
2268
|
-
if (!record || record.source !== "path") {
|
|
2269
|
-
return false;
|
|
2270
|
-
}
|
|
2271
|
-
if (!record.sourcePath || !record.installPath) {
|
|
2272
|
-
return true;
|
|
2273
|
-
}
|
|
2274
|
-
return path7.resolve(record.sourcePath) === path7.resolve(record.installPath);
|
|
2275
|
-
}
|
|
2276
|
-
function pushUniquePath(targets, candidate) {
|
|
2277
|
-
if (!candidate) {
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
const resolved = path7.resolve(candidate);
|
|
2281
|
-
if (!targets.includes(resolved)) {
|
|
2282
|
-
targets.push(resolved);
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
function resolveUninstallDirectoryTarget(params) {
|
|
2286
|
-
if (!params.hasInstall) {
|
|
2287
|
-
return null;
|
|
2288
|
-
}
|
|
2289
|
-
if (isLinkedPathInstall(params.installRecord)) {
|
|
2290
|
-
return null;
|
|
2291
|
-
}
|
|
2292
|
-
let defaultPath;
|
|
2293
|
-
try {
|
|
2294
|
-
defaultPath = resolvePluginInstallDir(params.pluginId, params.extensionsDir);
|
|
2295
|
-
} catch {
|
|
2296
|
-
return null;
|
|
2297
|
-
}
|
|
2298
|
-
const configuredPath = params.installRecord?.installPath;
|
|
2299
|
-
if (!configuredPath) {
|
|
2300
|
-
return defaultPath;
|
|
2301
|
-
}
|
|
2302
|
-
if (path7.resolve(configuredPath) === path7.resolve(defaultPath)) {
|
|
2303
|
-
return configuredPath;
|
|
2304
|
-
}
|
|
2305
|
-
return defaultPath;
|
|
2306
|
-
}
|
|
2307
|
-
function resolveUninstallDirectoryTargets(params) {
|
|
2308
|
-
if (!params.hasInstall || isLinkedPathInstall(params.installRecord)) {
|
|
2309
|
-
return [];
|
|
2310
|
-
}
|
|
2311
|
-
const targets = [];
|
|
2312
|
-
pushUniquePath(
|
|
2313
|
-
targets,
|
|
2314
|
-
resolveUninstallDirectoryTarget({
|
|
2315
|
-
pluginId: params.pluginId,
|
|
2316
|
-
hasInstall: params.hasInstall,
|
|
2317
|
-
installRecord: params.installRecord,
|
|
2318
|
-
extensionsDir: params.extensionsDir
|
|
2319
|
-
})
|
|
2320
|
-
);
|
|
2321
|
-
pushUniquePath(targets, params.installRecord?.installPath);
|
|
2322
|
-
const workspaceDir = getWorkspacePathFromConfig3(params.config);
|
|
2323
|
-
pushUniquePath(targets, path7.join(workspaceDir, ".nextclaw", "extensions", params.pluginId));
|
|
2324
|
-
return targets;
|
|
2325
|
-
}
|
|
2326
|
-
function removePluginFromConfig(config, pluginId) {
|
|
2327
|
-
const actions = {
|
|
2328
|
-
entry: false,
|
|
2329
|
-
install: false,
|
|
2330
|
-
allowlist: false,
|
|
2331
|
-
loadPath: false
|
|
2332
|
-
};
|
|
2333
|
-
const pluginsConfig = config.plugins ?? {};
|
|
2334
|
-
let entries = pluginsConfig.entries;
|
|
2335
|
-
if (entries && pluginId in entries) {
|
|
2336
|
-
const rest = { ...entries };
|
|
2337
|
-
delete rest[pluginId];
|
|
2338
|
-
entries = Object.keys(rest).length > 0 ? rest : void 0;
|
|
2339
|
-
actions.entry = true;
|
|
2340
|
-
}
|
|
2341
|
-
let installs = pluginsConfig.installs;
|
|
2342
|
-
const installRecord = installs?.[pluginId];
|
|
2343
|
-
if (installs && pluginId in installs) {
|
|
2344
|
-
const rest = { ...installs };
|
|
2345
|
-
delete rest[pluginId];
|
|
2346
|
-
installs = Object.keys(rest).length > 0 ? rest : void 0;
|
|
2347
|
-
actions.install = true;
|
|
2348
|
-
}
|
|
2349
|
-
let allow = pluginsConfig.allow;
|
|
2350
|
-
if (Array.isArray(allow) && allow.includes(pluginId)) {
|
|
2351
|
-
allow = allow.filter((id) => id !== pluginId);
|
|
2352
|
-
if (allow.length === 0) {
|
|
2353
|
-
allow = void 0;
|
|
2354
|
-
}
|
|
2355
|
-
actions.allowlist = true;
|
|
2356
|
-
}
|
|
2357
|
-
let load = pluginsConfig.load;
|
|
2358
|
-
const configuredLoadPaths = Array.isArray(load?.paths) ? load.paths : [];
|
|
2359
|
-
const nextLoadPaths = configuredLoadPaths.filter((entry) => !matchesPluginLoadPath(entry, pluginId));
|
|
2360
|
-
if (nextLoadPaths.length !== configuredLoadPaths.length) {
|
|
2361
|
-
load = nextLoadPaths.length > 0 ? { ...load, paths: nextLoadPaths } : void 0;
|
|
2362
|
-
actions.loadPath = true;
|
|
2363
|
-
} else if (installRecord?.source === "path" && installRecord.sourcePath && configuredLoadPaths.includes(installRecord.sourcePath)) {
|
|
2364
|
-
const filteredLoadPaths = configuredLoadPaths.filter((entry) => entry !== installRecord.sourcePath);
|
|
2365
|
-
load = filteredLoadPaths.length > 0 ? { ...load, paths: filteredLoadPaths } : void 0;
|
|
2366
|
-
actions.loadPath = true;
|
|
2367
|
-
}
|
|
2368
|
-
const nextPlugins = {
|
|
2369
|
-
...pluginsConfig
|
|
2370
|
-
};
|
|
2371
|
-
if (entries === void 0) {
|
|
2372
|
-
delete nextPlugins.entries;
|
|
2373
|
-
} else {
|
|
2374
|
-
nextPlugins.entries = entries;
|
|
2375
|
-
}
|
|
2376
|
-
if (installs === void 0) {
|
|
2377
|
-
delete nextPlugins.installs;
|
|
2378
|
-
} else {
|
|
2379
|
-
nextPlugins.installs = installs;
|
|
2380
|
-
}
|
|
2381
|
-
if (allow === void 0) {
|
|
2382
|
-
delete nextPlugins.allow;
|
|
2383
|
-
} else {
|
|
2384
|
-
nextPlugins.allow = allow;
|
|
2385
|
-
}
|
|
2386
|
-
if (load === void 0) {
|
|
2387
|
-
delete nextPlugins.load;
|
|
2388
|
-
} else {
|
|
2389
|
-
nextPlugins.load = load;
|
|
2390
|
-
}
|
|
2391
|
-
return {
|
|
2392
|
-
config: {
|
|
2393
|
-
...config,
|
|
2394
|
-
plugins: nextPlugins
|
|
2395
|
-
},
|
|
2396
|
-
actions
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
function matchesPluginLoadPath(rawPath, pluginId) {
|
|
2400
|
-
const normalizedPath = rawPath.trim();
|
|
2401
|
-
if (!normalizedPath) {
|
|
2402
|
-
return false;
|
|
2403
|
-
}
|
|
2404
|
-
const resolvedPath = path7.resolve(normalizedPath);
|
|
2405
|
-
if (!existsSync(resolvedPath)) {
|
|
2406
|
-
return false;
|
|
2407
|
-
}
|
|
2408
|
-
const candidateRoot = (() => {
|
|
2409
|
-
try {
|
|
2410
|
-
return statSync(resolvedPath).isDirectory() ? resolvedPath : path7.dirname(resolvedPath);
|
|
2411
|
-
} catch {
|
|
2412
|
-
return null;
|
|
2413
|
-
}
|
|
2414
|
-
})();
|
|
2415
|
-
if (!candidateRoot) {
|
|
2416
|
-
return false;
|
|
2417
|
-
}
|
|
2418
|
-
const manifest = loadPluginManifest(candidateRoot);
|
|
2419
|
-
return manifest.ok && manifest.manifest.id === pluginId;
|
|
2420
|
-
}
|
|
2421
|
-
async function uninstallPlugin(params) {
|
|
2422
|
-
const { config, pluginId, deleteFiles = true, extensionsDir } = params;
|
|
2423
|
-
const hasEntry = pluginId in (config.plugins.entries ?? {});
|
|
2424
|
-
const hasInstall = pluginId in (config.plugins.installs ?? {});
|
|
2425
|
-
if (!hasEntry && !hasInstall) {
|
|
2426
|
-
return { ok: false, error: `Plugin not found: ${pluginId}` };
|
|
2427
|
-
}
|
|
2428
|
-
const installRecord = config.plugins.installs?.[pluginId];
|
|
2429
|
-
const isLinked = isLinkedPathInstall(installRecord);
|
|
2430
|
-
const { config: nextConfig, actions: configActions } = removePluginFromConfig(config, pluginId);
|
|
2431
|
-
const actions = {
|
|
2432
|
-
...configActions,
|
|
2433
|
-
directory: false
|
|
2434
|
-
};
|
|
2435
|
-
const warnings = [];
|
|
2436
|
-
const deleteTargets = deleteFiles && !isLinked ? resolveUninstallDirectoryTargets({
|
|
2437
|
-
config,
|
|
2438
|
-
pluginId,
|
|
2439
|
-
hasInstall,
|
|
2440
|
-
installRecord,
|
|
2441
|
-
extensionsDir
|
|
2442
|
-
}) : [];
|
|
2443
|
-
for (const deleteTarget of deleteTargets) {
|
|
2444
|
-
const existed = await fs6.access(deleteTarget).then(() => true).catch(() => false);
|
|
2445
|
-
try {
|
|
2446
|
-
await fs6.rm(deleteTarget, { recursive: true, force: true });
|
|
2447
|
-
actions.directory = actions.directory || existed;
|
|
2448
|
-
} catch (error) {
|
|
2449
|
-
warnings.push(
|
|
2450
|
-
`Failed to remove plugin directory ${deleteTarget}: ${error instanceof Error ? error.message : String(error)}`
|
|
2451
|
-
);
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
return {
|
|
2455
|
-
ok: true,
|
|
2456
|
-
config: nextConfig,
|
|
2457
|
-
pluginId,
|
|
2458
|
-
actions,
|
|
2459
|
-
warnings
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
export {
|
|
2463
|
-
DEFAULT_ACCOUNT_ID,
|
|
2464
|
-
PLUGIN_MANIFEST_FILENAME,
|
|
2465
|
-
PLUGIN_MANIFEST_FILENAMES,
|
|
2466
|
-
__nextclawPluginSdkCompat,
|
|
2467
|
-
addPluginLoadPath,
|
|
2468
|
-
buildChannelConfigSchema,
|
|
2469
|
-
buildOauthProviderAuthResult,
|
|
2470
|
-
buildPluginLoaderAliases,
|
|
2471
|
-
buildPluginStatusReport,
|
|
2472
|
-
createNextclawBuiltinChannelPlugin,
|
|
2473
|
-
createPluginRegisterRuntime,
|
|
2474
|
-
createPluginRuntime,
|
|
2475
|
-
disablePluginInConfig,
|
|
2476
|
-
discoverOpenClawPlugins,
|
|
2477
|
-
emptyPluginConfigSchema,
|
|
2478
|
-
enablePluginInConfig,
|
|
2479
|
-
getPackageManifestMetadata,
|
|
2480
|
-
getPluginChannelBindings,
|
|
2481
|
-
getPluginUiMetadataFromRegistry,
|
|
2482
|
-
installPluginFromArchive,
|
|
2483
|
-
installPluginFromDir,
|
|
2484
|
-
installPluginFromFile,
|
|
2485
|
-
installPluginFromNpmSpec,
|
|
2486
|
-
installPluginFromPath,
|
|
2487
|
-
loadOpenClawPlugins,
|
|
2488
|
-
loadPluginManifest,
|
|
2489
|
-
loadPluginManifestRegistry,
|
|
2490
|
-
loadPluginUiMetadata,
|
|
2491
|
-
normalizeAccountId,
|
|
2492
|
-
normalizePluginHttpPath,
|
|
2493
|
-
normalizePluginsConfig,
|
|
2494
|
-
recordPluginInstall,
|
|
2495
|
-
registerPluginWithApi,
|
|
2496
|
-
removePluginFromConfig,
|
|
2497
|
-
resolveEnableState,
|
|
2498
|
-
resolvePluginChannelMessageToolHints,
|
|
2499
|
-
resolvePluginInstallDir,
|
|
2500
|
-
resolvePluginManifestPath,
|
|
2501
|
-
resolveUninstallDirectoryTarget,
|
|
2502
|
-
resolveUninstallDirectoryTargets,
|
|
2503
|
-
setPluginRuntimeBridge,
|
|
2504
|
-
sleep,
|
|
2505
|
-
startPluginChannelGateways,
|
|
2506
|
-
stopPluginChannelGateways,
|
|
2507
|
-
toPluginUiMetadata,
|
|
2508
|
-
uninstallPlugin,
|
|
2509
|
-
validateJsonSchemaValue
|
|
2510
|
-
};
|