clawnexus 0.3.0 → 0.4.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,17 +8,23 @@ import type { UnreachableInstance } from "../types.js";
8
8
  import { PolicyEngine } from "../agent/engine.js";
9
9
  import { TaskManager } from "../agent/tasks.js";
10
10
  import { AgentRouter } from "../agent/router.js";
11
+ import { TaskExecutor } from "../agent/executor.js";
11
12
  import { BroadcastDiscovery } from "../discovery/broadcast.js";
12
13
  import type { IdentityKeys } from "../crypto/keys.js";
13
14
  import { RegistryClient } from "../registry/client.js";
14
15
  import { AutoRegister } from "../registry/auto-register.js";
15
16
  import { RemoteDiscovery } from "../registry/discovery.js";
16
17
  import { RelayConnector } from "../relay/connector.js";
17
- export declare function registerRelayRoutes(app: FastifyInstance, getConnector: () => RelayConnector | null): void;
18
+ import { CardFetcher } from "../a2a/fetcher.js";
19
+ import { A2AHandler } from "../a2a/handler.js";
20
+ import { SkillsRegistry } from "../agent/services.js";
21
+ export declare function registerRelayRoutes(app: FastifyInstance, getConnector: () => RelayConnector | null, getTokenRefresher?: () => (() => Promise<string>) | null): void;
18
22
  export interface AgentDeps {
19
23
  engine: PolicyEngine;
20
24
  tasks: TaskManager;
21
25
  getRouter: () => AgentRouter | null;
26
+ getExecutor: () => TaskExecutor | null;
27
+ skillsRegistry?: SkillsRegistry;
22
28
  }
23
29
  export declare function registerAgentRoutes(app: FastifyInstance, deps: AgentDeps): void;
