clawmatrix 0.3.1 → 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/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 +416 -31
- package/src/cluster-service.ts +72 -2
- package/src/health-tracker.ts +40 -11
- package/src/index.ts +471 -28
- package/src/knowledge-sync.ts +18 -4
- package/src/model-proxy.ts +5 -0
- package/src/peer-manager.ts +33 -4
- package/src/tool-proxy.ts +40 -2
- package/src/tools/cluster-notify.ts +132 -0
- package/src/types.ts +1 -1
- package/src/cli.ts +0 -711
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ClawMatrix CLI — zero-dependency, connects directly to gateway WebSocket.
|
|
3
|
+
// Requires Node.js >= 22 (built-in WebSocket).
|
|
4
|
+
|
|
5
|
+
import crypto, { randomUUID } from "node:crypto";
|
|
6
|
+
import { readFileSync, mkdirSync, existsSync, readlinkSync, realpathSync, symlinkSync, unlinkSync, rmSync } from "node:fs";
|
|
7
|
+
import { join, resolve, dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
// ── Config resolution ────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PORT = 18789;
|
|
13
|
+
|
|
14
|
+
function readOpenClawConfig() {
|
|
15
|
+
const configPath = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
16
|
+
if (configPath) {
|
|
17
|
+
try { return JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
|
|
18
|
+
}
|
|
19
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || join(homedir(), ".openclaw");
|
|
20
|
+
for (const name of ["openclaw.json", "clawdbot.json"]) {
|
|
21
|
+
try { return JSON.parse(readFileSync(join(stateDir, name), "utf-8")); } catch {}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolvePort() {
|
|
27
|
+
const envRaw = process.env.OPENCLAW_GATEWAY_PORT?.trim() || process.env.CLAWDBOT_GATEWAY_PORT?.trim();
|
|
28
|
+
if (envRaw) {
|
|
29
|
+
const parsed = parseInt(envRaw, 10);
|
|
30
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const port = readOpenClawConfig()?.gateway?.port;
|
|
34
|
+
if (typeof port === "number" && port > 0) return port;
|
|
35
|
+
} catch {}
|
|
36
|
+
return DEFAULT_PORT;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveCredentials() {
|
|
40
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || undefined;
|
|
41
|
+
const password = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || undefined;
|
|
42
|
+
if (token || password) return { token, password };
|
|
43
|
+
try {
|
|
44
|
+
const cfg = readOpenClawConfig();
|
|
45
|
+
return {
|
|
46
|
+
token: cfg?.gateway?.auth?.token?.trim() || undefined,
|
|
47
|
+
password: cfg?.gateway?.auth?.password?.trim() || undefined,
|
|
48
|
+
};
|
|
49
|
+
} catch {}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Device identity (Ed25519 key pair for gateway auth) ──────────────
|
|
54
|
+
|
|
55
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
56
|
+
|
|
57
|
+
function base64UrlEncode(buf) {
|
|
58
|
+
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadDeviceIdentity() {
|
|
62
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || join(homedir(), ".openclaw");
|
|
63
|
+
const idPath = join(stateDir, "identity", "device.json");
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(idPath, "utf-8"));
|
|
66
|
+
if (parsed?.version === 1 && parsed.deviceId && parsed.publicKeyPem && parsed.privateKeyPem) {
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildDeviceAuth(identity, { nonce, token, scopes }) {
|
|
74
|
+
const signedAtMs = Date.now();
|
|
75
|
+
const payload = [
|
|
76
|
+
"v3", identity.deviceId, "cli", "cli", "operator",
|
|
77
|
+
scopes.join(","), String(signedAtMs), token ?? "", nonce, process.platform, "",
|
|
78
|
+
].join("|");
|
|
79
|
+
const key = crypto.createPrivateKey(identity.privateKeyPem);
|
|
80
|
+
const sig = crypto.sign(null, Buffer.from(payload, "utf8"), key);
|
|
81
|
+
const pubKey = crypto.createPublicKey(identity.publicKeyPem);
|
|
82
|
+
const spki = pubKey.export({ type: "spki", format: "der" });
|
|
83
|
+
const rawKey = spki.length === ED25519_SPKI_PREFIX.length + 32
|
|
84
|
+
? spki.subarray(ED25519_SPKI_PREFIX.length)
|
|
85
|
+
: spki;
|
|
86
|
+
return {
|
|
87
|
+
id: identity.deviceId,
|
|
88
|
+
publicKey: base64UrlEncode(rawKey),
|
|
89
|
+
signature: base64UrlEncode(sig),
|
|
90
|
+
signedAt: signedAtMs,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Gateway WebSocket RPC (built-in WebSocket, W3C API) ──────────────
|
|
95
|
+
|
|
96
|
+
const TIMEOUT_MS = 10_000;
|
|
97
|
+
|
|
98
|
+
function callGateway(method, params, timeoutMs = TIMEOUT_MS) {
|
|
99
|
+
const port = resolvePort();
|
|
100
|
+
const creds = resolveCredentials();
|
|
101
|
+
const url = `ws://127.0.0.1:${port}`;
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const ws = new WebSocket(url);
|
|
105
|
+
const callId = randomUUID();
|
|
106
|
+
let connectId;
|
|
107
|
+
let settled = false;
|
|
108
|
+
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
if (!settled) {
|
|
111
|
+
settled = true;
|
|
112
|
+
ws.close();
|
|
113
|
+
reject(new Error(`Gateway call timed out (${timeoutMs}ms). Is the gateway running?`));
|
|
114
|
+
}
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
|
|
117
|
+
const finish = (err, result) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
ws.close();
|
|
122
|
+
err ? reject(err) : resolve(result);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
ws.addEventListener("error", () => {
|
|
126
|
+
finish(new Error(`Could not connect to gateway at ${url}`));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
ws.addEventListener("close", () => {
|
|
130
|
+
finish(new Error("Gateway connection closed unexpectedly"));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
ws.addEventListener("message", (event) => {
|
|
134
|
+
try {
|
|
135
|
+
const frame = JSON.parse(event.data);
|
|
136
|
+
|
|
137
|
+
if (frame.type === "event" && frame.event === "connect.challenge") {
|
|
138
|
+
connectId = randomUUID();
|
|
139
|
+
const nonce = frame.payload?.nonce;
|
|
140
|
+
const scopes = ["operator.admin", "operator.read", "operator.write"];
|
|
141
|
+
const identity = loadDeviceIdentity();
|
|
142
|
+
const connectParams = {
|
|
143
|
+
minProtocol: 3,
|
|
144
|
+
maxProtocol: 3,
|
|
145
|
+
client: {
|
|
146
|
+
id: "cli",
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
platform: process.platform,
|
|
149
|
+
mode: "cli",
|
|
150
|
+
instanceId: randomUUID(),
|
|
151
|
+
},
|
|
152
|
+
auth: { token: creds.token, password: creds.password },
|
|
153
|
+
role: "operator",
|
|
154
|
+
scopes,
|
|
155
|
+
caps: [],
|
|
156
|
+
};
|
|
157
|
+
if (identity && nonce) {
|
|
158
|
+
const device = buildDeviceAuth(identity, {
|
|
159
|
+
nonce, token: creds.token, scopes,
|
|
160
|
+
});
|
|
161
|
+
device.nonce = nonce;
|
|
162
|
+
connectParams.device = device;
|
|
163
|
+
}
|
|
164
|
+
ws.send(JSON.stringify({
|
|
165
|
+
type: "req", id: connectId, method: "connect", params: connectParams,
|
|
166
|
+
}));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (frame.type === "res" && frame.id === connectId) {
|
|
171
|
+
if (!frame.ok) {
|
|
172
|
+
finish(new Error(`Gateway auth failed: ${frame.error?.message ?? "unknown error"}`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
ws.send(JSON.stringify({ type: "req", id: callId, method, params: params ?? {} }));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (frame.type === "res" && frame.id === callId) {
|
|
180
|
+
frame.ok ? finish(null, frame.payload) : finish(new Error(frame.error?.message ?? frame.payload?.error ?? `Gateway call failed: ${method}`));
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
finish(err instanceof Error ? err : new Error(String(err)));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Style helpers ────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const isTTY = process.stdout.isTTY === true;
|
|
192
|
+
const ansi = (code, reset) =>
|
|
193
|
+
isTTY ? (s) => `\x1b[${code}m${s}\x1b[${reset}m` : (s) => s;
|
|
194
|
+
|
|
195
|
+
const bold = ansi("1", "22");
|
|
196
|
+
const dim = ansi("2", "22");
|
|
197
|
+
const green = ansi("32", "39");
|
|
198
|
+
const red = ansi("31", "39");
|
|
199
|
+
const cyan = ansi("36", "39");
|
|
200
|
+
const yellow = ansi("33", "39");
|
|
201
|
+
|
|
202
|
+
const jsonOut = (data) => JSON.stringify(data, null, isTTY ? 2 : 0);
|
|
203
|
+
const bar = dim("│");
|
|
204
|
+
const lbl = (text) => dim(text.padEnd(13));
|
|
205
|
+
|
|
206
|
+
// ── Commands ─────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
async function cmdStatus(args) {
|
|
209
|
+
const isJson = args.includes("--json");
|
|
210
|
+
let data;
|
|
211
|
+
try {
|
|
212
|
+
data = await callGateway("clawmatrix.status");
|
|
213
|
+
} catch {
|
|
214
|
+
console.log("Could not reach gateway. Is it running?");
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (data.error) { console.log(String(data.error)); return; }
|
|
219
|
+
if (isJson) { console.log(jsonOut(data)); return; }
|
|
220
|
+
|
|
221
|
+
const agents = data.agents || [];
|
|
222
|
+
const models = data.models || [];
|
|
223
|
+
const tags = data.tags || [];
|
|
224
|
+
const peers = data.peers || [];
|
|
225
|
+
|
|
226
|
+
if (!isTTY) {
|
|
227
|
+
// Compact output for AI/scripting consumption
|
|
228
|
+
const parts = [`Node: ${data.nodeId}`];
|
|
229
|
+
if (tags.length > 0) parts.push(`Tags: ${tags.join(", ")}`);
|
|
230
|
+
parts.push(`Listen: ${data.listen !== false ? `:${data.listen}` : "disabled"}`);
|
|
231
|
+
parts.push(`Model Proxy: :${data.proxyPort}`);
|
|
232
|
+
if (agents.length > 0) parts.push(`Agents: ${agents.map((a) => a.id).join(", ")}`);
|
|
233
|
+
if (models.length > 0) parts.push(`Models: ${models.map((m) => m.id).join(", ")}`);
|
|
234
|
+
console.log(parts.join(" | "));
|
|
235
|
+
|
|
236
|
+
if (peers.length === 0) { console.log("Peers: none"); return; }
|
|
237
|
+
|
|
238
|
+
const reachable = peers.filter((p) => p.connected).length;
|
|
239
|
+
console.log(`Peers: ${reachable}/${peers.length} reachable`);
|
|
240
|
+
for (const peer of peers) {
|
|
241
|
+
const info = [`[${peer.status}]`, peer.nodeId];
|
|
242
|
+
if (peer.latencyMs > 0) info.push(`${peer.latencyMs}ms`);
|
|
243
|
+
if (peer.status === "relay") info.push(`via ${peer.reachableVia}`);
|
|
244
|
+
if (peer.tags?.length > 0) info.push(`Tags: ${peer.tags.join(", ")}`);
|
|
245
|
+
const pa = (peer.agents || []).map((a) => a.id).join(", ");
|
|
246
|
+
if (pa) info.push(`Agents: ${pa}`);
|
|
247
|
+
const pm = (peer.models || []).map((m) => m.id).join(", ");
|
|
248
|
+
if (pm) info.push(`Models: ${pm}`);
|
|
249
|
+
console.log(info.join(" | "));
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log();
|
|
255
|
+
console.log(` ${cyan("◆")} ${bold("ClawMatrix Cluster")}`);
|
|
256
|
+
console.log(` ${bar}`);
|
|
257
|
+
console.log(` ${bar} ${lbl("Node")}${bold(String(data.nodeId))}`);
|
|
258
|
+
if (tags.length > 0) console.log(` ${bar} ${lbl("Tags")}${tags.join(dim(", "))}`);
|
|
259
|
+
console.log(` ${bar} ${lbl("Listen")}${data.listen !== false ? `:${data.listen}` : dim("disabled")}`);
|
|
260
|
+
console.log(` ${bar} ${lbl("Model Proxy")}:${data.proxyPort}`);
|
|
261
|
+
console.log(` ${bar} ${lbl("Agents")}${agents.map((a) => a.id).join(dim(", ")) || dim("–")}`);
|
|
262
|
+
console.log(` ${bar} ${lbl("Models")}${models.map((m) => m.id).join(dim(", ")) || dim("–")}`);
|
|
263
|
+
|
|
264
|
+
if (peers.length === 0) {
|
|
265
|
+
console.log(` ${bar}`);
|
|
266
|
+
console.log(` ${dim("◇")} ${dim("No peers discovered")}`);
|
|
267
|
+
console.log();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const reachable = peers.filter((p) => p.connected).length;
|
|
272
|
+
const countStr = `${reachable}/${peers.length} reachable`;
|
|
273
|
+
const countColor = reachable === peers.length ? green : reachable > 0 ? yellow : red;
|
|
274
|
+
|
|
275
|
+
console.log(` ${bar}`);
|
|
276
|
+
console.log(` ${cyan("◆")} ${bold("Peers")} ${countColor(countStr)}`);
|
|
277
|
+
console.log(` ${bar}`);
|
|
278
|
+
|
|
279
|
+
for (let i = 0; i < peers.length; i++) {
|
|
280
|
+
const peer = peers[i];
|
|
281
|
+
const dot = peer.status === "direct" ? green("●")
|
|
282
|
+
: peer.status === "relay" ? yellow("●")
|
|
283
|
+
: peer.status === "sentinel-only" ? yellow("◐")
|
|
284
|
+
: red("○");
|
|
285
|
+
const latency = peer.connected && peer.latencyMs > 0 ? dim(` ${peer.latencyMs}ms`) : "";
|
|
286
|
+
const statusLabel = peer.status === "relay" ? yellow(` relay via ${peer.reachableVia}`)
|
|
287
|
+
: peer.status === "sentinel-only" ? yellow(" sentinel only")
|
|
288
|
+
: peer.status === "unreachable" ? red(" unreachable") : "";
|
|
289
|
+
console.log(` ${bar} ${dot} ${bold(peer.nodeId)}${statusLabel}${latency}`);
|
|
290
|
+
if (peer.tags?.length > 0) console.log(` ${bar} ${lbl("Tags")}${peer.tags.join(dim(", "))}`);
|
|
291
|
+
const peerAgents = (peer.agents || []).map((a) => a.id).join(dim(", "));
|
|
292
|
+
if (peerAgents) console.log(` ${bar} ${lbl("Agents")}${peerAgents}`);
|
|
293
|
+
const peerModels = (peer.models || []).map((m) => m.id).join(dim(", "));
|
|
294
|
+
if (peerModels) console.log(` ${bar} ${lbl("Models")}${peerModels}`);
|
|
295
|
+
if (i < peers.length - 1) console.log(` ${bar}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(` ${bar}`);
|
|
299
|
+
console.log(` ${dim("◇")}`);
|
|
300
|
+
console.log();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function cmdPeers() {
|
|
304
|
+
try {
|
|
305
|
+
const peers = await callGateway("clawmatrix.peers");
|
|
306
|
+
console.log(jsonOut(peers));
|
|
307
|
+
} catch {
|
|
308
|
+
console.log("[]");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function cmdCheck(nodeId) {
|
|
313
|
+
if (!nodeId) { console.error("Usage: clawmatrix check <nodeId>"); process.exit(1); }
|
|
314
|
+
try {
|
|
315
|
+
const peers = await callGateway("clawmatrix.peers");
|
|
316
|
+
const peer = peers.find((p) => p.nodeId === nodeId);
|
|
317
|
+
if (!peer) {
|
|
318
|
+
console.log(jsonOut({ nodeId, reachable: false, status: "unknown", error: "Node not found in peer list" }));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
console.log(jsonOut({
|
|
322
|
+
nodeId: peer.nodeId, reachable: peer.connected, status: peer.status,
|
|
323
|
+
latencyMs: peer.latencyMs, agents: peer.agents?.length ?? 0,
|
|
324
|
+
models: peer.models?.length ?? 0, toolProxy: peer.toolProxy?.enabled ?? false,
|
|
325
|
+
}));
|
|
326
|
+
if (!peer.connected) process.exitCode = 1;
|
|
327
|
+
} catch {
|
|
328
|
+
console.log(jsonOut({ nodeId, reachable: false, status: "error", error: "Could not reach gateway" }));
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function cmdTools(args) {
|
|
334
|
+
const isJson = args.includes("--json");
|
|
335
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
336
|
+
const filterIdx = args.indexOf("-f") !== -1 ? args.indexOf("-f") : args.indexOf("--filter");
|
|
337
|
+
const filterKw = filterIdx !== -1 ? args[filterIdx + 1] : undefined;
|
|
338
|
+
const descIdx = args.indexOf("-d") !== -1 ? args.indexOf("-d") : args.indexOf("--describe");
|
|
339
|
+
const describeTool = descIdx !== -1 ? args[descIdx + 1] : undefined;
|
|
340
|
+
const node = args.find((a) => !a.startsWith("-") && a !== filterKw && a !== describeTool);
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const data = await callGateway("clawmatrix.tools.list", node ? { node } : undefined);
|
|
344
|
+
if (isJson) { console.log(jsonOut(data)); return; }
|
|
345
|
+
if (data.length === 0) { console.log("No nodes with tool proxy enabled."); return; }
|
|
346
|
+
|
|
347
|
+
if (describeTool) {
|
|
348
|
+
const toolName = describeTool.toLowerCase();
|
|
349
|
+
for (const entry of data) {
|
|
350
|
+
const found = (entry.toolProxy.catalog || []).find((t) => t.name.toLowerCase() === toolName);
|
|
351
|
+
if (found) {
|
|
352
|
+
const parts = [`node: ${entry.nodeId}`, `tool: ${found.name}`, `description: ${found.description}`];
|
|
353
|
+
if (found.inputSchema) parts.push(`params: ${JSON.stringify(found.inputSchema)}`);
|
|
354
|
+
if (found.usage) parts.push(`usage:\n${found.usage}`);
|
|
355
|
+
parts.push(`invoke: clawmatrix call ${entry.nodeId} ${found.name} '{}'`);
|
|
356
|
+
console.log(parts.join(isTTY ? "\n" : " | "));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
console.error(`Tool "${describeTool}" not found. Run 'clawmatrix tools' to see available tools.`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const filterLower = filterKw?.toLowerCase();
|
|
365
|
+
const matchesFilter = (t) =>
|
|
366
|
+
!filterLower || t.name.toLowerCase().includes(filterLower) ||
|
|
367
|
+
t.description.toLowerCase().includes(filterLower) ||
|
|
368
|
+
(t.usage ?? "").toLowerCase().includes(filterLower);
|
|
369
|
+
|
|
370
|
+
if (!isTTY) {
|
|
371
|
+
// Compact output for AI/scripting consumption
|
|
372
|
+
for (const entry of data) {
|
|
373
|
+
const tools = entry.toolProxy.allow;
|
|
374
|
+
const catalog = entry.toolProxy.catalog || [];
|
|
375
|
+
const catalogMap = new Map(catalog.map((t) => [t.name, t]));
|
|
376
|
+
const isWildcard = tools.includes("*");
|
|
377
|
+
const toolsToShow = isWildcard
|
|
378
|
+
? catalog.filter(matchesFilter)
|
|
379
|
+
: tools.map((name) => catalogMap.get(name) ?? { name, description: "" }).filter(matchesFilter);
|
|
380
|
+
const totalCount = isWildcard ? catalog.length : tools.length;
|
|
381
|
+
const shownCount = toolsToShow.length;
|
|
382
|
+
const countLabel = filterLower ? `${shownCount}/${totalCount} matched` : `${totalCount} tools`;
|
|
383
|
+
const denied = entry.toolProxy.deny;
|
|
384
|
+
|
|
385
|
+
console.log(`${entry.nodeId} | ${entry.status} | ${countLabel}`);
|
|
386
|
+
for (const tool of toolsToShow) {
|
|
387
|
+
const desc = tool.description ? ` - ${tool.description}` : "";
|
|
388
|
+
console.log(` ${tool.name}${desc}`);
|
|
389
|
+
}
|
|
390
|
+
if (denied.length > 0) console.log(` Denied: ${denied.join(", ")}`);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log();
|
|
396
|
+
for (const entry of data) {
|
|
397
|
+
const tools = entry.toolProxy.allow;
|
|
398
|
+
const catalog = entry.toolProxy.catalog || [];
|
|
399
|
+
const catalogMap = new Map(catalog.map((t) => [t.name, t]));
|
|
400
|
+
const denied = entry.toolProxy.deny;
|
|
401
|
+
const isWildcard = tools.includes("*");
|
|
402
|
+
const toolsToShow = isWildcard
|
|
403
|
+
? catalog.filter(matchesFilter)
|
|
404
|
+
: tools.map((name) => catalogMap.get(name) ?? { name, description: "" }).filter(matchesFilter);
|
|
405
|
+
const totalCount = isWildcard ? catalog.length : tools.length;
|
|
406
|
+
const shownCount = toolsToShow.length;
|
|
407
|
+
const countLabel = filterLower ? `${shownCount}/${totalCount} matched` : `${totalCount} tools`;
|
|
408
|
+
|
|
409
|
+
console.log(` ${cyan("◆")} ${bold(entry.nodeId)} ${green(entry.status)} ${dim(countLabel)}`);
|
|
410
|
+
console.log(` ${bar}`);
|
|
411
|
+
|
|
412
|
+
if (toolsToShow.length === 0 && !filterLower) {
|
|
413
|
+
console.log(` ${bar} ${isWildcard ? green("*") + " " + dim("(all tools allowed)") : dim("No tools advertised")}`);
|
|
414
|
+
} else if (toolsToShow.length === 0) {
|
|
415
|
+
console.log(` ${bar} ${dim(`No tools matching "${filterKw}"`)}`);
|
|
416
|
+
} else if (verbose) {
|
|
417
|
+
for (const tool of toolsToShow) {
|
|
418
|
+
console.log(` ${bar} ${green("●")} ${bold(tool.name)}`);
|
|
419
|
+
if (tool.description) console.log(` ${bar} ${dim(tool.description)}`);
|
|
420
|
+
if (tool.usage) tool.usage.split("\n").forEach((l) => console.log(` ${bar} ${l}`));
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const maxNameLen = Math.max(...toolsToShow.map((t) => t.name.length));
|
|
424
|
+
for (const tool of toolsToShow) {
|
|
425
|
+
const desc = tool.description ? ` ${dim(tool.description)}` : "";
|
|
426
|
+
console.log(` ${bar} ${green("●")} ${tool.name.padEnd(maxNameLen)}${desc}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (denied.length > 0) console.log(` ${bar} ${dim("Denied:")} ${denied.join(", ")}`);
|
|
430
|
+
console.log();
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
console.log("Could not reach gateway. Is it running?");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function cmdCall(node, tool, paramsStr, args) {
|
|
438
|
+
if (!node || !tool) { console.error("Usage: clawmatrix call <node> <tool> [json-params]"); process.exit(1); }
|
|
439
|
+
try {
|
|
440
|
+
let toolParams = {};
|
|
441
|
+
if (paramsStr) {
|
|
442
|
+
try { toolParams = JSON.parse(paramsStr); } catch {
|
|
443
|
+
console.error(`Error: Invalid JSON params: ${paramsStr}`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const tIdx = args.indexOf("-t") !== -1 ? args.indexOf("-t") : args.indexOf("--timeout");
|
|
448
|
+
const timeout = tIdx !== -1 ? parseInt(args[tIdx + 1], 10) : undefined;
|
|
449
|
+
const result = await callGateway("clawmatrix.tools.call", {
|
|
450
|
+
node, tool, params: toolParams,
|
|
451
|
+
...(timeout !== undefined && { timeout }),
|
|
452
|
+
});
|
|
453
|
+
console.log(jsonOut(result));
|
|
454
|
+
} catch (err) {
|
|
455
|
+
const msg = err.message || String(err);
|
|
456
|
+
if (msg.includes("not reachable")) console.error(`Error: Node "${node}" is not reachable. Run 'clawmatrix status' to check connectivity.`);
|
|
457
|
+
else if (msg.includes("not allowed")) console.error(`Error: Tool "${tool}" is not allowed on node "${node}". Run 'clawmatrix tools ${node}' to see available tools.`);
|
|
458
|
+
else if (msg.includes("timed out")) console.error(`Error: Tool "${tool}" timed out on node "${node}". Try increasing timeout with -t <ms>.`);
|
|
459
|
+
else console.error(`Error: ${msg}`);
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function cmdBatch(node, itemsStr, args) {
|
|
465
|
+
if (!node) { console.error("Usage: clawmatrix batch <node> [json-items]"); process.exit(1); }
|
|
466
|
+
try {
|
|
467
|
+
let jsonStr = itemsStr;
|
|
468
|
+
if (!jsonStr && !process.stdin.isTTY) {
|
|
469
|
+
const chunks = [];
|
|
470
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
471
|
+
jsonStr = Buffer.concat(chunks).toString("utf-8").trim();
|
|
472
|
+
}
|
|
473
|
+
if (!jsonStr) {
|
|
474
|
+
console.error('Error: No items provided. Pass JSON array or pipe via stdin.');
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
const items = JSON.parse(jsonStr);
|
|
478
|
+
const tIdx = args.indexOf("-t") !== -1 ? args.indexOf("-t") : args.indexOf("--timeout");
|
|
479
|
+
const timeout = tIdx !== -1 ? parseInt(args[tIdx + 1], 10) : undefined;
|
|
480
|
+
const stopOnError = !args.includes("--no-stop-on-error");
|
|
481
|
+
const results = await callGateway("clawmatrix.tools.batch", {
|
|
482
|
+
node, items, stopOnError,
|
|
483
|
+
...(timeout !== undefined && { timeout }),
|
|
484
|
+
});
|
|
485
|
+
console.log(jsonOut(results));
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
488
|
+
process.exitCode = 1;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function cmdModels(args) {
|
|
493
|
+
const isJson = args.includes("--json");
|
|
494
|
+
const nIdx = args.indexOf("-n") !== -1 ? args.indexOf("-n") : args.indexOf("--node");
|
|
495
|
+
const nodeFilter = nIdx !== -1 ? args[nIdx + 1] : undefined;
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const data = await callGateway("clawmatrix.models.list", nodeFilter ? { node: nodeFilter } : undefined);
|
|
499
|
+
if (isJson) { console.log(jsonOut(data)); return; }
|
|
500
|
+
if (data.length === 0) { console.log("No models available in the cluster."); return; }
|
|
501
|
+
|
|
502
|
+
const byNode = new Map();
|
|
503
|
+
for (const m of data) {
|
|
504
|
+
const arr = byNode.get(m.nodeId) || [];
|
|
505
|
+
arr.push(m);
|
|
506
|
+
byNode.set(m.nodeId, arr);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!isTTY) {
|
|
510
|
+
// Compact output for AI/scripting consumption
|
|
511
|
+
for (const [nodeId, models] of byNode) {
|
|
512
|
+
const reachable = models[0]?.reachable !== false;
|
|
513
|
+
console.log(`${nodeId} | ${reachable ? "reachable" : "unreachable"} | ${models.length} models`);
|
|
514
|
+
for (const m of models) {
|
|
515
|
+
const details = [];
|
|
516
|
+
if (m.contextWindow) details.push(`${Math.round(m.contextWindow / 1000)}k ctx`);
|
|
517
|
+
if (m.reasoning) details.push("reasoning");
|
|
518
|
+
if (m.input?.includes("image")) details.push("vision");
|
|
519
|
+
const detailStr = details.length > 0 ? ` (${details.join(", ")})` : "";
|
|
520
|
+
console.log(` ${m.id}${detailStr}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log();
|
|
527
|
+
for (const [nodeId, models] of byNode) {
|
|
528
|
+
const reachable = models[0]?.reachable !== false;
|
|
529
|
+
const statusLabel = reachable ? green("reachable") : red("unreachable");
|
|
530
|
+
console.log(` ${cyan("◆")} ${bold(nodeId)} ${statusLabel} ${dim(`${models.length} models`)}`);
|
|
531
|
+
console.log(` ${bar}`);
|
|
532
|
+
const maxIdLen = Math.max(...models.map((m) => m.id.length));
|
|
533
|
+
for (const m of models) {
|
|
534
|
+
const details = [];
|
|
535
|
+
if (m.contextWindow) details.push(`${Math.round(m.contextWindow / 1000)}k ctx`);
|
|
536
|
+
if (m.reasoning) details.push("reasoning");
|
|
537
|
+
if (m.input?.includes("image")) details.push("vision");
|
|
538
|
+
const detailStr = details.length > 0 ? ` ${dim(details.join(", "))}` : "";
|
|
539
|
+
console.log(` ${bar} ${green("●")} ${m.id.padEnd(maxIdLen)}${detailStr}`);
|
|
540
|
+
}
|
|
541
|
+
console.log();
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
console.log("Could not reach gateway. Is it running?");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function cmdEvents(args) {
|
|
549
|
+
const isJson = args.includes("--json");
|
|
550
|
+
const consumeIdx = args.indexOf("--consume");
|
|
551
|
+
if (consumeIdx !== -1) {
|
|
552
|
+
const raw = args[consumeIdx + 1];
|
|
553
|
+
if (!raw) { console.error("Usage: clawmatrix events --consume <id1,id2,...>"); process.exit(1); }
|
|
554
|
+
const ids = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
555
|
+
try {
|
|
556
|
+
const result = await callGateway("clawmatrix.events.consume", { ids });
|
|
557
|
+
console.log(jsonOut(result));
|
|
558
|
+
} catch (e) {
|
|
559
|
+
console.error(e.message);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const params = {};
|
|
567
|
+
const tIdx = args.indexOf("-t") !== -1 ? args.indexOf("-t") : args.indexOf("--type");
|
|
568
|
+
if (tIdx !== -1) params.type = args[tIdx + 1];
|
|
569
|
+
const sIdx = args.indexOf("-s") !== -1 ? args.indexOf("-s") : args.indexOf("--source");
|
|
570
|
+
if (sIdx !== -1) params.source = args[sIdx + 1];
|
|
571
|
+
const lIdx = args.indexOf("-l") !== -1 ? args.indexOf("-l") : args.indexOf("--limit");
|
|
572
|
+
if (lIdx !== -1) params.limit = parseInt(args[lIdx + 1], 10);
|
|
573
|
+
if (args.includes("-a") || args.includes("--all")) params.unconsumed = false;
|
|
574
|
+
|
|
575
|
+
const events = await callGateway("clawmatrix.events.query", params);
|
|
576
|
+
if (isJson) { console.log(jsonOut(events)); return; }
|
|
577
|
+
if (events.length === 0) { console.log("No events found."); return; }
|
|
578
|
+
|
|
579
|
+
if (!isTTY) {
|
|
580
|
+
// Compact output for AI/scripting consumption
|
|
581
|
+
console.log(`Events: ${events.length} result(s)`);
|
|
582
|
+
for (const evt of events) {
|
|
583
|
+
const age = Math.floor((Date.now() - evt.ts) / 1000);
|
|
584
|
+
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
|
|
585
|
+
const consumed = evt.consumed ? " [consumed]" : "";
|
|
586
|
+
const dataStr = JSON.stringify(evt.data);
|
|
587
|
+
const truncated = dataStr.length > 200 ? dataStr.slice(0, 200) + "..." : dataStr;
|
|
588
|
+
console.log(`${evt.type} | ${evt.source} | ${ageStr}${consumed} | id:${evt.id} | ${truncated}`);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
console.log();
|
|
594
|
+
console.log(` ${cyan("◆")} ${bold("Events")} ${dim(`${events.length} result(s)`)}`);
|
|
595
|
+
console.log(` ${bar}`);
|
|
596
|
+
|
|
597
|
+
for (const evt of events) {
|
|
598
|
+
const age = Math.floor((Date.now() - evt.ts) / 1000);
|
|
599
|
+
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
|
|
600
|
+
const consumed = evt.consumed ? dim(" [consumed]") : "";
|
|
601
|
+
console.log(` ${bar} ${green("●")} ${bold(evt.type)} ${dim(`from ${evt.source}`)} ${dim(ageStr)}${consumed}`);
|
|
602
|
+
console.log(` ${bar} ${dim("id:")} ${evt.id}`);
|
|
603
|
+
const dataStr = JSON.stringify(evt.data);
|
|
604
|
+
const truncated = dataStr.length > 120 ? dataStr.slice(0, 120) + "..." : dataStr;
|
|
605
|
+
console.log(` ${bar} ${dim("data:")} ${truncated}`);
|
|
606
|
+
}
|
|
607
|
+
console.log();
|
|
608
|
+
} catch (err) {
|
|
609
|
+
const msg = err.message || String(err);
|
|
610
|
+
if (msg.includes("not enabled")) console.log("Events not available (web.enabled = false in config).");
|
|
611
|
+
else console.log("Could not reach gateway. Is it running?");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function cmdApprove(approvalId) {
|
|
616
|
+
if (!approvalId) { console.error("Usage: clawmatrix approve <approvalId>"); process.exit(1); }
|
|
617
|
+
try {
|
|
618
|
+
const result = await callGateway("clawmatrix.approval.resolve", { approvalId, decision: "approve" });
|
|
619
|
+
console.log(result.ok ? `Approved: ${approvalId}` : `Not found or already resolved: ${approvalId}`);
|
|
620
|
+
} catch { console.log("Could not reach gateway. Is it running?"); }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function cmdDeny(approvalId) {
|
|
624
|
+
if (!approvalId) { console.error("Usage: clawmatrix deny <approvalId>"); process.exit(1); }
|
|
625
|
+
try {
|
|
626
|
+
const result = await callGateway("clawmatrix.approval.resolve", { approvalId, decision: "deny" });
|
|
627
|
+
console.log(result.ok ? `Denied: ${approvalId}` : `Not found or already resolved: ${approvalId}`);
|
|
628
|
+
} catch { console.log("Could not reach gateway. Is it running?"); }
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function cmdApprovalList() {
|
|
632
|
+
try {
|
|
633
|
+
const result = await callGateway("clawmatrix.approval.list");
|
|
634
|
+
console.log(jsonOut(result));
|
|
635
|
+
} catch { console.log("Could not reach gateway. Is it running?"); }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function cmdApprovalRevoke(nodeId) {
|
|
639
|
+
if (!nodeId) { console.error("Usage: clawmatrix approval revoke <nodeId>"); process.exit(1); }
|
|
640
|
+
try {
|
|
641
|
+
const result = await callGateway("clawmatrix.approval.revoke", { nodeId });
|
|
642
|
+
console.log(result.ok ? `Revoked: ${nodeId}` : `Node not found in approved list: ${nodeId}`);
|
|
643
|
+
} catch { console.log("Could not reach gateway. Is it running?"); }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function cmdHelpJson() {
|
|
647
|
+
const commands = [
|
|
648
|
+
{ name: "status", args: "", options: ["--json"], description: "Show cluster topology and peer status" },
|
|
649
|
+
{ name: "peers", args: "", options: [], description: "List known peers (JSON)" },
|
|
650
|
+
{ name: "check", args: "<nodeId>", options: [], description: "Check if a specific node is reachable" },
|
|
651
|
+
{ name: "tools", args: "[node]", options: ["--json", "-v", "-f <keyword>", "-d <tool>"], description: "List available tools on remote nodes" },
|
|
652
|
+
{ name: "call", args: "<node> <tool> [json]", options: ["-t <ms>"], description: "Invoke a tool on a remote node" },
|
|
653
|
+
{ name: "batch", args: "<node> [json]", options: ["--no-stop-on-error", "-t <ms>"], description: "Invoke multiple tools in sequence" },
|
|
654
|
+
{ name: "models", args: "", options: ["--json", "-n <nodeId>"], description: "List all cluster models" },
|
|
655
|
+
{ name: "events", args: "", options: ["--json", "-t <type>", "-s <source>", "-l <n>", "-a", "--consume <ids>"], description: "Query and consume events" },
|
|
656
|
+
{ name: "send", args: "<node> <message>", options: [], description: "Send a message to a remote node" },
|
|
657
|
+
{ name: "handoff", args: "<task>", options: ["--agent <agent>", "--node <node>", "-t <ms>"], description: "Delegate a task to a remote agent" },
|
|
658
|
+
{ name: "acp", args: "<action>", options: ["-n <node>", "-a <agent>", "--session <id>", "--mode <mode>", "--cwd <dir>"], description: "Manage ACP agent sessions" },
|
|
659
|
+
{ name: "diagnostic", args: "<node>", options: ["--action status|exec", "-c <command>", "-t <seconds>"], description: "Diagnose a remote node via sentinel" },
|
|
660
|
+
{ name: "notify", args: "<title>", options: ["--detail <text>", "--progress <0-100>", "--action start|update|end", "--task-id <id>", "--tool <name>"], description: "Push notification to mobile devices" },
|
|
661
|
+
{ name: "terminal", args: "<node>", options: ["--shell <shell>", "--cwd <dir>", "--action list|close|read|input", "--session <id>"], description: "Open/manage remote terminal sessions" },
|
|
662
|
+
{ name: "transfer", args: "<node> <path> [remote]", options: ["--pull", "-t <ms>"], description: "Transfer files to/from a remote node" },
|
|
663
|
+
{ name: "approve", args: "<approvalId>", options: [], description: "Approve a pending peer" },
|
|
664
|
+
{ name: "deny", args: "<approvalId>", options: [], description: "Deny a pending peer" },
|
|
665
|
+
{ name: "approval list", args: "", options: [], description: "List approved/pending peers" },
|
|
666
|
+
{ name: "approval revoke", args: "<nodeId>", options: [], description: "Revoke an approved peer" },
|
|
667
|
+
{ name: "install-skill", args: "", options: [], description: "Symlink skill into ~/.claude/skills/" },
|
|
668
|
+
];
|
|
669
|
+
console.log(jsonOut({ prefix: "clawmatrix", commands }));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function cmdInstallSkill() {
|
|
673
|
+
const pluginSkillsDir = resolve(dirname(new URL(import.meta.url).pathname), "..", "skills", "clawmatrix");
|
|
674
|
+
if (!existsSync(pluginSkillsDir)) {
|
|
675
|
+
console.error("Skill source not found: " + pluginSkillsDir);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const claudeSkillsDir = join(homedir(), ".claude", "skills", "clawmatrix");
|
|
680
|
+
const claudeSkillsParent = dirname(claudeSkillsDir);
|
|
681
|
+
mkdirSync(claudeSkillsParent, { recursive: true });
|
|
682
|
+
|
|
683
|
+
// Remove existing (symlink, dir, or broken symlink)
|
|
684
|
+
try {
|
|
685
|
+
const existing = readlinkSync(claudeSkillsDir);
|
|
686
|
+
// It's a symlink — check if it already points to the right place
|
|
687
|
+
try {
|
|
688
|
+
if (realpathSync(claudeSkillsDir) === realpathSync(pluginSkillsDir)) {
|
|
689
|
+
console.log("Already installed: " + claudeSkillsDir + " -> " + pluginSkillsDir);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
} catch { /* broken symlink, remove it */ }
|
|
693
|
+
unlinkSync(claudeSkillsDir);
|
|
694
|
+
} catch {
|
|
695
|
+
// Not a symlink — remove if it's a regular dir
|
|
696
|
+
try { rmSync(claudeSkillsDir, { recursive: true }); } catch {}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
symlinkSync(pluginSkillsDir, claudeSkillsDir, "dir");
|
|
700
|
+
console.log("Installed: " + claudeSkillsDir + " -> " + pluginSkillsDir);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function cmdSend(node, message) {
|
|
704
|
+
if (!node || !message) { console.error("Usage: clawmatrix send <node> <message>"); process.exit(1); }
|
|
705
|
+
try {
|
|
706
|
+
const result = await callGateway("clawmatrix.send", { node, message });
|
|
707
|
+
console.log(result.sent ? `Sent to ${result.nodeId}` : "Failed to send");
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
710
|
+
process.exitCode = 1;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function cmdHandoff(args) {
|
|
715
|
+
const agentIdx = args.indexOf("--agent");
|
|
716
|
+
const agent = agentIdx !== -1 ? args[agentIdx + 1] : undefined;
|
|
717
|
+
const nodeIdx = args.indexOf("--node");
|
|
718
|
+
const node = nodeIdx !== -1 ? args[nodeIdx + 1] : undefined;
|
|
719
|
+
const tIdx = args.indexOf("-t") !== -1 ? args.indexOf("-t") : args.indexOf("--timeout");
|
|
720
|
+
const timeout = tIdx !== -1 ? parseInt(args[tIdx + 1], 10) : 600000;
|
|
721
|
+
const task = args.find((a) => !a.startsWith("-") && a !== agent && a !== node && a !== args[tIdx + 1]);
|
|
722
|
+
if (!task) { console.error("Usage: clawmatrix handoff <task> [--agent <agent>] [--node <node>]"); process.exit(1); }
|
|
723
|
+
const target = agent || (node ? `tags:${node}` : undefined);
|
|
724
|
+
if (!target) { console.error("Error: Provide --agent or --node"); process.exit(1); }
|
|
725
|
+
try {
|
|
726
|
+
const result = await callGateway("clawmatrix.handoff", { target, task }, timeout);
|
|
727
|
+
console.log(jsonOut(result));
|
|
728
|
+
} catch (err) {
|
|
729
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
730
|
+
process.exitCode = 1;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function cmdAcp(subCmd, args) {
|
|
735
|
+
const nodeFlag = (idx) => { const i = args.indexOf(idx); return i !== -1 ? args[i + 1] : undefined; };
|
|
736
|
+
const node = nodeFlag("--node") || nodeFlag("-n");
|
|
737
|
+
const agent = nodeFlag("--agent") || nodeFlag("-a");
|
|
738
|
+
|
|
739
|
+
switch (subCmd) {
|
|
740
|
+
case "list": {
|
|
741
|
+
if (!node) { console.error("Usage: clawmatrix acp list --node <node>"); process.exit(1); }
|
|
742
|
+
try {
|
|
743
|
+
const result = await callGateway("clawmatrix.acp.list", { node, agent });
|
|
744
|
+
console.log(jsonOut(result));
|
|
745
|
+
} catch (err) {
|
|
746
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
747
|
+
process.exitCode = 1;
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case "prompt": case "create": {
|
|
752
|
+
if (!node) { console.error("Usage: clawmatrix acp prompt --node <node> --agent <agent> <task>"); process.exit(1); }
|
|
753
|
+
const sessionId = nodeFlag("--session");
|
|
754
|
+
const mode = nodeFlag("--mode");
|
|
755
|
+
const cwd = nodeFlag("--cwd");
|
|
756
|
+
const task = args.find((a) => !a.startsWith("-") && a !== node && a !== agent && a !== sessionId && a !== mode && a !== cwd);
|
|
757
|
+
try {
|
|
758
|
+
const result = await callGateway("clawmatrix.acp.prompt", { node, agent, task, sessionId, mode, cwd }, 600000);
|
|
759
|
+
console.log(jsonOut(result));
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
762
|
+
process.exitCode = 1;
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case "resume": {
|
|
767
|
+
if (!node) { console.error("Usage: clawmatrix acp resume --node <node> --agent <agent> --session <acpSessionId>"); process.exit(1); }
|
|
768
|
+
const acpSessionId = nodeFlag("--session");
|
|
769
|
+
const cwd = nodeFlag("--cwd");
|
|
770
|
+
if (!acpSessionId || !agent) { console.error("Missing --agent and --session"); process.exit(1); }
|
|
771
|
+
try {
|
|
772
|
+
const result = await callGateway("clawmatrix.acp.resume", { node, agent, acpSessionId, cwd }, 600000);
|
|
773
|
+
console.log(jsonOut(result));
|
|
774
|
+
} catch (err) {
|
|
775
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
776
|
+
process.exitCode = 1;
|
|
777
|
+
}
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
case "cancel": {
|
|
781
|
+
if (!node) { console.error("Usage: clawmatrix acp cancel --node <node> --session <sessionId>"); process.exit(1); }
|
|
782
|
+
const sessionId = nodeFlag("--session");
|
|
783
|
+
if (!sessionId) { console.error("Missing --session"); process.exit(1); }
|
|
784
|
+
try {
|
|
785
|
+
const result = await callGateway("clawmatrix.acp.cancel", { node, sessionId });
|
|
786
|
+
console.log(jsonOut(result));
|
|
787
|
+
} catch (err) {
|
|
788
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
789
|
+
process.exitCode = 1;
|
|
790
|
+
}
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
case "close": {
|
|
794
|
+
if (!node) { console.error("Usage: clawmatrix acp close --node <node> --session <sessionId>"); process.exit(1); }
|
|
795
|
+
const sessionId = nodeFlag("--session");
|
|
796
|
+
if (!sessionId) { console.error("Missing --session"); process.exit(1); }
|
|
797
|
+
try {
|
|
798
|
+
const result = await callGateway("clawmatrix.acp.close", { node, sessionId });
|
|
799
|
+
console.log(jsonOut(result));
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
802
|
+
process.exitCode = 1;
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
default:
|
|
807
|
+
console.error("Usage: clawmatrix acp list|prompt|resume|cancel|close [options]");
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function cmdDiagnostic(node, args) {
|
|
813
|
+
if (!node) { console.error("Usage: clawmatrix diagnostic <node> [--action status|exec] [--command <cmd>]"); process.exit(1); }
|
|
814
|
+
const actionIdx = args.indexOf("--action");
|
|
815
|
+
const action = actionIdx !== -1 ? args[actionIdx + 1] : "status";
|
|
816
|
+
const cmdIdx = args.indexOf("--command") !== -1 ? args.indexOf("--command") : args.indexOf("-c");
|
|
817
|
+
const command = cmdIdx !== -1 ? args[cmdIdx + 1] : undefined;
|
|
818
|
+
const tIdx = args.indexOf("-t") !== -1 ? args.indexOf("-t") : args.indexOf("--timeout");
|
|
819
|
+
const timeout = tIdx !== -1 ? parseInt(args[tIdx + 1], 10) : 30;
|
|
820
|
+
if (action === "exec" && !command) { console.error("--command required for exec action"); process.exit(1); }
|
|
821
|
+
try {
|
|
822
|
+
const result = await callGateway("clawmatrix.diagnostic", { node, action, command, timeout }, (timeout + 10) * 1000);
|
|
823
|
+
console.log(jsonOut(result));
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
826
|
+
process.exitCode = 1;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function cmdNotify(title, args) {
|
|
831
|
+
if (!title) { console.error("Usage: clawmatrix notify <title> [--detail <text>] [--progress <0-100>]"); process.exit(1); }
|
|
832
|
+
const detailIdx = args.indexOf("--detail") !== -1 ? args.indexOf("--detail") : args.indexOf("-d");
|
|
833
|
+
const detail = detailIdx !== -1 ? args[detailIdx + 1] : undefined;
|
|
834
|
+
const progressIdx = args.indexOf("--progress") !== -1 ? args.indexOf("--progress") : args.indexOf("-p");
|
|
835
|
+
const progressRaw = progressIdx !== -1 ? parseInt(args[progressIdx + 1], 10) : undefined;
|
|
836
|
+
const progress = progressRaw !== undefined ? progressRaw / 100 : undefined;
|
|
837
|
+
const actionIdx = args.indexOf("--action");
|
|
838
|
+
const action = actionIdx !== -1 ? args[actionIdx + 1] : "start";
|
|
839
|
+
const taskIdIdx = args.indexOf("--task-id");
|
|
840
|
+
const taskId = taskIdIdx !== -1 ? args[taskIdIdx + 1] : undefined;
|
|
841
|
+
const toolIdx = args.indexOf("--tool");
|
|
842
|
+
const tool = toolIdx !== -1 ? args[toolIdx + 1] : undefined;
|
|
843
|
+
try {
|
|
844
|
+
const result = await callGateway("clawmatrix.notify", { action, taskId, title, detail, progress, tool });
|
|
845
|
+
if (isTTY) {
|
|
846
|
+
console.log(`${green("●")} Notification ${action}ed → ${result.targets} mobile peer(s) (taskId: ${result.taskId})`);
|
|
847
|
+
} else {
|
|
848
|
+
console.log(jsonOut(result));
|
|
849
|
+
}
|
|
850
|
+
} catch (err) {
|
|
851
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
852
|
+
process.exitCode = 1;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function cmdTerminal(node, args) {
|
|
857
|
+
if (!node) { console.error("Usage: clawmatrix terminal <node> [--shell <shell>] [--cwd <dir>]"); process.exit(1); }
|
|
858
|
+
const shellIdx = args.indexOf("--shell");
|
|
859
|
+
const shell = shellIdx !== -1 ? args[shellIdx + 1] : undefined;
|
|
860
|
+
const cwdIdx = args.indexOf("--cwd");
|
|
861
|
+
const cwd = cwdIdx !== -1 ? args[cwdIdx + 1] : undefined;
|
|
862
|
+
const actionIdx = args.indexOf("--action");
|
|
863
|
+
const action = actionIdx !== -1 ? args[actionIdx + 1] : undefined;
|
|
864
|
+
const sessionIdx = args.indexOf("--session");
|
|
865
|
+
const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : undefined;
|
|
866
|
+
|
|
867
|
+
if (action === "list") {
|
|
868
|
+
try {
|
|
869
|
+
const result = await callGateway("clawmatrix.terminal.list");
|
|
870
|
+
console.log(jsonOut(result));
|
|
871
|
+
} catch (err) { console.error(`Error: ${err.message || String(err)}`); process.exitCode = 1; }
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (action === "close" && sessionId) {
|
|
875
|
+
try {
|
|
876
|
+
await callGateway("clawmatrix.terminal.close", { sessionId });
|
|
877
|
+
console.log(`Session ${sessionId} closed`);
|
|
878
|
+
} catch (err) { console.error(`Error: ${err.message || String(err)}`); process.exitCode = 1; }
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (action === "read" && sessionId) {
|
|
882
|
+
try {
|
|
883
|
+
const result = await callGateway("clawmatrix.terminal.read", { sessionId });
|
|
884
|
+
if (result.data) process.stdout.write(result.data);
|
|
885
|
+
} catch (err) { console.error(`Error: ${err.message || String(err)}`); process.exitCode = 1; }
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (action === "input" && sessionId) {
|
|
889
|
+
const data = args.find((a) => !a.startsWith("-") && a !== node && a !== sessionId && a !== action);
|
|
890
|
+
if (!data) { console.error("Missing input data"); process.exit(1); }
|
|
891
|
+
try {
|
|
892
|
+
await callGateway("clawmatrix.terminal.input", { sessionId, data });
|
|
893
|
+
} catch (err) { console.error(`Error: ${err.message || String(err)}`); process.exitCode = 1; }
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Default: open a new session
|
|
898
|
+
try {
|
|
899
|
+
const result = await callGateway("clawmatrix.terminal.open", { node, shell, cwd }, 30000);
|
|
900
|
+
console.log(jsonOut(result));
|
|
901
|
+
} catch (err) {
|
|
902
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
903
|
+
process.exitCode = 1;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function cmdTransfer(node, localPath, args) {
|
|
908
|
+
if (!node || !localPath) { console.error("Usage: clawmatrix transfer <node> <local-path> [remote-path] [--pull]"); process.exit(1); }
|
|
909
|
+
const isPull = args.includes("--pull");
|
|
910
|
+
const remotePathIdx = args.findIndex((a) => !a.startsWith("-") && a !== node && a !== localPath);
|
|
911
|
+
const remotePath = remotePathIdx !== -1 ? args[remotePathIdx] : localPath;
|
|
912
|
+
const tIdx = args.indexOf("-t") !== -1 ? args.indexOf("-t") : args.indexOf("--timeout");
|
|
913
|
+
const timeout = tIdx !== -1 ? parseInt(args[tIdx + 1], 10) : 120000;
|
|
914
|
+
try {
|
|
915
|
+
let params;
|
|
916
|
+
if (isPull) {
|
|
917
|
+
params = { source_node: node, source_path: remotePath, target_path: localPath };
|
|
918
|
+
} else {
|
|
919
|
+
params = { target_node: node, source_path: localPath, target_path: remotePath };
|
|
920
|
+
}
|
|
921
|
+
const result = await callGateway("clawmatrix.transfer", params, timeout);
|
|
922
|
+
if (result.success) {
|
|
923
|
+
console.log(`Transfer complete: ${result.bytesTransferred} bytes`);
|
|
924
|
+
} else {
|
|
925
|
+
console.error(`Transfer failed: ${result.error}`);
|
|
926
|
+
process.exitCode = 1;
|
|
927
|
+
}
|
|
928
|
+
} catch (err) {
|
|
929
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
930
|
+
process.exitCode = 1;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const HELP = `ClawMatrix - Decentralized mesh cluster CLI
|
|
935
|
+
|
|
936
|
+
Usage: clawmatrix <command> [options]
|
|
937
|
+
|
|
938
|
+
Commands:
|
|
939
|
+
status Show cluster topology and peer status
|
|
940
|
+
check <nodeId> Check if a specific node is reachable
|
|
941
|
+
tools [node] List available tools on remote nodes
|
|
942
|
+
call <node> <tool> [p] Invoke a tool on a remote node
|
|
943
|
+
batch <node> [json] Invoke multiple tools in sequence
|
|
944
|
+
models List all models available across the cluster
|
|
945
|
+
events Query and consume ingested events
|
|
946
|
+
send <node> <message> Send a message to a remote node
|
|
947
|
+
handoff <task> [opts] Delegate a task to a remote agent
|
|
948
|
+
acp <action> [opts] Manage ACP agent sessions (list|prompt|resume|cancel|close)
|
|
949
|
+
diagnostic <node> Diagnose a remote node via sentinel
|
|
950
|
+
notify <title> [opts] Push notification to mobile devices (Dynamic Island)
|
|
951
|
+
terminal <node> Open/manage remote terminal sessions
|
|
952
|
+
transfer <node> <path> Transfer files to/from a remote node
|
|
953
|
+
approve <approvalId> Approve a pending peer join request
|
|
954
|
+
deny <approvalId> Deny a pending peer join request
|
|
955
|
+
approval list|revoke Manage approved peers
|
|
956
|
+
install-skill Symlink skill into ~/.claude/skills/
|
|
957
|
+
help-json Structured command reference (JSON)
|
|
958
|
+
|
|
959
|
+
Options:
|
|
960
|
+
--help, -h Show this help message
|
|
961
|
+
|
|
962
|
+
Run 'clawmatrix <command> --help' for command-specific options.`;
|
|
963
|
+
|
|
964
|
+
// ── Main dispatch ────────────────────────────────────────────────────
|
|
965
|
+
|
|
966
|
+
const args = process.argv.slice(2);
|
|
967
|
+
const cmd = args[0];
|
|
968
|
+
const rest = args.slice(1);
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
switch (cmd) {
|
|
972
|
+
case "status": await cmdStatus(rest); break;
|
|
973
|
+
case "peers": await cmdPeers(); break;
|
|
974
|
+
case "check": await cmdCheck(rest[0]); break;
|
|
975
|
+
case "tools": await cmdTools(rest); break;
|
|
976
|
+
case "call": await cmdCall(rest[0], rest[1], rest[2], rest); break;
|
|
977
|
+
case "batch": await cmdBatch(rest[0], rest.find((a) => a.startsWith("[") || a.startsWith("{")), rest); break;
|
|
978
|
+
case "models": await cmdModels(rest); break;
|
|
979
|
+
case "events": await cmdEvents(rest); break;
|
|
980
|
+
case "send": await cmdSend(rest[0], rest.slice(1).join(" ")); break;
|
|
981
|
+
case "handoff": await cmdHandoff(rest); break;
|
|
982
|
+
case "acp": await cmdAcp(rest[0], rest.slice(1)); break;
|
|
983
|
+
case "diagnostic": await cmdDiagnostic(rest[0], rest.slice(1)); break;
|
|
984
|
+
case "notify": await cmdNotify(rest[0], rest.slice(1)); break;
|
|
985
|
+
case "terminal": await cmdTerminal(rest[0], rest.slice(1)); break;
|
|
986
|
+
case "transfer": await cmdTransfer(rest[0], rest[1], rest.slice(2)); break;
|
|
987
|
+
case "approve": await cmdApprove(rest[0]); break;
|
|
988
|
+
case "deny": await cmdDeny(rest[0]); break;
|
|
989
|
+
case "approval":
|
|
990
|
+
if (rest[0] === "list") await cmdApprovalList();
|
|
991
|
+
else if (rest[0] === "revoke") await cmdApprovalRevoke(rest[1]);
|
|
992
|
+
else { console.error("Usage: clawmatrix approval list|revoke <nodeId>"); process.exit(1); }
|
|
993
|
+
break;
|
|
994
|
+
case "install-skill": cmdInstallSkill(); break;
|
|
995
|
+
case "help-json": cmdHelpJson(); break;
|
|
996
|
+
case "--help": case "-h": case undefined:
|
|
997
|
+
console.log(HELP); break;
|
|
998
|
+
default:
|
|
999
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
1000
|
+
console.log(HELP);
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
console.error(e.message);
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|