clawmatrix 0.2.11 → 0.4.0

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