@nextclaw/openclaw-compat 0.3.6 → 0.3.8

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