clawmatrix 0.4.2 → 0.5.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.
@@ -8,7 +8,7 @@ export function createClusterEventsTool(): AnyAgentTool {
8
8
  description:
9
9
  "Query and consume events from external sources (iOS Shortcuts, webhooks, etc.). " +
10
10
  "Common types: message_received, location_change, battery_status. " +
11
- "Events are persistent until consumed. Requires web.enabled in config.",
11
+ "Events are persistent until consumed. Requires listen mode (listen: true).",
12
12
  parameters: {
13
13
  type: "object",
14
14
  properties: {
@@ -58,10 +58,10 @@ export function createClusterEventsTool(): AnyAgentTool {
58
58
 
59
59
  try {
60
60
  const runtime = getClusterRuntime();
61
- const webHandler = runtime.webHandler;
62
- if (!webHandler) {
61
+ const apiHandler = runtime.apiHandler;
62
+ if (!apiHandler) {
63
63
  return {
64
- content: [{ type: "text" as const, text: "Event ingestion requires web dashboard to be enabled (web.enabled = true)" }],
64
+ content: [{ type: "text" as const, text: "Event ingestion requires listen mode (listen: true in config)" }],
65
65
  details: { error: true },
66
66
  };
67
67
  }
@@ -73,7 +73,7 @@ export function createClusterEventsTool(): AnyAgentTool {
73
73
  details: { error: true },
74
74
  };
75
75
  }
76
- const consumed = webHandler.consumeEvents(ids);
76
+ const consumed = apiHandler.consumeEvents(ids);
77
77
  return {
78
78
  content: [{ type: "text" as const, text: `Consumed ${consumed} event(s)` }],
79
79
  details: { consumed },
@@ -81,7 +81,7 @@ export function createClusterEventsTool(): AnyAgentTool {
81
81
  }
82
82
 
83
83
  // action === "query"
84
- const events = webHandler.queryEvents({
84
+ const events = apiHandler.queryEvents({
85
85
  type,
86
86
  source,
87
87
  since,
@@ -0,0 +1,345 @@
1
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
2
+ import { getClusterRuntime } from "../cluster-service.ts";
3
+ import type { CardStage, CardPriority, CardAnnotation } from "../types.ts";
4
+
5
+ export function createClusterKanbanTool(): AnyAgentTool {
6
+ return {
7
+ name: "cluster_kanban",
8
+ label: "Cluster Kanban Board",
9
+ description:
10
+ "Manage the distributed kanban board for tracking work items across the cluster. " +
11
+ "Actions: " +
12
+ '"create" — create a new card. ' +
13
+ '"list" — list cards (optionally filtered by stage, label, node, priority). ' +
14
+ '"get" — get card details by ID. ' +
15
+ '"claim" — claim a backlog card for execution. ' +
16
+ '"move" — move a card to a different stage. ' +
17
+ '"annotate" — add a note, progress update, artifact link, or error to a card. ' +
18
+ '"update" — update card fields (title, description, priority, labels, etc.). ' +
19
+ '"delete" — delete a card. ' +
20
+ '"summary" — get board summary with counts by stage and priority.',
21
+ parameters: {
22
+ type: "object",
23
+ properties: {
24
+ action: {
25
+ type: "string",
26
+ enum: ["create", "list", "get", "claim", "move", "annotate", "update", "delete", "summary"],
27
+ description: 'Action to perform. Default: "list"',
28
+ },
29
+ cardId: {
30
+ type: "string",
31
+ description: "Card ID (e.g. CM-abc1-1) for get, claim, move, annotate, update, delete",
32
+ },
33
+ title: {
34
+ type: "string",
35
+ description: "Card title (for create or update)",
36
+ },
37
+ description: {
38
+ type: "string",
39
+ description: "Card description in Markdown (for create or update)",
40
+ },
41
+ stage: {
42
+ type: "string",
43
+ enum: ["backlog", "claimed", "in_progress", "review", "done", "archived"],
44
+ description: "Target stage (for move) or filter (for list)",
45
+ },
46
+ priority: {
47
+ type: "string",
48
+ enum: ["low", "medium", "high", "urgent"],
49
+ description: "Card priority (for create, update, or list filter)",
50
+ },
51
+ targetNode: {
52
+ type: "string",
53
+ description: "Preferred node ID for execution (for create or update)",
54
+ },
55
+ targetAgent: {
56
+ type: "string",
57
+ description: 'Preferred agent ID or "tags:<tag>" (for create or update)',
58
+ },
59
+ cwd: {
60
+ type: "string",
61
+ description: "Working directory for the task (for create or update)",
62
+ },
63
+ labels: {
64
+ type: "array",
65
+ items: { type: "string" },
66
+ description: "Labels for filtering (for create, update, or list filter)",
67
+ },
68
+ assignedNode: {
69
+ type: "string",
70
+ description: "Filter by assigned node (for list)",
71
+ },
72
+ agent: {
73
+ type: "string",
74
+ description: "Agent name (for claim or annotate)",
75
+ },
76
+ annotationType: {
77
+ type: "string",
78
+ enum: ["note", "session_link", "artifact", "progress", "error"],
79
+ description: "Type of annotation (for annotate action)",
80
+ },
81
+ content: {
82
+ type: "string",
83
+ description: "Annotation content (for annotate action)",
84
+ },
85
+ handoffId: {
86
+ type: "string",
87
+ description: "Associated handoff ID (for update)",
88
+ },
89
+ acpSessionId: {
90
+ type: "string",
91
+ description: "Associated ACP session ID (for update)",
92
+ },
93
+ },
94
+ required: [],
95
+ },
96
+ async execute(_toolCallId, params) {
97
+ const {
98
+ action = "list",
99
+ cardId,
100
+ title,
101
+ description,
102
+ stage,
103
+ priority,
104
+ targetNode,
105
+ targetAgent,
106
+ cwd,
107
+ labels,
108
+ assignedNode,
109
+ agent,
110
+ annotationType,
111
+ content,
112
+ handoffId,
113
+ acpSessionId,
114
+ } = params as {
115
+ action?: string;
116
+ cardId?: string;
117
+ title?: string;
118
+ description?: string;
119
+ stage?: CardStage;
120
+ priority?: CardPriority;
121
+ targetNode?: string;
122
+ targetAgent?: string;
123
+ cwd?: string;
124
+ labels?: string[];
125
+ assignedNode?: string;
126
+ agent?: string;
127
+ annotationType?: CardAnnotation["type"];
128
+ content?: string;
129
+ handoffId?: string;
130
+ acpSessionId?: string;
131
+ };
132
+
133
+ try {
134
+ const runtime = getClusterRuntime();
135
+ const km = runtime.kanbanManager;
136
+
137
+ if (!km) {
138
+ return {
139
+ content: [{ type: "text" as const, text: "Kanban board not available (kanban.enabled is false in config)" }],
140
+ details: { error: true },
141
+ };
142
+ }
143
+
144
+ if (action === "summary") {
145
+ const summary = km.getSummary();
146
+ return {
147
+ content: [{ type: "text" as const, text: JSON.stringify(summary) }],
148
+ details: summary,
149
+ };
150
+ }
151
+
152
+ if (action === "list") {
153
+ const cards = km.listCards({
154
+ stage,
155
+ label: labels?.[0],
156
+ assignedNode,
157
+ priority,
158
+ });
159
+ // Return compact list (no annotations to save tokens)
160
+ const compact = cards.map((c) => ({
161
+ id: c.id,
162
+ title: c.title,
163
+ stage: c.stage,
164
+ priority: c.priority,
165
+ assignedNode: c.assignedNode,
166
+ assignedAgent: c.assignedAgent,
167
+ labels: c.labels,
168
+ createdAt: c.createdAt,
169
+ updatedAt: c.updatedAt,
170
+ }));
171
+ return {
172
+ content: [{ type: "text" as const, text: JSON.stringify(compact) }],
173
+ details: compact,
174
+ };
175
+ }
176
+
177
+ if (action === "create") {
178
+ if (!title) {
179
+ return {
180
+ content: [{ type: "text" as const, text: "title is required for create action" }],
181
+ details: { error: true },
182
+ };
183
+ }
184
+ const card = km.createCard({
185
+ title,
186
+ description,
187
+ priority,
188
+ targetNode,
189
+ targetAgent,
190
+ cwd,
191
+ labels,
192
+ });
193
+ return {
194
+ content: [{ type: "text" as const, text: JSON.stringify(card) }],
195
+ details: card,
196
+ };
197
+ }
198
+
199
+ if (action === "get") {
200
+ if (!cardId) {
201
+ return {
202
+ content: [{ type: "text" as const, text: "cardId is required for get action" }],
203
+ details: { error: true },
204
+ };
205
+ }
206
+ const card = km.getCard(cardId);
207
+ if (!card) {
208
+ return {
209
+ content: [{ type: "text" as const, text: `Card not found: ${cardId}` }],
210
+ details: { error: true },
211
+ };
212
+ }
213
+ return {
214
+ content: [{ type: "text" as const, text: JSON.stringify(card) }],
215
+ details: card,
216
+ };
217
+ }
218
+
219
+ if (action === "claim") {
220
+ if (!cardId) {
221
+ return {
222
+ content: [{ type: "text" as const, text: "cardId is required for claim action" }],
223
+ details: { error: true },
224
+ };
225
+ }
226
+ const card = km.claimCard(
227
+ cardId,
228
+ runtime.config.nodeId,
229
+ agent ?? runtime.config.agents[0]?.id ?? "unknown",
230
+ );
231
+ if (!card) {
232
+ return {
233
+ content: [{ type: "text" as const, text: `Cannot claim card ${cardId} (not found or not in backlog)` }],
234
+ details: { error: true },
235
+ };
236
+ }
237
+ return {
238
+ content: [{ type: "text" as const, text: JSON.stringify(card) }],
239
+ details: card,
240
+ };
241
+ }
242
+
243
+ if (action === "move") {
244
+ if (!cardId || !stage) {
245
+ return {
246
+ content: [{ type: "text" as const, text: "cardId and stage are required for move action" }],
247
+ details: { error: true },
248
+ };
249
+ }
250
+ const card = km.moveCard(cardId, stage);
251
+ if (!card) {
252
+ return {
253
+ content: [{ type: "text" as const, text: `Cannot move card ${cardId} (not found or invalid stage)` }],
254
+ details: { error: true },
255
+ };
256
+ }
257
+ return {
258
+ content: [{ type: "text" as const, text: JSON.stringify(card) }],
259
+ details: card,
260
+ };
261
+ }
262
+
263
+ if (action === "annotate") {
264
+ if (!cardId || !content) {
265
+ return {
266
+ content: [{ type: "text" as const, text: "cardId and content are required for annotate action" }],
267
+ details: { error: true },
268
+ };
269
+ }
270
+ const card = km.annotateCard(cardId, {
271
+ nodeId: runtime.config.nodeId,
272
+ agent: agent ?? runtime.config.agents[0]?.id ?? "unknown",
273
+ type: annotationType ?? "note",
274
+ content,
275
+ });
276
+ if (!card) {
277
+ return {
278
+ content: [{ type: "text" as const, text: `Card not found: ${cardId}` }],
279
+ details: { error: true },
280
+ };
281
+ }
282
+ return {
283
+ content: [{ type: "text" as const, text: JSON.stringify(card) }],
284
+ details: card,
285
+ };
286
+ }
287
+
288
+ if (action === "update") {
289
+ if (!cardId) {
290
+ return {
291
+ content: [{ type: "text" as const, text: "cardId is required for update action" }],
292
+ details: { error: true },
293
+ };
294
+ }
295
+ const card = km.updateCard(cardId, {
296
+ title,
297
+ description,
298
+ priority,
299
+ targetNode,
300
+ targetAgent,
301
+ cwd,
302
+ labels,
303
+ handoffId,
304
+ acpSessionId,
305
+ });
306
+ if (!card) {
307
+ return {
308
+ content: [{ type: "text" as const, text: `Card not found: ${cardId}` }],
309
+ details: { error: true },
310
+ };
311
+ }
312
+ return {
313
+ content: [{ type: "text" as const, text: JSON.stringify(card) }],
314
+ details: card,
315
+ };
316
+ }
317
+
318
+ if (action === "delete") {
319
+ if (!cardId) {
320
+ return {
321
+ content: [{ type: "text" as const, text: "cardId is required for delete action" }],
322
+ details: { error: true },
323
+ };
324
+ }
325
+ const ok = km.deleteCard(cardId);
326
+ const result = { deleted: ok, cardId };
327
+ return {
328
+ content: [{ type: "text" as const, text: JSON.stringify(result) }],
329
+ details: result,
330
+ };
331
+ }
332
+
333
+ return {
334
+ content: [{ type: "text" as const, text: `Unknown action: ${action}` }],
335
+ details: { error: true },
336
+ };
337
+ } catch (err) {
338
+ return {
339
+ content: [{ type: "text" as const, text: `Kanban error: ${err instanceof Error ? err.message : String(err)}` }],
340
+ details: { error: true },
341
+ };
342
+ }
343
+ },
344
+ };
345
+ }
@@ -82,7 +82,7 @@ export function createClusterPeersTool(): AnyAgentTool {
82
82
  }
83
83
 
84
84
  // Include satellite nodes (minimal fields — no agents/models)
85
- const satellites = runtime.webHandler?.getSatelliteContexts() ?? runtime.peerManager.satelliteContexts;
85
+ const satellites = runtime.apiHandler?.getSatelliteContexts() ?? runtime.peerManager.satelliteContexts;
86
86
  for (const sat of satellites) {
87
87
  if (Date.now() - sat.ts >= 600_000) continue;
88
88
  peers.push({
@@ -0,0 +1,145 @@
1
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
2
+ import { getClusterRuntime } from "../cluster-service.ts";
3
+
4
+ export function createClusterQueryTool(): AnyAgentTool {
5
+ return {
6
+ name: "cluster_query",
7
+ label: "Cluster Query",
8
+ description:
9
+ "Query replicated data from the local SQLite store: audit_log (security events), " +
10
+ "health_events (node lifecycle & peer connectivity), handoff_history (task execution records). " +
11
+ "Data is replicated across peers via log sync, so each node has a cluster-wide view.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ table: {
16
+ type: "string",
17
+ enum: ["audit_log", "health_events", "handoff_history"],
18
+ description: "Which replicated table to query",
19
+ },
20
+ since: {
21
+ type: "number",
22
+ description: "Events after this unix timestamp (ms). Shorthand: use negative values for relative offsets, e.g. -3600000 for last hour",
23
+ },
24
+ node_id: {
25
+ type: "string",
26
+ description: "Filter by originating node ID",
27
+ },
28
+ event: {
29
+ type: "string",
30
+ description: "Filter audit_log by event type (conn_open, auth_failure, peer_join, etc.)",
31
+ },
32
+ type: {
33
+ type: "string",
34
+ description: "Filter health_events by type (start, stop, peer_online, peer_offline)",
35
+ },
36
+ peer: {
37
+ type: "string",
38
+ description: "Filter health_events by peer node ID",
39
+ },
40
+ agent: {
41
+ type: "string",
42
+ description: "Filter handoff_history by agent name",
43
+ },
44
+ status: {
45
+ type: "string",
46
+ description: "Filter handoff_history by status (completed, failed)",
47
+ },
48
+ limit: {
49
+ type: "number",
50
+ description: "Max rows to return (default 50)",
51
+ },
52
+ },
53
+ required: ["table"],
54
+ },
55
+ async execute(_toolCallId, params) {
56
+ const {
57
+ table, since, node_id, event, type, peer, agent, status, limit,
58
+ } = params as {
59
+ table: "audit_log" | "health_events" | "handoff_history";
60
+ since?: number;
61
+ node_id?: string;
62
+ event?: string;
63
+ type?: string;
64
+ peer?: string;
65
+ agent?: string;
66
+ status?: string;
67
+ limit?: number;
68
+ };
69
+
70
+ try {
71
+ const runtime = getClusterRuntime();
72
+ const store = runtime.store;
73
+ if (!store) {
74
+ return {
75
+ content: [{ type: "text" as const, text: "Store not initialized" }],
76
+ details: { error: true },
77
+ };
78
+ }
79
+
80
+ // Handle relative since values (negative = offset from now)
81
+ const resolvedSince = since != null && since < 0 ? Date.now() + since : since;
82
+ const maxRows = Math.max(1, Math.min(limit ?? 50, 500));
83
+
84
+ let rows: unknown[];
85
+ let summary: string;
86
+
87
+ switch (table) {
88
+ case "audit_log": {
89
+ const result = store.queryAudit({
90
+ since: resolvedSince,
91
+ nodeId: node_id,
92
+ event,
93
+ limit: maxRows,
94
+ });
95
+ rows = result;
96
+ summary = `${result.length} audit event(s)`;
97
+ break;
98
+ }
99
+ case "health_events": {
100
+ const result = store.queryHealth({
101
+ since: resolvedSince,
102
+ nodeId: node_id,
103
+ type,
104
+ peer,
105
+ limit: maxRows,
106
+ });
107
+ rows = result;
108
+ summary = `${result.length} health event(s)`;
109
+ break;
110
+ }
111
+ case "handoff_history": {
112
+ const result = store.queryHandoff({
113
+ since: resolvedSince,
114
+ nodeId: node_id,
115
+ agent,
116
+ status,
117
+ limit: maxRows,
118
+ });
119
+ rows = result;
120
+ summary = `${result.length} handoff record(s)`;
121
+ break;
122
+ }
123
+ }
124
+
125
+ if (rows.length === 0) {
126
+ return {
127
+ content: [{ type: "text" as const, text: `No ${table} records found matching filters` }],
128
+ details: { count: 0, table },
129
+ };
130
+ }
131
+
132
+ const text = `${summary}\n${JSON.stringify(rows, null, 2)}`;
133
+ return {
134
+ content: [{ type: "text" as const, text }],
135
+ details: { count: rows.length, table },
136
+ };
137
+ } catch (err) {
138
+ return {
139
+ content: [{ type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
140
+ details: { error: true },
141
+ };
142
+ }
143
+ },
144
+ };
145
+ }
package/src/types.ts CHANGED
@@ -89,6 +89,14 @@ export interface PeerSync extends ClusterFrame {
89
89
  payload: {
90
90
  peers: PeerInfo[];
91
91
  satellites?: SatelliteContext[];
92
+ /** Sync version number for delta sync. */
93
+ version?: number;
94
+ /** Delta: newly added or changed peers since last sync. */
95
+ added?: PeerInfo[];
96
+ /** Delta: removed nodeIds since last sync. */
97
+ removed?: string[];
98
+ /** Delta: peers with updated capabilities. */
99
+ updated?: PeerInfo[];
92
100
  };
93
101
  }
94
102
 
@@ -173,12 +181,6 @@ export interface ImageContent {
173
181
  // ── Handoff ────────────────────────────────────────────────────────
174
182
  export type HandoffStatus = "working" | "input_required" | "completed" | "failed" | "canceled";
175
183
 
176
- export interface Artifact {
177
- name: string;
178
- mimeType: string;
179
- data: string; // text content or base64-encoded binary
180
- }
181
-
182
184
  export interface HandoffRequest extends ClusterFrame {
183
185
  type: "handoff_req";
184
186
  id: string;
@@ -197,7 +199,6 @@ export interface HandoffStreamChunk extends ClusterFrame {
197
199
  payload: {
198
200
  delta: string;
199
201
  done: boolean;
200
- artifacts?: Artifact[];
201
202
  sessionId?: string; // included for session watchers (multi-device sync)
202
203
  };
203
204
  }
@@ -211,7 +212,6 @@ export interface HandoffResponse extends ClusterFrame {
211
212
  agent?: string;
212
213
  result?: string;
213
214
  error?: string;
214
- artifacts?: Artifact[];
215
215
  inputRequired?: boolean;
216
216
  handoffId?: string;
217
217
  sessionId?: string; // For multi-turn conversation reuse
@@ -388,6 +388,8 @@ export interface DeviceInfo {
388
388
  totalMemoryMB: number; // total system memory in MB
389
389
  hostname: string; // machine hostname
390
390
  openclawVersion?: string; // e.g. "2026.3.7"
391
+ cwd?: string; // process.cwd() at gateway startup
392
+ workspace?: string; // OpenClaw workspace dir (agents.defaults.workspace)
391
393
  }
392
394
 
393
395
  // ── Shared info types ──────────────────────────────────────────────
@@ -955,6 +957,87 @@ export interface TerminalCloseResponse extends ClusterFrame {
955
957
  };
956
958
  }
957
959
 
960
+ // ── Kanban board ─────────────────────────────────────────────────
961
+
962
+ export type CardStage = "backlog" | "claimed" | "in_progress" | "review" | "done" | "archived";
963
+ export type CardPriority = "low" | "medium" | "high" | "urgent";
964
+
965
+ export interface CardAnnotation {
966
+ id: string;
967
+ nodeId: string;
968
+ agent: string;
969
+ type: "note" | "session_link" | "artifact" | "progress" | "error";
970
+ content: string;
971
+ ts: number;
972
+ }
973
+
974
+ export interface KanbanCard {
975
+ id: string;
976
+ title: string;
977
+ description: string;
978
+ stage: CardStage;
979
+ priority: CardPriority;
980
+ targetNode?: string;
981
+ targetAgent?: string;
982
+ cwd?: string;
983
+ assignedNode?: string;
984
+ assignedAgent?: string;
985
+ claimedAt?: number;
986
+ handoffId?: string;
987
+ acpSessionId?: string;
988
+ annotations: CardAnnotation[];
989
+ createdBy: string;
990
+ createdAt: number;
991
+ updatedAt: number;
992
+ completedAt?: number;
993
+ labels: string[];
994
+ }
995
+
996
+ export interface KanbanBoardDoc {
997
+ cards: Record<string, KanbanCard>;
998
+ nextSeq: number;
999
+ config: {
1000
+ prefix: string;
1001
+ autoAssign: boolean;
1002
+ stages: CardStage[];
1003
+ };
1004
+ }
1005
+
1006
+ export interface KanbanSyncFrame extends ClusterFrame {
1007
+ type: "kanban_sync";
1008
+ payload: { data: string };
1009
+ }
1010
+
1011
+ export interface KanbanNotifyFrame extends ClusterFrame {
1012
+ type: "kanban_notify";
1013
+ payload: {
1014
+ event: "card_created" | "card_claimed" | "card_stage_changed" | "card_completed" | "card_annotated";
1015
+ cardId: string;
1016
+ cardTitle: string;
1017
+ stage?: CardStage;
1018
+ nodeId?: string;
1019
+ agent?: string;
1020
+ };
1021
+ }
1022
+
1023
+ // ── Log replication ──────────────────────────────────────────────
1024
+
1025
+ export interface LogSyncFrame extends ClusterFrame {
1026
+ type: "log_sync";
1027
+ payload: {
1028
+ /** Which replicated table this sync concerns. */
1029
+ table: "audit_log" | "health_events" | "handoff_history";
1030
+ /** Sender's vector clock for this table: { nodeId: maxSourceSeq }. */
1031
+ vector?: Record<string, number>;
1032
+ /** Delta rows for the receiver to apply. */
1033
+ rows?: Array<Record<string, unknown>>;
1034
+ /** True when this is a sync request (receiver should compute and send delta). */
1035
+ request?: boolean;
1036
+ /** True when more batches follow (receiver should expect continuation). */
1037
+ hasMore?: boolean;
1038
+ };
1039
+ }
1040
+
958
1041
  // ── Union of all frame types ───────────────────────────────────────
959
1042
  export type AnyClusterFrame =
960
1043
  | AuthChallenge
@@ -1029,4 +1112,7 @@ export type AnyClusterFrame =
1029
1112
  | FileTransferAck
1030
1113
  | FileTransferChunk
1031
1114
  | FileTransferChunkAck
1032
- | FileTransferComplete;
1115
+ | FileTransferComplete
1116
+ | KanbanSyncFrame
1117
+ | KanbanNotifyFrame
1118
+ | LogSyncFrame;