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
|
@@ -5,7 +5,10 @@ export function createClusterReadTool(): AnyAgentTool {
|
|
|
5
5
|
return {
|
|
6
6
|
name: "cluster_read",
|
|
7
7
|
label: "Cluster Read",
|
|
8
|
-
description:
|
|
8
|
+
description:
|
|
9
|
+
"Read a file from a remote cluster node. " +
|
|
10
|
+
"Returns {content} with the file text. " +
|
|
11
|
+
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
9
12
|
parameters: {
|
|
10
13
|
type: "object",
|
|
11
14
|
properties: {
|
|
@@ -6,7 +6,8 @@ export function createClusterSendTool(): AnyAgentTool {
|
|
|
6
6
|
name: "cluster_send",
|
|
7
7
|
label: "Cluster Send",
|
|
8
8
|
description:
|
|
9
|
-
"Send a one-way
|
|
9
|
+
"Send a one-way notification to a remote agent. Fire-and-forget: does not wait for a response. " +
|
|
10
|
+
"Use cluster_handoff instead when you need the remote agent to process a task and return a result.",
|
|
10
11
|
parameters: {
|
|
11
12
|
type: "object",
|
|
12
13
|
properties: {
|
|
@@ -7,13 +7,10 @@ export function createClusterTerminalTool(): AnyAgentTool {
|
|
|
7
7
|
label: "Cluster Terminal",
|
|
8
8
|
description:
|
|
9
9
|
"Interactive terminal (PTY) on a remote cluster node. " +
|
|
10
|
-
"
|
|
11
|
-
'"open"
|
|
12
|
-
'"
|
|
13
|
-
|
|
14
|
-
'"resize" — resize the terminal. ' +
|
|
15
|
-
'"list" — list active terminal sessions. ' +
|
|
16
|
-
'"close" — close a terminal session.',
|
|
10
|
+
"Workflow: open → input/read (repeat) → close. " +
|
|
11
|
+
'Actions: "open" (requires node), "input" (requires sessionId + data), "read" (requires sessionId), ' +
|
|
12
|
+
'"resize" (requires sessionId), "list" (no params), "close" (requires sessionId). ' +
|
|
13
|
+
"Prefer cluster_exec for one-shot commands; use this for interactive/stateful sessions.",
|
|
17
14
|
parameters: {
|
|
18
15
|
type: "object",
|
|
19
16
|
properties: {
|
|
@@ -7,8 +7,8 @@ export function createClusterToolInvokeTool(): AnyAgentTool {
|
|
|
7
7
|
label: "Cluster Tool Invoke",
|
|
8
8
|
description:
|
|
9
9
|
"Invoke any tool on a remote cluster node by name. " +
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"Preferred for device-specific tools (screenshot, battery, location, clipboard, etc.). " +
|
|
11
|
+
"Run cluster_peers first to discover available tools, or use CLI: clawmatrix tools --describe <tool> for params. " +
|
|
12
12
|
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
13
13
|
parameters: {
|
|
14
14
|
type: "object",
|
|
@@ -5,7 +5,9 @@ export function createClusterWriteTool(): AnyAgentTool {
|
|
|
5
5
|
return {
|
|
6
6
|
name: "cluster_write",
|
|
7
7
|
label: "Cluster Write",
|
|
8
|
-
description:
|
|
8
|
+
description:
|
|
9
|
+
"Write content to a file on a remote cluster node (creates or overwrites). " +
|
|
10
|
+
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
9
11
|
parameters: {
|
|
10
12
|
type: "object",
|
|
11
13
|
properties: {
|
package/src/types.ts
CHANGED
|
@@ -420,10 +420,20 @@ export interface ModelInfo {
|
|
|
420
420
|
compat?: ModelCompatInfo;
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
export interface ToolCatalogEntry {
|
|
424
|
+
name: string;
|
|
425
|
+
description: string;
|
|
426
|
+
usage?: string;
|
|
427
|
+
/** JSON Schema for the tool's input parameters (for LLM callers to construct invocations). */
|
|
428
|
+
inputSchema?: Record<string, unknown>;
|
|
429
|
+
}
|
|
430
|
+
|
|
423
431
|
export interface ToolProxyInfo {
|
|
424
432
|
enabled: boolean;
|
|
425
433
|
allow: string[];
|
|
426
434
|
deny: string[];
|
|
435
|
+
/** Optional tool catalog with descriptions and usage hints for remote callers. */
|
|
436
|
+
catalog?: ToolCatalogEntry[];
|
|
427
437
|
}
|
|
428
438
|
|
|
429
439
|
export interface AcpAgentInfo {
|
|
@@ -559,9 +569,10 @@ export interface AcpStreamChunk extends ClusterFrame {
|
|
|
559
569
|
id: string;
|
|
560
570
|
payload: {
|
|
561
571
|
delta: string;
|
|
562
|
-
event?: string; // "agent_message_chunk" | "tool_call" | etc.
|
|
572
|
+
event?: string; // "agent_message_chunk" | "tool_call" | "plan" | "available_commands" | "config_options" | "usage" | "session_info" | etc.
|
|
563
573
|
done: boolean;
|
|
564
574
|
sessionId?: string; // included for session watchers (multi-device sync)
|
|
575
|
+
data?: unknown; // structured data for rich events (plan entries, slash commands, config options, usage stats)
|
|
565
576
|
};
|
|
566
577
|
}
|
|
567
578
|
|
|
@@ -577,6 +588,10 @@ export interface AcpTaskResponse extends ClusterFrame {
|
|
|
577
588
|
acpSessionId?: string; // ACP-level session ID for future resume
|
|
578
589
|
stopReason?: string; // ACP stop reason
|
|
579
590
|
error?: string;
|
|
591
|
+
configOptions?: AcpConfigOption[]; // initial config options (model, thinking level, etc.)
|
|
592
|
+
slashCommands?: AcpSlashCommand[]; // available slash commands
|
|
593
|
+
availableModes?: AcpModeInfo[]; // available session modes
|
|
594
|
+
currentModeId?: string; // current mode
|
|
580
595
|
};
|
|
581
596
|
}
|
|
582
597
|
|
|
@@ -605,6 +620,7 @@ export interface AcpSessionInfo {
|
|
|
605
620
|
description?: string; // first user message (for display in session list)
|
|
606
621
|
updatedAt?: string;
|
|
607
622
|
agent?: string; // which ACP agent (claude, codex, etc.)
|
|
623
|
+
status?: "busy" | "active" | "idle"; // busy = currently executing a prompt, active = daemon alive, idle = on disk only
|
|
608
624
|
}
|
|
609
625
|
|
|
610
626
|
export interface AcpListRequest extends ClusterFrame {
|
|
@@ -706,6 +722,86 @@ export interface AcpGetModesResponse extends ClusterFrame {
|
|
|
706
722
|
};
|
|
707
723
|
}
|
|
708
724
|
|
|
725
|
+
// ── ACP config options ───────────────────────────────────────────
|
|
726
|
+
|
|
727
|
+
export interface AcpConfigOption {
|
|
728
|
+
id: string;
|
|
729
|
+
name: string;
|
|
730
|
+
type: "select" | "boolean";
|
|
731
|
+
currentValue: string | boolean;
|
|
732
|
+
options?: Array<{ id: string; name: string; description?: string }>;
|
|
733
|
+
description?: string;
|
|
734
|
+
category?: string; // "mode" | "model" | "thought_level" | custom
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export interface AcpSetConfigRequest extends ClusterFrame {
|
|
738
|
+
type: "acp_set_config";
|
|
739
|
+
id: string;
|
|
740
|
+
payload: {
|
|
741
|
+
sessionId: string;
|
|
742
|
+
configId: string;
|
|
743
|
+
value: string | boolean;
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export interface AcpSetConfigResponse extends ClusterFrame {
|
|
748
|
+
type: "acp_set_config_res";
|
|
749
|
+
id: string;
|
|
750
|
+
payload: {
|
|
751
|
+
success: boolean;
|
|
752
|
+
configOptions?: AcpConfigOption[];
|
|
753
|
+
error?: string;
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── ACP subscribe / observe ──────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
export interface AcpSubscribeRequest extends ClusterFrame {
|
|
760
|
+
type: "acp_subscribe";
|
|
761
|
+
id: string;
|
|
762
|
+
payload: {
|
|
763
|
+
sessionId: string; // ClawMatrix session ID to observe
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export interface AcpSubscribeResponse extends ClusterFrame {
|
|
768
|
+
type: "acp_subscribe_res";
|
|
769
|
+
id: string;
|
|
770
|
+
payload: {
|
|
771
|
+
success: boolean;
|
|
772
|
+
history?: ChatHistoryMessage[]; // snapshot at subscribe time
|
|
773
|
+
error?: string;
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export interface AcpUnsubscribeRequest extends ClusterFrame {
|
|
778
|
+
type: "acp_unsubscribe";
|
|
779
|
+
payload: {
|
|
780
|
+
sessionId: string;
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Pushed to all peers when a session is created, its title changes, or it completes. */
|
|
785
|
+
export interface AcpSessionNotify extends ClusterFrame {
|
|
786
|
+
type: "acp_session_notify";
|
|
787
|
+
payload: {
|
|
788
|
+
sessionId: string;
|
|
789
|
+
nodeId: string;
|
|
790
|
+
event: "created" | "updated" | "completed";
|
|
791
|
+
title?: string;
|
|
792
|
+
updatedAt?: string;
|
|
793
|
+
agent?: string;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ── ACP slash commands ──────────────────────────────────────────
|
|
798
|
+
|
|
799
|
+
export interface AcpSlashCommand {
|
|
800
|
+
name: string;
|
|
801
|
+
description: string;
|
|
802
|
+
input?: { hint?: string };
|
|
803
|
+
}
|
|
804
|
+
|
|
709
805
|
// ── Chat history ──────────────────────────────────────────────────
|
|
710
806
|
|
|
711
807
|
export interface ChatHistoryMessage {
|
|
@@ -908,6 +1004,12 @@ export type AnyClusterFrame =
|
|
|
908
1004
|
| AcpSetModeResponse
|
|
909
1005
|
| AcpGetModesRequest
|
|
910
1006
|
| AcpGetModesResponse
|
|
1007
|
+
| AcpSetConfigRequest
|
|
1008
|
+
| AcpSetConfigResponse
|
|
1009
|
+
| AcpSubscribeRequest
|
|
1010
|
+
| AcpSubscribeResponse
|
|
1011
|
+
| AcpUnsubscribeRequest
|
|
1012
|
+
| AcpSessionNotify
|
|
911
1013
|
| ChatHistoryRequest
|
|
912
1014
|
| ChatHistoryResponse
|
|
913
1015
|
| PeerApprovalNotify
|
package/src/web.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ClawMatrixConfig } from "./config.ts";
|
|
|
5
5
|
import type { SatelliteContext, IngestedEvent } from "./types.ts";
|
|
6
6
|
import type { HealthTracker } from "./health-tracker.ts";
|
|
7
7
|
import { timingSafeEqual } from "./auth.ts";
|
|
8
|
-
import { renderDashboard } from "./web-ui.ts";
|
|
9
8
|
import { readBody } from "./http-utils.ts";
|
|
10
9
|
|
|
11
10
|
const COOKIE_NAME = "clawmatrix_token";
|
|
@@ -135,13 +134,6 @@ export class WebHandler {
|
|
|
135
134
|
return true;
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
// Serve dashboard HTML (auth checked client-side via API)
|
|
139
|
-
if (path === "/" && req.method === "GET") {
|
|
140
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
141
|
-
res.end(renderDashboard(this.config.nodeId));
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
137
|
// All /api/* routes require auth (async)
|
|
146
138
|
if (path.startsWith("/api/")) {
|
|
147
139
|
this.handleAuthenticatedRoute(req, res, path);
|
|
@@ -224,7 +216,7 @@ export class WebHandler {
|
|
|
224
216
|
private async checkAuth(req: IncomingMessage): Promise<boolean> {
|
|
225
217
|
// Check Authorization header
|
|
226
218
|
const authHeader = req.headers.authorization;
|
|
227
|
-
if (authHeader?.startsWith("Bearer ") &&
|
|
219
|
+
if (authHeader?.startsWith("Bearer ") && timingSafeEqual(authHeader.slice(7), this.token)) {
|
|
228
220
|
return true;
|
|
229
221
|
}
|
|
230
222
|
|
|
@@ -658,7 +650,7 @@ export class WebHandler {
|
|
|
658
650
|
ssid: ctx.ssid || undefined,
|
|
659
651
|
ip: ctx.ip || undefined,
|
|
660
652
|
router: ctx.router || undefined,
|
|
661
|
-
cellular: !ctx.ssid,
|
|
653
|
+
cellular: typeof ctx.cellular === "boolean" ? ctx.cellular : !ctx.ssid,
|
|
662
654
|
country: ctx.country || undefined,
|
|
663
655
|
tools: Array.isArray(ctx.tools) ? ctx.tools : undefined,
|
|
664
656
|
ts: Date.now(),
|
package/src/cli.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { spawnProcess } from "./compat.ts";
|
|
3
|
-
|
|
4
|
-
async function callGateway(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
5
|
-
const args = ["openclaw", "gateway", "call", method, "--json"];
|
|
6
|
-
if (params) {
|
|
7
|
-
args.push("--params", JSON.stringify(params));
|
|
8
|
-
}
|
|
9
|
-
const proc = spawnProcess(args, {
|
|
10
|
-
stdout: "pipe",
|
|
11
|
-
stderr: "pipe",
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const stdoutChunks: Uint8Array[] = [];
|
|
15
|
-
const stderrChunks: Uint8Array[] = [];
|
|
16
|
-
|
|
17
|
-
const readStream = async (stream: ReadableStream | null, target: Uint8Array[]) => {
|
|
18
|
-
if (!stream) return;
|
|
19
|
-
const reader = stream.getReader();
|
|
20
|
-
while (true) {
|
|
21
|
-
const { done, value } = await reader.read();
|
|
22
|
-
if (done) break;
|
|
23
|
-
target.push(value);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
await Promise.all([
|
|
28
|
-
readStream(proc.stdout, stdoutChunks),
|
|
29
|
-
readStream(proc.stderr, stderrChunks),
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
const code = await proc.exited;
|
|
33
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf-8").trim();
|
|
34
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
35
|
-
|
|
36
|
-
if (code !== 0) {
|
|
37
|
-
// Extract meaningful error from stderr
|
|
38
|
-
const errLine = stderr.split("\n").find((l) => l.includes("Error:") || l.includes("error"));
|
|
39
|
-
throw new Error(errLine || stderr || "Gateway call failed (exit code " + code + ")");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!stdout) {
|
|
43
|
-
throw new Error("Empty response from gateway");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return JSON.parse(stdout);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
50
|
-
const cmd = program.command("clawmatrix").description("ClawMatrix cluster management");
|
|
51
|
-
|
|
52
|
-
cmd
|
|
53
|
-
.command("status")
|
|
54
|
-
.description("Show cluster topology and peer status")
|
|
55
|
-
.action(async () => {
|
|
56
|
-
let data: Record<string, unknown>;
|
|
57
|
-
try {
|
|
58
|
-
data = (await callGateway("clawmatrix.status")) as Record<string, unknown>;
|
|
59
|
-
} catch {
|
|
60
|
-
console.log("Could not reach gateway. Is it running?");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (data.error) {
|
|
65
|
-
console.log(String(data.error));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Style helpers
|
|
70
|
-
const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
|
|
71
|
-
const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
|
|
72
|
-
const green = (s: string) => `\x1b[32m${s}\x1b[39m`;
|
|
73
|
-
const red = (s: string) => `\x1b[31m${s}\x1b[39m`;
|
|
74
|
-
const cyan = (s: string) => `\x1b[36m${s}\x1b[39m`;
|
|
75
|
-
const yellow = (s: string) => `\x1b[33m${s}\x1b[39m`;
|
|
76
|
-
const bar = dim("│");
|
|
77
|
-
const lbl = (text: string) => dim(text.padEnd(13));
|
|
78
|
-
|
|
79
|
-
const agents = data.agents as Array<{ id: string }>;
|
|
80
|
-
const models = data.models as Array<{ id: string }>;
|
|
81
|
-
const tags = data.tags as string[];
|
|
82
|
-
|
|
83
|
-
// Local node
|
|
84
|
-
console.log();
|
|
85
|
-
console.log(` ${cyan("◆")} ${bold("ClawMatrix Cluster")}`);
|
|
86
|
-
console.log(` ${bar}`);
|
|
87
|
-
console.log(` ${bar} ${lbl("Node")}${bold(String(data.nodeId))}`);
|
|
88
|
-
if (tags.length > 0) {
|
|
89
|
-
console.log(` ${bar} ${lbl("Tags")}${tags.join(dim(", "))}`);
|
|
90
|
-
}
|
|
91
|
-
console.log(` ${bar} ${lbl("Listen")}${data.listen !== false ? `:${data.listen}` : dim("disabled")}`);
|
|
92
|
-
console.log(` ${bar} ${lbl("Model Proxy")}:${data.proxyPort}`);
|
|
93
|
-
console.log(` ${bar} ${lbl("Agents")}${agents.map((a) => a.id).join(dim(", ")) || dim("–")}`);
|
|
94
|
-
console.log(` ${bar} ${lbl("Models")}${models.map((m) => m.id).join(dim(", ")) || dim("–")}`);
|
|
95
|
-
|
|
96
|
-
const peers = data.peers as Array<{
|
|
97
|
-
nodeId: string;
|
|
98
|
-
agents: Array<{ id: string }>;
|
|
99
|
-
models: Array<{ id: string }>;
|
|
100
|
-
tags: string[];
|
|
101
|
-
connected: boolean;
|
|
102
|
-
status: "direct" | "relay" | "unreachable" | "sentinel-only";
|
|
103
|
-
latencyMs: number;
|
|
104
|
-
reachableVia: string | null;
|
|
105
|
-
}>;
|
|
106
|
-
|
|
107
|
-
if (!peers || peers.length === 0) {
|
|
108
|
-
console.log(` ${bar}`);
|
|
109
|
-
console.log(` ${dim("◇")} ${dim("No peers discovered")}`);
|
|
110
|
-
console.log();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const reachable = peers.filter((p) => p.connected).length;
|
|
115
|
-
const countStr = `${reachable}/${peers.length} reachable`;
|
|
116
|
-
const countColor = reachable === peers.length ? green : reachable > 0 ? yellow : red;
|
|
117
|
-
|
|
118
|
-
console.log(` ${bar}`);
|
|
119
|
-
console.log(` ${cyan("◆")} ${bold("Peers")} ${countColor(countStr)}`);
|
|
120
|
-
console.log(` ${bar}`);
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < peers.length; i++) {
|
|
123
|
-
const peer = peers[i];
|
|
124
|
-
const dot = peer.status === "direct" ? green("●")
|
|
125
|
-
: peer.status === "relay" ? yellow("●")
|
|
126
|
-
: peer.status === "sentinel-only" ? yellow("◐")
|
|
127
|
-
: red("○");
|
|
128
|
-
const latency = peer.connected && peer.latencyMs > 0 ? dim(` ${peer.latencyMs}ms`) : "";
|
|
129
|
-
const statusLabel = peer.status === "relay"
|
|
130
|
-
? yellow(` relay via ${peer.reachableVia}`)
|
|
131
|
-
: peer.status === "sentinel-only"
|
|
132
|
-
? yellow(" sentinel only")
|
|
133
|
-
: peer.status === "unreachable"
|
|
134
|
-
? red(" unreachable")
|
|
135
|
-
: "";
|
|
136
|
-
console.log(` ${bar} ${dot} ${bold(peer.nodeId)}${statusLabel}${latency}`);
|
|
137
|
-
|
|
138
|
-
if (peer.tags.length > 0) {
|
|
139
|
-
console.log(` ${bar} ${lbl("Tags")}${peer.tags.join(dim(", "))}`);
|
|
140
|
-
}
|
|
141
|
-
const peerAgents = peer.agents.map((a) => a.id).join(dim(", "));
|
|
142
|
-
if (peerAgents) {
|
|
143
|
-
console.log(` ${bar} ${lbl("Agents")}${peerAgents}`);
|
|
144
|
-
}
|
|
145
|
-
const peerModels = peer.models.map((m) => m.id).join(dim(", "));
|
|
146
|
-
if (peerModels) {
|
|
147
|
-
console.log(` ${bar} ${lbl("Models")}${peerModels}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (i < peers.length - 1) {
|
|
151
|
-
console.log(` ${bar}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
console.log(` ${bar}`);
|
|
156
|
-
console.log(` ${dim("◇")}`);
|
|
157
|
-
console.log();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
cmd
|
|
161
|
-
.command("peers")
|
|
162
|
-
.description("List known peers (JSON)")
|
|
163
|
-
.action(async () => {
|
|
164
|
-
let peers: unknown;
|
|
165
|
-
try {
|
|
166
|
-
peers = await callGateway("clawmatrix.peers");
|
|
167
|
-
} catch {
|
|
168
|
-
console.log("[]");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
console.log(JSON.stringify(peers, null, 2));
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// ── Peer approval commands ──────────────────────────────────────
|
|
175
|
-
|
|
176
|
-
cmd
|
|
177
|
-
.command("approve <approvalId>")
|
|
178
|
-
.description("Approve a pending peer join request")
|
|
179
|
-
.action(async (approvalId: string) => {
|
|
180
|
-
try {
|
|
181
|
-
const result = await callGateway("clawmatrix.approval.resolve", {
|
|
182
|
-
approvalId,
|
|
183
|
-
decision: "approve",
|
|
184
|
-
}) as Record<string, unknown>;
|
|
185
|
-
if (result.ok) {
|
|
186
|
-
console.log(`Approved: ${approvalId}`);
|
|
187
|
-
} else {
|
|
188
|
-
console.log(`Approval not found or already resolved: ${approvalId}`);
|
|
189
|
-
}
|
|
190
|
-
} catch {
|
|
191
|
-
console.log("Could not reach gateway. Is it running?");
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
cmd
|
|
196
|
-
.command("deny <approvalId>")
|
|
197
|
-
.description("Deny a pending peer join request")
|
|
198
|
-
.action(async (approvalId: string) => {
|
|
199
|
-
try {
|
|
200
|
-
const result = await callGateway("clawmatrix.approval.resolve", {
|
|
201
|
-
approvalId,
|
|
202
|
-
decision: "deny",
|
|
203
|
-
}) as Record<string, unknown>;
|
|
204
|
-
if (result.ok) {
|
|
205
|
-
console.log(`Denied: ${approvalId}`);
|
|
206
|
-
} else {
|
|
207
|
-
console.log(`Approval not found or already resolved: ${approvalId}`);
|
|
208
|
-
}
|
|
209
|
-
} catch {
|
|
210
|
-
console.log("Could not reach gateway. Is it running?");
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const approval = cmd.command("approval").description("Manage peer approvals");
|
|
215
|
-
|
|
216
|
-
approval
|
|
217
|
-
.command("list")
|
|
218
|
-
.description("List approved and pending peers")
|
|
219
|
-
.action(async () => {
|
|
220
|
-
try {
|
|
221
|
-
const data = await callGateway("clawmatrix.approval.list") as Record<string, unknown>;
|
|
222
|
-
console.log(JSON.stringify(data, null, 2));
|
|
223
|
-
} catch {
|
|
224
|
-
console.log("Could not reach gateway. Is it running?");
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
approval
|
|
229
|
-
.command("revoke <nodeId>")
|
|
230
|
-
.description("Revoke an approved peer")
|
|
231
|
-
.action(async (nodeId: string) => {
|
|
232
|
-
try {
|
|
233
|
-
const result = await callGateway("clawmatrix.approval.revoke", { nodeId }) as Record<string, unknown>;
|
|
234
|
-
if (result.ok) {
|
|
235
|
-
console.log(`Revoked: ${nodeId}`);
|
|
236
|
-
} else {
|
|
237
|
-
console.log(`Node not found in approved list: ${nodeId}`);
|
|
238
|
-
}
|
|
239
|
-
} catch {
|
|
240
|
-
console.log("Could not reach gateway. Is it running?");
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
};
|