clawmatrix 0.2.11 → 0.4.0

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/src/index.ts CHANGED
@@ -17,7 +17,7 @@ import { createClusterAcpTool } from "./tools/cluster-acp.ts";
17
17
  import { createClusterTerminalTool } from "./tools/cluster-terminal.ts";
18
18
  import { createClusterToolInvokeTool } from "./tools/cluster-tool.ts";
19
19
  import { createClusterTransferTool } from "./tools/cluster-transfer.ts";
20
- import { registerClusterCli } from "./cli.ts";
20
+ import { createClusterNotifyTool } from "./tools/cluster-notify.ts";
21
21
  import { spawnProcess } from "./compat.ts";
22
22
 
23
23
  /**
@@ -272,7 +272,7 @@ const plugin = {
272
272
 
273
273
  const repatchTimer = setInterval(patchAllConfigs, 10_000);
274
274
  repatchTimer.unref?.();
275
- api.on("dispose", () => clearInterval(repatchTimer));
275
+ api.on("gateway_stop", () => clearInterval(repatchTimer));
276
276
 
277
277
  for (const [nodeId, models] of Object.entries(modelsByNode)) {
278
278
  api.registerProvider({
@@ -304,6 +304,7 @@ const plugin = {
304
304
  api.registerTool(createClusterTerminalTool(), { optional: true });
305
305
  api.registerTool(createClusterToolInvokeTool(), { optional: true });
306
306
  api.registerTool(createClusterTransferTool(), { optional: true });
307
+ api.registerTool(createClusterNotifyTool(), { optional: true });
307
308
 
308
309
  // Wire up peer approval with OpenClaw channel API
309
310
  if (config.peerApproval.enabled) {
@@ -555,13 +556,632 @@ const plugin = {
555
556
  },
556
557
  );
557
558
 
559
+ // ── Tool proxy CLI gateway methods ──────────────────────────────
560
+
561
+ api.registerGatewayMethod(
562
+ "clawmatrix.tools.list",
563
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
564
+ try {
565
+ const runtime = getClusterRuntime();
566
+ const { node } = (params ?? {}) as { node?: string };
567
+ const allPeers = runtime.peerManager.router.getAllPeers();
568
+ const peers = mergeSentinelPeers(allPeers, runtime)
569
+ .filter((p) => (p as { nodeId: string }).nodeId !== config.nodeId);
570
+
571
+ type PeerToolInfo = { nodeId: string; status: string; toolProxy?: import("./types.ts").ToolProxyInfo };
572
+ const result: PeerToolInfo[] = [];
573
+
574
+ for (const peer of peers) {
575
+ const p = peer as { nodeId: string; connected: boolean; status: string; toolProxy?: import("./types.ts").ToolProxyInfo };
576
+ if (node && p.nodeId !== node) continue;
577
+ if (!p.toolProxy?.enabled) continue;
578
+ result.push({
579
+ nodeId: p.nodeId,
580
+ status: p.status,
581
+ toolProxy: p.toolProxy,
582
+ });
583
+ }
584
+
585
+ respond(true, result);
586
+ } catch {
587
+ respond(false, { error: "ClawMatrix service not running" });
588
+ }
589
+ },
590
+ );
591
+
592
+ api.registerGatewayMethod(
593
+ "clawmatrix.tools.call",
594
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
595
+ try {
596
+ const runtime = getClusterRuntime();
597
+ const { node, tool, params: toolParams, timeout } = (params ?? {}) as {
598
+ node?: string; tool?: string; params?: Record<string, unknown>; timeout?: number;
599
+ };
600
+
601
+ if (!node || !tool) {
602
+ respond(false, { error: "Missing required params: node, tool" });
603
+ return;
604
+ }
605
+
606
+ const result = await runtime.toolProxy.invoke(node, tool, toolParams ?? {}, timeout);
607
+ respond(true, result);
608
+ } catch (err) {
609
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
610
+ }
611
+ },
612
+ );
613
+
614
+ api.registerGatewayMethod(
615
+ "clawmatrix.tools.batch",
616
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
617
+ try {
618
+ const runtime = getClusterRuntime();
619
+ const { node, items, stopOnError, timeout } = (params ?? {}) as {
620
+ node?: string;
621
+ items?: Array<{ tool: string; params?: Record<string, unknown> }>;
622
+ stopOnError?: boolean;
623
+ timeout?: number;
624
+ };
625
+
626
+ if (!node || !items || items.length === 0) {
627
+ respond(false, { error: "Missing required params: node, items" });
628
+ return;
629
+ }
630
+
631
+ const batchItems = items.map((item) => ({
632
+ tool: item.tool,
633
+ params: item.params ?? {},
634
+ }));
635
+
636
+ const results = await runtime.toolProxy.invokeBatch(node, batchItems, { stopOnError, timeout });
637
+ respond(true, results);
638
+ } catch (err) {
639
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
640
+ }
641
+ },
642
+ );
643
+
644
+ // ── Models CLI gateway method ──────────────────────────────────
645
+
646
+ api.registerGatewayMethod(
647
+ "clawmatrix.models.list",
648
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
649
+ try {
650
+ const runtime = getClusterRuntime();
651
+ const { node } = (params ?? {}) as { node?: string };
652
+ const allProxyModels = runtime.modelProxy.allProxyModels;
653
+
654
+ const reachable = new Set(
655
+ runtime.peerManager.router.getAllPeers()
656
+ .filter((p) => p.connection?.isOpen || p.reachableVia)
657
+ .map((p) => p.nodeId),
658
+ );
659
+
660
+ const models = allProxyModels
661
+ .filter((m) => !node || m.nodeId === node)
662
+ .map((m) => ({
663
+ id: m.id,
664
+ nodeId: m.nodeId,
665
+ provider: m.provider ?? m.nodeId,
666
+ ...(m.description && { description: m.description }),
667
+ ...(m.contextWindow && { contextWindow: m.contextWindow }),
668
+ ...(m.maxTokens && { maxTokens: m.maxTokens }),
669
+ ...(m.reasoning !== undefined && { reasoning: m.reasoning }),
670
+ ...(m.input && { input: m.input }),
671
+ ...(m.api && { api: m.api }),
672
+ ...(m.cost && { cost: m.cost }),
673
+ reachable: reachable.has(m.nodeId),
674
+ }));
675
+
676
+ respond(true, models);
677
+ } catch {
678
+ respond(false, { error: "ClawMatrix service not running" });
679
+ }
680
+ },
681
+ );
682
+
683
+ // ── Events CLI gateway methods ──────────────────────────────────
684
+
685
+ api.registerGatewayMethod(
686
+ "clawmatrix.events.query",
687
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
688
+ try {
689
+ const runtime = getClusterRuntime();
690
+ if (!runtime.webHandler) {
691
+ respond(false, { error: "Events not enabled (web.enabled = false)" });
692
+ return;
693
+ }
694
+ const { type, source, since, unconsumed, limit } = (params ?? {}) as {
695
+ type?: string; source?: string; since?: number; unconsumed?: boolean; limit?: number;
696
+ };
697
+ const events = runtime.webHandler.queryEvents({
698
+ type,
699
+ source,
700
+ since,
701
+ unconsumed: unconsumed ?? true,
702
+ limit: limit ?? 20,
703
+ });
704
+ respond(true, events);
705
+ } catch {
706
+ respond(false, { error: "ClawMatrix service not running" });
707
+ }
708
+ },
709
+ );
710
+
711
+ api.registerGatewayMethod(
712
+ "clawmatrix.events.consume",
713
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
714
+ try {
715
+ const runtime = getClusterRuntime();
716
+ if (!runtime.webHandler) {
717
+ respond(false, { error: "Events not enabled (web.enabled = false)" });
718
+ return;
719
+ }
720
+ const { ids } = (params ?? {}) as { ids?: string[] };
721
+ if (!ids || ids.length === 0) {
722
+ respond(false, { error: "Missing required param: ids (array of event IDs)" });
723
+ return;
724
+ }
725
+ const consumed = runtime.webHandler.consumeEvents(ids);
726
+ respond(true, { consumed, ids });
727
+ } catch {
728
+ respond(false, { error: "ClawMatrix service not running" });
729
+ }
730
+ },
731
+ );
732
+
733
+ // ── Send / Handoff gateway methods ──────────────────────────────
734
+
735
+ api.registerGatewayMethod(
736
+ "clawmatrix.send",
737
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
738
+ try {
739
+ const runtime = getClusterRuntime();
740
+ const { node, message } = (params ?? {}) as { node?: string; message?: string };
741
+ if (!node || !message) {
742
+ respond(false, { error: "Missing required params: node, message" });
743
+ return;
744
+ }
745
+ const route = runtime.peerManager.router.resolveAgent(node);
746
+ if (!route) {
747
+ respond(false, { error: `No reachable agent for target "${node}"` });
748
+ return;
749
+ }
750
+ const sent = runtime.peerManager.sendTo(route.nodeId, {
751
+ type: "send",
752
+ from: runtime.config.nodeId,
753
+ to: route.nodeId,
754
+ timestamp: Date.now(),
755
+ payload: { target: node, message },
756
+ });
757
+ respond(true, { sent, nodeId: route.nodeId });
758
+ } catch {
759
+ respond(false, { error: "ClawMatrix service not running" });
760
+ }
761
+ },
762
+ );
763
+
764
+ api.registerGatewayMethod(
765
+ "clawmatrix.handoff",
766
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
767
+ try {
768
+ const runtime = getClusterRuntime();
769
+ const { target, task, context } = (params ?? {}) as {
770
+ target?: string; task?: string; context?: string;
771
+ };
772
+ if (!target || !task) {
773
+ respond(false, { error: "Missing required params: target, task" });
774
+ return;
775
+ }
776
+ const result = await runtime.handoffManager.handoff(target, task, context);
777
+ respond(true, result);
778
+ } catch (err) {
779
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
780
+ }
781
+ },
782
+ );
783
+
784
+ // ── ACP gateway methods ──────────────────────────────────────────
785
+
786
+ api.registerGatewayMethod(
787
+ "clawmatrix.acp.list",
788
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
789
+ try {
790
+ const runtime = getClusterRuntime();
791
+ if (!runtime.acpProxy) {
792
+ respond(false, { error: "ACP proxy not available" });
793
+ return;
794
+ }
795
+ const { node, agent, cwd } = (params ?? {}) as { node?: string; agent?: string; cwd?: string };
796
+ if (!node) {
797
+ respond(false, { error: "Missing required param: node" });
798
+ return;
799
+ }
800
+ const result = await runtime.acpProxy.listSessions(node, { agent, cwd });
801
+ respond(true, result);
802
+ } catch (err) {
803
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
804
+ }
805
+ },
806
+ );
807
+
808
+ api.registerGatewayMethod(
809
+ "clawmatrix.acp.prompt",
810
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
811
+ try {
812
+ const runtime = getClusterRuntime();
813
+ if (!runtime.acpProxy) {
814
+ respond(false, { error: "ACP proxy not available" });
815
+ return;
816
+ }
817
+ const { node, agent, task, sessionId, mode, cwd } = (params ?? {}) as {
818
+ node?: string; agent?: string; task?: string; sessionId?: string;
819
+ mode?: "oneshot" | "persistent"; cwd?: string;
820
+ };
821
+ if (!node) {
822
+ respond(false, { error: "Missing required param: node" });
823
+ return;
824
+ }
825
+ if (!agent && !sessionId) {
826
+ respond(false, { error: "Missing required param: agent (or sessionId for follow-up)" });
827
+ return;
828
+ }
829
+ const result = await runtime.acpProxy.prompt(node, agent ?? "", task ?? "", { sessionId, mode, cwd });
830
+ respond(true, result);
831
+ } catch (err) {
832
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
833
+ }
834
+ },
835
+ );
836
+
837
+ api.registerGatewayMethod(
838
+ "clawmatrix.acp.resume",
839
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
840
+ try {
841
+ const runtime = getClusterRuntime();
842
+ if (!runtime.acpProxy) {
843
+ respond(false, { error: "ACP proxy not available" });
844
+ return;
845
+ }
846
+ const { node, agent, acpSessionId, cwd } = (params ?? {}) as {
847
+ node?: string; agent?: string; acpSessionId?: string; cwd?: string;
848
+ };
849
+ if (!node || !agent || !acpSessionId) {
850
+ respond(false, { error: "Missing required params: node, agent, acpSessionId" });
851
+ return;
852
+ }
853
+ const result = await runtime.acpProxy.resumeSession(node, agent, acpSessionId, cwd ?? process.cwd());
854
+ respond(true, result);
855
+ } catch (err) {
856
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
857
+ }
858
+ },
859
+ );
860
+
861
+ api.registerGatewayMethod(
862
+ "clawmatrix.acp.cancel",
863
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
864
+ try {
865
+ const runtime = getClusterRuntime();
866
+ if (!runtime.acpProxy) {
867
+ respond(false, { error: "ACP proxy not available" });
868
+ return;
869
+ }
870
+ const { node, sessionId } = (params ?? {}) as { node?: string; sessionId?: string };
871
+ if (!node || !sessionId) {
872
+ respond(false, { error: "Missing required params: node, sessionId" });
873
+ return;
874
+ }
875
+ const result = await runtime.acpProxy.cancelSession(node, sessionId);
876
+ respond(true, result);
877
+ } catch (err) {
878
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
879
+ }
880
+ },
881
+ );
882
+
883
+ api.registerGatewayMethod(
884
+ "clawmatrix.acp.close",
885
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
886
+ try {
887
+ const runtime = getClusterRuntime();
888
+ if (!runtime.acpProxy) {
889
+ respond(false, { error: "ACP proxy not available" });
890
+ return;
891
+ }
892
+ const { node, sessionId } = (params ?? {}) as { node?: string; sessionId?: string };
893
+ if (!node || !sessionId) {
894
+ respond(false, { error: "Missing required params: node, sessionId" });
895
+ return;
896
+ }
897
+ const result = await runtime.acpProxy.closeSession(node, sessionId);
898
+ respond(true, result);
899
+ } catch (err) {
900
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
901
+ }
902
+ },
903
+ );
904
+
905
+ // ── Diagnostic gateway method ────────────────────────────────────
906
+
907
+ api.registerGatewayMethod(
908
+ "clawmatrix.diagnostic",
909
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
910
+ try {
911
+ const runtime = getClusterRuntime();
912
+ const { node, action, command, timeout = 30 } = (params ?? {}) as {
913
+ node?: string; action?: string; command?: string; timeout?: number;
914
+ };
915
+ if (!node || !action) {
916
+ respond(false, { error: "Missing required params: node, action" });
917
+ return;
918
+ }
919
+ const sentinelNodeId = node.endsWith(":sentinel") ? node : `${node}:sentinel`;
920
+ const route = runtime.peerManager.router.getRoute(sentinelNodeId);
921
+ if (!route) {
922
+ respond(false, { error: `Sentinel "${sentinelNodeId}" is not reachable` });
923
+ return;
924
+ }
925
+ const id = (await import("node:crypto")).randomUUID();
926
+ const timeoutMs = timeout * 1000;
927
+
928
+ const frameType = action === "exec" ? "diagnostic_exec" : "diagnostic_status";
929
+ const responseType = action === "exec" ? "diagnostic_exec_res" : "diagnostic_status_res";
930
+ const frame = {
931
+ type: frameType,
932
+ id,
933
+ from: runtime.config.nodeId,
934
+ to: sentinelNodeId,
935
+ timestamp: Date.now(),
936
+ ...(action === "exec" ? { payload: { command, timeout } } : {}),
937
+ };
938
+
939
+ const result = await new Promise((resolve, reject) => {
940
+ const timer = setTimeout(() => {
941
+ cleanup();
942
+ reject(new Error(`Diagnostic timed out after ${timeoutMs}ms`));
943
+ }, timeoutMs + (action === "exec" ? 5000 : 0));
944
+
945
+ const handler = (incoming: any) => {
946
+ if (incoming.type === responseType && incoming.id === id) {
947
+ cleanup();
948
+ resolve(incoming.payload);
949
+ }
950
+ };
951
+
952
+ const cleanup = () => {
953
+ clearTimeout(timer);
954
+ runtime.peerManager.off("frame", handler);
955
+ };
956
+
957
+ runtime.peerManager.on("frame", handler);
958
+ const sent = runtime.peerManager.router.sendTo(sentinelNodeId, frame as any);
959
+ if (!sent) {
960
+ cleanup();
961
+ reject(new Error(`No route to ${sentinelNodeId}`));
962
+ }
963
+ });
964
+
965
+ respond(true, result);
966
+ } catch (err) {
967
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
968
+ }
969
+ },
970
+ );
971
+
972
+ // ── Notify gateway method ────────────────────────────────────────
973
+
974
+ api.registerGatewayMethod(
975
+ "clawmatrix.notify",
976
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
977
+ try {
978
+ const runtime = getClusterRuntime();
979
+ const { action = "start", taskId: providedTaskId, title, detail, progress, tool, success = true } = (params ?? {}) as {
980
+ action?: string; taskId?: string; title?: string; detail?: string;
981
+ progress?: number; tool?: string; success?: boolean;
982
+ };
983
+
984
+ const peers = runtime.peerManager.router.getAllPeers();
985
+ const mobileTargets = peers.filter((p) =>
986
+ p.tags.some((t: string) => t === "mobile" || t === "ios" || t === "phone"),
987
+ );
988
+
989
+ if (mobileTargets.length === 0) {
990
+ respond(false, undefined, { code: "NO_MOBILE_PEERS", message: "No mobile peers connected" });
991
+ return;
992
+ }
993
+
994
+ const { randomUUID } = require("node:crypto");
995
+ const taskId = providedTaskId || randomUUID();
996
+ const now = Date.now();
997
+
998
+ let status: string;
999
+ if (action === "start") status = "started";
1000
+ else if (action === "end") status = success ? "completed" : "failed";
1001
+ else status = "progress";
1002
+
1003
+ const frame = {
1004
+ type: "task_activity",
1005
+ from: runtime.config.nodeId,
1006
+ timestamp: now,
1007
+ payload: {
1008
+ taskId,
1009
+ taskType: "notify",
1010
+ status,
1011
+ agent: title || runtime.config.nodeId,
1012
+ nodeId: runtime.config.nodeId,
1013
+ title: title || runtime.config.nodeId,
1014
+ detail,
1015
+ startedAt: now,
1016
+ elapsedMs: 0,
1017
+ progress,
1018
+ tool,
1019
+ },
1020
+ };
1021
+
1022
+ for (const target of mobileTargets) {
1023
+ runtime.peerManager.sendTo(target.nodeId, { ...frame, to: target.nodeId });
1024
+ }
1025
+
1026
+ respond(true, { taskId, action, targets: mobileTargets.length });
1027
+ } catch {
1028
+ respond(false, undefined, { code: "SERVICE_ERROR", message: "ClawMatrix service not running" });
1029
+ }
1030
+ },
1031
+ );
1032
+
1033
+ // ── Terminal gateway methods ──────────────────────────────────────
1034
+
1035
+ api.registerGatewayMethod(
1036
+ "clawmatrix.terminal.list",
1037
+ ({ respond }: GatewayRequestHandlerOptions) => {
1038
+ try {
1039
+ const runtime = getClusterRuntime();
1040
+ respond(true, runtime.terminalManager.listSessions());
1041
+ } catch {
1042
+ respond(false, { error: "ClawMatrix service not running" });
1043
+ }
1044
+ },
1045
+ );
1046
+
1047
+ api.registerGatewayMethod(
1048
+ "clawmatrix.terminal.open",
1049
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
1050
+ try {
1051
+ const runtime = getClusterRuntime();
1052
+ const { node, shell, cols, rows, cwd } = (params ?? {}) as {
1053
+ node?: string; shell?: string; cols?: number; rows?: number; cwd?: string;
1054
+ };
1055
+ if (!node) {
1056
+ respond(false, { error: "Missing required param: node" });
1057
+ return;
1058
+ }
1059
+ const sessionId = await runtime.terminalManager.open(node, { shell, cols, rows, cwd });
1060
+ await new Promise((r) => setTimeout(r, 500));
1061
+ const initial = runtime.terminalManager.readOutput(sessionId);
1062
+ respond(true, { sessionId, nodeId: node, initialOutput: initial.data });
1063
+ } catch (err) {
1064
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
1065
+ }
1066
+ },
1067
+ );
1068
+
1069
+ api.registerGatewayMethod(
1070
+ "clawmatrix.terminal.input",
1071
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1072
+ try {
1073
+ const runtime = getClusterRuntime();
1074
+ const { sessionId, data } = (params ?? {}) as { sessionId?: string; data?: string };
1075
+ if (!sessionId || !data) {
1076
+ respond(false, { error: "Missing required params: sessionId, data" });
1077
+ return;
1078
+ }
1079
+ runtime.terminalManager.sendInput(sessionId, data);
1080
+ respond(true, { sent: true });
1081
+ } catch (err) {
1082
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
1083
+ }
1084
+ },
1085
+ );
1086
+
1087
+ api.registerGatewayMethod(
1088
+ "clawmatrix.terminal.read",
1089
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1090
+ try {
1091
+ const runtime = getClusterRuntime();
1092
+ const { sessionId } = (params ?? {}) as { sessionId?: string };
1093
+ if (!sessionId) {
1094
+ respond(false, { error: "Missing required param: sessionId" });
1095
+ return;
1096
+ }
1097
+ const output = runtime.terminalManager.readOutput(sessionId);
1098
+ respond(true, output);
1099
+ } catch (err) {
1100
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
1101
+ }
1102
+ },
1103
+ );
1104
+
1105
+ api.registerGatewayMethod(
1106
+ "clawmatrix.terminal.resize",
1107
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1108
+ try {
1109
+ const runtime = getClusterRuntime();
1110
+ const { sessionId, cols, rows } = (params ?? {}) as { sessionId?: string; cols?: number; rows?: number };
1111
+ if (!sessionId) {
1112
+ respond(false, { error: "Missing required param: sessionId" });
1113
+ return;
1114
+ }
1115
+ runtime.terminalManager.resize(sessionId, cols ?? 80, rows ?? 24);
1116
+ respond(true, { resized: true });
1117
+ } catch (err) {
1118
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
1119
+ }
1120
+ },
1121
+ );
1122
+
1123
+ api.registerGatewayMethod(
1124
+ "clawmatrix.terminal.close",
1125
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1126
+ try {
1127
+ const runtime = getClusterRuntime();
1128
+ const { sessionId } = (params ?? {}) as { sessionId?: string };
1129
+ if (!sessionId) {
1130
+ respond(false, { error: "Missing required param: sessionId" });
1131
+ return;
1132
+ }
1133
+ runtime.terminalManager.close(sessionId);
1134
+ respond(true, { closed: true });
1135
+ } catch (err) {
1136
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
1137
+ }
1138
+ },
1139
+ );
1140
+
1141
+ // ── File transfer gateway method ─────────────────────────────────
1142
+
1143
+ api.registerGatewayMethod(
1144
+ "clawmatrix.transfer",
1145
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
1146
+ try {
1147
+ const runtime = getClusterRuntime();
1148
+ const ftm = runtime.fileTransferManager;
1149
+ if (!ftm) {
1150
+ respond(false, { error: "File transfer not enabled" });
1151
+ return;
1152
+ }
1153
+ const { source_node, source_path, target_node, target_path } = (params ?? {}) as {
1154
+ source_node?: string; source_path?: string; target_node?: string; target_path?: string;
1155
+ };
1156
+ if (!source_path || !target_path) {
1157
+ respond(false, { error: "Missing required params: source_path, target_path" });
1158
+ return;
1159
+ }
1160
+ if ((source_node && target_node) || (!source_node && !target_node)) {
1161
+ respond(false, { error: "Provide exactly one of source_node (pull) or target_node (push)" });
1162
+ return;
1163
+ }
1164
+ let result;
1165
+ if (source_node) {
1166
+ result = await ftm.pullFile(source_node, source_path, target_path);
1167
+ } else {
1168
+ result = await ftm.pushFile(target_node!, source_path, target_path);
1169
+ }
1170
+ respond(true, result);
1171
+ } catch (err) {
1172
+ respond(false, { error: err instanceof Error ? err.message : String(err) });
1173
+ }
1174
+ },
1175
+ );
1176
+
558
1177
  // Log model selection on each LLM call (fire-and-forget)
559
1178
  api.on("llm_input", (event) => {
560
1179
  api.logger.debug(`[clawmatrix] llm_input: provider=${event.provider} model=${event.model}`);
561
1180
  });
562
1181
 
563
- // CLI subcommand
564
- api.registerCli(registerClusterCli, { commands: ["clawmatrix"] });
1182
+ // Auto-install global `clawmatrix` shim next to the `openclaw` binary.
1183
+ // Runs once on plugin load; non-blocking, best-effort.
1184
+ installGlobalCliShim(api.logger);
565
1185
 
566
1186
  // Plugin command: /clawmatrix approve|deny|revoke
567
1187
  // Handles Telegram callback buttons and other chat surfaces.
@@ -646,19 +1266,11 @@ const plugin = {
646
1266
  // Rebuild system context only when peer count changes
647
1267
  if (peerCount !== cachedPeerCount) {
648
1268
  cachedPeerCount = peerCount;
649
- const lines: string[] = [];
650
1269
  if (peerCount === 0) {
651
- lines.push("[ClawMatrix] No peers online. Use cluster_peers to check cluster status.");
1270
+ cachedSystemContext = `[ClawMatrix] node="${config.nodeId}" no peers online.`;
652
1271
  } else {
653
- lines.push(
654
- `[ClawMatrix Cluster] YOU ARE node="${config.nodeId}"${config.tags.length ? ` tags=${config.tags.join(",")}` : ""}. This is YOUR identity — never target yourself with cluster tools.`,
655
- ...(config.agents.length > 0 ? [`Role: ${config.agents[0]!.description}`] : []),
656
- `${peerCount} remote peer(s) online. Use cluster_peers to see topology, agents, and models.`,
657
- "Prefer cluster_tool for device-specific tools (screenshot, battery, etc.); cluster_exec/read/write for file/shell ops; cluster_handoff for complex multi-step tasks.",
658
- "IMPORTANT: Always tell user which remote node you're targeting before calling cluster tools.",
659
- );
1272
+ cachedSystemContext = `[ClawMatrix] node="${config.nodeId}"${config.tags.length ? ` tags=${config.tags.join(",")}` : ""}, ${peerCount} peer(s) online.${config.agents.length > 0 ? ` Role: ${config.agents[0]!.description}` : ""}`;
660
1273
  }
661
- cachedSystemContext = lines.join("\n");
662
1274
  }
663
1275
 
664
1276
  // Per-turn: only push pending events (agent must react proactively)
@@ -753,6 +1365,9 @@ function mergeSentinelPeers(
753
1365
  status: effectiveStatus,
754
1366
  reachableVia: p.reachableVia,
755
1367
  latencyMs: p.latencyMs,
1368
+ toolProxy: p.toolProxy,
1369
+ acpAgents: p.acpAgents,
1370
+ deviceInfo: p.deviceInfo,
756
1371
  ...(sentinel ? { sentinel: sentinelOnline ? "online" : "offline" } : {}),
757
1372
  });
758
1373
  }
@@ -777,4 +1392,61 @@ function mergeSentinelPeers(
777
1392
  return result;
778
1393
  }
779
1394
 
1395
+ /** Auto-install a global `clawmatrix` CLI shim next to the `openclaw` binary. */
1396
+ function installGlobalCliShim(logger: { info: (msg: string) => void; warn: (msg: string) => void }) {
1397
+ try {
1398
+ const fs = require("node:fs") as typeof import("node:fs");
1399
+ const path = require("node:path") as typeof import("node:path");
1400
+
1401
+ // Find the bin directory where `openclaw` symlink lives (NOT the realpath).
1402
+ // e.g. fnm puts symlinks in .../bin/openclaw -> ../lib/node_modules/openclaw/openclaw.mjs
1403
+ // We want the .../bin/ directory so clawmatrix shim is on PATH.
1404
+ let binDir: string | null = null;
1405
+ const envPath = process.env.PATH ?? "";
1406
+ for (const dir of envPath.split(path.delimiter)) {
1407
+ const candidate = path.join(dir, "openclaw");
1408
+ try {
1409
+ fs.accessSync(candidate, fs.constants.X_OK);
1410
+ binDir = dir;
1411
+ break;
1412
+ } catch {
1413
+ // Not in this dir
1414
+ }
1415
+ }
1416
+ if (!binDir) return;
1417
+
1418
+ const shimPath = path.join(binDir, "clawmatrix");
1419
+
1420
+ // CRITICAL: If shimPath is a symlink (e.g. fnm/npm may create one),
1421
+ // writeFileSync follows the symlink and overwrites the TARGET file.
1422
+ // This previously destroyed openclaw.mjs. Always remove stale symlinks first.
1423
+ try {
1424
+ const stat = fs.lstatSync(shimPath);
1425
+ if (stat.isSymbolicLink()) {
1426
+ fs.unlinkSync(shimPath);
1427
+ // Proceed to create a regular file below
1428
+ } else if (stat.isFile()) {
1429
+ // Regular file — check if it's already our shim
1430
+ const existing = fs.readFileSync(shimPath, "utf-8");
1431
+ if (existing.includes("clawmatrix-shim")) return; // already installed
1432
+ }
1433
+ } catch {
1434
+ // File doesn't exist, proceed to create
1435
+ }
1436
+
1437
+ // Standalone CLI: bypasses openclaw plugin loading entirely.
1438
+ // Resolves cli/bin/clawmatrix.mjs relative to the plugin directory.
1439
+ const cliScript = path.resolve(__dirname, "..", "cli", "bin", "clawmatrix.mjs");
1440
+ const shim = `#!/usr/bin/env sh
1441
+ # clawmatrix-shim: auto-installed by clawmatrix plugin
1442
+ exec node "${cliScript}" "$@"
1443
+ `;
1444
+
1445
+ fs.writeFileSync(shimPath, shim, { mode: 0o755 });
1446
+ logger.info(`[clawmatrix] Installed global CLI shim: ${shimPath}`);
1447
+ } catch {
1448
+ // Best-effort: don't break plugin loading if shim install fails
1449
+ }
1450
+ }
1451
+
780
1452
  export default plugin;