nextclaw 0.6.1 → 0.6.3

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
@@ -6,13 +6,13 @@ import { APP_NAME as APP_NAME5, APP_TAGLINE } from "@nextclaw/core";
6
6
 
7
7
  // src/cli/runtime.ts
8
8
  import {
9
- loadConfig as loadConfig5,
10
- saveConfig as saveConfig3,
9
+ loadConfig as loadConfig6,
10
+ saveConfig as saveConfig5,
11
11
  getConfigPath as getConfigPath3,
12
12
  getDataDir as getDataDir6,
13
13
  ConfigSchema as ConfigSchema2,
14
- getWorkspacePath as getWorkspacePath3,
15
- expandHome,
14
+ getWorkspacePath as getWorkspacePath5,
15
+ expandHome as expandHome2,
16
16
  MessageBus as MessageBus2,
17
17
  AgentLoop as AgentLoop2,
18
18
  ProviderManager as ProviderManager2,
@@ -20,9 +20,10 @@ import {
20
20
  DEFAULT_WORKSPACE_DIR,
21
21
  DEFAULT_WORKSPACE_PATH
22
22
  } from "@nextclaw/core";
23
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24
- import { join as join6, resolve as resolve7 } from "path";
25
- import { createInterface } from "readline";
23
+ import { resolvePluginChannelMessageToolHints as resolvePluginChannelMessageToolHints2 } from "@nextclaw/openclaw-compat";
24
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
25
+ import { join as join6, resolve as resolve8 } from "path";
26
+ import { createInterface as createInterface2 } from "readline";
26
27
  import { fileURLToPath as fileURLToPath3 } from "url";
27
28
  import { spawn as spawn3 } from "child_process";
28
29
 
@@ -254,7 +255,7 @@ async function waitForExit(pid, timeoutMs) {
254
255
  if (!isProcessRunning(pid)) {
255
256
  return true;
256
257
  }
257
- await new Promise((resolve8) => setTimeout(resolve8, 200));
258
+ await new Promise((resolve9) => setTimeout(resolve9, 200));
258
259
  }
259
260
  return !isProcessRunning(pid);
260
261
  }
