nextclaw 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import { APP_NAME as APP_NAME2, APP_TAGLINE } from "nextclaw-core";
5
+ import { APP_NAME as APP_NAME2, APP_TAGLINE } from "@nextclaw/core";
6
6
 
7
7
  // src/cli/runtime.ts
8
8
  import {
@@ -30,8 +30,21 @@ import {
30
30
  APP_NAME,
31
31
  DEFAULT_WORKSPACE_DIR,
32
32
  DEFAULT_WORKSPACE_PATH
33
- } from "nextclaw-core";
34
- import { startUiServer } from "nextclaw-server";
33
+ } from "@nextclaw/core";
34
+ import {
35
+ loadOpenClawPlugins,
36
+ buildPluginStatusReport,
37
+ enablePluginInConfig,
38
+ disablePluginInConfig,
39
+ addPluginLoadPath,
40
+ recordPluginInstall,
41
+ installPluginFromPath,
42
+ installPluginFromNpmSpec,
43
+ uninstallPlugin,
44
+ resolveUninstallDirectoryTarget,
45
+ loadPluginUiMetadata
46
+ } from "@nextclaw/openclaw-compat";
47
+ import { startUiServer } from "@nextclaw/server";
35
48
  import {
36
49
  closeSync,
37
50
  cpSync,
@@ -57,7 +70,7 @@ import {
57
70
  buildConfigSchema,
58
71
  ConfigSchema,
59
72
  redactConfigObject
60
- } from "nextclaw-core";
73
+ } from "@nextclaw/core";
61
74
 
62
75
  // src/cli/utils.ts
63
76
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
@@ -65,7 +78,7 @@ import { join, resolve } from "path";
65
78
  import { spawn } from "child_process";
66
79
  import { createServer } from "net";
67
80
  import { fileURLToPath } from "url";
