clawmatrix 0.5.1 → 0.6.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.
@@ -3,102 +3,58 @@ name: clawmatrix
3
3
  description: Use the clawmatrix CLI to interact with remote devices (phones, computers, servers) in a mesh cluster — run tools, check location, get battery status, read/write files, execute commands, and more on any connected node.
4
4
  ---
5
5
 
6
- Use the `clawmatrix` CLI to interact with the ClawMatrix mesh cluster. Remote nodes can be phones (iPhone/Android), computers, or servers. You can invoke any tool available on remote nodes — including getting device location, battery status, running shell commands, reading/writing files, and more.
6
+ Use the `clawmatrix` CLI to interact with the ClawMatrix mesh cluster. Remote nodes can be phones (iPhone/Android), computers, or servers.
7
7
 
8
- All commands output LLM-friendly text (no ANSI colors) when stdout is not a TTY.
8
+ Run `clawmatrix --help` or `clawmatrix <command> --help` for detailed usage.
9
9
 
10
- ## Quick Start
10
+ ## Capabilities
11
11
 
12
- 1. Run `clawmatrix status` to see connected nodes and their capabilities
13
- 2. Run `clawmatrix tools <nodeId>` to see what tools a node offers
14
- 3. Run `clawmatrix call <nodeId> <tool> '<params>'` to invoke a tool
12
+ **Cluster & Discovery**
13
+ - View cluster topology, peer status, node config, and uptime/availability
14
+ - Check reachability of specific nodes with latency
15
+ - Approve/deny/revoke peer connections
15
16
 
16
- ## Cluster Status
17
+ **Remote Tool Execution**
18
+ - Discover tools across all nodes (filter by keyword, describe params)
19
+ - Invoke single tools or batch multiple in one round-trip
20
+ - Tools include device-specific capabilities: location, battery, camera, clipboard, contacts, calendar, health data, HomeKit, etc.
17
21
 
18
- ```bash
19
- clawmatrix status # Cluster topology: peers, agents, models, tags
20
- clawmatrix status --json # Structured JSON output
21
- clawmatrix check <nodeId> # Quick reachability check with latency
22
- ```
22
+ **Models**
23
+ - List all LLM models available across the cluster, filterable by node
23
24
 
24
- Always start with `clawmatrix status` to understand the current topology before performing other operations.
25
+ **Agent Delegation**
26
+ - `handoff` — delegate tasks to remote agents with streaming output and failover
27
+ - `send` — fire-and-forget messages to remote nodes
28
+ - `acp` — manage persistent coding agent sessions (Claude Code, Codex, Gemini) with prompt/resume/cancel/close
25
29
 
26
- ## Remote Tools
30
+ **Events & Automations**
31
+ - Query, consume, and ingest events from external sources (iOS Shortcuts, webhooks, etc.)
32
+ - Manage automation rules: list, create/save, manually trigger, replay historical executions
27
33
 
28
- ```bash
29
- clawmatrix tools # List all remote tools (compact)
30
- clawmatrix tools <nodeId> # Tools on a specific node
31
- clawmatrix tools --describe <tool> # Full usage and parameter schema
32
- clawmatrix tools --filter <keyword> # Search by name or description
33
- ```
34
+ **Infrastructure**
35
+ - `diagnostic` sentinel-based diagnostics (works even when gateway is down)
36
+ - `terminal` interactive PTY sessions on remote nodes
37
+ - `transfer` push/pull files up to 100MB between nodes with integrity verification
38
+ - `notify` push Dynamic Island / Live Activity notifications to iOS devices
34
39
 
35
- Use `--describe` to understand a tool's parameters before calling it.
40
+ **Knowledge Sync (CRDT)**
41
+ - List synced workspace files, view change history, line-by-line blame, read file content
42
+ - All knowledge is CRDT-based and mesh-synced across nodes
36
43
 
37
- ## Invoke Tools
44
+ **Kanban Board**
45
+ - Distributed task board: create, list, get, update, claim, move, annotate, delete cards
46
+ - Filter by stage/priority/label/node; stages flow from `backlog` → `done` → `archived`
38
47
 