@@ -344,8 +345,8 @@ function printAgentResponse(response) {
344
345
  async function prompt(rl, question) {
345
346
  rl.setPrompt(question);
346
347
  rl.prompt();
347
- return new Promise((resolve8) => {
348
- rl.once("line", (line) => resolve8(line));
348
+ return new Promise((resolve9) => {
349
+ rl.once("line", (line) => resolve9(line));
349
350
  });
350
351
  }
351
352
 
@@ -391,8 +392,549 @@ function runSelfUpdate(options = {}) {
391
392
  return { ok: false, error: "no update strategy available", strategy: "none", steps };
392
393
  }
393
394
 
395
+ // src/cli/commands/plugins.ts
396
+ import {
397
+ addPluginLoadPath,
398
+ buildPluginStatusReport,
399
+ disablePluginInConfig,
400
+ enablePluginInConfig,
401
+ installPluginFromNpmSpec,
402
+ installPluginFromPath,
403
+ loadOpenClawPlugins,
404
+ recordPluginInstall,
405
+ resolveUninstallDirectoryTarget,
406
+ uninstallPlugin
407
+ } from "@nextclaw/openclaw-compat";
408
+ import {
409
+ loadConfig,
410
+ saveConfig,
411
+ getWorkspacePath,
412
+ PROVIDERS,
413
+ expandHome
414
+ } from "@nextclaw/core";
415
+ import { createInterface } from "readline";
416
+ import { existsSync as existsSync3 } from "fs";
417
+ import { resolve as resolve4 } from "path";
418
+ function loadPluginRegistry(config2, workspaceDir) {
419
+ return loadOpenClawPlugins({
420
+ config: config2,
421
+ workspaceDir,
422
+ reservedToolNames: [
423
+ "read_file",
424
+ "write_file",
425
+ "edit_file",
426
+ "list_dir",
427
+ "exec",
428
+ "web_search",
429
+ "web_fetch",
430
+ "message",
431
+ "spawn",
432
+ "sessions_list",
433
+ "sessions_history",
434
+ "sessions_send",
435
+ "memory_search",
436
+ "memory_get",
437
+ "subagents",
438
+ "gateway",
439
+ "cron"
440
+ ],
441
+ reservedChannelIds: Object.keys(config2.channels),
442
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name),
443
+ logger: {
444
+ info: (message) => console.log(message),
445
+ warn: (message) => console.warn(message),
446
+ error: (message) => console.error(message),
447
+ debug: (message) => console.debug(message)
448
+ }
449
+ });
450
+ }
451
+ function toExtensionRegistry(pluginRegistry) {
452
+ return {
453
+ tools: pluginRegistry.tools.map((tool) => ({
454
+ extensionId: tool.pluginId,
455
+ factory: tool.factory,
456
+ names: tool.names,
457
+ optional: tool.optional,
458
+ source: tool.source
459
+ })),
460
+ channels: pluginRegistry.channels.map((channel) => ({
461
+ extensionId: channel.pluginId,
462
+ channel: channel.channel,
463
+ source: channel.source
464
+ })),
465
+ diagnostics: pluginRegistry.diagnostics.map((diag) => ({
466
+ level: diag.level,
467
+ message: diag.message,
468
+ extensionId: diag.pluginId,
469
+ source: diag.source
470
+ }))
471
+ };
472
+ }
473
+ function logPluginDiagnostics(registry) {
474
+ for (const diag of registry.diagnostics) {
475
+ const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
476
+ const text = `${prefix}${diag.message}`;
477
+ if (diag.level === "error") {
478
+ console.error(`[plugins] ${text}`);
479
+ } else {
480
+ console.warn(`[plugins] ${text}`);
481
+ }
482
+ }
483
+ }
484
+ function toPluginConfigView(config2, bindings) {
485
+ const view = JSON.parse(JSON.stringify(config2));
486
+ const channels2 = view.channels && typeof view.channels === "object" && !Array.isArray(view.channels) ? { ...view.channels } : {};
487
+ for (const binding of bindings) {
488
+ const pluginConfig = config2.plugins.entries?.[binding.pluginId]?.config;
489
+ if (!pluginConfig || typeof pluginConfig !== "object" || Array.isArray(pluginConfig)) {
490
+ continue;
491
+ }
492
+ channels2[binding.channelId] = JSON.parse(JSON.stringify(pluginConfig));
493
+ }
494
+ view.channels = channels2;
495
+ return view;
496
+ }
497
+ function mergePluginConfigView(baseConfig, pluginViewConfig, bindings) {
498
+ const next = JSON.parse(JSON.stringify(baseConfig));
499
+ const pluginChannels = pluginViewConfig.channels && typeof pluginViewConfig.channels === "object" && !Array.isArray(pluginViewConfig.channels) ? pluginViewConfig.channels : {};
500
+ const entries = { ...next.plugins.entries ?? {} };
501
+ for (const binding of bindings) {
502
+ if (!Object.prototype.hasOwnProperty.call(pluginChannels, binding.channelId)) {
503
+ continue;
504
+ }
505
+ const channelConfig = pluginChannels[binding.channelId];
506
+ if (!channelConfig || typeof channelConfig !== "object" || Array.isArray(channelConfig)) {
507
+ continue;
508
+ }
509
+ entries[binding.pluginId] = {
510
+ ...entries[binding.pluginId] ?? {},
511
+ config: channelConfig
512
+ };
513
+ }
514
+ next.plugins = {
515
+ ...next.plugins,
516
+ entries
517
+ };
518
+ return next;
519
+ }
520
+ var PluginCommands = class {
521
+ constructor(deps) {
522
+ this.deps = deps;
523
+ }
524
+ pluginsList(opts = {}) {
525
+ const config2 = loadConfig();
526
+ const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
527
+ const report = buildPluginStatusReport({
528
+ config: config2,
529
+ workspaceDir,
530
+ reservedChannelIds: Object.keys(config2.channels),
531
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
532
+ });
533
+ const list = opts.enabled ? report.plugins.filter((plugin) => plugin.status === "loaded") : report.plugins;
534
+ if (opts.json) {
535
+ console.log(
536
+ JSON.stringify(
537
+ {
538
+ workspaceDir,
539
+ plugins: list,
540
+ diagnostics: report.diagnostics
541
+ },
542
+ null,
543
+ 2
544
+ )
545
+ );
546
+ return;
547
+ }
548
+ if (list.length === 0) {
549
+ console.log("No plugins discovered.");
550
+ return;
551
+ }
552
+ for (const plugin of list) {
553
+ const status = plugin.status === "loaded" ? "loaded" : plugin.status === "disabled" ? "disabled" : "error";
554
+ const title = plugin.name && plugin.name !== plugin.id ? `${plugin.name} (${plugin.id})` : plugin.id;
555
+ if (!opts.verbose) {
556
+ const desc = plugin.description ? plugin.description.length > 80 ? `${plugin.description.slice(0, 77)}...` : plugin.description : "(no description)";
557
+ console.log(`${title} ${status} - ${desc}`);
558
+ continue;
559
+ }
560
+ console.log(`${title} ${status}`);
561
+ console.log(` source: ${plugin.source}`);
562
+ console.log(` origin: ${plugin.origin}`);
563
+ if (plugin.version) {
564
+ console.log(` version: ${plugin.version}`);
565
+ }
566
+ if (plugin.toolNames.length > 0) {
567
+ console.log(` tools: ${plugin.toolNames.join(", ")}`);
568
+ }
569
+ if (plugin.channelIds.length > 0) {
570
+ console.log(` channels: ${plugin.channelIds.join(", ")}`);
571
+ }
572
+ if (plugin.providerIds.length > 0) {
573
+ console.log(` providers: ${plugin.providerIds.join(", ")}`);
574
+ }
575
+ if (plugin.error) {
576
+ console.log(` error: ${plugin.error}`);
577
+ }
578
+ console.log("");
579
+ }
580
+ }
581
+ pluginsInfo(id, opts = {}) {
582
+ const config2 = loadConfig();
583
+ const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
584
+ const report = buildPluginStatusReport({
585
+ config: config2,
586
+ workspaceDir,
587
+ reservedChannelIds: Object.keys(config2.channels),
588
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
589
+ });
590
+ const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
591
+ if (!plugin) {
592
+ console.error(`Plugin not found: ${id}`);
593
+ process.exit(1);
594
+ }
595
+ if (opts.json) {
596
+ console.log(JSON.stringify(plugin, null, 2));
597
+ return;
598
+ }
599
+ const install = config2.plugins.installs?.[plugin.id];
600
+ const lines = [];
601
+ lines.push(plugin.name || plugin.id);
602
+ if (plugin.name && plugin.name !== plugin.id) {
603
+ lines.push(`id: ${plugin.id}`);
604
+ }
605
+ if (plugin.description) {
606
+ lines.push(plugin.description);
607
+ }
608
+ lines.push("");
609
+ lines.push(`Status: ${plugin.status}`);
610
+ lines.push(`Source: ${plugin.source}`);
611
+ lines.push(`Origin: ${plugin.origin}`);
612
+ if (plugin.version) {
613
+ lines.push(`Version: ${plugin.version}`);
614
+ }
615
+ if (plugin.toolNames.length > 0) {
616
+ lines.push(`Tools: ${plugin.toolNames.join(", ")}`);
617
+ }
618
+ if (plugin.channelIds.length > 0) {
619
+ lines.push(`Channels: ${plugin.channelIds.join(", ")}`);
620
+ }
621
+ if (plugin.providerIds.length > 0) {
622
+ lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
623
+ }
624
+ if (plugin.error) {
625
+ lines.push(`Error: ${plugin.error}`);
626
+ }
627
+ if (install) {
628
+ lines.push("");
629
+ lines.push(`Install: ${install.source}`);
630
+ if (install.spec) {
631
+ lines.push(`Spec: ${install.spec}`);
632
+ }
633
+ if (install.sourcePath) {
634
+ lines.push(`Source path: ${install.sourcePath}`);
635
+ }
636
+ if (install.installPath) {
637
+ lines.push(`Install path: ${install.installPath}`);
638
+ }
639
+ if (install.version) {
640
+ lines.push(`Recorded version: ${install.version}`);
641
+ }
642
+ if (install.installedAt) {
643
+ lines.push(`Installed at: ${install.installedAt}`);
644
+ }
645
+ }
646
+ console.log(lines.join("\n"));
647
+ }
648
+ async pluginsEnable(id) {
649
+ const config2 = loadConfig();
650
+ const next = enablePluginInConfig(config2, id);
651
+ saveConfig(next);
652
+ await this.deps.requestRestart({
653
+ reason: `plugin enabled: ${id}`,
654
+ manualMessage: `Enabled plugin "${id}". Restart the gateway to apply.`
655
+ });
656
+ }
657
+ async pluginsDisable(id) {
658
+ const config2 = loadConfig();
659
+ const next = disablePluginInConfig(config2, id);
660
+ saveConfig(next);
661
+ await this.deps.requestRestart({
662
+ reason: `plugin disabled: ${id}`,
663
+ manualMessage: `Disabled plugin "${id}". Restart the gateway to apply.`
664
+ });
665
+ }
666
+ async pluginsUninstall(id, opts = {}) {
667
+ const config2 = loadConfig();
668
+ const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
669
+ const report = buildPluginStatusReport({
670
+ config: config2,
671
+ workspaceDir,
672
+ reservedChannelIds: Object.keys(config2.channels),
673
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
674
+ });
675
+ const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
676
+ if (opts.keepConfig) {
677
+ console.log("`--keep-config` is deprecated, use `--keep-files`.");
678
+ }
679
+ const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
680
+ const pluginId = plugin?.id ?? id;
681
+ const hasEntry = pluginId in (config2.plugins.entries ?? {});
682
+ const hasInstall = pluginId in (config2.plugins.installs ?? {});
683
+ if (!hasEntry && !hasInstall) {
684
+ if (plugin) {
685
+ console.error(
686
+ `Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`
687
+ );
688
+ } else {
689
+ console.error(`Plugin not found: ${id}`);
690
+ }
691
+ process.exit(1);
692
+ }
693
+ const install = config2.plugins.installs?.[pluginId];
694
+ const isLinked = install?.source === "path" && (!install.installPath || !install.sourcePath || resolve4(install.installPath) === resolve4(install.sourcePath));
695
+ const preview = [];
696
+ if (hasEntry) {
697
+ preview.push("config entry");
698
+ }
699
+ if (hasInstall) {
700
+ preview.push("install record");
701
+ }
702
+ if (config2.plugins.allow?.includes(pluginId)) {
703
+ preview.push("allowlist entry");
704
+ }
705
+ if (isLinked && install?.sourcePath && config2.plugins.load?.paths?.includes(install.sourcePath)) {
706
+ preview.push("load path");
707
+ }
708
+ const deleteTarget = !keepFiles ? resolveUninstallDirectoryTarget({
709
+ pluginId,
710
+ hasInstall,
711
+ installRecord: install
712
+ }) : null;
713
+ if (deleteTarget) {
714
+ preview.push(`directory: ${deleteTarget}`);
715
+ }
716
+ const pluginName = plugin?.name || pluginId;
717
+ const pluginTitle = pluginName !== pluginId ? `${pluginName} (${pluginId})` : pluginName;
718
+ console.log(`Plugin: ${pluginTitle}`);
719
+ console.log(`Will remove: ${preview.length > 0 ? preview.join(", ") : "(nothing)"}`);
720
+ if (opts.dryRun) {
721
+ console.log("Dry run, no changes made.");
722
+ return;
723
+ }
724
+ if (!opts.force) {
725
+ const confirmed = await this.confirmYesNo(`Uninstall plugin "${pluginId}"?`);
726
+ if (!confirmed) {
727
+ console.log("Cancelled.");
728
+ return;
729
+ }
730
+ }
731
+ const result = await uninstallPlugin({
732
+ config: config2,
733
+ pluginId,
734
+ deleteFiles: !keepFiles
735
+ });
736
+ if (!result.ok) {
737
+ console.error(result.error);
738
+ process.exit(1);
739
+ }
740
+ for (const warning of result.warnings) {
741
+ console.warn(warning);
742
+ }
743
+ saveConfig(result.config);
744
+ const removed = [];
745
+ if (result.actions.entry) {
746
+ removed.push("config entry");
747
+ }
748
+ if (result.actions.install) {
749
+ removed.push("install record");
750
+ }
751
+ if (result.actions.allowlist) {
752
+ removed.push("allowlist");
753
+ }
754
+ if (result.actions.loadPath) {
755
+ removed.push("load path");
756
+ }
757
+ if (result.actions.directory) {
758
+ removed.push("directory");
759
+ }
760
+ console.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`);
761
+ await this.deps.requestRestart({
762
+ reason: `plugin uninstalled: ${pluginId}`,
763
+ manualMessage: "Restart the gateway to apply changes."
764
+ });
765
+ }
766
+ async pluginsInstall(pathOrSpec, opts = {}) {
767
+ const fileSpec = this.resolveFileNpmSpecToLocalPath(pathOrSpec);
768
+ if (fileSpec && !fileSpec.ok) {
769
+ console.error(fileSpec.error);
770
+ process.exit(1);
771
+ }
772
+ const normalized = fileSpec && fileSpec.ok ? fileSpec.path : pathOrSpec;
773
+ const resolved = resolve4(expandHome(normalized));
774
+ const config2 = loadConfig();
775
+ if (existsSync3(resolved)) {
776
+ if (opts.link) {
777
+ const probe = await installPluginFromPath({ path: resolved, dryRun: true });
778
+ if (!probe.ok) {
779
+ console.error(probe.error);
780
+ process.exit(1);
781
+ }
782
+ let next3 = addPluginLoadPath(config2, resolved);
783
+ next3 = enablePluginInConfig(next3, probe.pluginId);
784
+ next3 = recordPluginInstall(next3, {
785
+ pluginId: probe.pluginId,
786
+ source: "path",
787
+ sourcePath: resolved,
788
+ installPath: resolved,
789
+ version: probe.version
790
+ });
791
+ saveConfig(next3);
792
+ console.log(`Linked plugin path: ${resolved}`);
793
+ await this.deps.requestRestart({
794
+ reason: `plugin linked: ${probe.pluginId}`,
795
+ manualMessage: "Restart the gateway to load plugins."
796
+ });
797
+ return;
798
+ }
799
+ const result2 = await installPluginFromPath({
800
+ path: resolved,
801
+ logger: {
802
+ info: (message) => console.log(message),
803
+ warn: (message) => console.warn(message)
804
+ }
805
+ });
806
+ if (!result2.ok) {
807
+ console.error(result2.error);
808
+ process.exit(1);
809
+ }
810
+ let next2 = enablePluginInConfig(config2, result2.pluginId);
811
+ next2 = recordPluginInstall(next2, {
812
+ pluginId: result2.pluginId,
813
+ source: this.isArchivePath(resolved) ? "archive" : "path",
814
+ sourcePath: resolved,
815
+ installPath: result2.targetDir,
816
+ version: result2.version
817
+ });
818
+ saveConfig(next2);
819
+ console.log(`Installed plugin: ${result2.pluginId}`);
820
+ await this.deps.requestRestart({
821
+ reason: `plugin installed: ${result2.pluginId}`,
822
+ manualMessage: "Restart the gateway to load plugins."
823
+ });
824
+ return;
825
+ }
826
+ if (opts.link) {
827
+ console.error("`--link` requires a local path.");
828
+ process.exit(1);
829
+ }
830
+ if (this.looksLikePath(pathOrSpec)) {
831
+ console.error(`Path not found: ${resolved}`);
832
+ process.exit(1);
833
+ }
834
+ const result = await installPluginFromNpmSpec({
835
+ spec: pathOrSpec,
836
+ logger: {
837
+ info: (message) => console.log(message),
838
+ warn: (message) => console.warn(message)
839
+ }
840
+ });
841
+ if (!result.ok) {
842
+ console.error(result.error);
843
+ process.exit(1);
844
+ }
845
+ let next = enablePluginInConfig(config2, result.pluginId);
846
+ next = recordPluginInstall(next, {
847
+ pluginId: result.pluginId,
848
+ source: "npm",
849
+ spec: pathOrSpec,
850
+ installPath: result.targetDir,
851
+ version: result.version
852
+ });
853
+ saveConfig(next);
854
+ console.log(`Installed plugin: ${result.pluginId}`);
855
+ await this.deps.requestRestart({
856
+ reason: `plugin installed: ${result.pluginId}`,
857
+ manualMessage: "Restart the gateway to load plugins."
858
+ });
859
+ }
860
+ pluginsDoctor() {
861
+ const config2 = loadConfig();
862
+ const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
863
+ const report = buildPluginStatusReport({
864
+ config: config2,
865
+ workspaceDir,
866
+ reservedChannelIds: Object.keys(config2.channels),
867
+ reservedProviderIds: PROVIDERS.map((provider) => provider.name)
868
+ });
869
+ const pluginErrors = report.plugins.filter((plugin) => plugin.status === "error");
870
+ const diagnostics = report.diagnostics.filter((diag) => diag.level === "error");
871
+ if (pluginErrors.length === 0 && diagnostics.length === 0) {
872
+ console.log("No plugin issues detected.");
873
+ return;
874
+ }
875
+ if (pluginErrors.length > 0) {
876
+ console.log("Plugin errors:");
877
+ for (const entry of pluginErrors) {
878
+ console.log(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`);
879
+ }
880
+ }
881
+ if (diagnostics.length > 0) {
882
+ if (pluginErrors.length > 0) {
883
+ console.log("");
884
+ }
885
+ console.log("Diagnostics:");
886
+ for (const diag of diagnostics) {
887
+ const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
888
+ console.log(`- ${prefix}${diag.message}`);
889
+ }
890
+ }
891
+ }
892
+ async confirmYesNo(question) {
893
+ const rl = createInterface({
894
+ input: process.stdin,
895
+ output: process.stdout
896
+ });
897
+ const answer = await new Promise((resolve9) => {
898
+ rl.question(`${question} [y/N] `, (line) => resolve9(line));
899
+ });
900
+ rl.close();
901
+ const normalized = answer.trim().toLowerCase();
902
+ return normalized === "y" || normalized === "yes";
903
+ }
904
+ resolveFileNpmSpecToLocalPath(raw) {
905
+ const trimmed = raw.trim();
906
+ if (!trimmed.toLowerCase().startsWith("file:")) {
907
+ return null;
908
+ }
909
+ const rest = trimmed.slice("file:".length);
910
+ if (!rest) {
911
+ return { ok: false, error: "unsupported file: spec: missing path" };
912
+ }
913
+ if (rest.startsWith("///")) {
914
+ return { ok: true, path: rest.slice(2) };
915
+ }
916
+ if (rest.startsWith("//localhost/")) {
917
+ return { ok: true, path: rest.slice("//localhost".length) };
918
+ }
919
+ if (rest.startsWith("//")) {
920
+ return {
921
+ ok: false,
922
+ error: 'unsupported file: URL host (expected "file:<path>" or "file:///abs/path")'
923
+ };
924
+ }
925
+ return { ok: true, path: rest };
926
+ }
927
+ looksLikePath(raw) {
928
+ 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");
929
+ }
930
+ isArchivePath(filePath) {
931
+ const lower = filePath.toLowerCase();
932
+ return lower.endsWith(".zip") || lower.endsWith(".tgz") || lower.endsWith(".tar.gz") || lower.endsWith(".tar");
933
+ }
934
+ };
935
+
394
936
  // src/cli/commands/config.ts