68
- import { getDataDir, getPackageVersion } from "nextclaw-core";
81
+ import { getDataDir, getPackageVersion } from "@nextclaw/core";
69
82
  function resolveUiConfig(config, overrides) {
70
83
  const base = config.ui ?? { enabled: false, host: "127.0.0.1", port: 18791, open: false };
71
84
  return { ...base, ...overrides ?? {} };
@@ -331,7 +344,7 @@ function runSelfUpdate(options = {}) {
331
344
 
332
345
  // src/cli/gateway/controller.ts
333
346
  var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
334
- var readConfigSnapshot = (getConfigPath2) => {
347
+ var readConfigSnapshot = (getConfigPath2, plugins2) => {
335
348
  const path = getConfigPath2();
336
349
  let raw = "";
337
350
  let parsed = {};
@@ -355,12 +368,12 @@ var readConfigSnapshot = (getConfigPath2) => {
355
368
  raw = JSON.stringify(config, null, 2);
356
369
  }
357
370
  const hash = hashRaw(raw);
358
- const schema = buildConfigSchema({ version: getPackageVersion() });
371
+ const schema = buildConfigSchema({ version: getPackageVersion(), plugins: plugins2 });
359
372
  const redacted = redactConfigObject(config, schema.uiHints);
360
373
  return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config, redacted, valid };
361
374
  };
362
- var redactValue = (value) => {
363
- const schema = buildConfigSchema({ version: getPackageVersion() });
375
+ var redactValue = (value, plugins2) => {
376
+ const schema = buildConfigSchema({ version: getPackageVersion(), plugins: plugins2 });
364
377
  return redactConfigObject(value, schema.uiHints);
365
378
  };
366
379
  var mergeDeep = (base, patch) => {
@@ -405,7 +418,8 @@ var GatewayControllerImpl = class {
405
418
  return "Restart scheduled";
406
419
  }
407
420
  async getConfig() {
408
- const snapshot = readConfigSnapshot(this.deps.getConfigPath);
421
+ const plugins2 = this.deps.getPluginUiMetadata?.() ?? [];
422
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath, plugins2);
409
423
  return {
410
424
  raw: snapshot.raw,
411
425
  hash: snapshot.hash,
@@ -417,10 +431,11 @@ var GatewayControllerImpl = class {
417
431
  };
418
432
  }
419
433
  async getConfigSchema() {
420
- return buildConfigSchema({ version: getPackageVersion() });
434
+ return buildConfigSchema({ version: getPackageVersion(), plugins: this.deps.getPluginUiMetadata?.() ?? [] });
421
435
  }
422
436
  async applyConfig(params) {
423
- const snapshot = readConfigSnapshot(this.deps.getConfigPath);
437
+ const plugins2 = this.deps.getPluginUiMetadata?.() ?? [];
438
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath, plugins2);
424
439
  if (!params.baseHash) {
425
440
  return { ok: false, error: "config base hash required; re-run config.get and retry" };
426
441
  }
@@ -449,12 +464,13 @@ var GatewayControllerImpl = class {
449
464
  ok: true,
450
465
  note: params.note ?? null,
451
466
  path: this.deps.getConfigPath(),
452
- config: redactValue(validated),
467
+ config: redactValue(validated, plugins2),
453
468
  restart: { scheduled: true, delayMs }
454
469
  };
455
470
  }
456
471
  async patchConfig(params) {
457
- const snapshot = readConfigSnapshot(this.deps.getConfigPath);
472
+ const plugins2 = this.deps.getPluginUiMetadata?.() ?? [];
473
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath, plugins2);
458
474
  if (!params.baseHash) {
459
475
  return { ok: false, error: "config base hash required; re-run config.get and retry" };
460
476
  }
@@ -484,7 +500,7 @@ var GatewayControllerImpl = class {
484
500
  ok: true,
485
501
  note: params.note ?? null,
486
502
  path: this.deps.getConfigPath(),
487
- config: redactValue(validated),
503
+ config: redactValue(validated, plugins2),
488
504
  restart: { scheduled: true, delayMs }
489
505
  };
490
506
  }
@@ -653,7 +669,12 @@ var ConfigReloader = class {
653
669
  }
654
670
  this.reloadTask = (async () => {
655
671
  await this.channels.stopAll();
656
- this.channels = new ChannelManager(nextConfig, this.options.bus, this.options.sessionManager);
672
+ this.channels = new ChannelManager(
673
+ nextConfig,
674
+ this.options.bus,
675
+ this.options.sessionManager,
676
+ this.options.getExtensionChannels?.() ?? []
677
+ );
657
678
  await this.channels.startAll();
658
679
  })();
659
680
  try {
@@ -851,17 +872,23 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
851
872
  }
852
873
  async agent(opts) {
853
874
  const config = loadConfig();
875
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
876
+ const pluginRegistry = this.loadPluginRegistry(config, workspace);
877
+ const extensionRegistry = this.toExtensionRegistry(pluginRegistry);
878
+ this.logPluginDiagnostics(pluginRegistry);
854
879
  const bus = new MessageBus();
855
880
  const provider = this.makeProvider(config);
856
881
  const providerManager = new ProviderManager(provider);
857
882
  const agentLoop = new AgentLoop({
858
883
  bus,
859
884
  providerManager,
860
- workspace: getWorkspacePath(config.agents.defaults.workspace),
885
+ workspace,
861
886
  braveApiKey: config.tools.web.search.apiKey || void 0,
862
887
  execConfig: config.tools.exec,
863
888
  restrictToWorkspace: config.tools.restrictToWorkspace,
864
- contextConfig: config.agents.context
889
+ contextConfig: config.agents.context,
890
+ config,
891
+ extensionRegistry
865
892
  });
866
893
  if (opts.message) {
867
894
  const response = await agentLoop.processDirect({
@@ -939,6 +966,356 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
939
966
  console.log(`Tip: restart ${APP_NAME} to apply the update.`);
940
967
  }
941
968
  }
969
+ pluginsList(opts = {}) {
970
+ const config = loadConfig();
971
+ const workspaceDir = getWorkspacePath(config.agents.defaults.workspace);
972
+ const report = buildPluginStatusReport({
973
+ config,
974
+ workspaceDir,
975
+ reservedChannelIds: Object.keys(config.channels),
976
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
977
+ });
978
+ const list = opts.enabled ? report.plugins.filter((plugin) => plugin.status === "loaded") : report.plugins;
979
+ if (opts.json) {
980
+ console.log(
981
+ JSON.stringify(
982
+ {
983
+ workspaceDir,
984
+ plugins: list,
985
+ diagnostics: report.diagnostics
986
+ },
987
+ null,
988
+ 2
989
+ )
990
+ );
991
+ return;
992
+ }
993
+ if (list.length === 0) {
994
+ console.log("No plugins discovered.");
995
+ return;
996
+ }
997
+ for (const plugin of list) {
998
+ const status = plugin.status === "loaded" ? "loaded" : plugin.status === "disabled" ? "disabled" : "error";
999
+ const title = plugin.name && plugin.name !== plugin.id ? `${plugin.name} (${plugin.id})` : plugin.id;
1000
+ if (!opts.verbose) {
1001
+ const desc = plugin.description ? plugin.description.length > 80 ? `${plugin.description.slice(0, 77)}...` : plugin.description : "(no description)";
1002
+ console.log(`${title} ${status} - ${desc}`);
1003
+ continue;
1004
+ }
1005
+ console.log(`${title} ${status}`);
1006
+ console.log(` source: ${plugin.source}`);
1007
+ console.log(` origin: ${plugin.origin}`);
1008
+ if (plugin.version) {
1009
+ console.log(` version: ${plugin.version}`);
1010
+ }
1011
+ if (plugin.toolNames.length > 0) {
1012
+ console.log(` tools: ${plugin.toolNames.join(", ")}`);
1013
+ }
1014
+ if (plugin.channelIds.length > 0) {
1015
+ console.log(` channels: ${plugin.channelIds.join(", ")}`);
1016
+ }
1017
+ if (plugin.providerIds.length > 0) {
1018
+ console.log(` providers: ${plugin.providerIds.join(", ")}`);
1019
+ }
1020
+ if (plugin.error) {
1021
+ console.log(` error: ${plugin.error}`);
1022
+ }
1023
+ console.log("");
1024
+ }
1025
+ }
1026
+ pluginsInfo(id, opts = {}) {
1027
+ const config = loadConfig();
1028
+ const workspaceDir = getWorkspacePath(config.agents.defaults.workspace);
1029
+ const report = buildPluginStatusReport({
1030
+ config,
1031
+ workspaceDir,
1032
+ reservedChannelIds: Object.keys(config.channels),
1033
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1034
+ });
1035
+ const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
1036
+ if (!plugin) {
1037
+ console.error(`Plugin not found: ${id}`);
1038
+ process.exit(1);
1039
+ }
1040
+ if (opts.json) {
1041
+ console.log(JSON.stringify(plugin, null, 2));
1042
+ return;
1043
+ }
1044
+ const install = config.plugins.installs?.[plugin.id];
1045
+ const lines = [];
1046
+ lines.push(plugin.name || plugin.id);
1047
+ if (plugin.name && plugin.name !== plugin.id) {
1048
+ lines.push(`id: ${plugin.id}`);
1049
+ }
1050
+ if (plugin.description) {
1051
+ lines.push(plugin.description);
1052
+ }
1053
+ lines.push("");
1054
+ lines.push(`Status: ${plugin.status}`);
1055
+ lines.push(`Source: ${plugin.source}`);
1056
+ lines.push(`Origin: ${plugin.origin}`);
1057
+ if (plugin.version) {
1058
+ lines.push(`Version: ${plugin.version}`);
1059
+ }
1060
+ if (plugin.toolNames.length > 0) {
1061
+ lines.push(`Tools: ${plugin.toolNames.join(", ")}`);
1062
+ }
1063
+ if (plugin.channelIds.length > 0) {
1064
+ lines.push(`Channels: ${plugin.channelIds.join(", ")}`);
1065
+ }
1066
+ if (plugin.providerIds.length > 0) {
1067
+ lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
1068
+ }
1069
+ if (plugin.error) {
1070
+ lines.push(`Error: ${plugin.error}`);
1071
+ }
1072
+ if (install) {
1073
+ lines.push("");
1074
+ lines.push(`Install: ${install.source}`);
1075
+ if (install.spec) {
1076
+ lines.push(`Spec: ${install.spec}`);
1077
+ }
1078
+ if (install.sourcePath) {
1079
+ lines.push(`Source path: ${install.sourcePath}`);
1080
+ }
1081
+ if (install.installPath) {
1082
+ lines.push(`Install path: ${install.installPath}`);
1083
+ }
1084
+ if (install.version) {
1085
+ lines.push(`Recorded version: ${install.version}`);
1086
+ }
1087
+ if (install.installedAt) {
1088
+ lines.push(`Installed at: ${install.installedAt}`);
1089
+ }
1090
+ }
1091
+ console.log(lines.join("\n"));
1092
+ }
1093
+ pluginsEnable(id) {
1094
+ const config = loadConfig();
1095
+ const next = enablePluginInConfig(config, id);
1096
+ saveConfig(next);
1097
+ console.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
1098
+ }
1099
+ pluginsDisable(id) {
1100
+ const config = loadConfig();
1101
+ const next = disablePluginInConfig(config, id);
1102
+ saveConfig(next);
1103
+ console.log(`Disabled plugin "${id}". Restart the gateway to apply.`);
1104
+ }
1105
+ async pluginsUninstall(id, opts = {}) {
1106
+ const config = loadConfig();
1107
+ const workspaceDir = getWorkspacePath(config.agents.defaults.workspace);
1108
+ const report = buildPluginStatusReport({
1109
+ config,
1110
+ workspaceDir,
1111
+ reservedChannelIds: Object.keys(config.channels),
1112
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1113
+ });
1114
+ const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
1115
+ if (opts.keepConfig) {
1116
+ console.log("`--keep-config` is deprecated, use `--keep-files`.");
1117
+ }
1118
+ const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
1119
+ const pluginId = plugin?.id ?? id;
1120
+ const hasEntry = pluginId in (config.plugins.entries ?? {});
1121
+ const hasInstall = pluginId in (config.plugins.installs ?? {});
1122
+ if (!hasEntry && !hasInstall) {
1123
+ if (plugin) {
1124
+ console.error(
1125
+ `Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`
1126
+ );
1127
+ } else {
1128
+ console.error(`Plugin not found: ${id}`);
1129
+ }
1130
+ process.exit(1);
1131
+ }
1132
+ const install = config.plugins.installs?.[pluginId];
1133
+ const isLinked = install?.source === "path";
1134
+ const preview = [];
1135
+ if (hasEntry) {
1136
+ preview.push("config entry");
1137
+ }
1138
+ if (hasInstall) {
1139
+ preview.push("install record");
1140
+ }
1141
+ if (config.plugins.allow?.includes(pluginId)) {
1142
+ preview.push("allowlist entry");
1143
+ }
1144
+ if (isLinked && install?.sourcePath && config.plugins.load?.paths?.includes(install.sourcePath)) {
1145
+ preview.push("load path");
1146
+ }
1147
+ const deleteTarget = !keepFiles ? resolveUninstallDirectoryTarget({
1148
+ pluginId,
1149
+ hasInstall,
1150
+ installRecord: install
1151
+ }) : null;
1152
+ if (deleteTarget) {
1153
+ preview.push(`directory: ${deleteTarget}`);
1154
+ }
1155
+ const pluginName = plugin?.name || pluginId;
1156
+ const pluginTitle = pluginName !== pluginId ? `${pluginName} (${pluginId})` : pluginName;
1157
+ console.log(`Plugin: ${pluginTitle}`);
1158
+ console.log(`Will remove: ${preview.length > 0 ? preview.join(", ") : "(nothing)"}`);
1159
+ if (opts.dryRun) {
1160
+ console.log("Dry run, no changes made.");
1161
+ return;
1162
+ }
1163
+ if (!opts.force) {
1164
+ const confirmed = await this.confirmYesNo(`Uninstall plugin "${pluginId}"?`);
1165
+ if (!confirmed) {
1166
+ console.log("Cancelled.");
1167
+ return;
1168
+ }
1169
+ }
1170
+ const result = await uninstallPlugin({
1171
+ config,
1172
+ pluginId,
1173
+ deleteFiles: !keepFiles
1174
+ });
1175
+ if (!result.ok) {
1176
+ console.error(result.error);
1177
+ process.exit(1);
1178
+ }
1179
+ for (const warning of result.warnings) {
1180
+ console.warn(warning);
1181
+ }
1182
+ saveConfig(result.config);
1183
+ const removed = [];
1184
+ if (result.actions.entry) {
1185
+ removed.push("config entry");
1186
+ }
1187
+ if (result.actions.install) {
1188
+ removed.push("install record");
1189
+ }
1190
+ if (result.actions.allowlist) {
1191
+ removed.push("allowlist");
1192
+ }
1193
+ if (result.actions.loadPath) {
1194
+ removed.push("load path");
1195
+ }
1196
+ if (result.actions.directory) {
1197
+ removed.push("directory");
1198
+ }
1199
+ console.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`);
1200
+ console.log("Restart the gateway to apply changes.");
1201
+ }
1202
+ async pluginsInstall(pathOrSpec, opts = {}) {
1203
+ const fileSpec = this.resolveFileNpmSpecToLocalPath(pathOrSpec);
1204
+ if (fileSpec && !fileSpec.ok) {
1205
+ console.error(fileSpec.error);
1206
+ process.exit(1);
1207
+ }
1208
+ const normalized = fileSpec && fileSpec.ok ? fileSpec.path : pathOrSpec;
1209
+ const resolved = resolve4(expandHome(normalized));
1210
+ const config = loadConfig();
1211
+ if (existsSync4(resolved)) {
1212
+ if (opts.link) {
1213
+ const probe = await installPluginFromPath({ path: resolved, dryRun: true });
1214
+ if (!probe.ok) {
1215
+ console.error(probe.error);
1216
+ process.exit(1);
1217
+ }
1218
+ let next3 = addPluginLoadPath(config, resolved);
1219
+ next3 = enablePluginInConfig(next3, probe.pluginId);
1220
+ next3 = recordPluginInstall(next3, {
1221
+ pluginId: probe.pluginId,
1222
+ source: "path",
1223
+ sourcePath: resolved,
1224
+ installPath: resolved,
1225
+ version: probe.version
1226
+ });
1227
+ saveConfig(next3);
1228
+ console.log(`Linked plugin path: ${resolved}`);
1229
+ console.log("Restart the gateway to load plugins.");
1230
+ return;
1231
+ }
1232
+ const result2 = await installPluginFromPath({
1233
+ path: resolved,
1234
+ logger: {
1235
+ info: (message) => console.log(message),
1236
+ warn: (message) => console.warn(message)
1237
+ }
1238
+ });
1239
+ if (!result2.ok) {
1240
+ console.error(result2.error);
1241
+ process.exit(1);
1242
+ }
1243
+ let next2 = enablePluginInConfig(config, result2.pluginId);
1244
+ next2 = recordPluginInstall(next2, {
1245
+ pluginId: result2.pluginId,
1246
+ source: this.isArchivePath(resolved) ? "archive" : "path",
1247
+ sourcePath: resolved,
1248
+ installPath: result2.targetDir,
1249
+ version: result2.version
1250
+ });
1251
+ saveConfig(next2);
1252
+ console.log(`Installed plugin: ${result2.pluginId}`);
1253
+ console.log("Restart the gateway to load plugins.");
1254
+ return;
1255
+ }
1256
+ if (opts.link) {
1257
+ console.error("`--link` requires a local path.");
1258
+ process.exit(1);
1259
+ }
1260
+ if (this.looksLikePath(pathOrSpec)) {
1261
+ console.error(`Path not found: ${resolved}`);
1262
+ process.exit(1);
1263
+ }
1264
+ const result = await installPluginFromNpmSpec({
1265
+ spec: pathOrSpec,
1266
+ logger: {
1267
+ info: (message) => console.log(message),
1268
+ warn: (message) => console.warn(message)
1269
+ }
1270
+ });
1271
+ if (!result.ok) {
1272
+ console.error(result.error);
1273
+ process.exit(1);
1274
+ }
1275
+ let next = enablePluginInConfig(config, result.pluginId);
1276
+ next = recordPluginInstall(next, {
1277
+ pluginId: result.pluginId,
1278
+ source: "npm",
1279
+ spec: pathOrSpec,
1280
+ installPath: result.targetDir,
1281
+ version: result.version
1282
+ });
1283
+ saveConfig(next);
1284
+ console.log(`Installed plugin: ${result.pluginId}`);
1285
+ console.log("Restart the gateway to load plugins.");
1286
+ }
1287
+ pluginsDoctor() {
1288
+ const config = loadConfig();
1289
+ const workspaceDir = getWorkspacePath(config.agents.defaults.workspace);
1290
+ const report = buildPluginStatusReport({
1291
+ config,
1292
+ workspaceDir,
1293
+ reservedChannelIds: Object.keys(config.channels),
1294
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1295
+ });
1296
+ const pluginErrors = report.plugins.filter((plugin) => plugin.status === "error");
1297
+ const diagnostics = report.diagnostics.filter((diag) => diag.level === "error");
1298
+ if (pluginErrors.length === 0 && diagnostics.length === 0) {
1299
+ console.log("No plugin issues detected.");
1300
+ return;
1301
+ }
1302
+ if (pluginErrors.length > 0) {
1303
+ console.log("Plugin errors:");
1304
+ for (const entry of pluginErrors) {
1305
+ console.log(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`);
1306
+ }
1307
+ }
1308
+ if (diagnostics.length > 0) {
1309
+ if (pluginErrors.length > 0) {
1310
+ console.log("");
1311
+ }
1312
+ console.log("Diagnostics:");
1313
+ for (const diag of diagnostics) {
1314
+ const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
1315
+ console.log(`- ${prefix}${diag.message}`);
1316
+ }
1317
+ }
1318
+ }
942
1319
  async skillsInstall(options) {
943
1320
  const workdir = options.workdir ? expandHome(options.workdir) : getWorkspacePath();
944
1321
  const result = await installClawHubSkill({
@@ -1071,14 +1448,85 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1071
1448
  }
1072
1449
  }
1073
1450
  }
1451
+ loadPluginRegistry(config, workspaceDir) {
1452
+ return loadOpenClawPlugins({
1453
+ config,
1454
+ workspaceDir,
1455
+ reservedToolNames: [
1456
+ "read_file",
1457
+ "write_file",
1458
+ "edit_file",
1459
+ "list_dir",
1460
+ "exec",
1461
+ "web_search",
1462
+ "web_fetch",
1463
+ "message",
1464
+ "spawn",
1465
+ "sessions_list",
1466
+ "sessions_history",
1467
+ "sessions_send",
1468
+ "memory_search",
1469
+ "memory_get",
1470
+ "subagents",
1471
+ "gateway",
1472
+ "cron"
1473
+ ],
1474
+ reservedChannelIds: Object.keys(config.channels),
1475
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name),
1476
+ logger: {
1477
+ info: (message) => console.log(message),
1478
+ warn: (message) => console.warn(message),
1479
+ error: (message) => console.error(message),
1480
+ debug: (message) => console.debug(message)
1481
+ }
1482
+ });
1483
+ }
1484
+ toExtensionRegistry(pluginRegistry) {
1485
+ return {
1486
+ tools: pluginRegistry.tools.map((tool) => ({
1487
+ extensionId: tool.pluginId,
1488
+ factory: tool.factory,
1489
+ names: tool.names,
1490
+ optional: tool.optional,
1491
+ source: tool.source
1492
+ })),
1493
+ channels: pluginRegistry.channels.map((channel) => ({
1494
+ extensionId: channel.pluginId,
1495
+ channel: channel.channel,
1496
+ source: channel.source
1497
+ })),
1498
+ diagnostics: pluginRegistry.diagnostics.map((diag) => ({
1499
+ level: diag.level,
1500
+ message: diag.message,
1501
+ extensionId: diag.pluginId,
1502
+ source: diag.source
1503
+ }))
1504
+ };
1505
+ }
1506
+ logPluginDiagnostics(registry) {
1507
+ for (const diag of registry.diagnostics) {
1508
+ const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
1509
+ const text = `${prefix}${diag.message}`;
1510
+ if (diag.level === "error") {
1511
+ console.error(`[plugins] ${text}`);
1512
+ } else {
1513
+ console.warn(`[plugins] ${text}`);
1514
+ }
1515
+ }
1516
+ }
1074
1517
  async startGateway(options = {}) {
1075
1518
  const config = loadConfig();
1519
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
1520
+ const pluginRegistry = this.loadPluginRegistry(config, workspace);
1521
+ const extensionRegistry = this.toExtensionRegistry(pluginRegistry);
1522
+ this.logPluginDiagnostics(pluginRegistry);
1076
1523
  const bus = new MessageBus();
1077
1524
  const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
1078
1525
  const providerManager = provider ? new ProviderManager(provider) : null;
1079
- const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
1526
+ const sessionManager = new SessionManager(workspace);
1080
1527
  const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
1081
1528
  const cron2 = new CronService(cronStorePath);
1529
+ const pluginUiMetadata = loadPluginUiMetadata({ config, workspaceDir: workspace });
1082
1530
  const uiConfig = resolveUiConfig(config, options.uiOverrides);
1083
1531
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
1084
1532
  if (!provider) {
@@ -1088,7 +1536,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1088
1536
  });