24
30
  export interface RegistryDeps {
@@ -39,7 +45,7 @@ export interface DiagnosticsDeps {
39
45
  unreachable: UnreachableInstance[];
40
46
  }
41
47
  export declare function registerDiagnosticsRoutes(app: FastifyInstance, deps: DiagnosticsDeps): void;
42
- export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string): void;
48
+ export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string, skillsRegistry?: SkillsRegistry, a2aHandler?: A2AHandler): void;
43
49
  export interface DaemonOptions {
44
50
  port?: number;
45
51
  host?: string;
@@ -57,6 +63,8 @@ export interface DaemonHandle {
57
63
  engine: PolicyEngine;
58
64
  tasks: TaskManager;
59
65
  getRouter: () => AgentRouter | null;
66
+ skillsRegistry: SkillsRegistry;
67
+ cardFetcher: CardFetcher;
60
68
  registryClient: RegistryClient | null;
61
69
  autoRegister: AutoRegister | null;
62
70
  remoteDiscovery: RemoteDiscovery | null;
@@ -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,16 @@ 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 fetcher_js_1 = require("../a2a/fetcher.js");
34
+ const handler_js_1 = require("../a2a/handler.js");
35
+ const store_js_2 = require("../a2a/store.js");
36
+ const types_js_1 = require("../a2a/types.js");
37
+ const services_js_1 = require("../agent/services.js");
32
38
  const node_fs_1 = require("node:fs");
33
39
  const node_path_1 = require("node:path");
34
40
  const PORT = parseInt(process.env.CLAWNEXUS_PORT ?? "17890", 10);
35
41
  const HOST = process.env.CLAWNEXUS_HOST ?? "127.0.0.1";
36
- function registerRelayRoutes(app, getConnector) {
42
+ function registerRelayRoutes(app, getConnector, getTokenRefresher) {
37
43
  app.post("/relay/connect", async (request, reply) => {
38
44
  const connector = getConnector();
39
45
  if (!connector) {
@@ -47,6 +53,17 @@ function registerRelayRoutes(app, getConnector) {
47
53
  error: "Missing target_claw_id",
48
54
  });
49
55
  }
56
+ // Refresh auth token before JOIN (relay JWTs expire after 5 min)
57
+ const refresher = getTokenRefresher?.();
58
+ if (refresher) {
59
+ try {
60
+ const freshToken = await refresher();
61
+ connector.updateAuthToken(freshToken);
62
+ }
63
+ catch (err) {
64
+ app.log.warn(`Token refresh before JOIN failed: ${err}`);
65
+ }
66
+ }
50
67
  connector.join(target_claw_id);
51
68
  return { status: "connecting", target: target_claw_id };
52
69
  });
@@ -71,7 +88,33 @@ function registerRelayRoutes(app, getConnector) {
71
88
  });
72
89
  }
73
90
  function registerAgentRoutes(app, deps) {
74
- const { engine, tasks, getRouter } = deps;
91
+ const { engine, tasks, getRouter, getExecutor, skillsRegistry } = deps;
92
+ // --- Skills ---
93
+ app.get("/agent/skills", async () => {
94
+ if (!skillsRegistry) {
95
+ return { skills: [], status: { source: "not_initialized" } };
96
+ }
97
+ return { skills: skillsRegistry.getSkills(), status: skillsRegistry.getStatus() };
98
+ });
99
+ app.post("/agent/skills/refresh", async () => {
100
+ if (!skillsRegistry) {
101
+ return { status: "error", message: "Skills registry not initialized" };
102
+ }
103
+ const ok = await skillsRegistry.refresh();
104
+ return {
105
+ status: ok ? "ok" : "error",
106
+ skills: skillsRegistry.getSkills(),
107
+ ...skillsRegistry.getStatus(),
108
+ };
109
+ });
110
+ // --- Executor status ---
111
+ app.get("/agent/executor/status", async () => {
112
+ const executor = getExecutor();
113
+ if (!executor) {
114
+ return { gw_state: "not_initialized", queue_length: 0, executing: [], max_concurrent: 0 };
115
+ }
116
+ return executor.getStatus();
117
+ });
75
118
  // --- Policy ---
76
119
  app.get("/agent/policy", async () => engine.getConfig());
77
120
  app.put("/agent/policy", async (request) => {
@@ -305,19 +348,20 @@ function registerDiagnosticsRoutes(app, deps) {
305
348
  };
306
349
  });
307
350
  }
308
- function registerA2aRoutes(app, store, daemonVersion) {
351
+ function registerA2aRoutes(app, store, daemonVersion, skillsRegistry, a2aHandler) {
309
352
  // A2A standard well-known endpoint — returns card for the local (is_self) instance
310
353
  app.get("/.well-known/agent-card.json", async (_request, reply) => {
311
354
  const self = store.getAll().find((i) => i.is_self);
312
355
  if (!self) {
313
356
  return reply.status(404).send({ error: "No local instance discovered" });
314
357
  }
315
- return (0, card_js_1.buildAgentCard)(self, daemonVersion);
358
+ return (0, card_js_1.buildAgentCard)(self, daemonVersion, skillsRegistry?.getSkills());
316
359
  });
317
360
  // All instances as Agent Cards
318
361
  app.get("/a2a/cards", async () => {
319
362
  const instances = store.getAll();
320
- const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion));
363
+ const localSkills = skillsRegistry?.getSkills();
364
+ const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion, i.is_self ? localSkills : undefined));
321
365
  return { count: cards.length, cards };
322
366
  });
323
367
  // Single instance Agent Card by name
