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.
- package/cli/bin/clawmatrix.mjs +487 -12
- package/cli/skills/clawmatrix/SKILL.md +42 -86
- package/package.json +1 -1
- package/src/api.ts +410 -3
- package/src/automation.ts +90 -1
- package/src/cluster-service.ts +24 -9
- package/src/config.ts +22 -16
- package/src/connection.ts +10 -0
- package/src/device-info.ts +10 -0
- package/src/health-tracker.ts +91 -13
- package/src/index.ts +285 -0
- package/src/knowledge-sync.ts +7 -0
- package/src/peer-manager.ts +54 -23
- package/src/router.ts +21 -3
- package/src/types.ts +1 -0
|
@@ -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.
|
|
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
|
-
|
|
8
|
+
Run `clawmatrix --help` or `clawmatrix <command> --help` for detailed usage.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Capabilities
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```
|
|
48
|
+
**Config Management**
|
|
49
|
+
- View runtime config summary
|
|
50
|
+
- Read/write config files under `~/.openclaw/` (for remote node configuration)
|
|
43
51
|
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
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
|
-
|
|
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(
|
|
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) {
|