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.
package/src/index.ts CHANGED
@@ -18,6 +18,9 @@ import { createClusterTerminalTool } from "./tools/cluster-terminal.ts";
18
18
  import { createClusterToolInvokeTool } from "./tools/cluster-tool.ts";
19
19
  import { createClusterTransferTool } from "./tools/cluster-transfer.ts";
20
20
  import { createClusterNotifyTool } from "./tools/cluster-notify.ts";
21
+ import { createClusterKanbanTool } from "./tools/cluster-kanban.ts";
22
+ import { createClusterQueryTool } from "./tools/cluster-query.ts";
23
+ import { nanoid } from "nanoid";
21
24
  import { spawnProcess } from "./compat.ts";
22
25
 
23
26
  /**
@@ -80,6 +83,33 @@ function discoverModels(
80
83
  return result;
81
84
  }
82
85
 
86
+ /**
87
+ * Auto-discover agents from OpenClaw's agents.list config.
88
+ * Only used when no agents are explicitly configured in ClawMatrix.
89
+ */
90
+ function discoverAgents(
91
+ openclawConfig: OpenClawConfig,
92
+ ): ClawMatrixConfig["agents"] {
93
+ const cfg = openclawConfig as Record<string, unknown>;
94
+ const agentsConfig = cfg.agents as { list?: Array<{ id: string; name?: string }> } | undefined;
95
+ if (!agentsConfig?.list?.length) return [];
96
+
97
+ const result: ClawMatrixConfig["agents"] = [];
98
+ for (const a of agentsConfig.list) {
99
+ if (!a.id || typeof a.id !== "string") continue;
100
+ result.push({
101
+ id: a.id,
102
+ description: a.name ?? a.id,
103
+ tags: [],
104
+ });
105
+ }
106
+
107
+ if (result.length > 0) {
108
+ debug("agents", `Auto-discovered ${result.length} agent(s) from OpenClaw config: ${result.map((a) => a.id).join(", ")}`);
109
+ }
110
+ return result;
111
+ }
112
+
83
113
  const plugin = {
84
114
  id: "clawmatrix",
85
115
  name: "ClawMatrix",
@@ -106,6 +136,14 @@ const plugin = {
106
136
  return;
107
137
  }
108
138
 
139
+ // Auto-discover agents from OpenClaw config when none explicitly configured
140
+ if (config.agents.length === 0) {
141
+ const discovered = discoverAgents(api.config);
142
+ if (discovered.length > 0) {
143
+ config = { ...config, agents: discovered };
144
+ }
145
+ }
146
+
109
147
  // Auto-discover models from OpenClaw providers and merge with explicit config
110
148
  {
111
149
  const discovered = discoverModels(api.config, config);
@@ -305,6 +343,8 @@ const plugin = {
305
343
  api.registerTool(createClusterToolInvokeTool(), { optional: true });
306
344
  api.registerTool(createClusterTransferTool(), { optional: true });
307
345
  api.registerTool(createClusterNotifyTool(), { optional: true });
346
+ api.registerTool(createClusterKanbanTool(), { optional: true });
347
+ api.registerTool(createClusterQueryTool(), { optional: true });
308
348
 
309
349
  // Wire up peer approval with OpenClaw channel API
310
350
  if (config.peerApproval.enabled) {
@@ -326,7 +366,7 @@ const plugin = {
326
366
  to: params.to,
327
367
  message: params.message,
328
368
  channel: params.channel,
329
- idempotencyKey: crypto.randomUUID(),
369
+ idempotencyKey: nanoid(),
330
370
  };
331
371
  if (params.accountId) sendParams.accountId = params.accountId;
332
372
  if (params.threadId) sendParams.threadId = params.threadId;
@@ -687,14 +727,14 @@ const plugin = {
687
727
  ({ params, respond }: GatewayRequestHandlerOptions) => {
688
728
  try {
689
729
  const runtime = getClusterRuntime();
690
- if (!runtime.webHandler) {
691
- respond(false, { error: "Events not enabled (web.enabled = false)" });
730
+ if (!runtime.apiHandler) {
731
+ respond(false, { error: "Events not available (listen mode not enabled)" });
692
732
  return;
693
733
  }
694
734
  const { type, source, since, unconsumed, limit } = (params ?? {}) as {
695
735
  type?: string; source?: string; since?: number; unconsumed?: boolean; limit?: number;
696
736
  };
697
- const events = runtime.webHandler.queryEvents({
737
+ const events = runtime.apiHandler.queryEvents({
698
738
  type,
699
739
  source,
700
740
  since,
@@ -713,8 +753,8 @@ const plugin = {
713
753
  ({ params, respond }: GatewayRequestHandlerOptions) => {
714
754
  try {
715
755
  const runtime = getClusterRuntime();
716
- if (!runtime.webHandler) {
717
- respond(false, { error: "Events not enabled (web.enabled = false)" });
756
+ if (!runtime.apiHandler) {
757
+ respond(false, { error: "Events not available (listen mode not enabled)" });
718
758
  return;
719
759
  }
720
760
  const { ids } = (params ?? {}) as { ids?: string[] };
@@ -722,7 +762,7 @@ const plugin = {
722
762
  respond(false, { error: "Missing required param: ids (array of event IDs)" });
723
763
  return;
724
764
  }
725
- const consumed = runtime.webHandler.consumeEvents(ids);
765
+ const consumed = runtime.apiHandler.consumeEvents(ids);
726
766
  respond(true, { consumed, ids });
727
767
  } catch {
728
768
  respond(false, { error: "ClawMatrix service not running" });
@@ -1174,6 +1214,329 @@ const plugin = {
1174
1214
  },
1175
1215
  );
1176
1216
 
1217
+ // ── Kanban board gateway methods ──────────────────────────────────
1218
+
1219
+ api.registerGatewayMethod(
1220
+ "clawmatrix.board.summary",
1221
+ ({ respond }: GatewayRequestHandlerOptions) => {
1222
+ try {
1223
+ const runtime = getClusterRuntime();
1224
+ if (!runtime.kanbanManager) {
1225
+ respond(false, { error: "Kanban not enabled" });
1226
+ return;
1227
+ }
1228
+ respond(true, runtime.kanbanManager.getSummary());
1229
+ } catch {
1230
+ respond(false, { error: "ClawMatrix service not running" });
1231
+ }
1232
+ },
1233
+ );
1234
+
1235
+ api.registerGatewayMethod(
1236
+ "clawmatrix.board.list",
1237
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1238
+ try {
1239
+ const runtime = getClusterRuntime();
1240
+ if (!runtime.kanbanManager) {
1241
+ respond(false, { error: "Kanban not enabled" });
1242
+ return;
1243
+ }
1244
+ const { stage, label, assignedNode, priority } = (params ?? {}) as {
1245
+ stage?: string; label?: string; assignedNode?: string; priority?: string;
1246
+ };
1247
+ const cards = runtime.kanbanManager.listCards({
1248
+ stage: stage as import("./types.ts").CardStage | undefined,
1249
+ label,
1250
+ assignedNode,
1251
+ priority: priority as import("./types.ts").CardPriority | undefined,
1252
+ });
1253
+ respond(true, cards);
1254
+ } catch {
1255
+ respond(false, { error: "ClawMatrix service not running" });
1256
+ }
1257
+ },
1258
+ );
1259
+
1260
+ api.registerGatewayMethod(
1261
+ "clawmatrix.board.get",
1262
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1263
+ try {
1264
+ const runtime = getClusterRuntime();
1265
+ if (!runtime.kanbanManager) {
1266
+ respond(false, { error: "Kanban not enabled" });
1267
+ return;
1268
+ }
1269
+ const { cardId } = (params ?? {}) as { cardId?: string };
1270
+ if (!cardId) {
1271
+ respond(false, { error: "Missing required param: cardId" });
1272
+ return;
1273
+ }
1274
+ const card = runtime.kanbanManager.getCard(cardId);
1275
+ if (!card) {
1276
+ respond(false, { error: `Card not found: ${cardId}` });
1277
+ return;
1278
+ }
1279
+ respond(true, card);
1280
+ } catch {
1281
+ respond(false, { error: "ClawMatrix service not running" });
1282
+ }
1283
+ },
1284
+ );
1285
+
1286
+ api.registerGatewayMethod(
1287
+ "clawmatrix.board.create",
1288
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1289
+ try {
1290
+ const runtime = getClusterRuntime();
1291
+ if (!runtime.kanbanManager) {
1292
+ respond(false, { error: "Kanban not enabled" });
1293
+ return;
1294
+ }
1295
+ const { title, description, priority, targetNode, targetAgent, cwd, labels } = (params ?? {}) as {
1296
+ title?: string; description?: string; priority?: string;
1297
+ targetNode?: string; targetAgent?: string; cwd?: string; labels?: string[];
1298
+ };
1299
+ if (!title) {
1300
+ respond(false, { error: "Missing required param: title" });
1301
+ return;
1302
+ }
1303
+ const card = runtime.kanbanManager.createCard({
1304
+ title,
1305
+ description,
1306
+ priority: priority as import("./types.ts").CardPriority | undefined,
1307
+ targetNode,
1308
+ targetAgent,
1309
+ cwd,
1310
+ labels,
1311
+ });
1312
+ respond(true, card);
1313
+ } catch {
1314
+ respond(false, { error: "ClawMatrix service not running" });
1315
+ }
1316
+ },
1317
+ );
1318
+
1319
+ api.registerGatewayMethod(
1320
+ "clawmatrix.board.claim",
1321
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1322
+ try {
1323
+ const runtime = getClusterRuntime();
1324
+ if (!runtime.kanbanManager) {
1325
+ respond(false, { error: "Kanban not enabled" });
1326
+ return;
1327
+ }
1328
+ const { cardId, agent } = (params ?? {}) as { cardId?: string; agent?: string };
1329
+ if (!cardId) {
1330
+ respond(false, { error: "Missing required param: cardId" });
1331
+ return;
1332
+ }
1333
+ const card = runtime.kanbanManager.claimCard(
1334
+ cardId,
1335
+ runtime.config.nodeId,
1336
+ agent ?? runtime.config.agents[0]?.id ?? "unknown",
1337
+ );
1338
+ if (!card) {
1339
+ respond(false, { error: `Cannot claim card ${cardId}` });
1340
+ return;
1341
+ }
1342
+ respond(true, card);
1343
+ } catch {
1344
+ respond(false, { error: "ClawMatrix service not running" });
1345
+ }
1346
+ },
1347
+ );
1348
+
1349
+ api.registerGatewayMethod(
1350
+ "clawmatrix.board.move",
1351
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1352
+ try {
1353
+ const runtime = getClusterRuntime();
1354
+ if (!runtime.kanbanManager) {
1355
+ respond(false, { error: "Kanban not enabled" });
1356
+ return;
1357
+ }
1358
+ const { cardId, stage } = (params ?? {}) as { cardId?: string; stage?: string };
1359
+ if (!cardId || !stage) {
1360
+ respond(false, { error: "Missing required params: cardId, stage" });
1361
+ return;
1362
+ }
1363
+ const card = runtime.kanbanManager.moveCard(cardId, stage as import("./types.ts").CardStage);
1364
+ if (!card) {
1365
+ respond(false, { error: `Cannot move card ${cardId}` });
1366
+ return;
1367
+ }
1368
+ respond(true, card);
1369
+ } catch {
1370
+ respond(false, { error: "ClawMatrix service not running" });
1371
+ }
1372
+ },
1373
+ );
1374
+
1375
+ api.registerGatewayMethod(
1376
+ "clawmatrix.board.annotate",
1377
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1378
+ try {
1379
+ const runtime = getClusterRuntime();
1380
+ if (!runtime.kanbanManager) {
1381
+ respond(false, { error: "Kanban not enabled" });
1382
+ return;
1383
+ }
1384
+ const { cardId, content, annotationType, agent } = (params ?? {}) as {
1385
+ cardId?: string; content?: string; annotationType?: string; agent?: string;
1386
+ };
1387
+ if (!cardId || !content) {
1388
+ respond(false, { error: "Missing required params: cardId, content" });
1389
+ return;
1390
+ }
1391
+ const card = runtime.kanbanManager.annotateCard(cardId, {
1392
+ nodeId: runtime.config.nodeId,
1393
+ agent: agent ?? runtime.config.agents[0]?.id ?? "unknown",
1394
+ type: (annotationType ?? "note") as import("./types.ts").CardAnnotation["type"],
1395
+ content,
1396
+ });
1397
+ if (!card) {
1398
+ respond(false, { error: `Card not found: ${cardId}` });
1399
+ return;
1400
+ }
1401
+ respond(true, card);
1402
+ } catch {
1403
+ respond(false, { error: "ClawMatrix service not running" });
1404
+ }
1405
+ },
1406
+ );
1407
+
1408
+ api.registerGatewayMethod(
1409
+ "clawmatrix.board.delete",
1410
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1411
+ try {
1412
+ const runtime = getClusterRuntime();
1413
+ if (!runtime.kanbanManager) {
1414
+ respond(false, { error: "Kanban not enabled" });
1415
+ return;
1416
+ }
1417
+ const { cardId } = (params ?? {}) as { cardId?: string };
1418
+ if (!cardId) {
1419
+ respond(false, { error: "Missing required param: cardId" });
1420
+ return;
1421
+ }
1422
+ const ok = runtime.kanbanManager.deleteCard(cardId);
1423
+ respond(true, { deleted: ok, cardId });
1424
+ } catch {
1425
+ respond(false, { error: "ClawMatrix service not running" });
1426
+ }
1427
+ },
1428
+ );
1429
+
1430
+ // ── Knowledge sync gateway methods ──────────────────────────────
1431
+
1432
+ api.registerGatewayMethod(
1433
+ "clawmatrix.kb.files",
1434
+ ({ respond }: GatewayRequestHandlerOptions) => {
1435
+ try {
1436
+ const runtime = getClusterRuntime();
1437
+ if (!runtime.knowledgeSync) {
1438
+ respond(false, { error: "Knowledge sync not enabled" });
1439
+ return;
1440
+ }
1441
+ respond(true, runtime.knowledgeSync.listSyncedFiles());
1442
+ } catch {
1443
+ respond(false, { error: "ClawMatrix service not running" });
1444
+ }
1445
+ },
1446
+ );
1447
+
1448
+ api.registerGatewayMethod(
1449
+ "clawmatrix.kb.history",
1450
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1451
+ try {
1452
+ const runtime = getClusterRuntime();
1453
+ if (!runtime.knowledgeSync) {
1454
+ respond(false, { error: "Knowledge sync not enabled" });
1455
+ return;
1456
+ }
1457
+ const { path } = (params ?? {}) as { path?: string };
1458
+ if (!path) {
1459
+ respond(false, { error: "Missing required param: path" });
1460
+ return;
1461
+ }
1462
+ const rawHistory = runtime.knowledgeSync.getFileHistory(path);
1463
+ const history = rawHistory.map((entry) => {
1464
+ let nodeId: string | undefined;
1465
+ let agentId: string | undefined;
1466
+ let agentType: string | undefined;
1467
+ let sessionKey: string | undefined;
1468
+ try {
1469
+ const parsed = JSON.parse(entry.message);
1470
+ nodeId = parsed.nodeId;
1471
+ agentId = parsed.agentId;
1472
+ agentType = parsed.agentType;
1473
+ sessionKey = parsed.sessionKey;
1474
+ } catch {
1475
+ // message is not JSON
1476
+ }
1477
+ return {
1478
+ timestamp: entry.timestamp,
1479
+ actor: entry.actor,
1480
+ ...(nodeId ? { nodeId } : {}),
1481
+ ...(agentId ? { agentId } : {}),
1482
+ ...(agentType ? { agentType } : {}),
1483
+ ...(sessionKey ? { sessionKey } : {}),
1484
+ };
1485
+ });
1486
+ respond(true, { path, history });
1487
+ } catch {
1488
+ respond(false, { error: "ClawMatrix service not running" });
1489
+ }
1490
+ },
1491
+ );
1492
+
1493
+ api.registerGatewayMethod(
1494
+ "clawmatrix.kb.blame",
1495
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
1496
+ try {
1497
+ const runtime = getClusterRuntime();
1498
+ if (!runtime.knowledgeSync) {
1499
+ respond(false, { error: "Knowledge sync not enabled" });
1500
+ return;
1501
+ }
1502
+ const { path } = (params ?? {}) as { path?: string };
1503
+ if (!path) {
1504
+ respond(false, { error: "Missing required param: path" });
1505
+ return;
1506
+ }
1507
+ const blame = runtime.knowledgeSync.getFileBlame(path);
1508
+ if (!blame) {
1509
+ respond(false, { error: "File not found in sync" });
1510
+ return;
1511
+ }
1512
+ // Parse JSON attribution from message field
1513
+ const enriched = blame.map((entry) => {
1514
+ let nodeId: string | undefined;
1515
+ let agentId: string | undefined;
1516
+ let agentType: string | undefined;
1517
+ let sessionKey: string | undefined;
1518
+ try {
1519
+ const parsed = JSON.parse(entry.message);
1520
+ nodeId = parsed.nodeId;
1521
+ agentId = parsed.agentId;
1522
+ agentType = parsed.agentType;
1523
+ sessionKey = parsed.sessionKey;
1524
+ } catch { /* not JSON */ }
1525
+ return {
1526
+ ...entry,
1527
+ ...(nodeId ? { nodeId } : {}),
1528
+ ...(agentId ? { agentId } : {}),
1529
+ ...(agentType ? { agentType } : {}),
1530
+ ...(sessionKey ? { sessionKey } : {}),
1531
+ };
1532
+ });
1533
+ respond(true, { path, blame: enriched });
1534
+ } catch {
1535
+ respond(false, { error: "ClawMatrix service not running" });
1536
+ }
1537
+ },
1538
+ );
1539
+
1177
1540
  // Log model selection on each LLM call (fire-and-forget)
1178
1541
  api.on("llm_input", (event) => {
1179
1542
  api.logger.debug(`[clawmatrix] llm_input: provider=${event.provider} model=${event.model}`);
@@ -1256,7 +1619,6 @@ const plugin = {
1256
1619
 
1257
1620
  let cachedPeerCount = -1;
1258
1621
  let cachedSystemContext = "";
1259
-
1260
1622
  api.on("before_prompt_build", () => {
1261
1623
  try {
1262
1624
  const runtime = getClusterRuntime();
@@ -1274,21 +1636,47 @@ const plugin = {
1274
1636
  }
1275
1637
 
1276
1638
  // Per-turn: only push pending events (agent must react proactively)
1277
- const pendingEvents = runtime.webHandler?.getUnconsumedEvents(5) ?? [];
1278
- let prependContext: string | undefined;
1639
+ const pendingEvents = runtime.apiHandler?.getUnconsumedEvents(5) ?? [];
1640
+ const contextLines: string[] = [];
1641
+
1279
1642
  if (pendingEvents.length > 0) {
1280
- const evtLines = ["Pending events (use cluster_events to query details or consume):"];
1643
+ contextLines.push("Pending events (use cluster_events to query details or consume):");
1281
1644
  for (const evt of pendingEvents) {
1282
1645
  const age = Math.floor((Date.now() - evt.ts) / 1000);
1283
1646
  const dataStr = Object.entries(evt.data)
1284
1647
  .map(([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`)
1285
1648
  .join(",");
1286
1649
  const truncated = dataStr.length > 120 ? dataStr.slice(0, 120) + "…" : dataStr;
1287
- evtLines.push(` [${evt.type}] ${evt.source} (${age}s,id:${evt.id}): ${truncated}`);
1650
+ contextLines.push(` [${evt.type}] ${evt.source} (${age}s,id:${evt.id}): ${truncated}`);
1651
+ }
1652
+ }
1653
+
1654
+ // Kanban board summary
1655
+ if (runtime.kanbanManager) {
1656
+ const claimable = runtime.kanbanManager.getClaimableCards(config.nodeId).length;
1657
+ const inProgress = runtime.kanbanManager.listCards({ stage: "in_progress", assignedNode: config.nodeId }).length;
1658
+ if (claimable > 0 || inProgress > 0) {
1659
+ const parts: string[] = [];
1660
+ if (claimable > 0) parts.push(`${claimable} claimable`);
1661
+ if (inProgress > 0) parts.push(`${inProgress} in-progress`);
1662
+ contextLines.push(`[Kanban] ${parts.join(", ")} card(s). Use cluster_kanban tool to interact.`);
1288
1663
  }
1289
- prependContext = evtLines.join("\n");
1290
1664
  }
1291
1665
 
1666
+ // Knowledge base hint
1667
+ if (runtime.knowledgeSync) {
1668
+ const syncPaths = config.knowledge?.paths ?? [];
1669
+ if (syncPaths.length > 0) {
1670
+ contextLines.push(
1671
+ `[Knowledge Base] Files in synced paths (${syncPaths.join(", ")}) are shared across all cluster nodes via CRDT.` +
1672
+ ` When producing docs, analysis, or plans, save to a synced path so other nodes can access them.` +
1673
+ ` For project-specific docs, write in the project directory and suggest the user sync to the knowledge base if useful.`,
1674
+ );
1675
+ }
1676
+ }
1677
+
1678
+ const prependContext = contextLines.length > 0 ? contextLines.join("\n") : undefined;
1679
+
1292
1680
  return {
1293
1681
  prependSystemContext: cachedSystemContext,
1294
1682
  ...(prependContext ? { prependContext } : {}),
@@ -1297,6 +1685,26 @@ const plugin = {
1297
1685
  return;
1298
1686
  }
1299
1687
  });
1688
+
1689
+ // Track file write attribution for knowledge-sync CRDT history
1690
+ api.on("after_tool_call", (event, ctx) => {
1691
+ try {
1692
+ const runtime = getClusterRuntime();
1693
+ if (!runtime.knowledgeSync) return;
1694
+ const toolName = (event.toolName || "").toLowerCase();
1695
+ if (toolName !== "write" && toolName !== "edit") return;
1696
+ const filePath = (event.params?.file_path ?? event.params?.path) as string | undefined;
1697
+ if (!filePath) return;
1698
+ runtime.knowledgeSync.setPendingAttribution(filePath, {
1699
+ nodeId: config.nodeId,
1700
+ agentId: ctx.agentId ?? "unknown",
1701
+ agentType: "OpenClaw",
1702
+ sessionKey: ctx.sessionKey,
1703
+ });
1704
+ } catch {
1705
+ // non-critical, silently ignore
1706
+ }
1707
+ });
1300
1708
  },
1301
1709
  };
1302
1710