clawmatrix 0.6.1 → 0.6.2
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/skills/clawmatrix/SKILL.md +86 -42
- package/package.json +1 -1
- package/src/connection.ts +27 -29
- package/src/index.ts +0 -285
- package/src/knowledge-sync.ts +7 -1
- package/src/peer-manager.ts +9 -3
- package/src/sentinel.ts +1 -1
|
@@ -3,58 +3,102 @@ 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. You can invoke any tool available on remote nodes — including getting device location, battery status, running shell commands, reading/writing files, and more.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
All commands output LLM-friendly text (no ANSI colors) when stdout is not a TTY.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Quick Start
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Approve/deny/revoke peer connections
|
|
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
|
|
16
15
|
|
|
17
|
-
|
|
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.
|
|
16
|
+
## Cluster Status
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
```
|
|
24
23
|
|
|
25
|
-
|
|
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
|
|
24
|
+
Always start with `clawmatrix status` to understand the current topology before performing other operations.
|
|
29
25
|
|
|
30
|
-
|
|
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
|
|
26
|
+
## Remote Tools
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
```
|
|
39
34
|
|
|
40
|
-
|
|
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
|
|
35
|
+
Use `--describe` to understand a tool's parameters before calling it.
|
|
43
36
|
|
|
44
|
-
|
|
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`
|
|
37
|
+
## Invoke Tools
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
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
|
+
```
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
```bash
|
|
45
|
+
clawmatrix batch <nodeId> '[{"tool":"t1","params":{}},{"tool":"t2","params":{}}]'
|
|
46
|
+
clawmatrix batch <nodeId> --no-stop-on-error '[...]' # Continue on failure
|
|
47
|
+
```
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
package/package.json
CHANGED
package/src/connection.ts
CHANGED
|
@@ -147,7 +147,17 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
147
147
|
|
|
148
148
|
/** Bind standard WebSocket event listeners. Call this for outbound connections. */
|
|
149
149
|
bindWebSocket(ws: WebSocket) {
|
|
150
|
-
|
|
150
|
+
// Node.js 22+ built-in WebSocket defaults binaryType to "blob", which
|
|
151
|
+
// onRawMessage cannot handle (Blob is async-only). Set "arraybuffer"
|
|
152
|
+
// so binary frames arrive as ArrayBuffer, then normalize to Buffer below.
|
|
153
|
+
if ("binaryType" in ws) {
|
|
154
|
+
(ws as any).binaryType = "arraybuffer";
|
|
155
|
+
}
|
|
156
|
+
ws.addEventListener("message", (ev) => {
|
|
157
|
+
// Normalize ArrayBuffer → Buffer for consistent handling across runtimes
|
|
158
|
+
const data = ev.data instanceof ArrayBuffer ? Buffer.from(ev.data) : ev.data;
|
|
159
|
+
this.onRawMessage(data);
|
|
160
|
+
});
|
|
151
161
|
ws.addEventListener("close", (ev) => {
|
|
152
162
|
this.close(ev.code, ev.reason);
|
|
153
163
|
});
|
|
@@ -208,11 +218,19 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
208
218
|
|
|
209
219
|
/** Send raw data. Buffers sent as binary frames; strings as-is; objects JSON-encoded. */
|
|
210
220
|
private sendRaw(data: unknown) {
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
try {
|
|
222
|
+
if (this.transport.readyState === WebSocket.OPEN) {
|
|
223
|
+
if (Buffer.isBuffer(data)) {
|
|
224
|
+
this.transport.send(data);
|
|
225
|
+
} else {
|
|
226
|
+
this.transport.send(typeof data === "string" ? data : JSON.stringify(data));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
debug("send", `sendRaw failed for ${this.remoteNodeId ?? "unknown"}: ${err}`);
|
|
231
|
+
// Transport is broken — schedule close on next tick to avoid re-entrancy
|
|
232
|
+
if (!this.closed) {
|
|
233
|
+
queueMicrotask(() => this.close(4002, "send failed"));
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
}
|
|
@@ -269,25 +287,6 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
269
287
|
private async onRawMessage(data: unknown) {
|
|
270
288
|
this.lastReceivedAt = Date.now();
|
|
271
289
|
|
|
272
|
-
// Debug: log data type for unauthenticated connections to diagnose auth issues
|
|
273
|
-
if (!this.authenticated && this.role === "outbound") {
|
|
274
|
-
const dtype = Buffer.isBuffer(data) ? `Buffer(${(data as Buffer).length})` :
|
|
275
|
-
data instanceof ArrayBuffer ? `ArrayBuffer(${data.byteLength})` :
|
|
276
|
-
typeof data === "string" ? `string(${data.length})` :
|
|
277
|
-
`${data?.constructor?.name ?? typeof data}`;
|
|
278
|
-
debug("auth", `onRawMessage[outbound] dataType=${dtype}`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Normalize non-Buffer binary types to Buffer.
|
|
282
|
-
// Node.js 24+'s built-in WebSocket (undici) delivers binary frames as Blob
|
|
283
|
-
// (binaryType "blob") or ArrayBuffer (binaryType "arraybuffer").
|
|
284
|
-
// The ws package delivers Buffer (binaryType "nodebuffer").
|
|
285
|
-
if (data instanceof ArrayBuffer) {
|
|
286
|
-
data = Buffer.from(data);
|
|
287
|
-
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
288
|
-
data = Buffer.from(await data.arrayBuffer());
|
|
289
|
-
}
|
|
290
|
-
|
|
291
290
|
let frame: AnyClusterFrame | undefined;
|
|
292
291
|
|
|
293
292
|
// Binary frame (Buffer) — decrypt directly without base64
|
|
@@ -403,7 +402,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
403
402
|
}
|
|
404
403
|
|
|
405
404
|
if (frame.type === "ping") {
|
|
406
|
-
this.
|
|
405
|
+
this.sendDirect({ type: "pong", from: this.nodeId, timestamp: Date.now() } as AnyClusterFrame);
|
|
407
406
|
return;
|
|
408
407
|
}
|
|
409
408
|
if (frame.type === "pong") {
|
|
@@ -579,7 +578,6 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
579
578
|
}
|
|
580
579
|
|
|
581
580
|
// auth_ok (decrypted from binary envelope, or plaintext legacy)
|
|
582
|
-
debug("auth", `Outbound received frame type=${frame.type} (authenticated=${this.authenticated})`);
|
|
583
581
|
if (frame.type === "auth_ok") {
|
|
584
582
|
const ok = frame as AuthOk;
|
|
585
583
|
this.remoteNodeId = ok.payload.nodeId;
|
|
@@ -718,7 +716,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
718
716
|
return;
|
|
719
717
|
}
|
|
720
718
|
this.lastPingSentAt = Date.now();
|
|
721
|
-
this.
|
|
719
|
+
this.sendDirect({
|
|
722
720
|
type: "ping",
|
|
723
721
|
from: this.nodeId,
|
|
724
722
|
timestamp: this.lastPingSentAt,
|
|
@@ -755,7 +753,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
755
753
|
close(code = 1000, reason = "normal") {
|
|
756
754
|
if (this.closed) return;
|
|
757
755
|
// Flush any pending batch before closing
|
|
758
|
-
this.flushBatch();
|
|
756
|
+
try { this.flushBatch(); } catch { /* transport may already be dead */ }
|
|
759
757
|
this.closed = true;
|
|
760
758
|
this.clearAuthTimer();
|
|
761
759
|
if (this.heartbeatTimer) {
|
package/src/index.ts
CHANGED
|
@@ -1537,291 +1537,6 @@ const plugin = {
|
|
|
1537
1537
|
},
|
|
1538
1538
|
);
|
|
1539
1539
|
|
|
1540
|
-
// ── Availability gateway method ──────────────────────────────────
|
|
1541
|
-
|
|
1542
|
-
api.registerGatewayMethod(
|
|
1543
|
-
"clawmatrix.availability",
|
|
1544
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1545
|
-
try {
|
|
1546
|
-
const runtime = getClusterRuntime();
|
|
1547
|
-
const { range } = (params ?? {}) as { range?: string };
|
|
1548
|
-
const validRanges = ["24h", "7d", "90d"];
|
|
1549
|
-
const r = validRanges.includes(range ?? "") ? (range as "24h" | "7d" | "90d") : "24h";
|
|
1550
|
-
const result = runtime.healthTracker.getAvailability(r);
|
|
1551
|
-
respond(true, result);
|
|
1552
|
-
} catch {
|
|
1553
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1554
|
-
}
|
|
1555
|
-
},
|
|
1556
|
-
);
|
|
1557
|
-
|
|
1558
|
-
// ── Automation gateway methods ──────────────────────────────────
|
|
1559
|
-
|
|
1560
|
-
api.registerGatewayMethod(
|
|
1561
|
-
"clawmatrix.automations.rules",
|
|
1562
|
-
({ respond }: GatewayRequestHandlerOptions) => {
|
|
1563
|
-
try {
|
|
1564
|
-
const runtime = getClusterRuntime();
|
|
1565
|
-
if (!runtime.automationManager) {
|
|
1566
|
-
respond(true, { rules: [] });
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
respond(true, { rules: runtime.automationManager.getRules() });
|
|
1570
|
-
} catch {
|
|
1571
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1572
|
-
}
|
|
1573
|
-
},
|
|
1574
|
-
);
|
|
1575
|
-
|
|
1576
|
-
api.registerGatewayMethod(
|
|
1577
|
-
"clawmatrix.automations.save",
|
|
1578
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1579
|
-
try {
|
|
1580
|
-
const runtime = getClusterRuntime();
|
|
1581
|
-
if (!runtime.automationManager) {
|
|
1582
|
-
respond(false, { error: "Automations not available" });
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
const { rules } = (params ?? {}) as { rules?: unknown[] };
|
|
1586
|
-
if (!Array.isArray(rules)) {
|
|
1587
|
-
respond(false, { error: "Missing required param: rules (array)" });
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
await runtime.automationManager.saveRules(rules as Parameters<typeof runtime.automationManager.saveRules>[0]);
|
|
1591
|
-
respond(true, { ok: true, count: rules.length });
|
|
1592
|
-
} catch (err) {
|
|
1593
|
-
respond(false, { error: String(err) });
|
|
1594
|
-
}
|
|
1595
|
-
},
|
|
1596
|
-
);
|
|
1597
|
-
|
|
1598
|
-
api.registerGatewayMethod(
|
|
1599
|
-
"clawmatrix.automations.history",
|
|
1600
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1601
|
-
try {
|
|
1602
|
-
const runtime = getClusterRuntime();
|
|
1603
|
-
if (!runtime.automationManager) {
|
|
1604
|
-
respond(true, { executions: [] });
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
const { limit } = (params ?? {}) as { limit?: number };
|
|
1608
|
-
respond(true, { executions: runtime.automationManager.getExecutions(limit ?? 50) });
|
|
1609
|
-
} catch {
|
|
1610
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1611
|
-
}
|
|
1612
|
-
},
|
|
1613
|
-
);
|
|
1614
|
-
|
|
1615
|
-
api.registerGatewayMethod(
|
|
1616
|
-
"clawmatrix.automations.run",
|
|
1617
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1618
|
-
try {
|
|
1619
|
-
const runtime = getClusterRuntime();
|
|
1620
|
-
if (!runtime.automationManager) {
|
|
1621
|
-
respond(false, { error: "Automations not available" });
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
const { ruleId, event } = (params ?? {}) as { ruleId?: string; event?: Record<string, unknown> };
|
|
1625
|
-
if (!ruleId) {
|
|
1626
|
-
respond(false, { error: "Missing required param: ruleId" });
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
const evt = event ? {
|
|
1630
|
-
id: nanoid(),
|
|
1631
|
-
source: String(event.source || "cli"),
|
|
1632
|
-
type: String(event.type || "manual"),
|
|
1633
|
-
data: (event.data ?? {}) as Record<string, unknown>,
|
|
1634
|
-
ts: typeof event.ts === "number" ? event.ts : Date.now(),
|
|
1635
|
-
consumed: false,
|
|
1636
|
-
} as import("./types.ts").IngestedEvent : undefined;
|
|
1637
|
-
const execution = await runtime.automationManager.runRuleById(ruleId, evt);
|
|
1638
|
-
respond(true, { ok: true, execution });
|
|
1639
|
-
} catch (err) {
|
|
1640
|
-
respond(false, { error: String(err) });
|
|
1641
|
-
}
|
|
1642
|
-
},
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
api.registerGatewayMethod(
|
|
1646
|
-
"clawmatrix.automations.replay",
|
|
1647
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1648
|
-
try {
|
|
1649
|
-
const runtime = getClusterRuntime();
|
|
1650
|
-
if (!runtime.automationManager) {
|
|
1651
|
-
respond(false, { error: "Automations not available" });
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
1654
|
-
const { executionId } = (params ?? {}) as { executionId?: string };
|
|
1655
|
-
if (!executionId) {
|
|
1656
|
-
respond(false, { error: "Missing required param: executionId" });
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
const execution = await runtime.automationManager.replayExecution(executionId);
|
|
1660
|
-
respond(true, { ok: true, execution });
|
|
1661
|
-
} catch (err) {
|
|
1662
|
-
respond(false, { error: String(err) });
|
|
1663
|
-
}
|
|
1664
|
-
},
|
|
1665
|
-
);
|
|
1666
|
-
|
|
1667
|
-
// ── Events ingest gateway method ──────────────────────────────────
|
|
1668
|
-
|
|
1669
|
-
api.registerGatewayMethod(
|
|
1670
|
-
"clawmatrix.events.ingest",
|
|
1671
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1672
|
-
try {
|
|
1673
|
-
const runtime = getClusterRuntime();
|
|
1674
|
-
if (!runtime.apiHandler) {
|
|
1675
|
-
respond(false, { error: "API handler not available (listen mode required)" });
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
const { events } = (params ?? {}) as { events?: unknown[] };
|
|
1679
|
-
if (!Array.isArray(events) || events.length === 0) {
|
|
1680
|
-
respond(false, { error: "Missing required param: events (non-empty array)" });
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
const result = runtime.apiHandler.ingestEvents(events as Array<Record<string, unknown>>);
|
|
1684
|
-
respond(true, { ok: true, ...result });
|
|
1685
|
-
} catch {
|
|
1686
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1687
|
-
}
|
|
1688
|
-
},
|
|
1689
|
-
);
|
|
1690
|
-
|
|
1691
|
-
// ── Board update gateway method ──────────────────────────────────
|
|
1692
|
-
|
|
1693
|
-
api.registerGatewayMethod(
|
|
1694
|
-
"clawmatrix.board.update",
|
|
1695
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1696
|
-
try {
|
|
1697
|
-
const runtime = getClusterRuntime();
|
|
1698
|
-
if (!runtime.kanbanManager) {
|
|
1699
|
-
respond(false, { error: "Kanban not enabled" });
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
const { cardId, ...updates } = (params ?? {}) as {
|
|
1703
|
-
cardId?: string; title?: string; description?: string;
|
|
1704
|
-
priority?: string; targetNode?: string; targetAgent?: string;
|
|
1705
|
-
cwd?: string; labels?: string[];
|
|
1706
|
-
};
|
|
1707
|
-
if (!cardId) {
|
|
1708
|
-
respond(false, { error: "Missing required param: cardId" });
|
|
1709
|
-
return;
|
|
1710
|
-
}
|
|
1711
|
-
const card = runtime.kanbanManager.updateCard(cardId, updates as Parameters<typeof runtime.kanbanManager.updateCard>[1]);
|
|
1712
|
-
if (!card) {
|
|
1713
|
-
respond(false, { error: `Card not found: ${cardId}` });
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
respond(true, card);
|
|
1717
|
-
} catch {
|
|
1718
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1719
|
-
}
|
|
1720
|
-
},
|
|
1721
|
-
);
|
|
1722
|
-
|
|
1723
|
-
// ── Config gateway method ──────────────────────────────────────────
|
|
1724
|
-
|
|
1725
|
-
api.registerGatewayMethod(
|
|
1726
|
-
"clawmatrix.config.get",
|
|
1727
|
-
({ respond }: GatewayRequestHandlerOptions) => {
|
|
1728
|
-
try {
|
|
1729
|
-
const runtime = getClusterRuntime();
|
|
1730
|
-
const c = runtime.config;
|
|
1731
|
-
respond(true, {
|
|
1732
|
-
nodeId: c.nodeId,
|
|
1733
|
-
listen: c.listen,
|
|
1734
|
-
tags: c.tags,
|
|
1735
|
-
agents: c.agents.map((a) => ({ id: a.id, model: a.model })),
|
|
1736
|
-
models: c.models.map((m) => ({ id: m.id, provider: m.provider })),
|
|
1737
|
-
e2ee: c.e2ee,
|
|
1738
|
-
toolProxy: c.toolProxy ? { enabled: c.toolProxy.enabled, allow: c.toolProxy.allow, deny: c.toolProxy.deny } : undefined,
|
|
1739
|
-
terminal: c.terminal,
|
|
1740
|
-
acp: c.acp ? { enabled: c.acp.enabled } : undefined,
|
|
1741
|
-
knowledge: c.knowledge ? { enabled: c.knowledge.enabled } : undefined,
|
|
1742
|
-
proxyModels: c.proxyModels?.length ?? 0,
|
|
1743
|
-
peers: c.peers?.length ?? 0,
|
|
1744
|
-
});
|
|
1745
|
-
} catch {
|
|
1746
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1747
|
-
}
|
|
1748
|
-
},
|
|
1749
|
-
);
|
|
1750
|
-
|
|
1751
|
-
// ── Config file read/write gateway methods ──────────────────────
|
|
1752
|
-
|
|
1753
|
-
api.registerGatewayMethod(
|
|
1754
|
-
"clawmatrix.config.read",
|
|
1755
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1756
|
-
try {
|
|
1757
|
-
const runtime = getClusterRuntime();
|
|
1758
|
-
if (!runtime.apiHandler) {
|
|
1759
|
-
respond(false, { error: "API handler not available" });
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const { path: configPath } = (params ?? {}) as { path?: string };
|
|
1763
|
-
if (!configPath) {
|
|
1764
|
-
respond(false, { error: "Missing required param: path" });
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const result = await runtime.apiHandler.readConfigFile(configPath);
|
|
1768
|
-
respond(result.success, result);
|
|
1769
|
-
} catch (err) {
|
|
1770
|
-
respond(false, { error: String(err) });
|
|
1771
|
-
}
|
|
1772
|
-
},
|
|
1773
|
-
);
|
|
1774
|
-
|
|
1775
|
-
api.registerGatewayMethod(
|
|
1776
|
-
"clawmatrix.config.write",
|
|
1777
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1778
|
-
try {
|
|
1779
|
-
const runtime = getClusterRuntime();
|
|
1780
|
-
if (!runtime.apiHandler) {
|
|
1781
|
-
respond(false, { error: "API handler not available" });
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
const { path: configPath, content } = (params ?? {}) as { path?: string; content?: string };
|
|
1785
|
-
if (!configPath || typeof content !== "string") {
|
|
1786
|
-
respond(false, { error: "Missing required params: path, content" });
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
const result = await runtime.apiHandler.writeConfigFile(configPath, content);
|
|
1790
|
-
respond(result.success, result);
|
|
1791
|
-
} catch (err) {
|
|
1792
|
-
respond(false, { error: String(err) });
|
|
1793
|
-
}
|
|
1794
|
-
},
|
|
1795
|
-
);
|
|
1796
|
-
|
|
1797
|
-
// ── Knowledge content gateway method ──────────────────────────────
|
|
1798
|
-
|
|
1799
|
-
api.registerGatewayMethod(
|
|
1800
|
-
"clawmatrix.kb.content",
|
|
1801
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1802
|
-
try {
|
|
1803
|
-
const runtime = getClusterRuntime();
|
|
1804
|
-
if (!runtime.knowledgeSync) {
|
|
1805
|
-
respond(false, { error: "Knowledge sync not enabled" });
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
const { path } = (params ?? {}) as { path?: string };
|
|
1809
|
-
if (!path) {
|
|
1810
|
-
respond(false, { error: "Missing required param: path" });
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
const content = runtime.knowledgeSync.getFileContent(path);
|
|
1814
|
-
if (content === null) {
|
|
1815
|
-
respond(false, { error: "File not found or content not yet synced" });
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
respond(true, { path, content });
|
|
1819
|
-
} catch {
|
|
1820
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1821
|
-
}
|
|
1822
|
-
},
|
|
1823
|
-
);
|
|
1824
|
-
|
|
1825
1540
|
// Log model selection on each LLM call (fire-and-forget)
|
|
1826
1541
|
api.on("llm_input", (event) => {
|
|
1827
1542
|
api.logger.debug(`[clawmatrix] llm_input: provider=${event.provider} model=${event.model}`);
|
package/src/knowledge-sync.ts
CHANGED
|
@@ -191,6 +191,11 @@ export class KnowledgeSync {
|
|
|
191
191
|
|
|
192
192
|
// ── Public API ─────────────────────────────────────────────────
|
|
193
193
|
|
|
194
|
+
/** The resolved workspace directory that knowledge files are synced under. */
|
|
195
|
+
get workspacePath(): string {
|
|
196
|
+
return this.opts.workspacePath;
|
|
197
|
+
}
|
|
198
|
+
|
|
194
199
|
async start() {
|
|
195
200
|
debug(TAG, `starting knowledge sync: workspace=${this.opts.workspacePath}`);
|
|
196
201
|
|
|
@@ -536,7 +541,7 @@ export class KnowledgeSync {
|
|
|
536
541
|
}
|
|
537
542
|
|
|
538
543
|
/** List all synced files with metadata. */
|
|
539
|
-
listSyncedFiles(): Array<{ path: string; version: number; updatedAt: number; deleted: boolean }> {
|
|
544
|
+
listSyncedFiles(): Array<{ path: string; version: number; updatedAt: number; deleted: boolean; synced: boolean }> {
|
|
540
545
|
const files = this.registry.files;
|
|
541
546
|
if (!files) return [];
|
|
542
547
|
return Object.entries(files).map(([path, meta]) => ({
|
|
@@ -544,6 +549,7 @@ export class KnowledgeSync {
|
|
|
544
549
|
version: meta.version,
|
|
545
550
|
updatedAt: meta.updatedAt,
|
|
546
551
|
deleted: meta.deleted,
|
|
552
|
+
synced: this.fileDocs.has(path) && this.fileDocs.get(path)!.content !== undefined,
|
|
547
553
|
})).filter(f => !f.deleted);
|
|
548
554
|
}
|
|
549
555
|
|
package/src/peer-manager.ts
CHANGED
|
@@ -719,9 +719,15 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
719
719
|
debug("peer", `connectToChannel(${nodeId}): self-connection, will not reconnect`);
|
|
720
720
|
return;
|
|
721
721
|
}
|
|
722
|
-
// Don't reconnect
|
|
723
|
-
|
|
724
|
-
|
|
722
|
+
// Don't reconnect deliberate closures that shouldn't trigger reconnection
|
|
723
|
+
const skipReasons = ["warm-up pruned", "route switch", "replaced by new connection"];
|
|
724
|
+
if (skipReasons.includes(ev.reason)) {
|
|
725
|
+
debug("peer", `connectToChannel(${nodeId}): ${ev.reason}, will not reconnect`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
// Don't reconnect when peer approval was denied — retrying is futile
|
|
729
|
+
if (ev.code === 4005) {
|
|
730
|
+
debug("peer", `connectToChannel(${nodeId}): approval denied, will not reconnect`);
|
|
725
731
|
return;
|
|
726
732
|
}
|
|
727
733
|
// Record close code for adaptive backoff
|
package/src/sentinel.ts
CHANGED
|
@@ -177,7 +177,7 @@ function buildCapabilities(): NodeCapabilities {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
function connectToPeer(peer: { nodeId: string; url: string }) {
|
|
180
|
-
const ws = new
|
|
180
|
+
const ws = new WebSocket(peer.url, ["graphql-transport-ws"]);
|
|
181
181
|
const e2eeOpts: ConnectionE2eeOptions = {
|
|
182
182
|
e2ee: config.e2ee,
|
|
183
183
|
compression: config.compression,
|