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/LICENSE +27 -0
- package/README.md +123 -12
- 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 +3 -1
- package/src/acp-proxy.ts +820 -96
- package/src/cluster-service.ts +186 -16
- package/src/compat.ts +0 -6
- package/src/config.ts +8 -5
- package/src/connection.ts +61 -55
- package/src/e2e/helpers.ts +1 -5
- package/src/file-transfer.ts +64 -14
- package/src/handoff.ts +21 -8
- package/src/health-tracker.ts +40 -11
- package/src/index.ts +686 -14
- package/src/knowledge-sync.ts +62 -10
- package/src/model-proxy.ts +40 -10
- package/src/peer-manager.ts +114 -17
- package/src/rate-limiter.ts +16 -10
- package/src/router.ts +115 -33
- package/src/sentinel-manager.ts +51 -0
- package/src/sentinel.ts +13 -3
- package/src/tool-proxy.ts +52 -6
- package/src/tools/cluster-diagnostic.ts +3 -2
- package/src/tools/cluster-edit.ts +2 -1
- package/src/tools/cluster-events.ts +3 -1
- package/src/tools/cluster-exec.ts +2 -0
- package/src/tools/cluster-handoff.ts +3 -1
- package/src/tools/cluster-notify.ts +132 -0
- package/src/tools/cluster-peers.ts +3 -1
- package/src/tools/cluster-read.ts +4 -1
- package/src/tools/cluster-send.ts +2 -1
- package/src/tools/cluster-terminal.ts +4 -7
- package/src/tools/cluster-tool.ts +2 -2
- package/src/tools/cluster-write.ts +3 -1
- package/src/types.ts +103 -1
- package/src/web.ts +2 -10
- package/src/cli.ts +0 -243
- package/src/web-ui.ts +0 -1622
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
|
/**
|
|
@@ -272,7 +272,7 @@ const plugin = {
|
|
|
272
272
|
|
|
273
273
|
const repatchTimer = setInterval(patchAllConfigs, 10_000);
|
|
274
274
|
repatchTimer.unref?.();
|
|
275
|
-
api.on("
|
|
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
|
-
//
|
|
564
|
-
|
|
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
|
-
|
|
1270
|
+
cachedSystemContext = `[ClawMatrix] node="${config.nodeId}" — no peers online.`;
|
|
652
1271
|
} else {
|
|
653
|
-
|
|
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;
|