clawmatrix 0.2.11 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +123 -12
- package/package.json +2 -1
- package/src/acp-proxy.ts +407 -68
- package/src/cli.ts +478 -10
- package/src/cluster-service.ts +114 -14
- 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/index.ts +234 -5
- package/src/knowledge-sync.ts +44 -6
- package/src/model-proxy.ts +35 -10
- package/src/peer-manager.ts +81 -13
- 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 +12 -4
- 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-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/web-ui.ts +0 -1622
package/src/cli.ts
CHANGED
|
@@ -46,13 +46,30 @@ async function callGateway(method: string, params?: Record<string, unknown>): Pr
|
|
|
46
46
|
return JSON.parse(stdout);
|
|
47
47
|
}
|
|
48
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
|
+
|
|
49
65
|
export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
50
66
|
const cmd = program.command("clawmatrix").description("ClawMatrix cluster management");
|
|
51
67
|
|
|
52
68
|
cmd
|
|
53
69
|
.command("status")
|
|
54
70
|
.description("Show cluster topology and peer status")
|
|
55
|
-
.
|
|
71
|
+
.option("--json", "Output raw JSON (structured, machine-readable)")
|
|
72
|
+
.action(async (opts: { json?: boolean }) => {
|
|
56
73
|
let data: Record<string, unknown>;
|
|
57
74
|
try {
|
|
58
75
|
data = (await callGateway("clawmatrix.status")) as Record<string, unknown>;
|
|
@@ -66,13 +83,12 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
66
83
|
return;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const yellow = (s: string) => `\x1b[33m${s}\x1b[39m`;
|
|
86
|
+
// --json: raw structured output for programmatic consumption
|
|
87
|
+
if (opts.json) {
|
|
88
|
+
console.log(jsonOut(data));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
const bar = dim("│");
|
|
77
93
|
const lbl = (text: string) => dim(text.padEnd(13));
|
|
78
94
|
|
|
@@ -168,7 +184,459 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
168
184
|
console.log("[]");
|
|
169
185
|
return;
|
|
170
186
|
}
|
|
171
|
-
console.log(
|
|
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 }));
|
|
172
640
|
});
|
|
173
641
|
|
|
174
642
|
// ── Peer approval commands ──────────────────────────────────────
|
|
@@ -219,7 +687,7 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
219
687
|
.action(async () => {
|
|
220
688
|
try {
|
|
221
689
|
const data = await callGateway("clawmatrix.approval.list") as Record<string, unknown>;
|
|
222
|
-
console.log(
|
|
690
|
+
console.log(jsonOut(data));
|
|
223
691
|
} catch {
|
|
224
692
|
console.log("Could not reach gateway. Is it running?");
|
|
225
693
|
}
|