@@ -326,8 +370,69 @@ function registerA2aRoutes(app, store, daemonVersion) {
326
370
  if (!inst) {
327
371
  return reply.status(404).send({ error: "Instance not found" });
328
372
  }
329
- return (0, card_js_1.buildAgentCard)(inst, daemonVersion);
330
- });
373
+ const localSkills = inst.is_self ? skillsRegistry?.getSkills() : undefined;
374
+ return (0, card_js_1.buildAgentCard)(inst, daemonVersion, localSkills);
375
+ });
376
+ // A2A JSON-RPC 2.0 endpoint
377
+ if (a2aHandler) {
378
+ app.post("/a2a", async (request, reply) => {
379
+ const body = request.body;
380
+ if (!body || body.jsonrpc !== "2.0" || typeof body.method !== "string") {
381
+ const id = body?.id ?? null;
382
+ const resp = {
383
+ jsonrpc: "2.0",
384
+ id: id,
385
+ error: { code: types_js_1.JSON_RPC_INVALID_REQUEST, message: "Invalid JSON-RPC 2.0 request" },
386
+ };
387
+ return reply.status(200).send(resp);
388
+ }
389
+ const req = body;
390
+ if (req.method === "tasks/send") {
391
+ const result = await a2aHandler.handleTaskSend(req.params);
392
+ // Check if result is an error
393
+ if ("code" in result && "message" in result && !("id" in result)) {
394
+ return reply.status(200).send({
395
+ jsonrpc: "2.0",
396
+ id: req.id,
397
+ error: result,
398
+ });
399
+ }
400
+ return reply.status(200).send({
401
+ jsonrpc: "2.0",
402
+ id: req.id,
403
+ result,
404
+ });
405
+ }
406
+ if (req.method === "tasks/get") {
407
+ const params = req.params;
408
+ if (!params?.id) {
409
+ return reply.status(200).send({
410
+ jsonrpc: "2.0",
411
+ id: req.id,
412
+ error: { code: -32602, message: "Missing task id" },
413
+ });
414
+ }
415
+ const task = a2aHandler.getTask(params.id);
416
+ if (!task) {
417
+ return reply.status(200).send({
418
+ jsonrpc: "2.0",
419
+ id: req.id,
420
+ error: { code: -32001, message: "Task not found" },
421
+ });
422
+ }
423
+ return reply.status(200).send({
424
+ jsonrpc: "2.0",
425
+ id: req.id,
426
+ result: task,
427
+ });
428
+ }
429
+ return reply.status(200).send({
430
+ jsonrpc: "2.0",
431
+ id: req.id,
432
+ error: { code: types_js_1.JSON_RPC_METHOD_NOT_FOUND, message: `Method not found: ${req.method}` },
433
+ });
434
+ });
435
+ }
331
436
  }
