clawmatrix 0.2.9 → 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.
@@ -10,9 +10,10 @@ export function createClusterDiagnosticTool(): AnyAgentTool {
10
10
  label: "Cluster Diagnostic",
11
11
  description:
12
12
  "Diagnose or execute commands on a remote node's sentinel process. " +
13
- "Works even when the remote OpenClaw gateway is down. " +
13
+ "Works even when the remote OpenClaw gateway is down (sentinel is a lightweight always-on process). " +
14
14
  "Use action 'status' to check if the remote gateway is alive, " +
15
- "or 'exec' to run a shell command on the remote machine.",
15
+ "or 'exec' to run a shell command on the remote machine. " +
16
+ "Note: exec runs commands without restrictions — only use in trusted networks.",
16
17
  parameters: {
17
18
  type: "object",
18
19
  properties: {
@@ -88,6 +89,8 @@ export function createClusterDiagnosticTool(): AnyAgentTool {
88
89
  };
89
90
  }
90
91
 
92
+ // TODO(security): exec 允许任何已认证 peer 在远程 sentinel 执行任意命令,无 allowlist 或 capability check。
93
+ // 当前仅用于受信任网络。开放前需添加命令白名单或 per-peer 授权。
91
94
  if (action === "exec") {
92
95
  if (!command) {
93
96
  return {
@@ -7,7 +7,8 @@ export function createClusterEditTool(): AnyAgentTool {
7
7
  label: "Cluster Edit",
8
8
  description:
9
9
  "Edit a file on a remote cluster node by replacing exact text. " +
10
- "The oldText must match exactly (including whitespace).",
10
+ "IMPORTANT: Use cluster_read first to get exact file content — oldText must match precisely (including whitespace and indentation). " +
11
+ "For creating new files use cluster_write instead.",
11
12
  parameters: {
12
13
  type: "object",
13
14
  properties: {
@@ -6,7 +6,9 @@ export function createClusterEventsTool(): AnyAgentTool {
6
6
  name: "cluster_events",
7
7
  label: "Cluster Events",
8
8
  description:
9
- "Query and consume events from external sources (messages, calls, location changes).",
9
+ "Query and consume events from external sources (iOS Shortcuts, webhooks, etc.). " +
10
+ "Common types: message_received, location_change, battery_status. " +
11
+ "Events are persistent until consumed. Requires web.enabled in config.",
10
12
  parameters: {
11
13
  type: "object",
12
14
  properties: {
@@ -7,6 +7,8 @@ export function createClusterExecTool(): AnyAgentTool {
7
7
  label: "Cluster Exec",
8
8
  description:
9
9
  "Execute a shell command on a remote cluster node. " +
10
+ "Use for file/shell operations on remote machines. " +
11
+ "Returns {stdout, stderr, exitCode}. " +
10
12
  "Use nodeId or 'tags:<tag>' to specify the target.",
11
13
  parameters: {
12
14
  type: "object",
@@ -6,7 +6,9 @@ export function createClusterHandoffTool(): AnyAgentTool {
6
6
  name: "cluster_handoff",
7
7
  label: "Cluster Handoff",
8
8
  description:
9
- "Hand off a task to another agent in the cluster and wait for the result. " +
9
+ "Hand off a complex task to another agent in the cluster and wait for the result. " +
10
+ "Use for multi-step tasks that require the remote agent's full capabilities. " +
11
+ "If the remote agent needs more info, response includes inputRequired=true — use cluster_handoff_reply to continue. " +
10
12
  "Use agent ID (e.g. 'coder') or tag query (e.g. 'tags:coding') as target.",
11
13
  parameters: {
12
14
  type: "object",
@@ -6,7 +6,9 @@ export function createClusterPeersTool(): AnyAgentTool {
6
6
  name: "cluster_peers",
7
7
  label: "Cluster Peers",
8
8
  description:
9
- "List all reachable peers in the cluster, their agents, models, tools, and connection status.",
9
+ "List all peers in the cluster with their agents, models, tools, tags, and connection status. " +
10
+ "Call this first to discover what's available before using other cluster tools. " +
11
+ "Status: direct (connected), relay (via another node), sentinel-only (gateway down, sentinel up), satellite (HTTP polling device), unreachable.",
10
12
  parameters: {
11
13
  type: "object",
12
14
  properties: {},
@@ -5,7 +5,10 @@ export function createClusterReadTool(): AnyAgentTool {
5
5
  return {
6
6
  name: "cluster_read",
7
7
  label: "Cluster Read",
8
- description: "Read a file from a remote cluster node.",
8
+ description:
9
+ "Read a file from a remote cluster node. " +
10
+ "Returns {content} with the file text. " +
11
+ "Use nodeId or 'tags:<tag>' to specify the target.",
9
12
  parameters: {
10
13
  type: "object",
11
14
  properties: {
@@ -6,7 +6,8 @@ export function createClusterSendTool(): AnyAgentTool {
6
6
  name: "cluster_send",
7
7
  label: "Cluster Send",
8
8
  description:
9
- "Send a one-way message to a remote agent. Does not wait for a response.",
9
+ "Send a one-way notification to a remote agent. Fire-and-forget: does not wait for a response. " +
10
+ "Use cluster_handoff instead when you need the remote agent to process a task and return a result.",
10
11
  parameters: {
11
12
  type: "object",
12
13
  properties: {
@@ -7,13 +7,10 @@ export function createClusterTerminalTool(): AnyAgentTool {
7
7
  label: "Cluster Terminal",
8
8
  description:
9
9
  "Interactive terminal (PTY) on a remote cluster node. " +
10
- "Actions: " +
11
- '"open" open a terminal session on a remote node. ' +
12
- '"input" send input (keystrokes/commands) to a session. ' +
13
- '"read" read buffered output from a session. ' +
14
- '"resize" — resize the terminal. ' +
15
- '"list" — list active terminal sessions. ' +
16
- '"close" — close a terminal session.',
10
+ "Workflow: open → input/read (repeat) → close. " +
11
+ 'Actions: "open" (requires node), "input" (requires sessionId + data), "read" (requires sessionId), ' +
12
+ '"resize" (requires sessionId), "list" (no params), "close" (requires sessionId). ' +
13
+ "Prefer cluster_exec for one-shot commands; use this for interactive/stateful sessions.",
17
14
  parameters: {
18
15
  type: "object",
19
16
  properties: {
@@ -7,8 +7,8 @@ export function createClusterToolInvokeTool(): AnyAgentTool {
7
7
  label: "Cluster Tool Invoke",
8
8
  description:
9
9
  "Invoke any tool on a remote cluster node by name. " +
10
- "Use this for device-specific tools (e.g. screenshot, battery, location on mobile nodes). " +
11
- "Use cluster_peers to discover available tools on each peer. " +
10
+ "Preferred for device-specific tools (screenshot, battery, location, clipboard, etc.). " +
11
+ "Run cluster_peers first to discover available tools, or use CLI: clawmatrix tools --describe <tool> for params. " +
12
12
  "Use nodeId or 'tags:<tag>' to specify the target.",
13
13
  parameters: {
14
14
  type: "object",
@@ -0,0 +1,91 @@
1
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
2
+ import { getClusterRuntime } from "../cluster-service.ts";
3
+
4
+ export function createClusterTransferTool(): AnyAgentTool {
5
+ return {
6
+ name: "cluster_transfer",
7
+ label: "Cluster File Transfer",
8
+ description:
9
+ "Transfer a file between the local node and a remote cluster node. " +
10
+ "Supports large files (up to 100MB) with chunked transfer and SHA-256 integrity check. " +
11
+ "Specify source_node to pull from remote, or target_node to push to remote.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ source_node: {
16
+ type: "string",
17
+ description: "Source nodeId (omit for local). Exactly one of source_node or target_node must be provided.",
18
+ },
19
+ source_path: {
20
+ type: "string",
21
+ description: "File path on the source node",
22
+ },
23
+ target_node: {
24
+ type: "string",
25
+ description: "Target nodeId (omit for local). Exactly one of source_node or target_node must be provided.",
26
+ },
27
+ target_path: {
28
+ type: "string",
29
+ description: "File path on the target node",
30
+ },
31
+ },
32
+ required: ["source_path", "target_path"],
33
+ },
34
+ async execute(_toolCallId, params) {
35
+ const { source_node, source_path, target_node, target_path } = params as {
36
+ source_node?: string;
37
+ source_path: string;
38
+ target_node?: string;
39
+ target_path: string;
40
+ };
41
+
42
+ // Validate: exactly one of source_node or target_node must be provided
43
+ if (source_node && target_node) {
44
+ return {
45
+ content: [{ type: "text" as const, text: "Error: Provide either source_node or target_node, not both." }],
46
+ details: { error: true },
47
+ };
48
+ }
49
+ if (!source_node && !target_node) {
50
+ return {
51
+ content: [{ type: "text" as const, text: "Error: Provide either source_node (to pull) or target_node (to push)." }],
52
+ details: { error: true },
53
+ };
54
+ }
55
+
56
+ try {
57
+ const runtime = getClusterRuntime();
58
+ const ftm = runtime.fileTransferManager;
59
+ if (!ftm) {
60
+ return {
61
+ content: [{ type: "text" as const, text: "Error: File transfer is not enabled on this node." }],
62
+ details: { error: true },
63
+ };
64
+ }
65
+
66
+ let result;
67
+ if (source_node) {
68
+ // Pull: remote → local
69
+ result = await ftm.pullFile(source_node, source_path, target_path);
70
+ } else {
71
+ // Push: local → remote
72
+ result = await ftm.pushFile(target_node!, source_path, target_path);
73
+ }
74
+
75
+ const text = result.success
76
+ ? `Transfer complete: ${result.bytesTransferred} bytes transferred.`
77
+ : `Transfer failed: ${result.error}`;
78
+
79
+ return {
80
+ content: [{ type: "text" as const, text }],
81
+ details: result,
82
+ };
83
+ } catch (err) {
84
+ return {
85
+ content: [{ type: "text" as const, text: `Transfer error: ${err instanceof Error ? err.message : String(err)}` }],
86
+ details: { error: true },
87
+ };
88
+ }
89
+ },
90
+ };
91
+ }
@@ -5,7 +5,9 @@ export function createClusterWriteTool(): AnyAgentTool {
5
5
  return {
6
6
  name: "cluster_write",
7
7
  label: "Cluster Write",
8
- description: "Write content to a file on a remote cluster node.",
8
+ description:
9
+ "Write content to a file on a remote cluster node (creates or overwrites). " +
10
+ "Use nodeId or 'tags:<tag>' to specify the target.",
9
11
  parameters: {
10
12
  type: "object",
11
13
  properties: {
package/src/types.ts CHANGED
@@ -317,6 +317,68 @@ export interface ToolBatchResponse extends ClusterFrame {
317
317
  };
318
318
  }
319
319
 
320
+ // ── File transfer ─────────────────────────────────────────────────
321
+ export interface FileTransferInit extends ClusterFrame {
322
+ type: "file_transfer_init";
323
+ id: string;
324
+ payload: {
325
+ sessionId: string;
326
+ direction: "push" | "pull";
327
+ filePath: string;
328
+ targetPath: string;
329
+ fileSize: number;
330
+ totalChunks: number;
331
+ chunkSize: number;
332
+ checksum: string; // SHA-256 hex
333
+ };
334
+ }
335
+
336
+ export interface FileTransferAck extends ClusterFrame {
337
+ type: "file_transfer_ack";
338
+ id: string;
339
+ payload: {
340
+ sessionId: string;
341
+ accepted: boolean;
342
+ error?: string;
343
+ // Pull mode: responder includes file metadata
344
+ fileSize?: number;
345
+ totalChunks?: number;
346
+ checksum?: string;
347
+ };
348
+ }
349
+
350
+ export interface FileTransferChunk extends ClusterFrame {
351
+ type: "file_transfer_chunk";
352
+ id: string;
353
+ payload: {
354
+ sessionId: string;
355
+ chunkIndex: number;
356
+ data: string; // base64-encoded
357
+ };
358
+ }
359
+
360
+ export interface FileTransferChunkAck extends ClusterFrame {
361
+ type: "file_transfer_chunk_ack";
362
+ id: string;
363
+ payload: {
364
+ sessionId: string;
365
+ chunkIndex: number;
366
+ success: boolean;
367
+ error?: string;
368
+ };
369
+ }
370
+
371
+ export interface FileTransferComplete extends ClusterFrame {
372
+ type: "file_transfer_complete";
373
+ id: string;
374
+ payload: {
375
+ sessionId: string;
376
+ success: boolean;
377
+ error?: string;
378
+ bytesTransferred?: number;
379
+ };
380
+ }
381
+
320
382
  // ── Device info ───────────────────────────────────────────────────
321
383
  export interface DeviceInfo {
322
384
  os: string; // e.g. "Darwin 24.6.0", "Linux 6.1.0"
@@ -358,10 +420,20 @@ export interface ModelInfo {
358
420
  compat?: ModelCompatInfo;
359
421
  }
360
422
 
423
+ export interface ToolCatalogEntry {
424
+ name: string;
425
+ description: string;
426
+ usage?: string;
427
+ /** JSON Schema for the tool's input parameters (for LLM callers to construct invocations). */
428
+ inputSchema?: Record<string, unknown>;
429
+ }
430
+
361
431
  export interface ToolProxyInfo {
362
432
  enabled: boolean;
363
433
  allow: string[];
364
434
  deny: string[];
435
+ /** Optional tool catalog with descriptions and usage hints for remote callers. */
436
+ catalog?: ToolCatalogEntry[];
365
437
  }
366
438
 
367
439
  export interface AcpAgentInfo {
@@ -419,6 +491,24 @@ export interface HealthSyncFrame extends ClusterFrame {
419
491
  };
420
492
  }
421
493
 
494
+ export interface AvailabilityRequest extends ClusterFrame {
495
+ type: "availability_req";
496
+ id: string;
497
+ payload: {
498
+ range: "24h" | "7d" | "90d";
499
+ };
500
+ }
501
+
502
+ export interface AvailabilityResponse extends ClusterFrame {
503
+ type: "availability_res";
504
+ id: string;
505
+ payload: {
506
+ success: boolean;
507
+ data?: unknown;
508
+ error?: string;
509
+ };
510
+ }
511
+
422
512
  // ── Diagnostic (sentinel) ────────────────────────────────────────
423
513
  export interface DiagnosticExec extends ClusterFrame {
424
514
  type: "diagnostic_exec";
@@ -479,9 +569,10 @@ export interface AcpStreamChunk extends ClusterFrame {
479
569
  id: string;
480
570
  payload: {
481
571
  delta: string;
482
- event?: string; // "agent_message_chunk" | "tool_call" | etc.
572
+ event?: string; // "agent_message_chunk" | "tool_call" | "plan" | "available_commands" | "config_options" | "usage" | "session_info" | etc.
483
573
  done: boolean;
484
574
  sessionId?: string; // included for session watchers (multi-device sync)
575
+ data?: unknown; // structured data for rich events (plan entries, slash commands, config options, usage stats)
485
576
  };
486
577
  }
487
578
 
@@ -497,6 +588,10 @@ export interface AcpTaskResponse extends ClusterFrame {
497
588
  acpSessionId?: string; // ACP-level session ID for future resume
498
589
  stopReason?: string; // ACP stop reason
499
590
  error?: string;
591
+ configOptions?: AcpConfigOption[]; // initial config options (model, thinking level, etc.)
592
+ slashCommands?: AcpSlashCommand[]; // available slash commands
593
+ availableModes?: AcpModeInfo[]; // available session modes
594
+ currentModeId?: string; // current mode
500
595
  };
501
596
  }
502
597
 
@@ -525,6 +620,7 @@ export interface AcpSessionInfo {
525
620
  description?: string; // first user message (for display in session list)
526
621
  updatedAt?: string;
527
622
  agent?: string; // which ACP agent (claude, codex, etc.)
623
+ status?: "active" | "idle"; // active = in-memory session with daemon, idle = persisted on disk
528
624
  }
529
625
 
530
626
  export interface AcpListRequest extends ClusterFrame {
@@ -626,6 +722,86 @@ export interface AcpGetModesResponse extends ClusterFrame {
626
722
  };
627
723
  }
628
724
 
725
+ // ── ACP config options ───────────────────────────────────────────
726
+
727
+ export interface AcpConfigOption {
728
+ id: string;
729
+ name: string;
730
+ type: "select" | "boolean";
731
+ currentValue: string | boolean;
732
+ options?: Array<{ id: string; name: string; description?: string }>;
733
+ description?: string;
734
+ category?: string; // "mode" | "model" | "thought_level" | custom
735
+ }
736
+
737
+ export interface AcpSetConfigRequest extends ClusterFrame {
738
+ type: "acp_set_config";
739
+ id: string;
740
+ payload: {
741
+ sessionId: string;
742
+ configId: string;
743
+ value: string | boolean;
744
+ };
745
+ }
746
+
747
+ export interface AcpSetConfigResponse extends ClusterFrame {
748
+ type: "acp_set_config_res";
749
+ id: string;
750
+ payload: {
751
+ success: boolean;
752
+ configOptions?: AcpConfigOption[];
753
+ error?: string;
754
+ };
755
+ }
756
+
757
+ // ── ACP subscribe / observe ──────────────────────────────────────
758
+
759
+ export interface AcpSubscribeRequest extends ClusterFrame {
760
+ type: "acp_subscribe";
761
+ id: string;
762
+ payload: {
763
+ sessionId: string; // ClawMatrix session ID to observe
764
+ };
765
+ }
766
+
767
+ export interface AcpSubscribeResponse extends ClusterFrame {
768
+ type: "acp_subscribe_res";
769
+ id: string;
770
+ payload: {
771
+ success: boolean;
772
+ history?: ChatHistoryMessage[]; // snapshot at subscribe time
773
+ error?: string;
774
+ };
775
+ }
776
+
777
+ export interface AcpUnsubscribeRequest extends ClusterFrame {
778
+ type: "acp_unsubscribe";
779
+ payload: {
780
+ sessionId: string;
781
+ };
782
+ }
783
+
784
+ /** Pushed to all peers when a session is created, its title changes, or it completes. */
785
+ export interface AcpSessionNotify extends ClusterFrame {
786
+ type: "acp_session_notify";
787
+ payload: {
788
+ sessionId: string;
789
+ nodeId: string;
790
+ event: "created" | "updated" | "completed";
791
+ title?: string;
792
+ updatedAt?: string;
793
+ agent?: string;
794
+ };
795
+ }
796
+
797
+ // ── ACP slash commands ──────────────────────────────────────────
798
+
799
+ export interface AcpSlashCommand {
800
+ name: string;
801
+ description: string;
802
+ input?: { hint?: string };
803
+ }
804
+
629
805
  // ── Chat history ──────────────────────────────────────────────────
630
806
 
631
807
  export interface ChatHistoryMessage {
@@ -828,6 +1004,12 @@ export type AnyClusterFrame =
828
1004
  | AcpSetModeResponse
829
1005
  | AcpGetModesRequest
830
1006
  | AcpGetModesResponse
1007
+ | AcpSetConfigRequest
1008
+ | AcpSetConfigResponse
1009
+ | AcpSubscribeRequest
1010
+ | AcpSubscribeResponse
1011
+ | AcpUnsubscribeRequest
1012
+ | AcpSessionNotify
831
1013
  | ChatHistoryRequest
832
1014
  | ChatHistoryResponse
833
1015
  | PeerApprovalNotify
@@ -840,4 +1022,11 @@ export type AnyClusterFrame =
840
1022
  | TerminalResize
841
1023
  | TerminalCloseRequest
842
1024
  | TerminalCloseResponse
843
- | HealthSyncFrame;
1025
+ | HealthSyncFrame
1026
+ | AvailabilityRequest
1027
+ | AvailabilityResponse
1028
+ | FileTransferInit
1029
+ | FileTransferAck
1030
+ | FileTransferChunk
1031
+ | FileTransferChunkAck
1032
+ | FileTransferComplete;
package/src/web.ts CHANGED
@@ -5,7 +5,6 @@ import type { ClawMatrixConfig } from "./config.ts";
5
5
  import type { SatelliteContext, IngestedEvent } from "./types.ts";
6
6
  import type { HealthTracker } from "./health-tracker.ts";
7
7
  import { timingSafeEqual } from "./auth.ts";
8
- import { renderDashboard } from "./web-ui.ts";
9
8
  import { readBody } from "./http-utils.ts";
10
9
 
11
10
  const COOKIE_NAME = "clawmatrix_token";
@@ -135,13 +134,6 @@ export class WebHandler {
135
134
  return true;
136
135
  }
137
136
 
138
- // Serve dashboard HTML (auth checked client-side via API)
139
- if (path === "/" && req.method === "GET") {
140
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
141
- res.end(renderDashboard(this.config.nodeId));
142
- return true;
143
- }
144
-
145
137
  // All /api/* routes require auth (async)
146
138
  if (path.startsWith("/api/")) {
147
139
  this.handleAuthenticatedRoute(req, res, path);
@@ -224,7 +216,7 @@ export class WebHandler {
224
216
  private async checkAuth(req: IncomingMessage): Promise<boolean> {
225
217
  // Check Authorization header
226
218
  const authHeader = req.headers.authorization;
227
- if (authHeader?.startsWith("Bearer ") && await timingSafeEqual(authHeader.slice(7), this.token)) {
219
+ if (authHeader?.startsWith("Bearer ") && timingSafeEqual(authHeader.slice(7), this.token)) {
228
220
  return true;
229
221
  }
230
222
 
@@ -658,7 +650,7 @@ export class WebHandler {
658
650
  ssid: ctx.ssid || undefined,
659
651
  ip: ctx.ip || undefined,
660
652
  router: ctx.router || undefined,
661
- cellular: !ctx.ssid,
653
+ cellular: typeof ctx.cellular === "boolean" ? ctx.cellular : !ctx.ssid,
662
654
  country: ctx.country || undefined,
663
655
  tools: Array.isArray(ctx.tools) ? ctx.tools : undefined,
664
656
  ts: Date.now(),