clawnexus 0.3.0 → 0.3.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.
@@ -22,6 +22,7 @@ const probe_js_1 = require("../local/probe.js");
22
22
  const engine_js_1 = require("../agent/engine.js");
23
23
  const tasks_js_1 = require("../agent/tasks.js");
24
24
  const router_js_1 = require("../agent/router.js");
25
+ const executor_js_1 = require("../agent/executor.js");
25
26
  const broadcast_js_1 = require("../discovery/broadcast.js");
26
27
  const keys_js_1 = require("../crypto/keys.js");
27
28
  const client_js_1 = require("../registry/client.js");
@@ -29,11 +30,12 @@ const auto_register_js_1 = require("../registry/auto-register.js");
29
30
  const discovery_js_1 = require("../registry/discovery.js");
30
31
  const connector_js_1 = require("../relay/connector.js");
31
32
  const card_js_1 = require("../a2a/card.js");
33
+ const services_js_1 = require("../agent/services.js");
32
34
  const node_fs_1 = require("node:fs");
33
35
  const node_path_1 = require("node:path");
34
36
  const PORT = parseInt(process.env.CLAWNEXUS_PORT ?? "17890", 10);
35
37
  const HOST = process.env.CLAWNEXUS_HOST ?? "127.0.0.1";
36
- function registerRelayRoutes(app, getConnector) {
38
+ function registerRelayRoutes(app, getConnector, getTokenRefresher) {
37
39
  app.post("/relay/connect", async (request, reply) => {
38
40
  const connector = getConnector();
39
41
  if (!connector) {
@@ -47,6 +49,17 @@ function registerRelayRoutes(app, getConnector) {
47
49
  error: "Missing target_claw_id",
48
50
  });
49
51
  }
52
+ // Refresh auth token before JOIN (relay JWTs expire after 5 min)
53
+ const refresher = getTokenRefresher?.();
54
+ if (refresher) {
55
+ try {
56
+ const freshToken = await refresher();
57
+ connector.updateAuthToken(freshToken);
58
+ }
59
+ catch (err) {
60
+ app.log.warn(`Token refresh before JOIN failed: ${err}`);
61
+ }
62
+ }
50
63
  connector.join(target_claw_id);
51
64
  return { status: "connecting", target: target_claw_id };
52
65
  });
@@ -71,7 +84,33 @@ function registerRelayRoutes(app, getConnector) {
71
84
  });
72
85
  }
73
86
  function registerAgentRoutes(app, deps) {
74
- const { engine, tasks, getRouter } = deps;
87
+ const { engine, tasks, getRouter, getExecutor, skillsRegistry } = deps;
88
+ // --- Skills ---
89
+ app.get("/agent/skills", async () => {
90
+ if (!skillsRegistry) {
91
+ return { skills: [], status: { source: "not_initialized" } };
92
+ }
93
+ return { skills: skillsRegistry.getSkills(), status: skillsRegistry.getStatus() };
94
+ });
95
+ app.post("/agent/skills/refresh", async () => {
96
+ if (!skillsRegistry) {
97
+ return { status: "error", message: "Skills registry not initialized" };
98
+ }
99
+ const ok = await skillsRegistry.refresh();
100
+ return {
101
+ status: ok ? "ok" : "error",
102
+ skills: skillsRegistry.getSkills(),
103
+ ...skillsRegistry.getStatus(),
104
+ };
105
+ });
106
+ // --- Executor status ---
107
+ app.get("/agent/executor/status", async () => {
108
+ const executor = getExecutor();
109
+ if (!executor) {
110
+ return { gw_state: "not_initialized", queue_length: 0, executing: [], max_concurrent: 0 };
111
+ }
112
+ return executor.getStatus();
113
+ });
75
114
  // --- Policy ---
76
115
  app.get("/agent/policy", async () => engine.getConfig());
77
116
  app.put("/agent/policy", async (request) => {
@@ -305,19 +344,20 @@ function registerDiagnosticsRoutes(app, deps) {
305
344
  };
306
345
  });
307
346
  }