39
- ```bash
40
- clawmatrix call <nodeId> <tool> '<json-params>' # Single tool invocation
41
- clawmatrix call <nodeId> <tool> '<json-params>' -t 30000 # With timeout (ms)
42
- ```
48
+ **Config Management**
49
+ - View runtime config summary
50
+ - Read/write config files under `~/.openclaw/` (for remote node configuration)
43
51
 
44
- ```bash
45
- clawmatrix batch <nodeId> '[{"tool":"t1","params":{}},{"tool":"t2","params":{}}]'
46
- clawmatrix batch <nodeId> --no-stop-on-error '[...]' # Continue on failure
47
- ```
52
+ ## Important Notes
48
53
 
49
- Batch supports stdin: `echo '<json>' | clawmatrix batch <nodeId>`
50
-
51
- ## Models
52
-
53
- ```bash
54
- clawmatrix models # All cluster models
55
- clawmatrix models --node <nodeId> # Filter by node
56
- ```
57
-
58
- ## Events
59
-
60
- ```bash
61
- clawmatrix events # Unconsumed events
62
- clawmatrix events --type <type> # Filter by type
63
- clawmatrix events --source <nodeId> # Filter by source node
64
- clawmatrix events --consume <id1,id2> # Mark events as consumed
65
- clawmatrix events --all # Include consumed events
66
- ```
67
-
68
- ## Peer Approval
69
-
70
- ```bash
71
- clawmatrix approve <approvalId> # Approve a pending peer
72
- clawmatrix deny <approvalId> # Deny a pending peer
73
- clawmatrix approval list # List pending/approved/denied peers
74
- clawmatrix approval revoke <nodeId> # Revoke an approved peer
75
- ```
76
-
77
- ## Notifications (Dynamic Island / Live Activity)
78
-
79
- Push progress notifications to the user's iPhone via `clawmatrix notify`. This triggers the Dynamic Island and lock screen Live Activity.
80
-
81
- **Use this proactively when running long tasks** (iOS builds, large refactors, test suites, batch operations) so the user can track progress without watching the terminal.
82
-
83
- ```bash
84
- # Start a notification
85
- clawmatrix notify "iOS 构建" --detail "正在编译..."
86
- # Returns: {"taskId":"<id>", "action":"start", "targets":1}
87
-
88
- # Update progress
89
- clawmatrix notify "iOS 构建" --action update --task-id <id> --detail "链接中..." --progress 80
90
-
91
- # End (success)
92
- clawmatrix notify "iOS 构建" --action end --task-id <id>
93
- ```
94
-
95
- Options: `--detail <text>`, `--progress <0-100>`, `--action start|update|end`, `--task-id <id>`, `--tool <name>`
96
-
97
- ## Workflow
98
-
99
- 1. Run `clawmatrix status` to see the cluster topology
100
- 2. Use `clawmatrix tools --filter <keyword>` to find relevant tools
101
- 3. Use `clawmatrix tools --describe <tool>` to check parameters
102
- 4. Use `clawmatrix call` or `clawmatrix batch` to invoke tools
103
- 5. If a call fails, run `clawmatrix check <nodeId>` to verify connectivity
104
- 6. For long-running tasks, use `clawmatrix notify` to push progress to the user's phone
54
+ - **Always start with `clawmatrix status`** to understand the cluster topology before other operations
55
+ - **Use `clawmatrix tools --describe <tool>`** to check a tool's parameter schema before calling it
56
+ - **Use `clawmatrix notify` proactively during long tasks** (builds, tests, large refactors) so the user can track progress on their phone without watching the terminal
57
+ - Output is **LLM-optimized**: no ANSI colors and compact format when stdout is not a TTY; add `--json` for structured output
58
+ - Target nodes by exact `nodeId` or by tag expression `tags:<tag>`
59
+ - Most commands support `--json` for structured output
60
+ - `batch` and `automations save` support stdin for piping data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmatrix",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api.ts CHANGED
@@ -10,6 +10,9 @@ import type { KnowledgeSync } from "./knowledge-sync.ts";
10
10
  import { nanoid } from "nanoid";
11
11
  import { timingSafeEqual } from "./auth.ts";
12
12
  import { readBody } from "./http-utils.ts";