332
437
  async function startDaemon(options = {}) {
333
438
  const port = options.port ?? PORT;
@@ -355,6 +460,15 @@ async function startDaemon(options = {}) {
355
460
  await engine.init();
356
461
  const taskManager = new tasks_js_1.TaskManager();
357
462
  await taskManager.init();
463
+ // 6b. Create TaskExecutor
464
+ const taskExecutor = new executor_js_1.TaskExecutor({
465
+ tasks: taskManager,
466
+ maxConcurrent: engine.getConfig().max_concurrent_tasks,
467
+ });
468
+ // 6c. Create SkillsRegistry
469
+ const skillsRegistry = new services_js_1.SkillsRegistry();
470
+ // 6d. Create CardFetcher (fetches remote Agent Cards from discovered instances)
471
+ const cardFetcher = new fetcher_js_1.CardFetcher(store);
358
472
  // 7. Detect WireGuard interfaces
359
473
  const wgInfo = await (0, wireguard_js_1.detectWireGuard)();
360
474
  // 8. Create and configure Fastify app
@@ -374,11 +488,12 @@ async function startDaemon(options = {}) {
374
488
  else {
375
489
  console.log("[clawnexus] [WireGuard] No WireGuard interfaces detected");
376
490
  }
491
+ const daemonPkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../../package.json"), "utf-8"));
377
492
  // Health endpoint with component status
378
493
  app.get("/health", async () => ({
379
494
  status: "ok",
380
495
  service: "clawnexus-daemon",
381
- version: "0.4.0",
496
+ version: daemonPkg.version,
382
497
  timestamp: new Date().toISOString(),
383
498
  components: {
384
499
  registry: { instances: store.size },
@@ -401,12 +516,23 @@ async function startDaemon(options = {}) {
401
516
  // Instance management routes
402
517
  registerInstanceRoutes(app, store, scanner);
403
518
  // Relay routes
404
- registerRelayRoutes(app, () => connector);
519
+ registerRelayRoutes(app, () => connector, () => {
520
+ const rc = registryClient;
521
+ const clawName = autoRegister?.clawName;
522
+ if (!rc || !clawName)
523
+ return null;
524
+ return async () => {
525
+ const result = await rc.getToken(clawName);
526
+ return result.token;
527
+ };
528
+ });
405
529
  // Agent routes (Layer B)
406
530
  registerAgentRoutes(app, {
407
531
  engine,
408
532
  tasks: taskManager,
409
533
  getRouter: () => agentRouter,
534
+ getExecutor: () => taskExecutor,
535
+ skillsRegistry,
410
536
  });
411
537
  // Diagnostics routes
412
538
  registerDiagnosticsRoutes(app, {
@@ -418,9 +544,11 @@ async function startDaemon(options = {}) {
418
544
  getAutoRegister: () => autoRegister,
419
545
  unreachable,
420
546
  });
421
- // A2A Agent Card routes
422
- 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);
547
+ // A2A Agent Card + JSON-RPC routes
548
+ const a2aTaskStore = new store_js_2.A2ATaskStore();
549
+ await a2aTaskStore.init();
550
+ const a2aHandler = new handler_js_1.A2AHandler({ store: a2aTaskStore });
551
+ registerA2aRoutes(app, store, daemonPkg.version, skillsRegistry, a2aHandler);
424
552
  // 9. Initialize Registry integration (non-fatal — LAN must work without it)
425
553
  let identityKeys = null;
426
554
  let registryClient = null;
@@ -431,15 +559,29 @@ async function startDaemon(options = {}) {
431
559
  localProbe.on("local:discovered", (instance) => {
432
560
  app.log.info({ agent_id: instance.agent_id }, "Local OpenClaw instance discovered");
433
561
  broadcast.sendAnnounce();
562
+ // Start skills registry once we know the local Gateway is available
563
+ skillsRegistry.start();
564
+ // Start fetching remote Agent Cards
565
+ cardFetcher.start();
434
566
  });
435
567
  localProbe.on("local:unavailable", () => {
436
568
  app.log.info("No local OpenClaw instance on :18789");
569
+ // Still start CardFetcher — remote cards are useful even without local instance
570
+ cardFetcher.start();
437
571
  });
438
572
  // Initialize registry after LocalProbe (needs agentId for registration)
439
573
  try {
440
574
  identityKeys = await (0, keys_js_1.loadOrCreateKeys)();
441
575
  registryClient = new client_js_1.RegistryClient(identityKeys);
442
- autoRegister = new auto_register_js_1.AutoRegister(registryClient, store, localProbe, identityKeys);
576
+ autoRegister = new auto_register_js_1.AutoRegister(registryClient, store, localProbe, identityKeys, daemonPkg.version, () => {
577
+ const skills = skillsRegistry.getSkills();
578
+ if (!skills || skills.length === 0)
579
+ return null;
580
+ return {
581
+ skills_count: skills.length,
582
+ skills: skills.map((s) => s.name ?? s.id ?? "unknown"),
583
+ };
584
+ });
443
585
  remoteDiscovery = new discovery_js_1.RemoteDiscovery(registryClient, store);
444
586
  registerRegistryRoutes(app, {
445
587
  autoRegister,
@@ -447,10 +589,12 @@ async function startDaemon(options = {}) {
447
589
  registryClient,
448
590
  identityKeys,
449
591
  });
592
+ let relayInitializing = false;
450
593
  autoRegister.on("registered", async (info) => {
451
594
  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) {
595
+ // Initialize relay connector after successful registration (once only)
596
+ if (!connector && !relayInitializing && registryClient && info.claw_name) {
597
+ relayInitializing = true;
454
598
  try {
455
599
  const tokenResult = await registryClient.getToken(info.claw_name);
456
600
  console.log(`[clawnexus] [Relay] Got auth token, relay_hint: ${tokenResult.relay_hint}`);
@@ -478,7 +622,8 @@ async function startDaemon(options = {}) {
478
622
  });
479
623
  newConnector.connect();
480
624
  setConnector(newConnector);
481
- // Start token refresh — every 4 minutes (token expires in 5 min)
625
+ // Start token refresh — every 55 minutes (relay keeps WebSocket alive after auth,
626
+ // only need to refresh for reconnection scenarios)
482
627
  if (tokenRefreshTimer)
483
628
  clearInterval(tokenRefreshTimer);
484
629
  tokenRefreshTimer = setInterval(async () => {
@@ -486,8 +631,16 @@ async function startDaemon(options = {}) {
486
631
  return;
487
632
  try {
488
633
  const fresh = await registryClient.getToken(autoRegister.clawName);
489
- // Reconnect with fresh token
490
- connector?.disconnect();
634
+ // Save peer claw_ids from existing rooms before reconnecting
635
+ const previousPeers = [];
636
+ const oldConnector = connector;
637
+ if (oldConnector) {
638
+ for (const room of oldConnector.getStatus().rooms) {
639
+ if (room.peer_claw_id)
640
+ previousPeers.push(room.peer_claw_id);
641
+ }
642
+ }
643
+ // Connect new first, then disconnect old (relay server handles replacement)
491
644
  const refreshed = new connector_js_1.RelayConnector({
492
645
  relayUrl: process.env.CLAWNEXUS_RELAY_URL ?? `wss://${fresh.relay_hint}/relay`,
493
646
  clawId: autoRegister.clawName,
@@ -496,6 +649,15 @@ async function startDaemon(options = {}) {
496
649
  });
497
650
  refreshed.on("registered", (clawId) => {
498
651
  console.log(`[clawnexus] [Relay] Reconnected (token refresh) as ${clawId}`);
652
+ // Old connector is now replaced on server side — disconnect it locally
653
+ if (oldConnector && oldConnector !== refreshed) {
654
+ oldConnector.disconnect();
655
+ }
656
+ // Re-join rooms with previous peers
657
+ for (const peerId of previousPeers) {
658
+ console.log(`[clawnexus] [Relay] Re-joining peer ${peerId} after token refresh`);
659
+ refreshed.join(peerId);
660
+ }
499
661
  });
500
662
  refreshed.on("relay_error", (code, message) => {
501
663
  console.log(`[clawnexus] [Relay] Error: ${code} — ${message}`);
@@ -506,7 +668,7 @@ async function startDaemon(options = {}) {
506
668
  catch (err) {
507
669
  console.log(`[clawnexus] [Relay] Token refresh failed (non-fatal): ${err}`);
508
670
  }
509
- }, 4 * 60 * 1000);
671
+ }, 55 * 60 * 1000);
510
672
  }
511
673
  catch (err) {
512
674
  console.log(`[clawnexus] [Relay] Failed to initialize (non-fatal): ${err}`);
@@ -552,6 +714,9 @@ async function startDaemon(options = {}) {
552
714
  app.addHook("onClose", async () => {
553
715
  if (tokenRefreshTimer)
554
716
  clearInterval(tokenRefreshTimer);
717
+ cardFetcher.stop();
718
+ skillsRegistry.stop();
719
+ await taskExecutor.close();
555
720
  autoRegister?.stop();
556
721
  agentRouter?.stop();
557
722
  taskManager.close();
@@ -560,9 +725,13 @@ async function startDaemon(options = {}) {
560
725
  mdns.stop();
561
726
  await broadcast.stop();
562
727
  connector?.disconnect();
728
+ a2aHandler.close();
729
+ await a2aTaskStore.close();
563
730
  await store.close();
564
731
  });
565
732
  await app.listen({ port, host });
733
+ // Start task executor (independent of relay — connects to local OpenClaw Gateway)
734
+ taskExecutor.start();
566
735
  const setConnector = (c) => {
567
736
  connector = c;
568
737
  // When relay connector is set, create and start AgentRouter
@@ -572,9 +741,17 @@ async function startDaemon(options = {}) {
572
741
  engine,
573
742
  tasks: taskManager,
574
743
  localClawId: c.getStatus().claw_id ?? "",
744
+ skillsRegistry,
575
745
  });
576
746
  agentRouter.start();
577
747
  app.log.info("Layer B agent router started");
748
+ // Give executor the router reference so it can send reports
749
+ taskExecutor.setRouter(agentRouter);
750
+ }
751
+ else {
752
+ // Update existing router with new connector (e.g. after token refresh)
753
+ agentRouter.setConnector(c);
754
+ app.log.info("Layer B agent router connector updated");
578
755
  }
579
756
  };
580
757
  return {
@@ -590,6 +767,8 @@ async function startDaemon(options = {}) {
590
767
  engine,
591
768
  tasks: taskManager,
592
769
  getRouter: () => agentRouter,
770
+ skillsRegistry,
771
+ cardFetcher,
593
772
  registryClient,
594
773
  autoRegister,
595
774
  remoteDiscovery,
package/dist/cli/index.js CHANGED
@@ -434,6 +434,12 @@ async function cmdInfo(args) {
434
434
  if (inst.labels && Object.keys(inst.labels).length > 0) {
435
435
  console.log(`Labels: ${JSON.stringify(inst.labels)}`);
436
436
  }
437
+ if (inst.remote_card?.skills?.length) {
438
+ const names = inst.remote_card.skills.map((s) => s.name || s.id).join(", ");
439
+ console.log(`Skills: ${names} (${inst.remote_card.skills.length} skill${inst.remote_card.skills.length === 1 ? "" : "s"})`);
440
+ console.log(`Card URL: ${inst.remote_card.card_url}`);
441
+ console.log(`Card Fetched: ${new Date(inst.remote_card.fetched_at).toLocaleString()}`);
442
+ }
437
443
  }
438
444
  }
439
445
  async function cmdForget(args) {
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import type { RegistryClient } from "./client.js";
3
+ import type { AgentCardSummary } from "./client.js";
3
4
  import type { RegistryStore } from "./store.js";
4
5
  import type { LocalProbe } from "../local/probe.js";
5
6
  import type { IdentityKeys } from "../crypto/keys.js";
@@ -8,10 +9,13 @@ export declare class AutoRegister extends EventEmitter {
8
9
  private readonly store;
9
10
  private readonly localProbe;
10
11
  private readonly keys;
12
+ private readonly daemonVersion;
13
+ private readonly getCardSummary?;
11
14
  private heartbeatTimer;
12
15
  private initialTimer;
13
16
  private registeredClawName;
14
- constructor(client: RegistryClient, store: RegistryStore, localProbe: LocalProbe, keys: IdentityKeys);
17
+ private readonly startedAt;
18
+ constructor(client: RegistryClient, store: RegistryStore, localProbe: LocalProbe, keys: IdentityKeys, daemonVersion?: string, getCardSummary?: (() => AgentCardSummary | null) | undefined);
15
19
  get clawName(): string | null;
16
20
  get publicKey(): string;
17
21
  start(): void;
@@ -4,6 +4,7 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.AutoRegister = void 0;
6
6
  const node_events_1 = require("node:events");
7
+ const node_os_1 = require("node:os");
7
8
  const client_js_1 = require("./client.js");
8
9
  const keys_js_1 = require("../crypto/keys.js");
9
10
  const INITIAL_DELAY_MS = 5_000;
@@ -13,15 +14,20 @@ class AutoRegister extends node_events_1.EventEmitter {
13
14
  store;
14
15
  localProbe;
15
16
  keys;
17
+ daemonVersion;
18
+ getCardSummary;
16
19
  heartbeatTimer = null;
17
20
  initialTimer = null;
18
21
  registeredClawName = null;
19
- constructor(client, store, localProbe, keys) {
22
+ startedAt = Date.now();
23
+ constructor(client, store, localProbe, keys, daemonVersion = "unknown", getCardSummary) {
20
24
  super();
21
25
  this.client = client;
22
26
  this.store = store;
23
27
  this.localProbe = localProbe;
24
28
  this.keys = keys;
29
+ this.daemonVersion = daemonVersion;
30
+ this.getCardSummary = getCardSummary;
25
31
  }
26
32
  get clawName() {
27
33
  return this.registeredClawName;
@@ -59,25 +65,51 @@ class AutoRegister extends node_events_1.EventEmitter {
59
65
  this.emit("skip", "No local OpenClaw instance detected");
60
66
  return;
61
67
  }
62
- // Try agentId, then agentId-2, agentId-3, ... if name already taken by another owner
68
+ // Build list of base names to try: agentId first, then auto_name as fallback
69
+ const selfInstance = this.store.getAll().find((i) => i.is_self);
70
+ const autoName = selfInstance?.auto_name;
71
+ const bases = [agentId];
72
+ if (autoName && autoName !== agentId)
73
+ bases.push(autoName);
74
+ // Build metadata and card summary for this heartbeat
75
+ const uptimeHours = Math.round((Date.now() - this.startedAt) / 3_600_000 * 100) / 100;
76
+ const metadata = {
77
+ software_version: this.daemonVersion,
78
+ uptime_hours: uptimeHours,
79
+ os_platform: (0, node_os_1.platform)(),
80
+ instance_count: this.store.getAll().length,
81
+ };
82
+ const cardSummary = this.getCardSummary?.() ?? undefined;
83
+ // Try each base with suffixes -1, -2, ... up to MAX_SUFFIX if taken by another owner
63
84
  const MAX_SUFFIX = 10;
64
85
  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
86
+ outer: for (const base of bases) {
87
+ for (let i = 0; i <= MAX_SUFFIX; i++) {
88
+ const clawId = i === 0 ? base : `${base}-${i}`;
89
+ try {
90
+ result = await this.client.register({
91
+ claw_id: clawId,
92
+ metadata,
93
+ agent_card: cardSummary,
94
+ });
95
+ break outer;
96
+ }
97
+ catch (err) {
98
+ if (err instanceof client_js_1.RegistryError && err.statusCode === 409 && i < MAX_SUFFIX) {
99
+ continue; // name taken by another owner, try next suffix
100
+ }
101
+ if (err instanceof client_js_1.RegistryError && err.statusCode === 409 && i === MAX_SUFFIX) {
102
+ break; // exhausted suffixes for this base, try next base
103
+ }
104
+ this.emit("error", err);
105
+ return;
74
106
  }
75
- this.emit("error", err);
76
- return;
77
107
  }
78
108
  }
79
- if (!result)
109
+ if (!result) {
110
+ this.emit("error", new Error(`All candidate names exhausted (bases: ${bases.join(", ")})`));
80
111
  return;
112
+ }
81
113
  this.registeredClawName = result.record.name;
82
114
  // Start heartbeat on first successful registration
83
115
  if (!this.heartbeatTimer) {
@@ -85,13 +117,12 @@ class AutoRegister extends node_events_1.EventEmitter {
85
117
  this.tryRegister().catch(() => { });
86
118
  }, HEARTBEAT_INTERVAL_MS);
87
119
  }
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);
120
+ // Write claw_name back to the local instance in store (re-fetch after possible state change)
121
+ const registeredSelf = this.store.getAll().find((i) => i.is_self && i.agent_id === agentId);
122
+ if (registeredSelf) {
123
+ registeredSelf.claw_name = result.record.name;
124
+ registeredSelf.owner_pubkey = result.record.ownerPubkey;
125
+ this.store.upsert(registeredSelf);
95
126
  }
96
127
  this.emit("registered", {
97
128
  action: result.action,
@@ -33,11 +33,28 @@ export interface CheckNameResult {
33
33
  name: string;
34
34
  available: boolean;
35
35
  }
36
+ export interface InstanceMetadata {
37
+ software_version: string;
38
+ openclaw_version?: string;
39
+ uptime_hours: number;
40
+ os_platform: string;
41
+ instance_count: number;
42
+ }
43
+ export interface AgentCardSummary {
44
+ skills_count: number;
45
+ skills: string[];
46
+ capabilities?: Record<string, unknown>;
47
+ input_modes?: string[];
48
+ output_modes?: string[];
49
+ card_url?: string;
50
+ }
36
51
  export interface RegisterParams {
37
52
  claw_id: string;
38
53
  capabilities?: string[];
39
54
  relay_hint?: string;
40
55
  visibility?: "public" | "unlisted";
56
+ metadata?: InstanceMetadata;
57
+ agent_card?: AgentCardSummary;
41
58
  }
42
59
  export declare class RegistryClient {
43
60
  private readonly keys;
@@ -36,6 +36,8 @@ class RegistryClient {
36
36
  ...(params.capabilities && { capabilities: params.capabilities }),
37
37
  ...(params.relay_hint && { relay_hint: params.relay_hint }),
38
38
  ...(params.visibility && { visibility: params.visibility }),
39
+ ...(params.metadata && { metadata: params.metadata }),
40
+ ...(params.agent_card && { agent_card: params.agent_card }),
39
41
  };
40
42
  const body = {
41
43
  payload,
@@ -65,12 +65,16 @@ class RegistryStore extends node_events_1.EventEmitter {
65
65
  try {
66
66
  const raw = await fs.promises.readFile(this.registryPath, "utf-8");
67
67
  const data = JSON.parse(raw);
68
- if (data.schema_version === "5" && Array.isArray(data.instances)) {
69
- // v5: current schema, load directly
68
+ if ((data.schema_version === "6" || data.schema_version === "5") && Array.isArray(data.instances)) {
69
+ // v5/v6: compatible schemas (remote_card is optional), load directly
70
70
  for (const inst of data.instances) {
71
71
  const key = this.networkKey(inst.address, inst.gateway_port);
72
72
  this.instances.set(key, inst);
73
73
  }
74
+ if (data.schema_version === "5") {
75
+ // Bump to v6 on next flush
76
+ this.scheduleDirtyFlush();
77
+ }
74
78
  }
75
79
  else if (data.schema_version === "4" && Array.isArray(data.instances)) {
76
80
  // v4 → v5 migration: no data changes, just bump version
@@ -203,6 +207,8 @@ class RegistryStore extends node_events_1.EventEmitter {
203
207
  instance.owner_pubkey = instance.owner_pubkey ?? existing.owner_pubkey;
204
208
  // Preserve implementation (prefer new value if provided)
205
209
  instance.implementation = instance.implementation ?? existing.implementation;
210
+ // Preserve remote_card (fetched by CardFetcher)
211
+ instance.remote_card = instance.remote_card ?? existing.remote_card;
206
212
  this.instances.set(key, instance);
207
213
  this.scheduleDirtyFlush();
208
214
  this.emit("upsert", instance);
@@ -318,6 +324,7 @@ class RegistryStore extends node_events_1.EventEmitter {
318
324
  connectivity: incoming.connectivity ?? existing.connectivity,
319
325
  is_self: existing.is_self || incoming.is_self,
320
326
  implementation: incoming.implementation ?? existing.implementation,
327
+ remote_card: existing.remote_card ?? incoming.remote_card,
321
328
  };
322
329
  }
323
330
  /** Numeric priority for network scope: local > vpn > public */
@@ -343,7 +350,7 @@ class RegistryStore extends node_events_1.EventEmitter {
343
350
  }
344
351
  async flushNow() {
345
352
  const data = {
346
- schema_version: "5",
353
+ schema_version: "6",
347
354
  updated_at: new Date().toISOString(),
348
355
  instances: Array.from(this.instances.values()),
349
356
  };