1089
1537
  return;
1090
1538
  }
1091
- const channels2 = new ChannelManager(config, bus, sessionManager);
1539
+ const channels2 = new ChannelManager(config, bus, sessionManager, extensionRegistry.channels);
1092
1540
  const reloader = new ConfigReloader({
1093
1541
  initialConfig: config,
1094
1542
  channels: channels2,
@@ -1097,6 +1545,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1097
1545
  providerManager,
1098
1546
  makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
1099
1547
  loadConfig,
1548
+ getExtensionChannels: () => extensionRegistry.channels,
1100
1549
  onRestartRequired: (paths) => {
1101
1550
  console.warn(`Config changes require restart: ${paths.join(", ")}`);
1102
1551
  }
@@ -1105,12 +1554,13 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1105
1554
  reloader,
1106
1555
  cron: cron2,
1107
1556
  getConfigPath,
1108
- saveConfig
1557
+ saveConfig,
1558
+ getPluginUiMetadata: () => pluginUiMetadata
1109
1559
  });
1110
1560
  const agent = new AgentLoop({
1111
1561
  bus,
1112
1562
  providerManager: providerManager ?? new ProviderManager(provider),
1113
- workspace: getWorkspacePath(config.agents.defaults.workspace),
1563
+ workspace,
1114
1564
  model: config.agents.defaults.model,
1115
1565
  maxIterations: config.agents.defaults.maxToolIterations,
1116
1566
  braveApiKey: config.tools.web.search.apiKey || void 0,
@@ -1119,7 +1569,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1119
1569
  restrictToWorkspace: config.tools.restrictToWorkspace,
1120
1570
  sessionManager,
1121
1571
  contextConfig: config.agents.context,
1122
- gatewayController
1572
+ gatewayController,
1573
+ config,
1574
+ extensionRegistry
1123
1575
  });
