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/src/cli.ts DELETED
@@ -1,711 +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
- // ── Style helpers (TTY-aware: strip ANSI when output goes to LLM/pipe) ──
50
-
51
- const isTTY = process.stdout.isTTY === true;
52
- const ansi = (code: string, reset: string) =>
53
- isTTY ? (s: string) => `\x1b[${code}m${s}\x1b[${reset}m` : (s: string) => s;
54
-
55
- const bold = ansi("1", "22");
56
- const dim = ansi("2", "22");
57
- const green = ansi("32", "39");
58
- const red = ansi("31", "39");
59
- const cyan = ansi("36", "39");
60
- const yellow = ansi("33", "39");
61
-
62
- /** TTY-aware JSON output: pretty for humans, compact for LLM callers. */
63
- const jsonOut = (data: unknown) => JSON.stringify(data, null, isTTY ? 2 : 0);
64
-
65
- export const registerClusterCli = ({ program }: { program: Command }) => {
66
- const cmd = program.command("clawmatrix").description("ClawMatrix cluster management");
67
-
68
- cmd
69
- .command("status")
70
- .description("Show cluster topology and peer status")
71
- .option("--json", "Output raw JSON (structured, machine-readable)")
72
- .action(async (opts: { json?: boolean }) => {
73
- let data: Record<string, unknown>;
74
- try {
75
- data = (await callGateway("clawmatrix.status")) as Record<string, unknown>;
76
- } catch {
77
- console.log("Could not reach gateway. Is it running?");
78
- return;
79
- }
80
-
81
- if (data.error) {
82
- console.log(String(data.error));
83
- return;
84
- }
85
-
86
- // --json: raw structured output for programmatic consumption
87
- if (opts.json) {
88
- console.log(jsonOut(data));
89
- return;
90
- }
91
-
92
- const bar = dim("│");
93
- const lbl = (text: string) => dim(text.padEnd(13));
94
-
95
- const agents = data.agents as Array<{ id: string }>;
96
- const models = data.models as Array<{ id: string }>;
97
- const tags = data.tags as string[];
98
-
99
- // Local node
100
- console.log();
101
- console.log(` ${cyan("◆")} ${bold("ClawMatrix Cluster")}`);
102
- console.log(` ${bar}`);
103
- console.log(` ${bar} ${lbl("Node")}${bold(String(data.nodeId))}`);
104
- if (tags.length > 0) {
105
- console.log(` ${bar} ${lbl("Tags")}${tags.join(dim(", "))}`);
106
- }
107
- console.log(` ${bar} ${lbl("Listen")}${data.listen !== false ? `:${data.listen}` : dim("disabled")}`);
108
- console.log(` ${bar} ${lbl("Model Proxy")}:${data.proxyPort}`);
109
- console.log(` ${bar} ${lbl("Agents")}${agents.map((a) => a.id).join(dim(", ")) || dim("–")}`);
110
- console.log(` ${bar} ${lbl("Models")}${models.map((m) => m.id).join(dim(", ")) || dim("–")}`);
111
-
112
- const peers = data.peers as Array<{
113
- nodeId: string;
114
- agents: Array<{ id: string }>;
115
- models: Array<{ id: string }>;
116
- tags: string[];
117
- connected: boolean;
118
- status: "direct" | "relay" | "unreachable" | "sentinel-only";
119
- latencyMs: number;
120
- reachableVia: string | null;
121
- }>;
122
-
123
- if (!peers || peers.length === 0) {
124
- console.log(` ${bar}`);
125
- console.log(` ${dim("◇")} ${dim("No peers discovered")}`);
126
- console.log();
127
- return;
128
- }
129
-
130
- const reachable = peers.filter((p) => p.connected).length;
131
- const countStr = `${reachable}/${peers.length} reachable`;
132
- const countColor = reachable === peers.length ? green : reachable > 0 ? yellow : red;
133
-
134
- console.log(` ${bar}`);
135
- console.log(` ${cyan("◆")} ${bold("Peers")} ${countColor(countStr)}`);
136
- console.log(` ${bar}`);
137
-
138
- for (let i = 0; i < peers.length; i++) {
139
- const peer = peers[i];
140
- const dot = peer.status === "direct" ? green("●")
141
- : peer.status === "relay" ? yellow("●")
142
- : peer.status === "sentinel-only" ? yellow("◐")
143
- : red("○");
144
- const latency = peer.connected && peer.latencyMs > 0 ? dim(` ${peer.latencyMs}ms`) : "";
145
- const statusLabel = peer.status === "relay"
146
- ? yellow(` relay via ${peer.reachableVia}`)
147
- : peer.status === "sentinel-only"
148
- ? yellow(" sentinel only")
149
- : peer.status === "unreachable"
150
- ? red(" unreachable")
151
- : "";
152
- console.log(` ${bar} ${dot} ${bold(peer.nodeId)}${statusLabel}${latency}`);
153
-
154
- if (peer.tags.length > 0) {
155
- console.log(` ${bar} ${lbl("Tags")}${peer.tags.join(dim(", "))}`);
156
- }
157
- const peerAgents = peer.agents.map((a) => a.id).join(dim(", "));
158
- if (peerAgents) {
159
- console.log(` ${bar} ${lbl("Agents")}${peerAgents}`);
160
- }
161
- const peerModels = peer.models.map((m) => m.id).join(dim(", "));
162
- if (peerModels) {
163
- console.log(` ${bar} ${lbl("Models")}${peerModels}`);
164
- }
165
-
166
- if (i < peers.length - 1) {
167
- console.log(` ${bar}`);
168
- }
169
- }
170
-
171
- console.log(` ${bar}`);
172
- console.log(` ${dim("◇")}`);
173
- console.log();
174
- });
175
-
176
- cmd
177
- .command("peers")
178
- .description("List known peers (JSON)")
179
- .action(async () => {
180
- let peers: unknown;
181
- try {
182
- peers = await callGateway("clawmatrix.peers");
183
- } catch {
184
- console.log("[]");
185
- return;
186
- }
187
- console.log(jsonOut(peers));
188
- });
189
-
190
- cmd
191
- .command("check <nodeId>")
192
- .description("Check if a specific node is reachable (minimal JSON output)")
193
- .action(async (nodeId: string) => {
194
- try {
195
- const peers = (await callGateway("clawmatrix.peers")) as Array<{
196
- nodeId: string;
197
- connected: boolean;
198
- status: string;
199
- latencyMs: number;
200
- agents?: Array<{ id: string }>;
201
- models?: Array<{ id: string }>;
202
- toolProxy?: { enabled: boolean };
203
- }>;
204
- const peer = peers.find((p) => p.nodeId === nodeId);
205
- if (!peer) {
206
- console.log(jsonOut({ nodeId, reachable: false, status: "unknown", error: "Node not found in peer list" }));
207
- process.exitCode = 1;
208
- return;
209
- }
210
- console.log(jsonOut({
211
- nodeId: peer.nodeId,
212
- reachable: peer.connected,
213
- status: peer.status,
214
- latencyMs: peer.latencyMs,
215
- agents: peer.agents?.length ?? 0,
216
- models: peer.models?.length ?? 0,
217
- toolProxy: peer.toolProxy?.enabled ?? false,
218
- }));
219
- if (!peer.connected) process.exitCode = 1;
220
- } catch {
221
- console.log(jsonOut({ nodeId, reachable: false, status: "error", error: "Could not reach gateway" }));
222
- process.exitCode = 1;
223
- }
224
- });
225
-
226
- // ── Tool proxy commands ────────────────────────────────────────
227
- //
228
- // Design: model-facing CLI (optimized for LLM consumption)
229
- //
230
- // 1. Non-TTY auto-compact: When called from Claude Code / Codex (non-TTY),
231
- // output is plain text without ANSI colors, brief by default.
232
- // 2. Progressive disclosure: `tools` lists names → `tools --describe <name>`
233
- // gives full usage for a single tool. LLMs should query incrementally.
234
- // 3. Structured output: JSON for programmatic use, compact text for LLMs.
235
-
236
- type ToolListEntry = {
237
- nodeId: string;
238
- status: string;
239
- toolProxy: {
240
- enabled: boolean;
241
- allow: string[];
242
- deny: string[];
243
- catalog?: Array<{ name: string; description: string; usage?: string; inputSchema?: Record<string, unknown> }>;
244
- };
245
- };
246
-
247
- async function fetchToolList(node?: string): Promise<ToolListEntry[]> {
248
- return (await callGateway("clawmatrix.tools.list", node ? { node } : undefined)) as ToolListEntry[];
249
- }
250
-
251
- cmd
252
- .command("tools [node]")
253
- .description("List available tools on remote nodes (compact by default for LLM callers)")
254
- .option("--json", "Output raw JSON")
255
- .option("-v, --verbose", "Show full usage/params for every tool (caution: large output)")
256
- .option("-f, --filter <keyword>", "Filter tools by keyword (matches name, description, usage)")
257
- .option("-d, --describe <tool>", "Show detailed usage for a single tool")
258
- .action(async (node: string | undefined, opts: { json?: boolean; verbose?: boolean; filter?: string; describe?: string }) => {
259
- try {
260
- const data = await fetchToolList(node);
261
-
262
- // --json: raw structured output
263
- if (opts.json) {
264
- console.log(jsonOut(data));
265
- return;
266
- }
267
-
268
- if (data.length === 0) {
269
- console.log("No nodes with tool proxy enabled.");
270
- return;
271
- }
272
-
273
- // --describe <tool>: show full detail for one tool then exit
274
- if (opts.describe) {
275
- const toolName = opts.describe.toLowerCase();
276
- for (const entry of data) {
277
- const catalog = entry.toolProxy.catalog ?? [];
278
- const found = catalog.find((t) => t.name.toLowerCase() === toolName);
279
- if (found) {
280
- console.log(`node: ${entry.nodeId}`);
281
- console.log(`tool: ${found.name}`);
282
- console.log(`description: ${found.description}`);
283
- if (found.inputSchema) {
284
- console.log(`params: ${JSON.stringify(found.inputSchema)}`);
285
- }
286
- if (found.usage) {
287
- console.log(`usage:\n${found.usage}`);
288
- }
289
- console.log(`\ninvoke: clawmatrix call ${entry.nodeId} ${found.name} '{}'`);
290
- return;
291
- }
292
- }
293
- console.error(`Tool "${opts.describe}" not found. Run 'clawmatrix tools' to see available tools.`);
294
- process.exitCode = 1;
295
- return;
296
- }
297
-
298
- const filterLower = opts.filter?.toLowerCase();
299
- const matchesFilter = (t: { name: string; description: string; usage?: string }) =>
300
- !filterLower ||
301
- t.name.toLowerCase().includes(filterLower) ||
302
- t.description.toLowerCase().includes(filterLower) ||
303
- (t.usage ?? "").toLowerCase().includes(filterLower);
304
-
305
- const bar = dim("│");
306
-
307
- console.log();
308
- for (const entry of data) {
309
- const tools = entry.toolProxy.allow;
310
- const catalog = entry.toolProxy.catalog ?? [];
311
- const catalogMap = new Map(catalog.map((t) => [t.name, t]));
312
- const denied = entry.toolProxy.deny;
313
- const isWildcard = tools.includes("*");
314
-
315
- const toolsToShow = isWildcard
316
- ? catalog.filter(matchesFilter)
317
- : tools
318
- .map((name) => catalogMap.get(name) ?? { name, description: "", usage: "" })
319
- .filter(matchesFilter);
320
-
321
- const totalCount = isWildcard ? catalog.length : tools.length;
322
- const shownCount = toolsToShow.length;
323
- const countLabel = filterLower
324
- ? `${shownCount}/${totalCount} matched`
325
- : `${totalCount} tools`;
326
-
327
- console.log(` ${cyan("◆")} ${bold(entry.nodeId)} ${green(entry.status)} ${dim(countLabel)}`);
328
- console.log(` ${bar}`);
329
-
330
- if (toolsToShow.length === 0 && !filterLower) {
331
- if (isWildcard) {
332
- console.log(` ${bar} ${green("*")} ${dim("(all tools allowed)")}`);
333
- } else {
334
- console.log(` ${bar} ${dim("No tools advertised")}`);
335
- }
336
- } else if (toolsToShow.length === 0) {
337
- console.log(` ${bar} ${dim(`No tools matching "${opts.filter}"`)}`);
338
- } else if (opts.verbose) {
339
- // Verbose: full name + description + usage (opt-in, large output)
340
- for (const tool of toolsToShow) {
341
- console.log(` ${bar} ${green("●")} ${bold(tool.name)}`);
342
- if (tool.description) {
343
- console.log(` ${bar} ${dim(tool.description)}`);
344
- }
345
- if (tool.usage) {
346
- for (const line of tool.usage.split("\n")) {
347
- console.log(` ${bar} ${line}`);
348
- }
349
- }
350
- }
351
- } else {
352
- // Default: compact — name + one-line description (LLM-friendly)
353
- const maxNameLen = Math.max(...toolsToShow.map((t) => t.name.length));
354
- for (const tool of toolsToShow) {
355
- const padded = tool.name.padEnd(maxNameLen);
356
- const desc = tool.description ? ` ${dim(tool.description)}` : "";
357
- console.log(` ${bar} ${green("●")} ${padded}${desc}`);
358
- }
359
-
360
- // Hint for LLMs: how to get more detail
361
- if (!isTTY && toolsToShow.length > 5) {
362
- console.log(` ${bar}`);
363
- console.log(` ${bar} ${dim("Tip: use --describe <tool> for full usage of a specific tool")}`);
364
- }
365
- }
366
-
367
- if (denied.length > 0) {
368
- console.log(` ${bar} ${dim("Denied:")} ${denied.join(", ")}`);
369
- }
370
- console.log();
371
- }
372
- } catch {
373
- console.log("Could not reach gateway. Is it running?");
374
- }
375
- });
376
-
377
- cmd
378
- .command("call <node> <tool> [params]")
379
- .description("Invoke a tool on a remote node")
380
- .option("-t, --timeout <ms>", "Timeout in milliseconds")
381
- .action(async (node: string, tool: string, paramsStr: string | undefined, opts: { timeout?: string }) => {
382
- try {
383
- let toolParams: Record<string, unknown> = {};
384
- if (paramsStr) {
385
- try {
386
- toolParams = JSON.parse(paramsStr);
387
- } catch {
388
- console.error(`Error: Invalid JSON params: ${paramsStr}`);
389
- console.error('Expected JSON object, e.g. \'{"days": 7}\'');
390
- process.exitCode = 1;
391
- return;
392
- }
393
- }
394
-
395
- const timeout = opts.timeout ? parseInt(opts.timeout, 10) : undefined;
396
- if (timeout !== undefined && isNaN(timeout)) {
397
- console.error("Error: --timeout must be a number");
398
- process.exitCode = 1;
399
- return;
400
- }
401
-
402
- const result = await callGateway("clawmatrix.tools.call", {
403
- node,
404
- tool,
405
- params: toolParams,
406
- ...(timeout !== undefined && { timeout }),
407
- });
408
-
409
- // Compact JSON for LLM callers (non-TTY), pretty for humans
410
- console.log(jsonOut(result));
411
- } catch (err) {
412
- const msg = err instanceof Error ? err.message : String(err);
413
- // Provide actionable hints for common errors
414
- if (msg.includes("not reachable")) {
415
- console.error(`Error: Node "${node}" is not reachable. Run 'clawmatrix status' to check connectivity.`);
416
- } else if (msg.includes("not allowed")) {
417
- console.error(`Error: Tool "${tool}" is not allowed on node "${node}". Run 'clawmatrix tools ${node}' to see available tools.`);
418
- } else if (msg.includes("timed out")) {
419
- console.error(`Error: Tool "${tool}" timed out on node "${node}". Try increasing timeout with -t <ms>.`);
420
- } else {
421
- console.error(`Error: ${msg}`);
422
- }
423
- process.exitCode = 1;
424
- }
425
- });
426
-
427
- cmd
428
- .command("batch <node> [items]")
429
- .description("Invoke multiple tools on a remote node in sequence (pass JSON array or pipe via stdin)")
430
- .option("--no-stop-on-error", "Continue on error")
431
- .option("-t, --timeout <ms>", "Timeout in milliseconds")
432
- .action(async (node: string, itemsStr: string | undefined, opts: { stopOnError?: boolean; timeout?: string }) => {
433
- try {
434
- let jsonStr = itemsStr;
435
-
436
- // Read from stdin if no argument provided and stdin is piped
437
- if (!jsonStr && !process.stdin.isTTY) {
438
- const chunks: Buffer[] = [];
439
- for await (const chunk of process.stdin) {
440
- chunks.push(Buffer.from(chunk));
441
- }
442
- jsonStr = Buffer.concat(chunks).toString("utf-8").trim();
443
- }
444
-
445
- if (!jsonStr) {
446
- console.error('Error: No items provided. Pass JSON array as argument or pipe via stdin.');
447
- console.error('Example: clawmatrix batch iphone \'[{"tool":"get_location"},{"tool":"battery"}]\'');
448
- process.exitCode = 1;
449
- return;
450
- }
451
-
452
- const items = JSON.parse(jsonStr) as Array<{ tool: string; params?: Record<string, unknown> }>;
453
- const timeout = opts.timeout ? parseInt(opts.timeout, 10) : undefined;
454
- if (timeout !== undefined && isNaN(timeout)) {
455
- console.error("Error: --timeout must be a number");
456
- process.exitCode = 1;
457
- return;
458
- }
459
-
460
- const results = await callGateway("clawmatrix.tools.batch", {
461
- node,
462
- items,
463
- stopOnError: opts.stopOnError !== false,
464
- ...(timeout !== undefined && { timeout }),
465
- });
466
-
467
- console.log(jsonOut(results));
468
- } catch (err) {
469
- const msg = err instanceof Error ? err.message : String(err);
470
- console.error(`Error: ${msg}`);
471
- process.exitCode = 1;
472
- }
473
- });
474
-
475
- // ── Models command ──────────────────────────────────────────────
476
-
477
- cmd
478
- .command("models")
479
- .description("List all models available across the cluster")
480
- .option("--json", "Output raw JSON")
481
- .option("-n, --node <nodeId>", "Filter by node ID")
482
- .action(async (opts: { json?: boolean; node?: string }) => {
483
- try {
484
- const data = (await callGateway("clawmatrix.models.list", opts.node ? { node: opts.node } : undefined)) as Array<{
485
- id: string;
486
- nodeId: string;
487
- provider: string;
488
- description?: string;
489
- contextWindow?: number;
490
- maxTokens?: number;
491
- reasoning?: boolean;
492
- input?: string[];
493
- reachable: boolean;
494
- }>;
495
-
496
- if (opts.json) {
497
- console.log(jsonOut(data));
498
- return;
499
- }
500
-
501
- if (data.length === 0) {
502
- console.log("No models available in the cluster.");
503
- return;
504
- }
505
-
506
- const bar = dim("│");
507
-
508
- // Group by nodeId
509
- const byNode = new Map<string, typeof data>();
510
- for (const m of data) {
511
- const arr = byNode.get(m.nodeId) ?? [];
512
- arr.push(m);
513
- byNode.set(m.nodeId, arr);
514
- }
515
-
516
- console.log();
517
- for (const [nodeId, models] of byNode) {
518
- const reachable = models[0]?.reachable !== false;
519
- const statusLabel = reachable ? green("reachable") : red("unreachable");
520
- console.log(` ${cyan("◆")} ${bold(nodeId)} ${statusLabel} ${dim(`${models.length} models`)}`);
521
- console.log(` ${bar}`);
522
-
523
- const maxIdLen = Math.max(...models.map((m) => m.id.length));
524
- for (const m of models) {
525
- const padded = m.id.padEnd(maxIdLen);
526
- const details: string[] = [];
527
- if (m.contextWindow) details.push(`${Math.round(m.contextWindow / 1000)}k ctx`);
528
- if (m.reasoning) details.push("reasoning");
529
- if (m.input && m.input.includes("image")) details.push("vision");
530
- const detailStr = details.length > 0 ? ` ${dim(details.join(", "))}` : "";
531
- console.log(` ${bar} ${green("●")} ${padded}${detailStr}`);
532
- }
533
- console.log();
534
- }
535
- } catch {
536
- console.log("Could not reach gateway. Is it running?");
537
- }
538
- });
539
-
540
- // ── Events command ─────────────────────────────────────────────
541
-
542
- cmd
543
- .command("events")
544
- .description("Query and consume ingested events (messages, calls, location changes)")
545
- .option("--json", "Output raw JSON")
546
- .option("-t, --type <type>", "Filter by event type (e.g. message_received)")
547
- .option("-s, --source <source>", "Filter by source (e.g. shortcuts)")
548
- .option("-l, --limit <n>", "Max events to return (default: 20)")
549
- .option("-a, --all", "Include already-consumed events")
550
- .option("--consume <ids>", "Consume events by ID (comma-separated)")
551
- .action(async (opts: { json?: boolean; type?: string; source?: string; limit?: string; all?: boolean; consume?: string }) => {
552
- try {
553
- if (opts.consume) {
554
- const ids = opts.consume.split(",").map((s) => s.trim()).filter(Boolean);
555
- const result = await callGateway("clawmatrix.events.consume", { ids });
556
- console.log(jsonOut(result));
557
- return;
558
- }
559
-
560
- const params: Record<string, unknown> = {};
561
- if (opts.type) params.type = opts.type;
562
- if (opts.source) params.source = opts.source;
563
- if (opts.limit) {
564
- const limit = parseInt(opts.limit, 10);
565
- if (isNaN(limit)) {
566
- console.error("Error: --limit must be a number");
567
- process.exitCode = 1;
568
- return;
569
- }
570
- params.limit = limit;
571
- }
572
- if (opts.all) params.unconsumed = false;
573
-
574
- const events = (await callGateway("clawmatrix.events.query", params)) as Array<{
575
- id: string;
576
- type: string;
577
- source: string;
578
- ts: number;
579
- consumed: boolean;
580
- data: Record<string, unknown>;
581
- }>;
582
-
583
- if (opts.json) {
584
- console.log(jsonOut(events));
585
- return;
586
- }
587
-
588
- if (events.length === 0) {
589
- console.log("No events found.");
590
- return;
591
- }
592
-
593
- const bar = dim("│");
594
- console.log();
595
- console.log(` ${cyan("◆")} ${bold("Events")} ${dim(`${events.length} result(s)`)}`);
596
- console.log(` ${bar}`);
597
-
598
- for (const evt of events) {
599
- const age = Math.floor((Date.now() - evt.ts) / 1000);
600
- const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
601
- const consumed = evt.consumed ? dim(" [consumed]") : "";
602
- console.log(` ${bar} ${green("●")} ${bold(evt.type)} ${dim(`from ${evt.source}`)} ${dim(ageStr)}${consumed}`);
603
- console.log(` ${bar} ${dim("id:")} ${evt.id}`);
604
- const dataStr = JSON.stringify(evt.data);
605
- const truncated = dataStr.length > 120 ? dataStr.slice(0, 120) + "..." : dataStr;
606
- console.log(` ${bar} ${dim("data:")} ${truncated}`);
607
- }
608
- console.log();
609
- } catch (err) {
610
- const msg = err instanceof Error ? err.message : String(err);
611
- if (msg.includes("not enabled")) {
612
- console.log("Events not available (web.enabled = false in config).");
613
- } else {
614
- console.log("Could not reach gateway. Is it running?");
615
- }
616
- }
617
- });
618
-
619
- // ── Structured help for LLM discovery ────────────────────────────
620
-
621
- cmd
622
- .command("help-json")
623
- .description("Output structured command reference (JSON) for programmatic consumption")
624
- .action(() => {
625
- const commands = [
626
- { name: "status", args: "", options: ["--json"], description: "Show cluster topology and peer status" },
627
- { name: "peers", args: "", options: [], description: "List known peers (JSON)" },
628
- { name: "check", args: "<nodeId>", options: [], description: "Check if a specific node is reachable (minimal JSON)" },
629
- { name: "tools", args: "[node]", options: ["--json", "-v/--verbose", "-f/--filter <keyword>", "-d/--describe <tool>"], description: "List available tools on remote nodes" },
630
- { name: "call", args: "<node> <tool> [json-params]", options: ["-t/--timeout <ms>"], description: "Invoke a tool on a remote node" },
631
- { name: "batch", args: "<node> [items-json]", options: ["--no-stop-on-error", "-t/--timeout <ms>"], description: "Invoke multiple tools in sequence" },
632
- { name: "models", args: "", options: ["--json", "-n/--node <nodeId>"], description: "List all models available across the cluster" },
633
- { name: "events", args: "", options: ["--json", "-t/--type", "-s/--source", "-l/--limit", "-a/--all", "--consume <ids>"], description: "Query and consume ingested events" },
634
- { name: "approve", args: "<approvalId>", options: [], description: "Approve a pending peer join request" },
635
- { name: "deny", args: "<approvalId>", options: [], description: "Deny a pending peer join request" },
636
- { name: "approval list", args: "", options: [], description: "List approved and pending peers" },
637
- { name: "approval revoke", args: "<nodeId>", options: [], description: "Revoke an approved peer" },
638
- ];
639
- console.log(jsonOut({ prefix: "clawmatrix", commands }));
640
- });
641
-
642
- // ── Peer approval commands ──────────────────────────────────────
643
-
644
- cmd
645
- .command("approve <approvalId>")
646
- .description("Approve a pending peer join request")
647
- .action(async (approvalId: string) => {
648
- try {
649
- const result = await callGateway("clawmatrix.approval.resolve", {
650
- approvalId,
651
- decision: "approve",
652
- }) as Record<string, unknown>;
653
- if (result.ok) {
654
- console.log(`Approved: ${approvalId}`);
655
- } else {
656
- console.log(`Approval not found or already resolved: ${approvalId}`);
657
- }
658
- } catch {
659
- console.log("Could not reach gateway. Is it running?");
660
- }
661
- });
662
-
663
- cmd
664
- .command("deny <approvalId>")
665
- .description("Deny a pending peer join request")
666
- .action(async (approvalId: string) => {
667
- try {
668
- const result = await callGateway("clawmatrix.approval.resolve", {
669
- approvalId,
670
- decision: "deny",
671
- }) as Record<string, unknown>;
672
- if (result.ok) {
673
- console.log(`Denied: ${approvalId}`);
674
- } else {
675
- console.log(`Approval not found or already resolved: ${approvalId}`);
676
- }
677
- } catch {
678
- console.log("Could not reach gateway. Is it running?");
679
- }
680
- });
681
-
682
- const approval = cmd.command("approval").description("Manage peer approvals");
683
-
684
- approval
685
- .command("list")
686
- .description("List approved and pending peers")
687
- .action(async () => {
688
- try {
689
- const data = await callGateway("clawmatrix.approval.list") as Record<string, unknown>;
690
- console.log(jsonOut(data));
691
- } catch {
692
- console.log("Could not reach gateway. Is it running?");
693
- }
694
- });
695
-
696
- approval
697
- .command("revoke <nodeId>")
698
- .description("Revoke an approved peer")
699
- .action(async (nodeId: string) => {
700
- try {
701
- const result = await callGateway("clawmatrix.approval.revoke", { nodeId }) as Record<string, unknown>;
702
- if (result.ok) {
703
- console.log(`Revoked: ${nodeId}`);
704
- } else {
705
- console.log(`Node not found in approved list: ${nodeId}`);
706
- }
707
- } catch {
708
- console.log("Could not reach gateway. Is it running?");
709
- }
710
- });
711
- };