@nextclaw/openclaw-compat 0.1.1

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