1124
1576
  cron2.onJob = async (job) => {
1125
1577
  const response = await agent.processDirect({
@@ -1140,7 +1592,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1140
1592
  return response;
1141
1593
  };
1142
1594
  const heartbeat = new HeartbeatService(
1143
- getWorkspacePath(config.agents.defaults.workspace),
1595
+ workspace,
1144
1596
  async (promptText) => agent.processDirect({ content: promptText, sessionKey: "heartbeat" }),
1145
1597
  30 * 60,
1146
1598
  true
@@ -1310,6 +1762,18 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1310
1762
  clearServiceState();
1311
1763
  console.log(`\u2713 ${APP_NAME} stopped`);
1312
1764
  }
1765
+ async confirmYesNo(question) {
1766
+ const rl = createInterface({
1767
+ input: process.stdin,
1768
+ output: process.stdout
1769
+ });
1770
+ const answer = await new Promise((resolve5) => {
1771
+ rl.question(`${question} [y/N] `, (line) => resolve5(line));
1772
+ });
1773
+ rl.close();
1774
+ const normalized = answer.trim().toLowerCase();
1775
+ return normalized === "y" || normalized === "yes";
1776
+ }
1313
1777
  makeProvider(config, options) {
1314
1778
  const provider = getProvider(config);
1315
1779
  const model = config.agents.defaults.model;
@@ -1330,6 +1794,36 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1330
1794
  wireApi: provider?.wireApi ?? null
1331
1795
  });
1332
1796
  }