308
- function registerA2aRoutes(app, store, daemonVersion) {
347
+ function registerA2aRoutes(app, store, daemonVersion, skillsRegistry) {
309
348
  // A2A standard well-known endpoint — returns card for the local (is_self) instance
310
349
  app.get("/.well-known/agent-card.json", async (_request, reply) => {
311
350
  const self = store.getAll().find((i) => i.is_self);
312
351
  if (!self) {
313
352
  return reply.status(404).send({ error: "No local instance discovered" });
314
353
  }
315
- return (0, card_js_1.buildAgentCard)(self, daemonVersion);
354
+ return (0, card_js_1.buildAgentCard)(self, daemonVersion, skillsRegistry?.getSkills());
316
355
  });
317
356
  // All instances as Agent Cards
318
357
  app.get("/a2a/cards", async () => {
319
358
  const instances = store.getAll();
320
- const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion));
359
+ const skills = skillsRegistry?.getSkills();
360
+ const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion, skills));
321
361
  return { count: cards.length, cards };
322
362
  });
323
363
  // Single instance Agent Card by name
@@ -326,7 +366,7 @@ function registerA2aRoutes(app, store, daemonVersion) {
326
366
  if (!inst) {
327
367
  return reply.status(404).send({ error: "Instance not found" });
328
368
  }
329
- return (0, card_js_1.buildAgentCard)(inst, daemonVersion);
369
+ return (0, card_js_1.buildAgentCard)(inst, daemonVersion, skillsRegistry?.getSkills());
330
370
  });
331
371
  }