395
- import { buildReloadPlan, diffConfigPaths, loadConfig, saveConfig } from "@nextclaw/core";
937
+ import { buildReloadPlan, diffConfigPaths, loadConfig as loadConfig2, saveConfig as saveConfig2 } from "@nextclaw/core";
396
938
 
397
939
  // src/cli/config-path.ts
398
940
  function isIndexSegment(raw) {
@@ -587,7 +1129,7 @@ var ConfigCommands = class {
587
1129
  this.deps = deps;
588
1130
  }
589
1131
  configGet(pathExpr, opts = {}) {
590
- const config2 = loadConfig();
1132
+ const config2 = loadConfig2();
591
1133
  let parsedPath;
592
1134
  try {
593
1135
  parsedPath = parseRequiredConfigPath(pathExpr);
@@ -629,7 +1171,7 @@ var ConfigCommands = class {
629
1171
  process.exit(1);
630
1172
  return;
631
1173
  }
632
- const prevConfig = loadConfig();
1174
+ const prevConfig = loadConfig2();
633
1175
  const nextConfig = structuredClone(prevConfig);
634
1176
  try {
635
1177
  setAtConfigPath(nextConfig, parsedPath, parsedValue);
@@ -638,7 +1180,7 @@ var ConfigCommands = class {
638
1180
  process.exit(1);
639
1181
  return;
640
1182
  }
641
- saveConfig(nextConfig);
1183
+ saveConfig2(nextConfig);
642
1184
  await this.requestRestartForConfigDiff({
643
1185
  prevConfig,
644
1186
  nextConfig,
@@ -655,7 +1197,7 @@ var ConfigCommands = class {
655
1197
  process.exit(1);
656
1198
  return;
657
1199
  }
658
- const prevConfig = loadConfig();
1200
+ const prevConfig = loadConfig2();
659
1201
  const nextConfig = structuredClone(prevConfig);
660
1202
  const removed = unsetAtConfigPath(nextConfig, parsedPath);
661
1203
  if (!removed) {
@@ -663,7 +1205,7 @@ var ConfigCommands = class {
663
1205
  process.exit(1);
664
1206
  return;
665
1207
  }
666
- saveConfig(nextConfig);
1208
+ saveConfig2(nextConfig);
667
1209
  await this.requestRestartForConfigDiff({
668
1210
  prevConfig,
669
1211
  nextConfig,
@@ -689,13 +1231,14 @@ var ConfigCommands = class {
689
1231
 
690
1232
  // src/cli/commands/channels.ts
691
1233
  import { spawnSync as spawnSync3 } from "child_process";
692
- import { loadConfig as loadConfig2 } from "@nextclaw/core";
1234
+ import { getWorkspacePath as getWorkspacePath2, loadConfig as loadConfig3, saveConfig as saveConfig3, PROVIDERS as PROVIDERS2 } from "@nextclaw/core";
1235
+ import { buildPluginStatusReport as buildPluginStatusReport2, enablePluginInConfig as enablePluginInConfig2, getPluginChannelBindings } from "@nextclaw/openclaw-compat";
693
1236
  var ChannelCommands = class {
694
1237
  constructor(deps) {
695
1238
  this.deps = deps;
696
1239
  }
697
1240
  channelsStatus() {
698
- const config2 = loadConfig2();
1241
+ const config2 = loadConfig3();
699
1242
  console.log("Channel Status");
700
1243
  console.log(`WhatsApp: ${config2.channels.whatsapp.enabled ? "\u2713" : "\u2717"}`);
701
1244
  console.log(`Discord: ${config2.channels.discord.enabled ? "\u2713" : "\u2717"}`);
@@ -704,6 +1247,21 @@ var ChannelCommands = class {
704
1247
  console.log(`Telegram: ${config2.channels.telegram.enabled ? "\u2713" : "\u2717"}`);
705
1248
  console.log(`Slack: ${config2.channels.slack.enabled ? "\u2713" : "\u2717"}`);
706
1249
  console.log(`QQ: ${config2.channels.qq.enabled ? "\u2713" : "\u2717"}`);
1250
+ const workspaceDir = getWorkspacePath2(config2.agents.defaults.workspace);
1251
+ const report = buildPluginStatusReport2({
1252
+ config: config2,
1253
+ workspaceDir,
1254
+ reservedChannelIds: Object.keys(config2.channels),
1255
+ reservedProviderIds: PROVIDERS2.map((provider) => provider.name)
1256
+ });
1257
+ const pluginChannels = report.plugins.filter((plugin) => plugin.status === "loaded" && plugin.channelIds.length > 0);
1258
+ if (pluginChannels.length > 0) {
1259
+ console.log("Plugin Channels:");
1260
+ for (const plugin of pluginChannels) {
1261
+ const channels2 = plugin.channelIds.join(", ");
1262
+ console.log(`- ${channels2} (plugin: ${plugin.id})`);
1263
+ }
1264
+ }
707
1265
  }
708
1266
  channelsLogin() {
709
1267
  const bridgeDir = this.deps.getBridgeDir();
@@ -714,6 +1272,62 @@ var ChannelCommands = class {
714
1272
  console.error(`Bridge failed: ${result.status ?? 1}`);
715
1273
  }
716
1274
  }
1275
+ async channelsAdd(opts) {
1276
+ const channelId = opts.channel?.trim();
1277
+ if (!channelId) {
1278
+ console.error("--channel is required");
1279
+ process.exit(1);
1280
+ }
1281
+ const config2 = loadConfig3();
1282
+ const workspaceDir = getWorkspacePath2(config2.agents.defaults.workspace);
1283
+ const pluginRegistry = loadPluginRegistry(config2, workspaceDir);
1284
+ const bindings = getPluginChannelBindings(pluginRegistry);
1285
+ const binding = bindings.find((entry) => entry.channelId === channelId || entry.pluginId === channelId);
1286
+ if (!binding) {
1287
+ console.error(`No plugin channel found for: ${channelId}`);
1288
+ process.exit(1);
1289
+ }
1290
+ const setup = binding.channel.setup;
1291
+ if (!setup?.applyAccountConfig) {
1292
+ console.error(`Channel "${binding.channelId}" does not support setup.`);
1293
+ process.exit(1);
1294
+ }
1295
+ const input = {
1296
+ name: opts.name,
1297
+ token: opts.token,
1298
+ code: opts.code,
1299
+ url: opts.url,
1300
+ httpUrl: opts.httpUrl
1301
+ };
1302
+ const currentView = toPluginConfigView(config2, bindings);
1303
+ const accountId = binding.channel.config?.defaultAccountId?.(currentView) ?? "default";
1304
+ const validateError = setup.validateInput?.({
1305
+ cfg: currentView,
1306
+ input,
1307
+ accountId
1308
+ });
1309
+ if (validateError) {
1310
+ console.error(`Channel setup validation failed: ${validateError}`);
1311
+ process.exit(1);
1312
+ }
1313
+ const nextView = setup.applyAccountConfig({
1314
+ cfg: currentView,
1315
+ input,
1316
+ accountId
1317
+ });
1318
+ if (!nextView || typeof nextView !== "object" || Array.isArray(nextView)) {
1319
+ console.error("Channel setup returned invalid config payload.");
1320
+ process.exit(1);
1321
+ }
1322
+ let next = mergePluginConfigView(config2, nextView, bindings);
1323
+ next = enablePluginInConfig2(next, binding.pluginId);
1324
+ saveConfig3(next);
1325
+ console.log(`Configured channel "${binding.channelId}" via plugin "${binding.pluginId}".`);
1326
+ await this.deps.requestRestart({
1327
+ reason: `channel configured via plugin: ${binding.pluginId}`,
1328
+ manualMessage: "Restart the gateway to apply changes."
1329
+ });
1330
+ }
717
1331
  };
718
1332
 
719
1333
  // src/cli/commands/cron.ts
@@ -794,15 +1408,15 @@ var CronCommands = class {
794
1408
 
795
1409
  // src/cli/commands/diagnostics.ts
796
1410
  import { createServer as createNetServer } from "net";
797
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
798
- import { resolve as resolve4 } from "path";
1411
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1412
+ import { resolve as resolve5 } from "path";
799
1413
  import {
800
1414
  APP_NAME,
801
1415
  getConfigPath,
802
1416
  getDataDir as getDataDir3,
803
- getWorkspacePath,
804
- loadConfig as loadConfig3,
805
- PROVIDERS
1417
+ getWorkspacePath as getWorkspacePath3,
1418
+ loadConfig as loadConfig4,
1419
+ PROVIDERS as PROVIDERS3
806
1420
  } from "@nextclaw/core";
807
1421
  var DiagnosticsCommands = class {
808
1422
  constructor(deps) {
@@ -963,9 +1577,9 @@ var DiagnosticsCommands = class {
963
1577
  }
964
1578
  async collectRuntimeStatus(params) {
965
1579
  const configPath = getConfigPath();
966
- const config2 = loadConfig3();
967
- const workspacePath = getWorkspacePath(config2.agents.defaults.workspace);
968
- const serviceStatePath = resolve4(getDataDir3(), "run", "service.json");
1580
+ const config2 = loadConfig4();
1581
+ const workspacePath = getWorkspacePath3(config2.agents.defaults.workspace);
1582
+ const serviceStatePath = resolve5(getDataDir3(), "run", "service.json");
969
1583
  const fixActions = [];
970
1584
  let serviceState = readServiceState();
971
1585
  if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
@@ -984,7 +1598,7 @@ var DiagnosticsCommands = class {
984
1598
  const managedHealth = running && managedApiUrl ? await this.probeApiHealth(`${managedApiUrl}/health`) : { state: "unreachable", detail: "service not running" };
985
1599
  const configuredHealth = await this.probeApiHealth(`${configuredApiUrl}/health`, 900);
986
1600
  const orphanSuspected = !running && configuredHealth.state === "ok";
987
- const providers = PROVIDERS.map((spec) => {
1601
+ const providers = PROVIDERS3.map((spec) => {
988
1602
  const provider = config2.providers[spec.name];
989
1603
  if (!provider) {
990
1604
  return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
@@ -1004,11 +1618,11 @@ var DiagnosticsCommands = class {
1004
1618
  });
1005
1619
  const issues = [];
1006
1620
  const recommendations = [];
1007
- if (!existsSync3(configPath)) {
1621
+ if (!existsSync4(configPath)) {
1008
1622
  issues.push("Config file is missing.");
1009
1623
  recommendations.push(`Run ${APP_NAME} init to create config files.`);
1010
1624
  }
1011
- if (!existsSync3(workspacePath)) {
1625
+ if (!existsSync4(workspacePath)) {
1012
1626
  issues.push("Workspace directory does not exist.");
1013
1627
  recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
1014
1628
  }
@@ -1036,13 +1650,13 @@ var DiagnosticsCommands = class {
1036
1650
  return {
1037
1651
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1038
1652
  configPath,
1039
- configExists: existsSync3(configPath),
1653
+ configExists: existsSync4(configPath),
1040
1654
  workspacePath,
1041
- workspaceExists: existsSync3(workspacePath),
1655
+ workspaceExists: existsSync4(workspacePath),
1042
1656
  model: config2.agents.defaults.model,
1043
1657
  providers,
1044
1658
  serviceStatePath,
1045
- serviceStateExists: existsSync3(serviceStatePath),
1659
+ serviceStateExists: existsSync4(serviceStatePath),
1046
1660
  fixActions,
1047
1661
  process: {
1048
1662
  managedByState,
@@ -1092,7 +1706,7 @@ var DiagnosticsCommands = class {
1092
1706
  }
1093
1707
  }
1094
1708
  readLogTail(path, maxLines = 25) {
1095
- if (!existsSync3(path)) {
1709
+ if (!existsSync4(path)) {
1096
1710
  return [];
1097
1711
  }
1098
1712
  try {
@@ -1106,17 +1720,17 @@ var DiagnosticsCommands = class {
1106
1720
  }
1107
1721
  }
1108
1722
  async checkPortAvailability(params) {
1109
- return await new Promise((resolve8) => {
1723
+ return await new Promise((resolve9) => {
1110
1724
  const server = createNetServer();
1111
1725
  server.once("error", (error) => {
1112
- resolve8({
1726
+ resolve9({
1113
1727
  available: false,
1114
1728
  detail: `bind failed on ${params.host}:${params.port} (${String(error)})`
1115
1729
  });
1116
1730
  });
1117
1731
  server.listen(params.port, params.host, () => {
1118
1732
  server.close(() => {
1119
- resolve8({
1733
+ resolve9({
1120
1734
  available: true,
1121
1735
  detail: `bind ok on ${params.host}:${params.port}`
1122
1736
  });
@@ -1137,24 +1751,31 @@ import {
1137
1751
  getDataDir as getDataDir4,
1138
1752
  getProvider,
1139
1753
  getProviderName,
1140
- getWorkspacePath as getWorkspacePath2,
1754
+ getWorkspacePath as getWorkspacePath4,
1141
1755
  HeartbeatService,
1142
1756
  LiteLLMProvider,
1143
- loadConfig as loadConfig4,
1757
+ loadConfig as loadConfig5,
1144
1758
  MessageBus,
1145
1759
  ProviderManager,
1146
- saveConfig as saveConfig2,
1760
+ saveConfig as saveConfig4,
1147
1761
  SessionManager
1148
1762
  } from "@nextclaw/core";
1763
+ import {
1764
+ getPluginChannelBindings as getPluginChannelBindings2,
1765
+ resolvePluginChannelMessageToolHints,
1766
+ setPluginRuntimeBridge,
1767
+ startPluginChannelGateways,
1768
+ stopPluginChannelGateways
1769
+ } from "@nextclaw/openclaw-compat";
1149
1770
  import { startUiServer } from "@nextclaw/server";
1150
1771
  import { closeSync, mkdirSync as mkdirSync2, openSync } from "fs";
1151
- import { join as join4, resolve as resolve5 } from "path";
1772
+ import { join as join4, resolve as resolve6 } from "path";
1152
1773
  import { spawn as spawn2 } from "child_process";
1153
1774
  import chokidar from "chokidar";
1154
1775
 
1155
1776
  // src/cli/gateway/controller.ts
1156
1777
  import { createHash } from "crypto";
1157
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1778
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
1158
1779
  import {
1159
1780
  buildConfigSchema,
1160
1781
  ConfigSchema,
@@ -1165,7 +1786,7 @@ var readConfigSnapshot = (getConfigPath4) => {
1165
1786
  const path = getConfigPath4();
1166
1787
  let raw = "";
1167
1788
  let parsed = {};
1168
- if (existsSync4(path)) {
1789
+ if (existsSync5(path)) {
1169
1790
  raw = readFileSync3(path, "utf-8");
1170
1791
  try {
1171
1792
  parsed = JSON.parse(raw);
@@ -1491,8 +2112,11 @@ var ServiceCommands = class {
1491
2112
  this.deps = deps;
1492
2113
  }
1493
2114
  async startGateway(options = {}) {
1494
- const config2 = loadConfig4();
1495
- const workspace = getWorkspacePath2(config2.agents.defaults.workspace);
2115
+ const config2 = loadConfig5();
2116
+ const workspace = getWorkspacePath4(config2.agents.defaults.workspace);
2117
+ const pluginRegistry = loadPluginRegistry(config2, workspace);
2118
+ const extensionRegistry = toExtensionRegistry(pluginRegistry);
2119
+ logPluginDiagnostics(pluginRegistry);
1496
2120
  const bus = new MessageBus();
1497
2121
  const provider = options.allowMissingProvider === true ? this.makeProvider(config2, { allowMissing: true }) : this.makeProvider(config2);
1498
2122
  const providerManager = new ProviderManager(provider ?? this.makeMissingProvider(config2));
@@ -1504,7 +2128,7 @@ var ServiceCommands = class {
1504
2128
  if (!provider) {
1505
2129
  console.warn("Warning: No API key configured. The gateway is running, but agent replies are disabled until provider config is set.");
1506
2130
  }
1507
- const channels2 = new ChannelManager2(config2, bus, sessionManager, []);
2131
+ const channels2 = new ChannelManager2(config2, bus, sessionManager, extensionRegistry.channels);
1508
2132
  const reloader = new ConfigReloader({
1509
2133
  initialConfig: config2,
1510
2134
  channels: channels2,
@@ -1512,7 +2136,8 @@ var ServiceCommands = class {
1512
2136
  sessionManager,
1513
2137
  providerManager,
1514
2138
  makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }) ?? this.makeMissingProvider(nextConfig),
1515
- loadConfig: loadConfig4,
2139
+ loadConfig: loadConfig5,
2140
+ getExtensionChannels: () => extensionRegistry.channels,
1516
2141
  onRestartRequired: (paths) => {
1517
2142
  void this.deps.requestRestart({
1518
2143
  reason: `config reload requires restart: ${paths.join(", ")}`,
@@ -1525,7 +2150,7 @@ var ServiceCommands = class {
1525
2150
  reloader,
1526
2151
  cron: cron2,
1527
2152
  getConfigPath: getConfigPath2,
1528
- saveConfig: saveConfig2,
2153
+ saveConfig: saveConfig4,
1529
2154
  requestRestart: async (options2) => {
1530
2155
  await this.deps.requestRestart({
1531
2156
  reason: options2?.reason ?? "gateway tool restart",
@@ -1551,9 +2176,55 @@ var ServiceCommands = class {
1551
2176
  sessionManager,
1552
2177
  contextConfig: config2.agents.context,
1553
2178
  gatewayController,
1554
- config: config2
2179
+ config: config2,
2180
+ extensionRegistry,
2181
+ resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
2182
+ registry: pluginRegistry,
2183
+ channel,
2184
+ cfg: loadConfig5(),
2185
+ accountId
2186
+ })
1555
2187
  });
1556
2188
  reloader.setApplyAgentRuntimeConfig((nextConfig) => agent.applyRuntimeConfig(nextConfig));
2189
+ const pluginChannelBindings = getPluginChannelBindings2(pluginRegistry);
2190
+ setPluginRuntimeBridge({
2191
+ loadConfig: () => toPluginConfigView(loadConfig5(), pluginChannelBindings),
2192
+ writeConfigFile: async (nextConfigView) => {
2193
+ if (!nextConfigView || typeof nextConfigView !== "object" || Array.isArray(nextConfigView)) {
2194
+ throw new Error("plugin runtime writeConfigFile expects an object config");
2195
+ }
2196
+ const current = loadConfig5();
2197
+ const next = mergePluginConfigView(current, nextConfigView, pluginChannelBindings);
2198
+ saveConfig4(next);
2199
+ },
2200
+ dispatchReplyWithBufferedBlockDispatcher: async ({ ctx, dispatcherOptions }) => {
2201
+ const bodyForAgent = typeof ctx.BodyForAgent === "string" ? ctx.BodyForAgent : "";
2202
+ const body = typeof ctx.Body === "string" ? ctx.Body : "";
2203
+ const content = (bodyForAgent || body).trim();
2204
+ if (!content) {
2205
+ return;
2206
+ }
2207
+ const sessionKey = typeof ctx.SessionKey === "string" && ctx.SessionKey.trim().length > 0 ? ctx.SessionKey : `plugin:${typeof ctx.OriginatingChannel === "string" ? ctx.OriginatingChannel : "channel"}:${typeof ctx.SenderId === "string" ? ctx.SenderId : "unknown"}`;
2208
+ const channel = typeof ctx.OriginatingChannel === "string" && ctx.OriginatingChannel.trim().length > 0 ? ctx.OriginatingChannel : "cli";
2209
+ const chatId = typeof ctx.OriginatingTo === "string" && ctx.OriginatingTo.trim().length > 0 ? ctx.OriginatingTo : typeof ctx.SenderId === "string" && ctx.SenderId.trim().length > 0 ? ctx.SenderId : "direct";
2210
+ try {
2211
+ const response = await agent.processDirect({
2212
+ content,
2213
+ sessionKey,
2214
+ channel,
2215
+ chatId,
2216
+ metadata: typeof ctx.AccountId === "string" && ctx.AccountId.trim().length > 0 ? { account_id: ctx.AccountId } : {}
2217
+ });
2218
+ const replyText = typeof response === "string" ? response : String(response ?? "");
2219
+ if (replyText.trim()) {
2220
+ await dispatcherOptions.deliver({ text: replyText }, { kind: "final" });
2221
+ }
2222
+ } catch (error) {
2223
+ dispatcherOptions.onError?.(error);
2224
+ throw error;
2225
+ }
2226
+ }
2227
+ });
1557
2228
  cron2.onJob = async (job) => {
1558
2229
  const response = await agent.processDirect({
1559
2230
  content: job.payload.message,
@@ -1599,10 +2270,35 @@ var ServiceCommands = class {
1599
2270
  watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
1600
2271
  await cron2.start();
1601
2272
  await heartbeat.start();
1602
- await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
2273
+ let pluginGatewayHandles = [];
2274
+ try {
2275
+ const startedPluginGateways = await startPluginChannelGateways({
2276
+ registry: pluginRegistry,
2277
+ logger: {
2278
+ info: (message) => console.log(`[plugins] ${message}`),
2279
+ warn: (message) => console.warn(`[plugins] ${message}`),
2280
+ error: (message) => console.error(`[plugins] ${message}`),
2281
+ debug: (message) => console.debug(`[plugins] ${message}`)
2282
+ }
2283
+ });
2284
+ pluginGatewayHandles = startedPluginGateways.handles;
2285
+ for (const diag of startedPluginGateways.diagnostics) {
2286
+ const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
2287
+ const text = `${prefix}${diag.message}`;
2288
+ if (diag.level === "error") {
2289
+ console.error(`[plugins] ${text}`);
2290
+ } else {
2291
+ console.warn(`[plugins] ${text}`);
2292
+ }
2293
+ }
2294
+ await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
2295
+ } finally {
2296
+ await stopPluginChannelGateways(pluginGatewayHandles);
2297
+ setPluginRuntimeBridge(null);
2298
+ }
1603
2299
  }
1604
2300
  async runForeground(options) {
1605
- const config2 = loadConfig4();
2301
+ const config2 = loadConfig5();
1606
2302
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
1607
2303
  const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
1608
2304
  if (options.open) {
@@ -1615,7 +2311,7 @@ var ServiceCommands = class {
1615
2311
  });
1616
2312
  }
1617
2313
  async startService(options) {
1618
- const config2 = loadConfig4();
2314
+ const config2 = loadConfig5();
1619
2315
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
1620
2316
  const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
1621
2317
  const apiUrl = `${uiUrl}/api`;
@@ -1664,7 +2360,7 @@ var ServiceCommands = class {
1664
2360
  console.log("Warning: UI frontend not found in package assets.");
1665
2361
  }
1666
2362
  const logPath = resolveServiceLogPath();
1667
- const logDir = resolve5(logPath, "..");
2363
+ const logDir = resolve6(logPath, "..");
1668
2364
  mkdirSync2(logDir, { recursive: true });
1669
2365
  const logFd = openSync(logPath, "a");
1670
2366
  const serveArgs = buildServeArgs({
@@ -1759,22 +2455,22 @@ var ServiceCommands = class {
1759
2455
  try {
1760
2456
  const response = await fetch(params.healthUrl, { method: "GET" });
1761
2457
  if (!response.ok) {
1762
- await new Promise((resolve8) => setTimeout(resolve8, 200));
2458
+ await new Promise((resolve9) => setTimeout(resolve9, 200));
1763
2459
  continue;
1764
2460
  }
1765
2461
  const payload = await response.json();
1766
2462
  const healthy = payload?.ok === true && payload?.data?.status === "ok";
1767
2463
  if (!healthy) {
1768
- await new Promise((resolve8) => setTimeout(resolve8, 200));
2464
+ await new Promise((resolve9) => setTimeout(resolve9, 200));
1769
2465
  continue;
1770
2466
  }
1771
- await new Promise((resolve8) => setTimeout(resolve8, 300));
2467
+ await new Promise((resolve9) => setTimeout(resolve9, 300));
1772
2468
  if (isProcessRunning(params.pid)) {
1773
2469
  return true;
1774
2470
  }
1775
2471
  } catch {
1776
2472
  }
1777
- await new Promise((resolve8) => setTimeout(resolve8, 200));
2473
+ await new Promise((resolve9) => setTimeout(resolve9, 200));
1778
2474
  }
1779
2475
  return false;
1780
2476
  }
@@ -1847,9 +2543,9 @@ var ServiceCommands = class {
1847
2543
  };
1848
2544
 
1849
2545
  // src/cli/workspace.ts
1850
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2546
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1851
2547
  import { createRequire } from "module";
1852
- import { dirname, join as join5, resolve as resolve6 } from "path";
2548
+ import { dirname, join as join5, resolve as resolve7 } from "path";
1853
2549
  import { fileURLToPath as fileURLToPath2 } from "url";
1854
2550
  import { APP_NAME as APP_NAME3, getDataDir as getDataDir5 } from "@nextclaw/core";
1855
2551
  import { spawnSync as spawnSync4 } from "child_process";
@@ -1880,11 +2576,11 @@ var WorkspaceManager = class {
1880
2576
  ];
1881
2577
  for (const entry of templateFiles) {
1882
2578
  const filePath = join5(workspace, entry.target);
1883
- if (!force && existsSync5(filePath)) {
2579
+ if (!force && existsSync6(filePath)) {
1884
2580
  continue;
1885
2581
  }
1886
2582
  const templatePath = join5(templateDir, entry.source);
1887
- if (!existsSync5(templatePath)) {
2583
+ if (!existsSync6(templatePath)) {
1888
2584
  console.warn(`Warning: Template file missing: ${templatePath}`);
1889
2585
  continue;
1890
2586
  }
@@ -1895,12 +2591,12 @@ var WorkspaceManager = class {
1895
2591
  created.push(entry.target);
1896
2592
  }
1897
2593
  const memoryDir = join5(workspace, "memory");
1898
- if (!existsSync5(memoryDir)) {
2594
+ if (!existsSync6(memoryDir)) {
1899
2595
  mkdirSync3(memoryDir, { recursive: true });
1900
2596
  created.push(join5("memory", ""));
1901
2597
  }
1902
2598
  const skillsDir = join5(workspace, "skills");
1903
- if (!existsSync5(skillsDir)) {
2599
+ if (!existsSync6(skillsDir)) {
1904
2600
  mkdirSync3(skillsDir, { recursive: true });
1905
2601
  created.push(join5("skills", ""));
1906
2602
  }
@@ -1922,11 +2618,11 @@ var WorkspaceManager = class {
1922
2618
  continue;
1923
2619
  }
1924
2620
  const src = join5(sourceDir, entry.name);
1925
- if (!existsSync5(join5(src, "SKILL.md"))) {
2621
+ if (!existsSync6(join5(src, "SKILL.md"))) {
1926
2622
  continue;
1927
2623
  }
1928
2624
  const dest = join5(targetDir, entry.name);
1929
- if (!force && existsSync5(dest)) {
2625
+ if (!force && existsSync6(dest)) {
1930
2626
  continue;
1931
2627
  }
1932
2628
  cpSync(src, dest, { recursive: true, force: true });
@@ -1938,13 +2634,13 @@ var WorkspaceManager = class {
1938
2634
  try {
1939
2635
  const require2 = createRequire(import.meta.url);
1940
2636
  const entry = require2.resolve("@nextclaw/core");
1941
- const pkgRoot = resolve6(dirname(entry), "..");
2637
+ const pkgRoot = resolve7(dirname(entry), "..");
1942
2638
  const distSkills = join5(pkgRoot, "dist", "skills");
1943
- if (existsSync5(distSkills)) {
2639
+ if (existsSync6(distSkills)) {
1944
2640
  return distSkills;
1945
2641
  }
1946
2642
  const srcSkills = join5(pkgRoot, "src", "agent", "skills");
1947
- if (existsSync5(srcSkills)) {
2643
+ if (existsSync6(srcSkills)) {
1948
2644
  return srcSkills;
1949
2645
  }
1950
2646
  return null;
@@ -1957,11 +2653,11 @@ var WorkspaceManager = class {
1957
2653
  if (override) {
1958
2654
  return override;
1959
2655
  }
1960
- const cliDir = resolve6(fileURLToPath2(new URL(".", import.meta.url)));
1961
- const pkgRoot = resolve6(cliDir, "..", "..");
2656
+ const cliDir = resolve7(fileURLToPath2(new URL(".", import.meta.url)));
2657
+ const pkgRoot = resolve7(cliDir, "..", "..");
1962
2658
  const candidates = [join5(pkgRoot, "templates")];
1963
2659
  for (const candidate of candidates) {
1964
- if (existsSync5(candidate)) {
2660
+ if (existsSync6(candidate)) {
1965
2661
  return candidate;
1966
2662
  }
1967
2663
  }
@@ -1969,21 +2665,21 @@ var WorkspaceManager = class {
1969
2665
  }
1970
2666
  getBridgeDir() {
1971
2667
  const userBridge = join5(getDataDir5(), "bridge");
1972
- if (existsSync5(join5(userBridge, "dist", "index.js"))) {
2668
+ if (existsSync6(join5(userBridge, "dist", "index.js"))) {
1973
2669
  return userBridge;
1974
2670
  }
1975
2671
  if (!which("npm")) {
1976
2672
  console.error("npm not found. Please install Node.js >= 18.");
1977
2673
  process.exit(1);
1978
2674
  }
1979
- const cliDir = resolve6(fileURLToPath2(new URL(".", import.meta.url)));
1980
- const pkgRoot = resolve6(cliDir, "..", "..");
2675
+ const cliDir = resolve7(fileURLToPath2(new URL(".", import.meta.url)));
2676
+ const pkgRoot = resolve7(cliDir, "..", "..");
1981
2677
  const pkgBridge = join5(pkgRoot, "bridge");
1982
2678
  const srcBridge = join5(pkgRoot, "..", "..", "bridge");
1983
2679
  let source = null;
1984
- if (existsSync5(join5(pkgBridge, "package.json"))) {
2680
+ if (existsSync6(join5(pkgBridge, "package.json"))) {
1985
2681
  source = pkgBridge;
1986
- } else if (existsSync5(join5(srcBridge, "package.json"))) {
2682
+ } else if (existsSync6(join5(srcBridge, "package.json"))) {
1987
2683
  source = srcBridge;
1988
2684
  }
1989
2685
  if (!source) {
@@ -1991,8 +2687,8 @@ var WorkspaceManager = class {
1991
2687
  process.exit(1);
1992
2688
  }
1993
2689
  console.log(`${this.logo} Setting up bridge...`);
1994
- mkdirSync3(resolve6(userBridge, ".."), { recursive: true });
1995
- if (existsSync5(userBridge)) {
2690
+ mkdirSync3(resolve7(userBridge, ".."), { recursive: true });
2691
+ if (existsSync6(userBridge)) {
1996
2692
  rmSync2(userBridge, { recursive: true, force: true });
1997
2693
  }
1998
2694
  cpSync(source, userBridge, {
@@ -2032,6 +2728,7 @@ var CliRuntime = class {
2032
2728
  workspaceManager;
2033
2729
  serviceCommands;
2034
2730
  configCommands;
2731
+ pluginCommands;
2035
2732
  channelCommands;
2036
2733
  cronCommands;
2037
2734
  diagnosticsCommands;
@@ -2044,9 +2741,13 @@ var CliRuntime = class {
2044
2741
  this.configCommands = new ConfigCommands({
2045
2742
  requestRestart: (params) => this.requestRestart(params)
2046
2743
  });
2744
+ this.pluginCommands = new PluginCommands({
2745
+ requestRestart: (params) => this.requestRestart(params)
2746
+ });
2047
2747
  this.channelCommands = new ChannelCommands({
2048
2748
  logo: this.logo,
2049
- getBridgeDir: () => this.workspaceManager.getBridgeDir()
2749
+ getBridgeDir: () => this.workspaceManager.getBridgeDir(),
2750
+ requestRestart: (params) => this.requestRestart(params)
2050
2751
  });
2051
2752
  this.cronCommands = new CronCommands();
2052
2753
  this.diagnosticsCommands = new DiagnosticsCommands({ logo: this.logo });
@@ -2112,7 +2813,7 @@ var CliRuntime = class {
2112
2813
  const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
2113
2814
  const cliPath = process.env.NEXTCLAW_SELF_RELAUNCH_CLI?.trim() || fileURLToPath3(new URL("./index.js", import.meta.url));
2114
2815
  const startArgs = [cliPath, "start", "--ui-port", String(uiPort)];
2115
- const serviceStatePath = resolve7(getDataDir6(), "run", "service.json");
2816
+ const serviceStatePath = resolve8(getDataDir6(), "run", "service.json");
2116
2817
  const helperScript = [
2117
2818
  'const { spawnSync } = require("node:child_process");',
2118
2819
  'const { readFileSync } = require("node:fs");',
@@ -2217,15 +2918,15 @@ var CliRuntime = class {
2217
2918
  const force = Boolean(options.force);
2218
2919
  const configPath = getConfigPath3();
2219
2920
  let createdConfig = false;
2220
- if (!existsSync6(configPath)) {
2921
+ if (!existsSync7(configPath)) {
2221
2922
  const config3 = ConfigSchema2.parse({});
2222
- saveConfig3(config3);
2923
+ saveConfig5(config3);
2223
2924
  createdConfig = true;
2224
2925
  }
2225
- const config2 = loadConfig5();
2926
+ const config2 = loadConfig6();
2226
2927
  const workspaceSetting = config2.agents.defaults.workspace;
2227
- const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join6(getDataDir6(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
2228
- const workspaceExisted = existsSync6(workspacePath);
2928
+ const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join6(getDataDir6(), DEFAULT_WORKSPACE_DIR) : expandHome2(workspaceSetting);
2929
+ const workspaceExisted = existsSync7(workspacePath);
2229
2930
  mkdirSync4(workspacePath, { recursive: true });
2230
2931
  const templateResult = this.workspaceManager.createWorkspaceTemplates(workspacePath, { force });
2231
2932
  if (createdConfig) {
@@ -2322,8 +3023,11 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2322
3023
  await this.serviceCommands.stopService();
2323
3024
  }
2324
3025
  async agent(opts) {
2325
- const config2 = loadConfig5();
2326
- const workspace = getWorkspacePath3(config2.agents.defaults.workspace);
3026
+ const config2 = loadConfig6();
3027
+ const workspace = getWorkspacePath5(config2.agents.defaults.workspace);
3028
+ const pluginRegistry = loadPluginRegistry(config2, workspace);
3029
+ const extensionRegistry = toExtensionRegistry(pluginRegistry);
3030
+ logPluginDiagnostics(pluginRegistry);
2327
3031
  const bus = new MessageBus2();
2328
3032
  const provider = this.serviceCommands.createProvider(config2) ?? this.serviceCommands.createMissingProvider(config2);
2329
3033
  const providerManager = new ProviderManager2(provider);
@@ -2339,7 +3043,14 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2339
3043
  execConfig: config2.tools.exec,
2340
3044
  restrictToWorkspace: config2.tools.restrictToWorkspace,
2341
3045
  contextConfig: config2.agents.context,
2342
- config: config2
3046
+ config: config2,
3047
+ extensionRegistry,
3048
+ resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints2({
3049
+ registry: pluginRegistry,
3050
+ channel,
3051
+ cfg: loadConfig6(),
3052
+ accountId
3053
+ })
2343
3054
  });
2344
3055
  if (opts.message) {
2345
3056
  const response = await agentLoop.processDirect({
@@ -2354,10 +3065,10 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2354
3065
  console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
2355
3066
  `);
2356
3067
  const historyFile = join6(getDataDir6(), "history", "cli_history");
2357
- const historyDir = resolve7(historyFile, "..");
3068
+ const historyDir = resolve8(historyFile, "..");
2358
3069
  mkdirSync4(historyDir, { recursive: true });
2359
- const history = existsSync6(historyFile) ? readFileSync5(historyFile, "utf-8").split("\n").filter(Boolean) : [];
2360
- const rl = createInterface({ input: process.stdin, output: process.stdout });
3070
+ const history = existsSync7(historyFile) ? readFileSync5(historyFile, "utf-8").split("\n").filter(Boolean) : [];
3071
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2361
3072
  rl.on("close", () => {
2362
3073
  const merged = history.concat(rl.history ?? []);
2363
3074
  writeFileSync3(historyFile, merged.join("\n"));
@@ -2417,6 +3128,27 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2417
3128
  console.log(`Tip: restart ${APP_NAME4} to apply the update.`);
2418
3129
  }
2419
3130
  }
3131
+ pluginsList(opts = {}) {
3132
+ this.pluginCommands.pluginsList(opts);
3133
+ }
3134
+ pluginsInfo(id, opts = {}) {
3135
+ this.pluginCommands.pluginsInfo(id, opts);
3136
+ }
3137
+ async pluginsEnable(id) {
3138
+ await this.pluginCommands.pluginsEnable(id);
3139
+ }
3140
+ async pluginsDisable(id) {
3141
+ await this.pluginCommands.pluginsDisable(id);
3142
+ }
3143
+ async pluginsUninstall(id, opts = {}) {
3144
+ await this.pluginCommands.pluginsUninstall(id, opts);
3145
+ }
3146
+ async pluginsInstall(pathOrSpec, opts = {}) {
3147
+ await this.pluginCommands.pluginsInstall(pathOrSpec, opts);
3148
+ }
3149
+ pluginsDoctor() {
3150
+ this.pluginCommands.pluginsDoctor();
3151
+ }
2420
3152
  configGet(pathExpr, opts = {}) {
2421
3153
  this.configCommands.configGet(pathExpr, opts);
2422
3154
  }
@@ -2432,6 +3164,9 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2432
3164
  channelsLogin() {
2433
3165
  this.channelCommands.channelsLogin();
2434
3166
  }
3167
+ async channelsAdd(opts) {
3168
+ await this.channelCommands.channelsAdd(opts);
3169
+ }
2435
3170
  cronList(opts) {
2436
3171
  this.cronCommands.cronList(opts);
2437
3172
  }
@@ -2454,7 +3189,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2454
3189
  await this.diagnosticsCommands.doctor(opts);
2455
3190
  }
2456
3191
  async skillsInstall(options) {
2457
- const workdir = options.workdir ? expandHome(options.workdir) : getWorkspacePath3();
3192
+ const workdir = options.workdir ? expandHome2(options.workdir) : getWorkspacePath5();
2458
3193
  const result = await installClawHubSkill({
2459
3194
  slug: options.slug,
2460
3195
  version: options.version,
@@ -2497,11 +3232,20 @@ var skills = program.command("skills").description("Manage skills");
2497
3232
  registerClawHubInstall(skills);
2498
3233
  var clawhub = program.command("clawhub").description("Install skills from ClawHub");
2499
3234
  registerClawHubInstall(clawhub);
3235
+ var plugins = program.command("plugins").description("Manage OpenClaw-compatible plugins");
3236
+ 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));
3237
+ plugins.command("info <id>").description("Show plugin details").option("--json", "Print JSON").action((id, opts) => runtime.pluginsInfo(id, opts));
3238
+ plugins.command("enable <id>").description("Enable a plugin in config").action((id) => runtime.pluginsEnable(id));
3239
+ plugins.command("disable <id>").description("Disable a plugin in config").action((id) => runtime.pluginsDisable(id));
3240
+ 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));
3241
+ 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));
3242
+ plugins.command("doctor").description("Report plugin load issues").action(() => runtime.pluginsDoctor());
2500
3243
  var config = program.command("config").description("Manage config values");
2501
3244
  config.command("get <path>").description("Get a config value by dot path").option("--json", "Output JSON", false).action((path, opts) => runtime.configGet(path, opts));
2502
3245
  config.command("set <path> <value>").description("Set a config value by dot path").option("--json", "Parse value as JSON", false).action((path, value, opts) => runtime.configSet(path, value, opts));
2503
3246
  config.command("unset <path>").description("Remove a config value by dot path").action((path) => runtime.configUnset(path));
2504
3247
  var channels = program.command("channels").description("Manage channels");
3248
+ channels.command("add").description("Configure a plugin channel (OpenClaw-compatible setup)").requiredOption("--channel <id>", "Plugin channel id").option("--code <code>", "Pairing code").option("--token <token>", "Connector token").option("--name <name>", "Display name").option("--url <url>", "API base URL").option("--http-url <url>", "Alias for --url").action((opts) => runtime.channelsAdd(opts));
2505
3249
  channels.command("status").description("Show channel status").action(() => runtime.channelsStatus());
2506
3250
  channels.command("login").description("Link device via QR code").action(() => runtime.channelsLogin());
2507
3251
  var cron = program.command("cron").description("Manage scheduled tasks");