1797
+ resolveFileNpmSpecToLocalPath(raw) {
1798
+ const trimmed = raw.trim();
1799
+ if (!trimmed.toLowerCase().startsWith("file:")) {
1800
+ return null;
1801
+ }
1802
+ const rest = trimmed.slice("file:".length);
1803
+ if (!rest) {
1804
+ return { ok: false, error: "unsupported file: spec: missing path" };
1805
+ }
1806
+ if (rest.startsWith("///")) {
1807
+ return { ok: true, path: rest.slice(2) };
1808
+ }
1809
+ if (rest.startsWith("//localhost/")) {
1810
+ return { ok: true, path: rest.slice("//localhost".length) };
1811
+ }
1812
+ if (rest.startsWith("//")) {
1813
+ return {
1814
+ ok: false,
1815
+ error: 'unsupported file: URL host (expected "file:<path>" or "file:///abs/path")'
1816
+ };
1817
+ }
1818
+ return { ok: true, path: rest };
1819
+ }
1820
+ looksLikePath(raw) {
1821
+ return raw.startsWith(".") || raw.startsWith("~") || raw.startsWith("/") || raw.endsWith(".ts") || raw.endsWith(".js") || raw.endsWith(".mjs") || raw.endsWith(".cjs") || raw.endsWith(".tgz") || raw.endsWith(".tar.gz") || raw.endsWith(".tar") || raw.endsWith(".zip");
1822
+ }
1823
+ isArchivePath(filePath) {
1824
+ const lower = filePath.toLowerCase();
1825
+ return lower.endsWith(".zip") || lower.endsWith(".tgz") || lower.endsWith(".tar.gz") || lower.endsWith(".tar");
1826
+ }
1333
1827
  createWorkspaceTemplates(workspace, options = {}) {
1334
1828
  const created = [];
1335
1829
  const force = Boolean(options.force);
@@ -1413,7 +1907,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1413
1907
  resolveBuiltinSkillsDir() {
1414
1908
  try {
1415
1909
  const require2 = createRequire(import.meta.url);
1416
- const entry = require2.resolve("nextclaw-core");
1910
+ const entry = require2.resolve("@nextclaw/core");
1417
1911
  const pkgRoot = resolve4(dirname(entry), "..");
1418
1912
  const distSkills = join3(pkgRoot, "dist", "skills");
1419
1913
  if (existsSync4(distSkills)) {
@@ -1516,6 +2010,14 @@ var skills = program.command("skills").description("Manage skills");
1516
2010
  registerClawHubInstall(skills);
1517
2011
  var clawhub = program.command("clawhub").description("Install skills from ClawHub");
1518
2012
  registerClawHubInstall(clawhub);
2013
+ var plugins = program.command("plugins").description("Manage OpenClaw-compatible plugins");
2014
+ plugins.command("list").description("List discovered plugins").option("--json", "Print JSON").option("--enabled", "Only show enabled plugins", false).option("--verbose", "Show detailed entries", false).action((opts) => runtime.pluginsList(opts));
2015
+ plugins.command("info <id>").description("Show plugin details").option("--json", "Print JSON").action((id, opts) => runtime.pluginsInfo(id, opts));
2016
+ plugins.command("enable <id>").description("Enable a plugin in config").action((id) => runtime.pluginsEnable(id));
2017
+ plugins.command("disable <id>").description("Disable a plugin in config").action((id) => runtime.pluginsDisable(id));
2018
+ plugins.command("uninstall <id>").description("Uninstall a plugin").option("--keep-files", "Keep installed files on disk", false).option("--keep-config", "Deprecated alias for --keep-files", false).option("--force", "Skip confirmation prompt", false).option("--dry-run", "Show what would be removed without making changes", false).action(async (id, opts) => runtime.pluginsUninstall(id, opts));
2019
+ plugins.command("install <path-or-spec>").description("Install a plugin (path, archive, or npm spec)").option("-l, --link", "Link a local path instead of copying", false).action(async (pathOrSpec, opts) => runtime.pluginsInstall(pathOrSpec, opts));
2020
+ plugins.command("doctor").description("Report plugin load issues").action(() => runtime.pluginsDoctor());
1519
2021
  var channels = program.command("channels").description("Manage channels");
1520
2022
  channels.command("status").description("Show channel status").action(() => runtime.channelsStatus());
1521
2023
  channels.command("login").description("Link device via QR code").action(() => runtime.channelsLogin());