332
372
  async function startDaemon(options = {}) {
@@ -355,6 +395,13 @@ async function startDaemon(options = {}) {
355
395
  await engine.init();
356
396
  const taskManager = new tasks_js_1.TaskManager();
357
397
  await taskManager.init();
398
+ // 6b. Create TaskExecutor
399
+ const taskExecutor = new executor_js_1.TaskExecutor({
400
+ tasks: taskManager,
401
+ maxConcurrent: engine.getConfig().max_concurrent_tasks,
402
+ });
403
+ // 6c. Create SkillsRegistry
404
+ const skillsRegistry = new services_js_1.SkillsRegistry();
358
405
  // 7. Detect WireGuard interfaces
359
406
  const wgInfo = await (0, wireguard_js_1.detectWireGuard)();
360
407
  // 8. Create and configure Fastify app
@@ -378,7 +425,7 @@ async function startDaemon(options = {}) {
378
425
  app.get("/health", async () => ({
379
426
  status: "ok",
380
427
  service: "clawnexus-daemon",
381
- version: "0.4.0",
428
+ version: "0.3.1",
382
429
  timestamp: new Date().toISOString(),
383
430
  components: {
384
431
  registry: { instances: store.size },
@@ -401,12 +448,23 @@ async function startDaemon(options = {}) {
401
448
  // Instance management routes
402
449
  registerInstanceRoutes(app, store, scanner);
403
450
  // Relay routes
404
- registerRelayRoutes(app, () => connector);
451
+ registerRelayRoutes(app, () => connector, () => {
452
+ const rc = registryClient;
453
+ const clawName = autoRegister?.clawName;
454
+ if (!rc || !clawName)
455
+ return null;
456
+ return async () => {
457
+ const result = await rc.getToken(clawName);
458
+ return result.token;
459
+ };
460
+ });
405
461
  // Agent routes (Layer B)
406
462
  registerAgentRoutes(app, {
407
463
  engine,
408
464
  tasks: taskManager,
409
465
  getRouter: () => agentRouter,
466
+ getExecutor: () => taskExecutor,
467
+ skillsRegistry,
410
468
  });
411
469
  // Diagnostics routes
412
470
  registerDiagnosticsRoutes(app, {
@@ -420,7 +478,7 @@ async function startDaemon(options = {}) {
420
478
  });
421
479
  // A2A Agent Card routes
422
480
  const daemonPkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../../package.json"), "utf-8"));
423
- registerA2aRoutes(app, store, daemonPkg.version);
481
+ registerA2aRoutes(app, store, daemonPkg.version, skillsRegistry);
424
482
  // 9. Initialize Registry integration (non-fatal — LAN must work without it)
425
483
  let identityKeys = null;
426
484
  let registryClient = null;
@@ -431,6 +489,8 @@ async function startDaemon(options = {}) {
431
489
  localProbe.on("local:discovered", (instance) => {
432
490
  app.log.info({ agent_id: instance.agent_id }, "Local OpenClaw instance discovered");
433
491
  broadcast.sendAnnounce();
492
+ // Start skills registry once we know the local Gateway is available
493
+ skillsRegistry.start();
434
494
  });
435
495
  localProbe.on("local:unavailable", () => {
436
496
  app.log.info("No local OpenClaw instance on :18789");
@@ -447,10 +507,12 @@ async function startDaemon(options = {}) {
447
507
  registryClient,
448
508
  identityKeys,
449
509
  });
510
+ let relayInitializing = false;
450
511
  autoRegister.on("registered", async (info) => {
451
512
  console.log(`[clawnexus] [Registry] Registered as ${info.claw_name} (${info.action})`);
452
- // Initialize relay connector after successful registration
453
- if (!connector && registryClient && info.claw_name) {
513
+ // Initialize relay connector after successful registration (once only)
514
+ if (!connector && !relayInitializing && registryClient && info.claw_name) {
515
+ relayInitializing = true;
454
516
  try {
455
517
  const tokenResult = await registryClient.getToken(info.claw_name);
456
518
  console.log(`[clawnexus] [Relay] Got auth token, relay_hint: ${tokenResult.relay_hint}`);
@@ -478,7 +540,8 @@ async function startDaemon(options = {}) {
478
540
  });
479
541
  newConnector.connect();
480
542
  setConnector(newConnector);
481
- // Start token refresh — every 4 minutes (token expires in 5 min)
543
+ // Start token refresh — every 55 minutes (relay keeps WebSocket alive after auth,
544
+ // only need to refresh for reconnection scenarios)
482
545
  if (tokenRefreshTimer)
483
546
  clearInterval(tokenRefreshTimer);
484
547
  tokenRefreshTimer = setInterval(async () => {
@@ -486,8 +549,16 @@ async function startDaemon(options = {}) {
486
549
  return;
487
550
  try {
488
551
  const fresh = await registryClient.getToken(autoRegister.clawName);
489
- // Reconnect with fresh token
490
- connector?.disconnect();
552
+ // Save peer claw_ids from existing rooms before reconnecting
553
+ const previousPeers = [];
554
+ const oldConnector = connector;
555
+ if (oldConnector) {
556
+ for (const room of oldConnector.getStatus().rooms) {
557
+ if (room.peer_claw_id)
558
+ previousPeers.push(room.peer_claw_id);
559
+ }
560
+ }
561
+ // Connect new first, then disconnect old (relay server handles replacement)
491
562
  const refreshed = new connector_js_1.RelayConnector({
492
563
  relayUrl: process.env.CLAWNEXUS_RELAY_URL ?? `wss://${fresh.relay_hint}/relay`,
493
564
  clawId: autoRegister.clawName,
@@ -496,6 +567,15 @@ async function startDaemon(options = {}) {
496
567
  });
497
568
  refreshed.on("registered", (clawId) => {
498
569
  console.log(`[clawnexus] [Relay] Reconnected (token refresh) as ${clawId}`);
570
+ // Old connector is now replaced on server side — disconnect it locally
571
+ if (oldConnector && oldConnector !== refreshed) {
572
+ oldConnector.disconnect();
573
+ }
574
+ // Re-join rooms with previous peers
575
+ for (const peerId of previousPeers) {
576
+ console.log(`[clawnexus] [Relay] Re-joining peer ${peerId} after token refresh`);
577
+ refreshed.join(peerId);
578
+ }
499
579
  });
500
580
  refreshed.on("relay_error", (code, message) => {
501
581
  console.log(`[clawnexus] [Relay] Error: ${code} — ${message}`);
@@ -506,7 +586,7 @@ async function startDaemon(options = {}) {
506
586
  catch (err) {
507
587
  console.log(`[clawnexus] [Relay] Token refresh failed (non-fatal): ${err}`);
508
588
  }
509
- }, 4 * 60 * 1000);
589
+ }, 55 * 60 * 1000);
510
590
  }
511
591
  catch (err) {
512
592
  console.log(`[clawnexus] [Relay] Failed to initialize (non-fatal): ${err}`);
@@ -552,6 +632,8 @@ async function startDaemon(options = {}) {
552
632
  app.addHook("onClose", async () => {
553
633
  if (tokenRefreshTimer)
554
634
  clearInterval(tokenRefreshTimer);
635
+ skillsRegistry.stop();
636
+ await taskExecutor.close();
555
637
  autoRegister?.stop();
556
638
  agentRouter?.stop();
557
639
  taskManager.close();
@@ -563,6 +645,8 @@ async function startDaemon(options = {}) {
563
645
  await store.close();
564
646
  });
565
647
  await app.listen({ port, host });
648
+ // Start task executor (independent of relay — connects to local OpenClaw Gateway)
649
+ taskExecutor.start();
566
650
  const setConnector = (c) => {
567
651
  connector = c;
568
652
  // When relay connector is set, create and start AgentRouter
@@ -572,9 +656,17 @@ async function startDaemon(options = {}) {
572
656
  engine,
573
657
  tasks: taskManager,
574
658
  localClawId: c.getStatus().claw_id ?? "",
659
+ skillsRegistry,
575
660
  });
576
661
  agentRouter.start();
577
662
  app.log.info("Layer B agent router started");
663
+ // Give executor the router reference so it can send reports
664
+ taskExecutor.setRouter(agentRouter);
665
+ }
666
+ else {
667
+ // Update existing router with new connector (e.g. after token refresh)
668
+ agentRouter.setConnector(c);
669
+ app.log.info("Layer B agent router connector updated");
578
670
  }
579
671
  };
580
672
  return {
@@ -590,6 +682,7 @@ async function startDaemon(options = {}) {
590
682
  engine,
591
683
  tasks: taskManager,
592
684
  getRouter: () => agentRouter,
685
+ skillsRegistry,
593
686
  registryClient,
594
687
  autoRegister,
595
688
  remoteDiscovery,
@@ -59,25 +59,38 @@ class AutoRegister extends node_events_1.EventEmitter {
59
59
  this.emit("skip", "No local OpenClaw instance detected");
60
60
  return;
61
61
  }
62
- // Try agentId, then agentId-2, agentId-3, ... if name already taken by another owner
62
+ // Build list of base names to try: agentId first, then auto_name as fallback
63
+ const selfInstance = this.store.getAll().find((i) => i.is_self);
64
+ const autoName = selfInstance?.auto_name;
65
+ const bases = [agentId];
66
+ if (autoName && autoName !== agentId)
67
+ bases.push(autoName);
68
+ // Try each base with suffixes -1, -2, ... up to MAX_SUFFIX if taken by another owner
63
69
  const MAX_SUFFIX = 10;
64
70
  let result = null;
65
- for (let i = 0; i <= MAX_SUFFIX; i++) {
66
- const clawId = i === 0 ? agentId : `${agentId}-${i}`;
67
- try {
68
- result = await this.client.register({ claw_id: clawId });
69
- break;
70
- }
71
- catch (err) {
72
- if (err instanceof client_js_1.RegistryError && err.statusCode === 409 && i < MAX_SUFFIX) {
73
- continue; // name taken by another owner, try next suffix
71
+ outer: for (const base of bases) {
72
+ for (let i = 0; i <= MAX_SUFFIX; i++) {
73
+ const clawId = i === 0 ? base : `${base}-${i}`;
74
+ try {
75
+ result = await this.client.register({ claw_id: clawId });
76
+ break outer;
77
+ }
78
+ catch (err) {
79
+ if (err instanceof client_js_1.RegistryError && err.statusCode === 409 && i < MAX_SUFFIX) {
80
+ continue; // name taken by another owner, try next suffix
81
+ }
82
+ if (err instanceof client_js_1.RegistryError && err.statusCode === 409 && i === MAX_SUFFIX) {
83
+ break; // exhausted suffixes for this base, try next base
84
+ }
85
+ this.emit("error", err);
86
+ return;
74
87
  }
75
- this.emit("error", err);
76
- return;
77
88
  }
78
89
  }
79
- if (!result)
90
+ if (!result) {
91
+ this.emit("error", new Error(`All candidate names exhausted (bases: ${bases.join(", ")})`));
80
92
  return;
93
+ }
81
94
  this.registeredClawName = result.record.name;
82
95
  // Start heartbeat on first successful registration
83
96
  if (!this.heartbeatTimer) {
@@ -85,13 +98,12 @@ class AutoRegister extends node_events_1.EventEmitter {
85
98
  this.tryRegister().catch(() => { });
86
99
  }, HEARTBEAT_INTERVAL_MS);
87
100
  }
88
- // Write claw_name back to the local instance in store
89
- const instances = this.store.getAll();
90
- const selfInstance = instances.find((i) => i.is_self && i.agent_id === agentId);
91
- if (selfInstance) {
92
- selfInstance.claw_name = result.record.name;
93
- selfInstance.owner_pubkey = result.record.ownerPubkey;
94
- this.store.upsert(selfInstance);
101
+ // Write claw_name back to the local instance in store (re-fetch after possible state change)
102
+ const registeredSelf = this.store.getAll().find((i) => i.is_self && i.agent_id === agentId);
103
+ if (registeredSelf) {
104
+ registeredSelf.claw_name = result.record.name;
105
+ registeredSelf.owner_pubkey = result.record.ownerPubkey;
106
+ this.store.upsert(registeredSelf);
95
107
  }
96
108
  this.emit("registered", {
97
109
  action: result.action,
@@ -8,7 +8,7 @@ export interface RelayConnectorOptions {
8
8
  autoAccept?: boolean;
9
9
  }
10
10
  export declare class RelayConnector extends EventEmitter {
11
- private readonly options;
11
+ private options;
12
12
  private ws;
13
13
  private state;
14
14
  private rooms;
@@ -17,10 +17,14 @@ export declare class RelayConnector extends EventEmitter {
17
17
  private reconnectTimer;
18
18
  private closed;
19
19
  constructor(options: RelayConnectorOptions);
20
+ /** Update the auth token (e.g. after token refresh). */
21
+ updateAuthToken(token: string): void;
20
22
  /** Connect to the relay and REGISTER this claw_id. */
21
23
  connect(): void;
22
24
  /** Disconnect and stop reconnecting. */
23
25
  disconnect(): void;
26
+ private pendingJoins;
27
+ private lastJoinTarget;
24
28
  /** Initiate a connection to a remote claw_id through the relay. */
25
29
  join(targetClawId: string): void;
26
30
  /** Send an encrypted message to a peer in a room. */
@@ -20,6 +20,10 @@ class RelayConnector extends node_events_1.EventEmitter {
20
20
  this.options = options;
21
21
  this.keyPair = (0, crypto_js_1.generateKeyPair)();
22
22
  }
23
+ /** Update the auth token (e.g. after token refresh). */
24
+ updateAuthToken(token) {
25
+ this.options = { ...this.options, authToken: token };
26
+ }
23
27
  /** Connect to the relay and REGISTER this claw_id. */
24
28
  connect() {
25
29
  if (this.state !== "disconnected")
@@ -66,8 +70,12 @@ class RelayConnector extends node_events_1.EventEmitter {
66
70
  this.ws?.close();
67
71
  this.cleanup();
68
72
  }
73
+ // Tracks target claw_id for pending JOIN requests
74
+ pendingJoins = new Map(); // target_claw_id → target_claw_id (for JOINED room_id lookup)
75
+ lastJoinTarget = null;
69
76
  /** Initiate a connection to a remote claw_id through the relay. */
70
77
  join(targetClawId) {
78
+ this.lastJoinTarget = targetClawId;
71
79
  this.send({
72
80
  type: "JOIN",
73
81
  claw_id: this.options.clawId,
@@ -78,8 +86,10 @@ class RelayConnector extends node_events_1.EventEmitter {
78
86
  /** Send an encrypted message to a peer in a room. */
79
87
  sendData(roomId, plaintext) {
80
88
  const room = this.rooms.get(roomId);
81
- if (!room || !room.session_key)
89
+ if (!room || !room.session_key) {
90
+ console.log(`[clawnexus] [Relay] sendData failed: room=${roomId} exists=${!!room} hasKey=${!!room?.session_key}`);
82
91
  return false;
92
+ }
83
93
  const payload = (0, crypto_js_1.encrypt)(room.session_key, plaintext);
84
94
  this.send({ type: "DATA", room_id: roomId, payload });
85
95
  return true;
@@ -98,6 +108,7 @@ class RelayConnector extends node_events_1.EventEmitter {
98
108
  room_id: r.room_id,
99
109
  peer_claw_id: r.peer_claw_id,
100
110
  state: r.state,
111
+ has_session_key: !!r.session_key,
101
112
  })),
102
113
  };
103
114
  }
@@ -139,7 +150,7 @@ class RelayConnector extends node_events_1.EventEmitter {
139
150
  // We are the initiator — create room entry
140
151
  this.rooms.set(msg.room_id, {
141
152
  room_id: msg.room_id,
142
- peer_claw_id: "", // will be known after key exchange
153
+ peer_claw_id: this.lastJoinTarget ?? "",
143
154
  state: "active",
144
155
  });
145
156
  // Send our public key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawnexus",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "ClawNexus daemon and CLI — AI instance registry for OpenClaw",
5
5
  "license": "MIT",
6
6
  "author": "alan-silverstreams <alan@silverstream.tech>",