@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.
Files changed (3) hide show
  1. package/package.json +13 -13
  2. package/dist/index.d.ts +0 -678
  3. 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
- };