13
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
14
+ import { homedir } from "node:os";
15
+ import { dirname } from "node:path";
13
16
 
14
17
  const COOKIE_NAME = "clawmatrix_token";
15
18
  const SESSION_MAX_AGE = 86400 * 7; // 7 days
@@ -27,6 +30,255 @@ const CLUSTER_TOOLS = [
27
30
  "cluster_notify", "cluster_query",
28
31
  ];
29
32
 
33
+ /** Full tool definitions for /api/tools endpoint. */
34
+ const CLUSTER_TOOL_DEFS: Array<{ name: string; description: string; inputSchema: Record<string, unknown> }> = [
35
+ {
36
+ name: "cluster_peers",
37
+ description: "List all peers in the cluster with their agents, models, tools, tags, and connection status. Call this first to discover what's available before using other cluster tools.",
38
+ inputSchema: { type: "object", properties: {} },
39
+ },
40
+ {
41
+ name: "cluster_exec",
42
+ description: "Execute a shell command on a remote cluster node. Returns {stdout, stderr, exitCode}. Use nodeId or 'tags:<tag>' to specify the target.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ node: { type: "string", description: "Target nodeId or \"tags:<tag>\"" },
47
+ command: { type: "string", description: "Shell command to execute" },
48
+ workdir: { type: "string", description: "Working directory (optional)" },
49
+ timeout: { type: "number", description: "Timeout in seconds (default 1800)" },
50
+ },
51
+ required: ["node", "command"],
52
+ },
53
+ },
54
+ {
55
+ name: "cluster_read",
56
+ description: "Read a file from a remote cluster node. Returns {content} with the file text.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ node: { type: "string", description: "Target nodeId or \"tags:<tag>\"" },
61
+ path: { type: "string", description: "File path to read" },
62
+ },
63
+ required: ["node", "path"],
64
+ },
65
+ },
66
+ {
67
+ name: "cluster_write",
68
+ description: "Write content to a file on a remote cluster node (creates or overwrites).",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ node: { type: "string", description: "Target nodeId or \"tags:<tag>\"" },
73
+ path: { type: "string", description: "File path to write" },
74
+ content: { type: "string", description: "Content to write" },
75
+ },
76
+ required: ["node", "path", "content"],
77
+ },
78
+ },
79
+ {
80
+ name: "cluster_edit",
81
+ description: "Edit a file on a remote cluster node by replacing exact text. Use cluster_read first to get exact file content.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ node: { type: "string", description: "Target nodeId or \"tags:<tag>\"" },
86
+ path: { type: "string", description: "File path to edit" },
87
+ oldText: { type: "string", description: "Exact text to find and replace" },
88
+ newText: { type: "string", description: "New text to replace with" },
89
+ },
90
+ required: ["node", "path", "oldText", "newText"],
91
+ },
92
+ },
93
+ {
94
+ name: "cluster_handoff",
95
+ description: "Hand off a complex task to another agent in the cluster and wait for the result. Use for multi-step tasks that require the remote agent's full capabilities.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ target: { type: "string", description: "Agent ID or \"tags:<tag>\" expression" },
100
+ task: { type: "string", description: "Task description for the remote agent" },
101
+ context: { type: "string", description: "Additional context for the task" },
102
+ },
103
+ required: ["target", "task"],
104
+ },
105
+ },
106
+ {
107
+ name: "cluster_handoff_reply",
108
+ description: "Reply to a remote agent that requested more input during a handoff. Use the handoff_id returned by cluster_handoff.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ handoff_id: { type: "string", description: "The handoff ID from cluster_handoff's response" },
113
+ message: { type: "string", description: "Your reply to the remote agent's question" },
114
+ },
115
+ required: ["handoff_id", "message"],
116
+ },
117
+ },
118
+ {
119
+ name: "cluster_send",
120
+ description: "Send a one-way notification to a remote agent. Fire-and-forget: does not wait for a response.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ target: { type: "string", description: "Agent ID or \"tags:<tag>\" expression" },
125
+ message: { type: "string", description: "Message content to send" },
126
+ },
127
+ required: ["target", "message"],
128
+ },
129
+ },
130
+ {
131
+ name: "cluster_batch",
132
+ description: "Execute multiple tools on a remote node in a single round-trip. Tools run sequentially (supports dependencies like read→edit).",
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ node: { type: "string", description: "Target nodeId or \"tags:<tag>\"" },
137
+ items: { type: "array", description: "Array of { tool, params } to execute in order", items: { type: "object", properties: { tool: { type: "string" }, params: { type: "object" } }, required: ["tool", "params"] } },
138
+ stopOnError: { type: "boolean", description: "Stop on first error (default true)" },
139
+ },
140
+ required: ["node", "items"],
141
+ },
142
+ },
143
+ {
144
+ name: "cluster_tool",
145
+ description: "Invoke any tool on a remote cluster node by name. Preferred for device-specific tools (screenshot, battery, location, clipboard, etc.).",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ node: { type: "string", description: "Target nodeId or \"tags:<tag>\"" },
150
+ tool: { type: "string", description: "Remote tool name to invoke" },
151
+ params: { type: "object", description: "Parameters to pass to the remote tool" },
152
+ timeout: { type: "number", description: "Timeout in seconds (default 30)" },
153
+ },
154
+ required: ["node", "tool"],
155
+ },
156
+ },
157
+ {
158
+ name: "cluster_terminal",
159
+ description: "Interactive terminal (PTY) on a remote cluster node. Actions: open, input, read, resize, list, close. Prefer cluster_exec for one-shot commands.",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ action: { type: "string", enum: ["open", "input", "read", "resize", "list", "close"], description: "Action to perform (default: open)" },
164
+ node: { type: "string", description: "Target node ID (required for open)" },
165
+ sessionId: { type: "string", description: "Session ID (required for input/read/resize/close)" },
166
+ data: { type: "string", description: "Input data to send (for input action)" },
167
+ shell: { type: "string", description: "Shell to use (default: $SHELL)" },
168
+ cols: { type: "number", description: "Terminal columns (default: 80)" },
169
+ rows: { type: "number", description: "Terminal rows (default: 24)" },
170
+ },
171
+ },
172
+ },
173
+ {
174
+ name: "cluster_transfer",
175
+ description: "Transfer a file between local and remote nodes. Supports up to 100MB with chunked transfer and SHA-256 integrity check.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ source_path: { type: "string", description: "File path on the source node" },
180
+ target_path: { type: "string", description: "File path on the target node" },
181
+ source_node: { type: "string", description: "Source nodeId (omit for local)" },
182
+ target_node: { type: "string", description: "Target nodeId (omit for local)" },
183
+ },
184
+ required: ["source_path", "target_path"],
185
+ },
186
+ },
187
+ {
188
+ name: "cluster_events",
189
+ description: "Query and consume events from external sources (iOS Shortcuts, webhooks, etc.). Events are persistent until consumed.",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ action: { type: "string", enum: ["query", "consume"], description: "\"query\" to list events, \"consume\" to mark as processed" },
194
+ type: { type: "string", description: "Filter by event type" },
195
+ source: { type: "string", description: "Filter by source" },
196
+ unconsumed: { type: "boolean", description: "Only unconsumed events (default true)" },
197
+ limit: { type: "number", description: "Max events to return (default 20)" },
198
+ ids: { type: "array", items: { type: "string" }, description: "Event IDs to consume" },
199
+ },
200
+ required: ["action"],
201
+ },
202
+ },
203
+ {
204
+ name: "cluster_diagnostic",
205
+ description: "Diagnose or execute commands on a remote node's sentinel process. Works even when the gateway is down.",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ node: { type: "string", description: "Target nodeId" },
210
+ action: { type: "string", enum: ["status", "exec"], description: "\"status\" to check health, \"exec\" to run a command" },
211
+ command: { type: "string", description: "Shell command (required for exec)" },
212
+ timeout: { type: "number", description: "Timeout in seconds (default 30)" },
213
+ },
214
+ required: ["node", "action"],
215
+ },
216
+ },
217
+ {
218
+ name: "cluster_acp",
219
+ description: "Run a task on a remote coding agent (Claude Code, Codex, Gemini CLI) via ACP protocol. Supports one-shot and persistent sessions.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ action: { type: "string", enum: ["prompt", "list", "resume", "cancel", "set_mode", "get_modes", "close"], description: "Action to perform (default: prompt)" },
224
+ node: { type: "string", description: "Target node ID or \"tags:<tag>\"" },
225
+ agent: { type: "string", description: "ACP agent name: \"claude\", \"codex\", \"gemini\"" },
226
+ task: { type: "string", description: "Task or prompt to send" },
227
+ sessionId: { type: "string", description: "Session ID for follow-up or management" },
228
+ mode: { type: "string", enum: ["oneshot", "persistent"], description: "Session mode (default: oneshot)" },
229
+ cwd: { type: "string", description: "Working directory on the remote node" },
230
+ },
231
+ required: ["node"],
232
+ },
233
+ },
234
+ {
235
+ name: "cluster_kanban",
236
+ description: "Manage the distributed kanban board for tracking work items across the cluster. Supports create, list, get, claim, move, annotate, update, delete, summary.",
237
+ inputSchema: {
238
+ type: "object",
239
+ properties: {
240
+ action: { type: "string", enum: ["create", "list", "get", "claim", "move", "annotate", "update", "delete", "summary"], description: "Action to perform (default: list)" },
241
+ cardId: { type: "string", description: "Card ID for get/claim/move/annotate/update/delete" },
242
+ title: { type: "string", description: "Card title (for create or update)" },
243
+ description: { type: "string", description: "Card description in Markdown" },
244
+ stage: { type: "string", enum: ["backlog", "claimed", "in_progress", "review", "done", "archived"], description: "Target stage or filter" },
245
+ priority: { type: "string", enum: ["low", "medium", "high", "urgent"], description: "Card priority" },
246
+ labels: { type: "array", items: { type: "string" }, description: "Labels" },
247
+ },
248
+ },
249
+ },
250
+ {
251
+ name: "cluster_notify",
252
+ description: "Push a notification to mobile devices in the cluster (Dynamic Island / Live Activity on iOS). Track long-running task progress.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ action: { type: "string", enum: ["start", "update", "end"], description: "Action to perform (default: start)" },
257
+ taskId: { type: "string", description: "Task ID for update/end actions" },
258
+ title: { type: "string", description: "Activity title" },
259
+ detail: { type: "string", description: "Activity detail text" },
260
+ progress: { type: "number", description: "Progress 0.0 to 1.0" },
261
+ tool: { type: "string", description: "Current tool being executed" },
262
+ success: { type: "boolean", description: "For end: true=completed, false=failed" },
263
+ },
264
+ },
265
+ },
266
+ {
267
+ name: "cluster_query",
268
+ description: "Query replicated data from SQLite: audit_log, health_events, handoff_history. Data is replicated across peers for cluster-wide view.",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ table: { type: "string", enum: ["audit_log", "health_events", "handoff_history"], description: "Which table to query" },
273
+ since: { type: "number", description: "Events after this unix timestamp (ms). Negative for relative." },
274
+ node_id: { type: "string", description: "Filter by node ID" },
275
+ limit: { type: "number", description: "Max rows (default 50)" },
276
+ },
277
+ required: ["table"],
278
+ },
279
+ },
280
+ ];
281
+
30
282
  interface SatelliteEvent {
31
283
  ts: number;
32
284
  type: "peer_online" | "peer_offline" | "handoff_done" | "context_update" | "event_ingested" | "kanban";
@@ -216,6 +468,11 @@ export class ApiHandler {
216
468
  return;
217
469
  }
218
470
 
471
+ if (path === "/api/tools" && req.method === "GET") {
472
+ this.handleToolsList(res);
473
+ return;
474
+ }
475
+
219
476
  if (path === "/api/chat" && req.method === "POST") {
220
477
  this.handleChat(req, res);
221
478
  return;
@@ -267,6 +524,16 @@ export class ApiHandler {
267
524
  return;
268
525
  }
269
526
 
527
+ if (path === "/api/config/read" && req.method === "GET") {
528
+ this.handleConfigRead(req, res);
529
+ return;
530
+ }
531
+
532
+ if (path === "/api/config/write" && req.method === "POST") {
533
+ this.handleConfigWrite(req, res);
534
+ return;
535
+ }
536
+
270
537
  if (path === "/api/availability" && req.method === "GET") {
271
538
  this.handleAvailability(req, res);
272
539
  return;
@@ -298,6 +565,11 @@ export class ApiHandler {
298
565
  return;
299
566
  }
300
567
 
568
+ if (path === "/api/knowledge/content" && req.method === "GET") {
569
+ this.handleKnowledgeContent(req, res);
570
+ return;
571
+ }
572
+
301
573
  // Board / Kanban API
302
574
  if (path === "/api/board" && req.method === "GET") {
303
575
  this.handleBoardSummary(res);
@@ -434,9 +706,112 @@ export class ApiHandler {
434
706
  return;
435
707
  }
436
708
 
437
- const result = this.healthTracker.getAvailability(range);
709
+ try {
710
+ const result = this.healthTracker.getAvailability(range);
711
+ res.writeHead(200, { "Content-Type": "application/json" });
712
+ res.end(JSON.stringify(result));
713
+ } catch (err) {
714
+ res.writeHead(500, { "Content-Type": "application/json" });
715
+ res.end(JSON.stringify({ error: String((err as Error)?.message ?? err) }));
716
+ }
717
+ }
718
+
719
+ // ── Config file API (local node) ────────────────────────────────
720
+
721
+ /** Resolve config path, restricted to ~/.openclaw/ for safety. */
722
+ private resolveConfigPath(configPath: string): string | null {
723
+ const home = homedir();
724
+ const openclawDir = `${home}/.openclaw`;
725
+ // Normalize: replace ~ with home dir
726
+ const resolved = configPath.startsWith("~")
727
+ ? configPath.replace(/^~/, home)
728
+ : configPath;
729
+ // Security: only allow files under ~/.openclaw/
730
+ if (!resolved.startsWith(openclawDir)) return null;
731
+ // Prevent path traversal
732
+ if (resolved.includes("..")) return null;
733
+ return resolved;
734
+ }
735
+
736
+ /** GET /api/config/read?path=<configPath> — read a local config file. */
737
+ private async handleConfigRead(req: IncomingMessage, res: ServerResponse) {
738
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
739
+ const configPath = url.searchParams.get("path");
740
+ if (!configPath) {
741
+ res.writeHead(400, { "Content-Type": "application/json" });
742
+ res.end(JSON.stringify({ error: "Missing required query param: path" }));
743
+ return;
744
+ }
745
+
746
+ const resolved = this.resolveConfigPath(configPath);
747
+ if (!resolved) {
748
+ res.writeHead(403, { "Content-Type": "application/json" });
749
+ res.end(JSON.stringify({ error: "Path must be under ~/.openclaw/" }));
750
+ return;
751
+ }
752
+
753
+ try {
754
+ const content = await readFile(resolved, "utf-8");
755
+ res.writeHead(200, { "Content-Type": "application/json" });
756
+ res.end(JSON.stringify({ success: true, content }));
757
+ } catch (err: unknown) {
758
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
759
+ res.writeHead(200, { "Content-Type": "application/json" });
760
+ res.end(JSON.stringify({ success: true, content: "{}" }));
761
+ } else {
762
+ res.writeHead(500, { "Content-Type": "application/json" });
763
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Failed to read config" }));
764
+ }
765
+ }
766
+ }
767
+
768
+ /** POST /api/config/write — write a local config file. Body: { path, content } */
769
+ private async handleConfigWrite(req: IncomingMessage, res: ServerResponse) {
770
+ try {
771
+ const body = await readBody(req);
772
+ const { path: configPath, content } = JSON.parse(body);
773
+ if (!configPath || typeof content !== "string") {
774
+ res.writeHead(400, { "Content-Type": "application/json" });
775
+ res.end(JSON.stringify({ error: "path and content required" }));
776
+ return;
777
+ }
778
+
779
+ const resolved = this.resolveConfigPath(configPath);
780
+ if (!resolved) {
781
+ res.writeHead(403, { "Content-Type": "application/json" });
782
+ res.end(JSON.stringify({ error: "Path must be under ~/.openclaw/" }));
783
+ return;
784
+ }
785
+
786
+ // Ensure directory exists
787
+ await mkdir(dirname(resolved), { recursive: true });
788
+ await writeFile(resolved, content, "utf-8");
789
+ res.writeHead(200, { "Content-Type": "application/json" });
790
+ res.end(JSON.stringify({ success: true }));
791
+ } catch (err) {
792
+ res.writeHead(500, { "Content-Type": "application/json" });
793
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Failed to write config" }));
794
+ }
795
+ }
796
+
797
+ /** GET /api/tools — list cluster tools with descriptions and input schemas. */
798
+ private handleToolsList(res: ServerResponse) {
799
+ // Build per-node tool info: local node tools + remote peer tools
800
+ const nodes: Array<{ nodeId: string; tools: typeof CLUSTER_TOOL_DEFS }> = [];
801
+
802
+ // Local node
803
+ nodes.push({ nodeId: this.config.nodeId, tools: CLUSTER_TOOL_DEFS });
804
+
805
+ // Remote peers — include cluster tools for each online peer with toolProxy
806
+ const peers = this.peerManager.router.getAllPeers();
807
+ for (const p of peers) {
808
+ if (!this.peerManager.canReach(p.nodeId)) continue;
809
+ if (!p.toolProxy?.enabled) continue;
810
+ nodes.push({ nodeId: p.nodeId, tools: CLUSTER_TOOL_DEFS });
811
+ }
812
+
438
813
  res.writeHead(200, { "Content-Type": "application/json" });
439
- res.end(JSON.stringify(result));
814
+ res.end(JSON.stringify({ nodes }));
440
815
  }
441
816
 
442
817
  private handleStatus(res: ServerResponse) {
@@ -1251,8 +1626,9 @@ export class ApiHandler {
1251
1626
  return;
1252
1627
  }
1253
1628
  const files = this.knowledgeSync.listSyncedFiles();
1629
+ const workspacePath = this.knowledgeSync.workspacePath;
1254
1630
  res.writeHead(200, { "Content-Type": "application/json" });
1255
- res.end(JSON.stringify({ success: true, files }));
1631
+ res.end(JSON.stringify({ success: true, files, workspacePath }));
1256
1632
  }
1257
1633
 
1258
1634
  /** GET /api/knowledge/history?path=<relPath> — file change history from CRDT. */
@@ -1292,6 +1668,37 @@ export class ApiHandler {
1292
1668
  res.end(JSON.stringify({ success: true, path: filePath, history }));
1293
1669
  }
1294
1670
 
1671
+ /** GET /api/knowledge/content?path=<relPath> — read synced file content from CRDT. */
1672
+ private handleKnowledgeContent(req: IncomingMessage, res: ServerResponse) {
1673
+ if (!this.knowledgeSync) {
1674
+ res.writeHead(503, { "Content-Type": "application/json" });
1675
+ res.end(JSON.stringify({ error: "Knowledge sync not enabled" }));
1676
+ return;
1677
+ }
1678
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1679
+ const filePath = url.searchParams.get("path");
1680
+ if (!filePath) {
1681
+ res.writeHead(400, { "Content-Type": "application/json" });
1682
+ res.end(JSON.stringify({ error: "Missing required query param: path" }));
1683
+ return;
1684
+ }
1685
+ const content = this.knowledgeSync.getFileContent(filePath);
1686
+ if (content === null) {
1687
+ // Distinguish between "not in registry" and "in registry but content not yet synced"
1688
+ const listed = this.knowledgeSync.listSyncedFiles().find(f => f.path === filePath);
1689
+ if (listed) {
1690
+ res.writeHead(202, { "Content-Type": "application/json" });
1691
+ res.end(JSON.stringify({ error: "File content not yet synced", syncing: true }));
1692
+ } else {
1693
+ res.writeHead(404, { "Content-Type": "application/json" });
1694
+ res.end(JSON.stringify({ error: "File not found" }));
1695
+ }
1696
+ return;
1697
+ }
1698
+ res.writeHead(200, { "Content-Type": "application/json" });
1699
+ res.end(JSON.stringify({ success: true, path: filePath, content }));
1700
+ }
1701
+
1295
1702
  // ── Board / Kanban API handlers ─────────────────────────────────
1296
1703
 
1297
1704
  private handleBoardSummary(res: ServerResponse) {