clawmatrix 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/bin/clawmatrix.mjs +1006 -0
- package/cli/package.json +27 -0
- package/cli/skills/clawmatrix/SKILL.md +104 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -1
- package/src/acp-proxy.ts +425 -37
- package/src/cluster-service.ts +82 -3
- package/src/config.ts +2 -1
- package/src/handoff.ts +8 -0
- package/src/health-tracker.ts +80 -19
- package/src/index.ts +471 -28
- package/src/knowledge-sync.ts +18 -4
- package/src/model-proxy.ts +16 -0
- package/src/peer-manager.ts +318 -25
- package/src/router.ts +93 -1
- package/src/tool-proxy.ts +40 -2
- package/src/tools/cluster-notify.ts +132 -0
- package/src/tools/cluster-peers.ts +2 -0
- package/src/types.ts +1 -1
- package/src/cli.ts +0 -711
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 {
|
|
20
|
+
import { createClusterNotifyTool } from "./tools/cluster-notify.ts";
|
|
21
21
|
import { spawnProcess } from "./compat.ts";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -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) {
|
|
@@ -729,14 +730,455 @@ const plugin = {
|
|
|
729
730
|
},
|
|
730
731
|
);
|
|
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
|
+
|
|
732
1177
|
// Log model selection on each LLM call (fire-and-forget)
|
|
733
1178
|
api.on("llm_input", (event) => {
|
|
734
1179
|
api.logger.debug(`[clawmatrix] llm_input: provider=${event.provider} model=${event.model}`);
|
|
735
1180
|
});
|
|
736
1181
|
|
|
737
|
-
// CLI subcommand
|
|
738
|
-
api.registerCli(registerClusterCli, { commands: ["clawmatrix"] });
|
|
739
|
-
|
|
740
1182
|
// Auto-install global `clawmatrix` shim next to the `openclaw` binary.
|
|
741
1183
|
// Runs once on plugin load; non-blocking, best-effort.
|
|
742
1184
|
installGlobalCliShim(api.logger);
|
|
@@ -824,17 +1266,11 @@ const plugin = {
|
|
|
824
1266
|
// Rebuild system context only when peer count changes
|
|
825
1267
|
if (peerCount !== cachedPeerCount) {
|
|
826
1268
|
cachedPeerCount = peerCount;
|
|
827
|
-
const lines: string[] = [];
|
|
828
1269
|
if (peerCount === 0) {
|
|
829
|
-
|
|
1270
|
+
cachedSystemContext = `[ClawMatrix] node="${config.nodeId}" — no peers online.`;
|
|
830
1271
|
} else {
|
|
831
|
-
|
|
832
|
-
`[ClawMatrix Cluster] YOU ARE node="${config.nodeId}"${config.tags.length ? ` tags=${config.tags.join(",")}` : ""}. ${peerCount} peer(s) online.`,
|
|
833
|
-
...(config.agents.length > 0 ? [`Role: ${config.agents[0]!.description}`] : []),
|
|
834
|
-
"Use cluster_peers to see topology. Always tell user which remote node you're targeting before calling cluster tools.",
|
|
835
|
-
);
|
|
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}` : ""}`;
|
|
836
1273
|
}
|
|
837
|
-
cachedSystemContext = lines.join("\n");
|
|
838
1274
|
}
|
|
839
1275
|
|
|
840
1276
|
// Per-turn: only push pending events (agent must react proactively)
|
|
@@ -962,19 +1398,16 @@ function installGlobalCliShim(logger: { info: (msg: string) => void; warn: (msg:
|
|
|
962
1398
|
const fs = require("node:fs") as typeof import("node:fs");
|
|
963
1399
|
const path = require("node:path") as typeof import("node:path");
|
|
964
1400
|
|
|
965
|
-
// Find the
|
|
966
|
-
|
|
967
|
-
//
|
|
968
|
-
// or we can resolve it from process.env.PATH
|
|
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.
|
|
969
1404
|
let binDir: string | null = null;
|
|
970
1405
|
const envPath = process.env.PATH ?? "";
|
|
971
1406
|
for (const dir of envPath.split(path.delimiter)) {
|
|
972
1407
|
const candidate = path.join(dir, "openclaw");
|
|
973
1408
|
try {
|
|
974
1409
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
975
|
-
|
|
976
|
-
const realPath = fs.realpathSync(candidate);
|
|
977
|
-
binDir = path.dirname(realPath);
|
|
1410
|
+
binDir = dir;
|
|
978
1411
|
break;
|
|
979
1412
|
} catch {
|
|
980
1413
|
// Not in this dir
|
|
@@ -984,20 +1417,30 @@ function installGlobalCliShim(logger: { info: (msg: string) => void; warn: (msg:
|
|
|
984
1417
|
|
|
985
1418
|
const shimPath = path.join(binDir, "clawmatrix");
|
|
986
1419
|
|
|
987
|
-
//
|
|
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.
|
|
988
1423
|
try {
|
|
989
|
-
const
|
|
990
|
-
if (
|
|
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
|
+
}
|
|
991
1433
|
} catch {
|
|
992
1434
|
// File doesn't exist, proceed to create
|
|
993
1435
|
}
|
|
994
1436
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
+
`;
|
|
1001
1444
|
|
|
1002
1445
|
fs.writeFileSync(shimPath, shim, { mode: 0o755 });
|
|
1003
1446
|
logger.info(`[clawmatrix] Installed global CLI shim: ${shimPath}`);
|
package/src/knowledge-sync.ts
CHANGED
|
@@ -87,6 +87,20 @@ async function pMap<T, R>(items: T[], fn: (item: T) => Promise<R>, concurrency:
|
|
|
87
87
|
return results;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Race a promise against a timeout; calls onTimeout before rejecting. */
|
|
91
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, onTimeout?: () => void): Promise<T> {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
onTimeout?.();
|
|
95
|
+
reject(new Error(`Timed out after ${ms}ms`));
|
|
96
|
+
}, ms);
|
|
97
|
+
promise.then(
|
|
98
|
+
(v) => { clearTimeout(timer); resolve(v); },
|
|
99
|
+
(e) => { clearTimeout(timer); reject(e); },
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
async function streamToString(stream: ReadableStream | null): Promise<string> {
|
|
91
105
|
if (!stream) return "";
|
|
92
106
|
const reader = stream.getReader();
|
|
@@ -1074,20 +1088,20 @@ export class KnowledgeSync {
|
|
|
1074
1088
|
}
|
|
1075
1089
|
|
|
1076
1090
|
private async gitCommit(message: string) {
|
|
1091
|
+
const GIT_TIMEOUT_MS = 3000;
|
|
1077
1092
|
try {
|
|
1078
1093
|
const add = spawnProcess(["git", "add", "-A"], {
|
|
1079
1094
|
cwd: this.opts.workspacePath,
|
|
1080
1095
|
stdout: "pipe",
|
|
1081
1096
|
stderr: "pipe",
|
|
1082
1097
|
});
|
|
1083
|
-
await add.exited;
|
|
1084
|
-
|
|
1098
|
+
await withTimeout(add.exited, GIT_TIMEOUT_MS, () => add.kill());
|
|
1085
1099
|
const diff = spawnProcess(["git", "diff", "--cached", "--quiet"], {
|
|
1086
1100
|
cwd: this.opts.workspacePath,
|
|
1087
1101
|
stdout: "pipe",
|
|
1088
1102
|
stderr: "pipe",
|
|
1089
1103
|
});
|
|
1090
|
-
const diffCode = await diff.exited;
|
|
1104
|
+
const diffCode = await withTimeout(diff.exited, GIT_TIMEOUT_MS, () => diff.kill());
|
|
1091
1105
|
if (diffCode === 0) return;
|
|
1092
1106
|
|
|
1093
1107
|
const commit = spawnProcess(
|
|
@@ -1098,7 +1112,7 @@ export class KnowledgeSync {
|
|
|
1098
1112
|
stderr: "pipe",
|
|
1099
1113
|
},
|
|
1100
1114
|
);
|
|
1101
|
-
const commitCode = await commit.exited;
|
|
1115
|
+
const commitCode = await withTimeout(commit.exited, GIT_TIMEOUT_MS, () => commit.kill());
|
|
1102
1116
|
if (commitCode !== 0) {
|
|
1103
1117
|
const stderr = await streamToString(commit.stderr);
|
|
1104
1118
|
debug(TAG, `git commit failed (exit ${commitCode}): ${stderr}`);
|
package/src/model-proxy.ts
CHANGED
|
@@ -43,6 +43,7 @@ interface PendingModelReq {
|
|
|
43
43
|
stream: boolean;
|
|
44
44
|
responseFormat: ResponseFormat;
|
|
45
45
|
model?: string;
|
|
46
|
+
targetNodeId?: string;
|
|
46
47
|
controller?: ReadableStreamDefaultController;
|
|
47
48
|
encoder?: TextEncoder;
|
|
48
49
|
/** Whether real content (not just setup events) has been sent to the stream. */
|
|
@@ -356,12 +357,25 @@ export class ModelProxy {
|
|
|
356
357
|
this.httpServer.listen(this.config.proxyPort, "127.0.0.1");
|
|
357
358
|
}
|
|
358
359
|
|
|
360
|
+
/** Check if there are pending model requests targeting a specific node. */
|
|
361
|
+
hasPendingForNode(nodeId: string): boolean {
|
|
362
|
+
for (const p of this.pending.values()) {
|
|
363
|
+
if (p.targetNodeId === nodeId) return true;
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
359
368
|
stop() {
|
|
360
369
|
if (this.cacheCleanupTimer) {
|
|
361
370
|
clearInterval(this.cacheCleanupTimer);
|
|
362
371
|
this.cacheCleanupTimer = null;
|
|
363
372
|
}
|
|
364
373
|
if (this.httpServer) {
|
|
374
|
+
// Force-close all keep-alive connections so the port is released immediately
|
|
375
|
+
const server = this.httpServer as typeof this.httpServer & { closeAllConnections?: () => void };
|
|
376
|
+
if (typeof server.closeAllConnections === "function") {
|
|
377
|
+
server.closeAllConnections();
|
|
378
|
+
}
|
|
365
379
|
this.httpServer.close();
|
|
366
380
|
this.httpServer = null;
|
|
367
381
|
}
|
|
@@ -648,6 +662,7 @@ export class ModelProxy {
|
|
|
648
662
|
this.pending.set(requestId, {
|
|
649
663
|
resolve: () => {}, reject: () => {},
|
|
650
664
|
timer, stream: true, responseFormat, model,
|
|
665
|
+
targetNodeId,
|
|
651
666
|
controller, encoder,
|
|
652
667
|
hasContent: false,
|
|
653
668
|
failoverCandidates,
|
|
@@ -822,6 +837,7 @@ export class ModelProxy {
|
|
|
822
837
|
this.pending.set(requestId, {
|
|
823
838
|
resolve: resolve as (v: unknown) => void,
|
|
824
839
|
reject, timer, stream: false, responseFormat,
|
|
840
|
+
targetNodeId,
|
|
825
841
|
});
|
|
826
842
|
|
|
827
843
|
const sent = this.peerManager.sendTo(targetNodeId, frame);
|