clawnexus 0.3.1 → 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.
- package/dist/a2a/card.js +11 -6
- package/dist/a2a/fetcher.d.ts +26 -0
- package/dist/a2a/fetcher.js +152 -0
- package/dist/a2a/handler.d.ts +30 -0
- package/dist/a2a/handler.js +228 -0
- package/dist/a2a/store.d.ts +17 -0
- package/dist/a2a/store.js +134 -0
- package/dist/a2a/types.d.ts +45 -0
- package/dist/a2a/types.js +11 -0
- package/dist/agent/router.js +7 -2
- package/dist/api/server.d.ts +4 -1
- package/dist/api/server.js +96 -10
- package/dist/cli/index.js +6 -0
- package/dist/registry/auto-register.d.ts +5 -1
- package/dist/registry/auto-register.js +21 -2
- package/dist/registry/client.d.ts +17 -0
- package/dist/registry/client.js +2 -0
- package/dist/registry/store.js +10 -3
- package/dist/relay/connector.js +5 -1
- package/dist/types.d.ts +15 -1
- package/package.json +1 -1
package/dist/a2a/card.js
CHANGED
|
@@ -9,19 +9,24 @@ const DEFAULT_SKILL = {
|
|
|
9
9
|
tags: ["general"],
|
|
10
10
|
};
|
|
11
11
|
function buildAgentCard(instance, daemonVersion, skills) {
|
|
12
|
+
// Skill priority: remote_card.skills (remote) > skills param (local) > DEFAULT_SKILL
|
|
13
|
+
const resolvedSkills = instance.remote_card?.skills?.length
|
|
14
|
+
? instance.remote_card.skills
|
|
15
|
+
: (skills && skills.length > 0 ? skills : [DEFAULT_SKILL]);
|
|
16
|
+
const remoteCapabilities = instance.remote_card?.capabilities;
|
|
12
17
|
return {
|
|
13
18
|
name: instance.alias ?? instance.auto_name,
|
|
14
19
|
description: instance.display_name || instance.assistant_name,
|
|
15
20
|
url: `http://${instance.address}:17890`,
|
|
16
21
|
version: daemonVersion,
|
|
17
22
|
capabilities: {
|
|
18
|
-
streaming: false,
|
|
19
|
-
pushNotifications: false,
|
|
20
|
-
stateTransitionHistory: false,
|
|
23
|
+
streaming: remoteCapabilities?.streaming ?? false,
|
|
24
|
+
pushNotifications: remoteCapabilities?.pushNotifications ?? false,
|
|
25
|
+
stateTransitionHistory: remoteCapabilities?.stateTransitionHistory ?? false,
|
|
21
26
|
},
|
|
22
|
-
skills:
|
|
23
|
-
defaultInputModes: ["text/plain"],
|
|
24
|
-
defaultOutputModes: ["text/plain"],
|
|
27
|
+
skills: resolvedSkills,
|
|
28
|
+
defaultInputModes: instance.remote_card?.input_modes ?? ["text/plain"],
|
|
29
|
+
defaultOutputModes: instance.remote_card?.output_modes ?? ["text/plain"],
|
|
25
30
|
provider: {
|
|
26
31
|
name: "ClawNexus",
|
|
27
32
|
url: "https://github.com/SilverstreamsAI/ClawNexus",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { RegistryStore } from "../registry/store.js";
|
|
3
|
+
import type { ClawInstance, RemoteCard } from "../types.js";
|
|
4
|
+
export interface CardFetcherOptions {
|
|
5
|
+
refreshIntervalMs?: number;
|
|
6
|
+
fetchTimeoutMs?: number;
|
|
7
|
+
staleMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class CardFetcher extends EventEmitter {
|
|
10
|
+
private readonly store;
|
|
11
|
+
private readonly refreshIntervalMs;
|
|
12
|
+
private readonly fetchTimeoutMs;
|
|
13
|
+
private readonly staleMs;
|
|
14
|
+
private refreshTimer;
|
|
15
|
+
private readonly pendingKeys;
|
|
16
|
+
private stopped;
|
|
17
|
+
constructor(store: RegistryStore, opts?: CardFetcherOptions);
|
|
18
|
+
start(): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
private onUpsert;
|
|
21
|
+
private shouldSkip;
|
|
22
|
+
private fetchAndApply;
|
|
23
|
+
fetchCard(instance: ClawInstance): Promise<RemoteCard | null>;
|
|
24
|
+
refreshAll(): Promise<void>;
|
|
25
|
+
determineCardUrl(instance: ClawInstance): string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// CardFetcher — fetches remote Agent Cards from discovered instances
|
|
3
|
+
// Listens to store "upsert" events and populates remote_card on each instance.
|
|
4
|
+
// Self instances are skipped (they use local SkillsRegistry).
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CardFetcher = void 0;
|
|
7
|
+
const node_events_1 = require("node:events");
|
|
8
|
+
const REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
9
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
10
|
+
const STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
+
class CardFetcher extends node_events_1.EventEmitter {
|
|
12
|
+
store;
|
|
13
|
+
refreshIntervalMs;
|
|
14
|
+
fetchTimeoutMs;
|
|
15
|
+
staleMs;
|
|
16
|
+
refreshTimer = null;
|
|
17
|
+
pendingKeys = new Set();
|
|
18
|
+
stopped = false;
|
|
19
|
+
constructor(store, opts = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.store = store;
|
|
22
|
+
this.refreshIntervalMs = opts.refreshIntervalMs ?? REFRESH_INTERVAL_MS;
|
|
23
|
+
this.fetchTimeoutMs = opts.fetchTimeoutMs ?? FETCH_TIMEOUT_MS;
|
|
24
|
+
this.staleMs = opts.staleMs ?? STALE_MS;
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this.refreshTimer !== null)
|
|
28
|
+
return; // already running
|
|
29
|
+
this.stopped = false;
|
|
30
|
+
this.store.on("upsert", this.onUpsert);
|
|
31
|
+
// Initial fetch for all existing instances
|
|
32
|
+
this.refreshAll().catch((err) => {
|
|
33
|
+
console.log(`[clawnexus] [CardFetcher] Initial refresh failed (non-fatal): ${err}`);
|
|
34
|
+
});
|
|
35
|
+
// Periodic refresh
|
|
36
|
+
this.refreshTimer = setInterval(() => {
|
|
37
|
+
this.refreshAll().catch((err) => {
|
|
38
|
+
console.log(`[clawnexus] [CardFetcher] Periodic refresh failed (non-fatal): ${err}`);
|
|
39
|
+
});
|
|
40
|
+
}, this.refreshIntervalMs);
|
|
41
|
+
}
|
|
42
|
+
stop() {
|
|
43
|
+
this.stopped = true;
|
|
44
|
+
this.store.off("upsert", this.onUpsert);
|
|
45
|
+
if (this.refreshTimer) {
|
|
46
|
+
clearInterval(this.refreshTimer);
|
|
47
|
+
this.refreshTimer = null;
|
|
48
|
+
}
|
|
49
|
+
this.pendingKeys.clear();
|
|
50
|
+
}
|
|
51
|
+
onUpsert = (instance) => {
|
|
52
|
+
if (this.stopped)
|
|
53
|
+
return;
|
|
54
|
+
if (this.shouldSkip(instance))
|
|
55
|
+
return;
|
|
56
|
+
const key = this.store.networkKey(instance.address, instance.gateway_port);
|
|
57
|
+
// Guard against infinite loop: if we're currently processing this key, skip
|
|
58
|
+
if (this.pendingKeys.has(key))
|
|
59
|
+
return;
|
|
60
|
+
this.pendingKeys.add(key);
|
|
61
|
+
this.fetchAndApply(instance, key).finally(() => {
|
|
62
|
+
this.pendingKeys.delete(key);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
shouldSkip(instance) {
|
|
66
|
+
// Skip self instances — they use local SkillsRegistry
|
|
67
|
+
if (instance.is_self)
|
|
68
|
+
return true;
|
|
69
|
+
// Skip offline instances
|
|
70
|
+
if (instance.status === "offline")
|
|
71
|
+
return true;
|
|
72
|
+
// Skip if remote_card is fresh enough
|
|
73
|
+
if (instance.remote_card) {
|
|
74
|
+
const age = Date.now() - new Date(instance.remote_card.fetched_at).getTime();
|
|
75
|
+
if (age < this.staleMs)
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
async fetchAndApply(instance, key) {
|
|
81
|
+
try {
|
|
82
|
+
const card = await this.fetchCard(instance);
|
|
83
|
+
if (!card || this.stopped)
|
|
84
|
+
return;
|
|
85
|
+
// Re-fetch the instance from store (it may have been updated during fetch)
|
|
86
|
+
const current = this.store.getByNetworkKey(instance.address, instance.gateway_port);
|
|
87
|
+
if (!current)
|
|
88
|
+
return;
|
|
89
|
+
// Apply remote_card directly and re-upsert
|
|
90
|
+
current.remote_card = card;
|
|
91
|
+
// Use upsert so it persists (the pendingKeys guard prevents re-entry)
|
|
92
|
+
this.store.upsert(current);
|
|
93
|
+
this.emit("card_fetched", { key, skills_count: card.skills.length });
|
|
94
|
+
console.log(`[clawnexus] [CardFetcher] Fetched card for ${current.auto_name}: ${card.skills.length} skill(s)`);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
this.emit("card_error", { key, error: err });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async fetchCard(instance) {
|
|
101
|
+
const url = this.determineCardUrl(instance);
|
|
102
|
+
try {
|
|
103
|
+
const resp = await fetch(url, {
|
|
104
|
+
signal: AbortSignal.timeout(this.fetchTimeoutMs),
|
|
105
|
+
headers: { Accept: "application/json" },
|
|
106
|
+
});
|
|
107
|
+
if (!resp.ok)
|
|
108
|
+
return null;
|
|
109
|
+
const data = (await resp.json());
|
|
110
|
+
if (!data || !Array.isArray(data.skills))
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
skills: data.skills,
|
|
114
|
+
capabilities: data.capabilities,
|
|
115
|
+
input_modes: data.defaultInputModes,
|
|
116
|
+
output_modes: data.defaultOutputModes,
|
|
117
|
+
card_url: url,
|
|
118
|
+
fetched_at: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Timeout, network error, JSON parse error — all graceful
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async refreshAll() {
|
|
127
|
+
if (this.stopped)
|
|
128
|
+
return;
|
|
129
|
+
const instances = this.store.getAll();
|
|
130
|
+
const promises = [];
|
|
131
|
+
for (const inst of instances) {
|
|
132
|
+
if (this.shouldSkip(inst))
|
|
133
|
+
continue;
|
|
134
|
+
const key = this.store.networkKey(inst.address, inst.gateway_port);
|
|
135
|
+
if (this.pendingKeys.has(key))
|
|
136
|
+
continue;
|
|
137
|
+
this.pendingKeys.add(key);
|
|
138
|
+
promises.push(this.fetchAndApply(inst, key).finally(() => {
|
|
139
|
+
this.pendingKeys.delete(key);
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
await Promise.allSettled(promises);
|
|
143
|
+
}
|
|
144
|
+
determineCardUrl(instance) {
|
|
145
|
+
const proto = instance.tls ? "https" : "http";
|
|
146
|
+
// CDP-discovered instances have a ClawNexus daemon on port 17890
|
|
147
|
+
// Other discovery sources: try the daemon port too (if they have ClawNexus)
|
|
148
|
+
const port = 17890;
|
|
149
|
+
return `${proto}://${instance.address}:${port}/.well-known/agent-card.json`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.CardFetcher = CardFetcher;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { A2ATask, A2AError } from "./types.js";
|
|
2
|
+
import type { A2ATaskStore } from "./store.js";
|
|
3
|
+
export interface A2AHandlerOptions {
|
|
4
|
+
gatewayUrl?: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
maxConcurrent?: number;
|
|
7
|
+
store?: A2ATaskStore;
|
|
8
|
+
}
|
|
9
|
+
export declare class A2AHandler {
|
|
10
|
+
private readonly gatewayUrl;
|
|
11
|
+
private readonly timeoutMs;
|
|
12
|
+
private readonly maxConcurrent;
|
|
13
|
+
private readonly store;
|
|
14
|
+
private conn;
|
|
15
|
+
private connecting;
|
|
16
|
+
private reconnectAttempt;
|
|
17
|
+
private readonly sessions;
|
|
18
|
+
private activeTasks;
|
|
19
|
+
constructor(opts?: A2AHandlerOptions);
|
|
20
|
+
handleTaskSend(params: unknown): Promise<A2ATask | A2AError>;
|
|
21
|
+
getTask(taskId: string): A2ATask | undefined;
|
|
22
|
+
/** Clean up connection and pending sessions. */
|
|
23
|
+
close(): void;
|
|
24
|
+
private getConnection;
|
|
25
|
+
private connect;
|
|
26
|
+
private scheduleReconnect;
|
|
27
|
+
private sendAndWait;
|
|
28
|
+
private extractResponse;
|
|
29
|
+
private persistTask;
|
|
30
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A2A Task Handler — manages tasks/send and tasks/get
|
|
3
|
+
// Uses a persistent Gateway connection (lazy init, auto-reconnect) shared across
|
|
4
|
+
// concurrent tasks. Each task is identified by a unique sessionKey for multiplexing.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.A2AHandler = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const ws_1 = require("ws");
|
|
9
|
+
const gateway_js_1 = require("../agent/gateway.js");
|
|
10
|
+
const types_js_1 = require("./types.js");
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
12
|
+
const DEFAULT_MAX_CONCURRENT = 5;
|
|
13
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
14
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
15
|
+
class A2AHandler {
|
|
16
|
+
gatewayUrl;
|
|
17
|
+
timeoutMs;
|
|
18
|
+
maxConcurrent;
|
|
19
|
+
store;
|
|
20
|
+
// Persistent Gateway connection
|
|
21
|
+
conn = null;
|
|
22
|
+
connecting = null;
|
|
23
|
+
reconnectAttempt = 0;
|
|
24
|
+
// Session-based dispatch for multiplexed tasks
|
|
25
|
+
sessions = new Map();
|
|
26
|
+
activeTasks = 0;
|
|
27
|
+
constructor(opts = {}) {
|
|
28
|
+
this.gatewayUrl = opts.gatewayUrl ?? "ws://127.0.0.1:18789";
|
|
29
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
30
|
+
this.maxConcurrent = opts.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
31
|
+
this.store = opts.store ?? null;
|
|
32
|
+
}
|
|
33
|
+
async handleTaskSend(params) {
|
|
34
|
+
const p = params;
|
|
35
|
+
if (!p?.message?.parts?.length) {
|
|
36
|
+
return { code: types_js_1.JSON_RPC_INVALID_PARAMS, message: "Missing message with parts" };
|
|
37
|
+
}
|
|
38
|
+
const textParts = p.message.parts.filter((part) => part.type === "text");
|
|
39
|
+
if (textParts.length === 0) {
|
|
40
|
+
return { code: types_js_1.JSON_RPC_INVALID_PARAMS, message: "No text parts in message" };
|
|
41
|
+
}
|
|
42
|
+
// Concurrency guard
|
|
43
|
+
if (this.activeTasks >= this.maxConcurrent) {
|
|
44
|
+
return {
|
|
45
|
+
code: types_js_1.JSON_RPC_TASK_LIMIT_EXCEEDED,
|
|
46
|
+
message: `Too many concurrent tasks (max: ${this.maxConcurrent})`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const userText = textParts.map((part) => part.text).join("\n");
|
|
50
|
+
const taskId = (0, node_crypto_1.randomUUID)();
|
|
51
|
+
const sessionKey = `agent:main:main:dm:a2a-task-${taskId}`;
|
|
52
|
+
const task = {
|
|
53
|
+
id: taskId,
|
|
54
|
+
status: { state: "submitted" },
|
|
55
|
+
history: [p.message],
|
|
56
|
+
};
|
|
57
|
+
this.persistTask(task);
|
|
58
|
+
this.activeTasks++;
|
|
59
|
+
// Acquire shared connection
|
|
60
|
+
let conn;
|
|
61
|
+
try {
|
|
62
|
+
conn = await this.getConnection();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
this.activeTasks--;
|
|
66
|
+
task.status = {
|
|
67
|
+
state: "failed",
|
|
68
|
+
message: { role: "agent", parts: [{ type: "text", text: `Gateway connection failed: ${err.message}` }] },
|
|
69
|
+
};
|
|
70
|
+
this.persistTask(task);
|
|
71
|
+
return task;
|
|
72
|
+
}
|
|
73
|
+
task.status.state = "working";
|
|
74
|
+
this.persistTask(task);
|
|
75
|
+
try {
|
|
76
|
+
const result = await this.sendAndWait(conn.ws, sessionKey, userText);
|
|
77
|
+
const agentMsg = { role: "agent", parts: [{ type: "text", text: result }] };
|
|
78
|
+
task.status = { state: "completed", message: agentMsg };
|
|
79
|
+
task.artifacts = [{ parts: [{ type: "text", text: result }] }];
|
|
80
|
+
if (task.history)
|
|
81
|
+
task.history.push(agentMsg);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
task.status = {
|
|
85
|
+
state: "failed",
|
|
86
|
+
message: { role: "agent", parts: [{ type: "text", text: err.message }] },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this.activeTasks--;
|
|
91
|
+
this.persistTask(task);
|
|
92
|
+
}
|
|
93
|
+
return task;
|
|
94
|
+
}
|
|
95
|
+
getTask(taskId) {
|
|
96
|
+
return this.store?.get(taskId);
|
|
97
|
+
}
|
|
98
|
+
/** Clean up connection and pending sessions. */
|
|
99
|
+
close() {
|
|
100
|
+
for (const [, session] of this.sessions) {
|
|
101
|
+
clearTimeout(session.timer);
|
|
102
|
+
session.reject(new Error("Handler closed"));
|
|
103
|
+
}
|
|
104
|
+
this.sessions.clear();
|
|
105
|
+
if (this.conn) {
|
|
106
|
+
this.conn.close();
|
|
107
|
+
this.conn = null;
|
|
108
|
+
}
|
|
109
|
+
this.connecting = null;
|
|
110
|
+
}
|
|
111
|
+
// --- Connection management ---
|
|
112
|
+
async getConnection() {
|
|
113
|
+
if (this.conn && this.conn.ws.readyState === ws_1.WebSocket.OPEN) {
|
|
114
|
+
return this.conn;
|
|
115
|
+
}
|
|
116
|
+
// Avoid duplicate connect attempts
|
|
117
|
+
if (this.connecting)
|
|
118
|
+
return this.connecting;
|
|
119
|
+
this.connecting = this.connect();
|
|
120
|
+
try {
|
|
121
|
+
return await this.connecting;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.connecting = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async connect() {
|
|
128
|
+
const conn = await (0, gateway_js_1.connectGateway)({ gatewayUrl: this.gatewayUrl });
|
|
129
|
+
this.conn = conn;
|
|
130
|
+
this.reconnectAttempt = 0;
|
|
131
|
+
// Shared event dispatch: route incoming events to the right session
|
|
132
|
+
conn.ws.on("message", (data) => {
|
|
133
|
+
let msg;
|
|
134
|
+
try {
|
|
135
|
+
msg = JSON.parse(data.toString());
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (msg.type !== "event")
|
|
141
|
+
return;
|
|
142
|
+
const event = msg.event;
|
|
143
|
+
if (event !== "chat" && event !== "chat.update")
|
|
144
|
+
return;
|
|
145
|
+
const payload = msg.payload;
|
|
146
|
+
const sk = payload?.sessionKey ?? msg.sessionKey;
|
|
147
|
+
if (!sk)
|
|
148
|
+
return;
|
|
149
|
+
const session = this.sessions.get(sk);
|
|
150
|
+
if (!session)
|
|
151
|
+
return;
|
|
152
|
+
const state = payload?.state;
|
|
153
|
+
if (state === "final") {
|
|
154
|
+
this.sessions.delete(sk);
|
|
155
|
+
clearTimeout(session.timer);
|
|
156
|
+
session.resolve(this.extractResponse(payload));
|
|
157
|
+
}
|
|
158
|
+
else if (state === "error") {
|
|
159
|
+
this.sessions.delete(sk);
|
|
160
|
+
clearTimeout(session.timer);
|
|
161
|
+
session.reject(new Error(payload?.errorMessage ?? "OpenClaw chat error"));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
conn.ws.on("close", () => {
|
|
165
|
+
this.conn = null;
|
|
166
|
+
// Reject all pending sessions — they'll fail their tasks gracefully
|
|
167
|
+
for (const [sk, session] of this.sessions) {
|
|
168
|
+
clearTimeout(session.timer);
|
|
169
|
+
session.reject(new Error("Gateway connection closed during task execution"));
|
|
170
|
+
this.sessions.delete(sk);
|
|
171
|
+
}
|
|
172
|
+
this.scheduleReconnect();
|
|
173
|
+
});
|
|
174
|
+
return conn;
|
|
175
|
+
}
|
|
176
|
+
scheduleReconnect() {
|
|
177
|
+
// Only reconnect if there could be future tasks (handler not closed)
|
|
178
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS);
|
|
179
|
+
this.reconnectAttempt++;
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
// Lazy: don't eagerly reconnect, just clear state so next getConnection() will connect fresh
|
|
182
|
+
this.connecting = null;
|
|
183
|
+
}, delay);
|
|
184
|
+
}
|
|
185
|
+
// --- Task execution ---
|
|
186
|
+
sendAndWait(ws, sessionKey, message) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const requestId = (0, node_crypto_1.randomUUID)();
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
this.sessions.delete(sessionKey);
|
|
191
|
+
reject(new Error("Task execution timed out"));
|
|
192
|
+
}, this.timeoutMs);
|
|
193
|
+
this.sessions.set(sessionKey, { resolve, reject, timer });
|
|
194
|
+
ws.send(JSON.stringify({
|
|
195
|
+
type: "req",
|
|
196
|
+
id: requestId,
|
|
197
|
+
method: "chat.send",
|
|
198
|
+
params: { sessionKey, message, idempotencyKey: requestId },
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
extractResponse(payload) {
|
|
203
|
+
if (!payload)
|
|
204
|
+
return "Task completed (no output)";
|
|
205
|
+
const messages = payload.messages;
|
|
206
|
+
if (messages && messages.length > 0) {
|
|
207
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
208
|
+
if (lastAssistant) {
|
|
209
|
+
const content = lastAssistant.content;
|
|
210
|
+
if (typeof content === "string")
|
|
211
|
+
return content;
|
|
212
|
+
if (Array.isArray(content)) {
|
|
213
|
+
return content
|
|
214
|
+
.filter((b) => b.type === "text")
|
|
215
|
+
.map((b) => b.text)
|
|
216
|
+
.join("\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return payload.content ?? "Task completed (no output)";
|
|
221
|
+
}
|
|
222
|
+
// --- Persistence ---
|
|
223
|
+
persistTask(task) {
|
|
224
|
+
if (this.store)
|
|
225
|
+
this.store.put(task);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
exports.A2AHandler = A2AHandler;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { A2ATask } from "./types.js";
|
|
2
|
+
export declare class A2ATaskStore {
|
|
3
|
+
private readonly tasks;
|
|
4
|
+
private readonly filePath;
|
|
5
|
+
private flushTimer;
|
|
6
|
+
private flushInProgress;
|
|
7
|
+
private dirty;
|
|
8
|
+
constructor(configDir?: string);
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
get(taskId: string): A2ATask | undefined;
|
|
11
|
+
put(task: A2ATask): void;
|
|
12
|
+
getAll(): A2ATask[];
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
private evict;
|
|
15
|
+
private scheduleDirtyFlush;
|
|
16
|
+
private flushNow;
|
|
17
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A2A Task Store — persists task records to ~/.clawnexus/a2a-tasks.json
|
|
3
|
+
// FIFO eviction: keeps at most MAX_TASKS entries.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.A2ATaskStore = void 0;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
const CLAWNEXUS_DIR = path.join(os.homedir(), ".clawnexus");
|
|
43
|
+
const DEFAULT_FILE = "a2a-tasks.json";
|
|
44
|
+
const MAX_TASKS = 100;
|
|
45
|
+
const DEBOUNCE_MS = 500;
|
|
46
|
+
class A2ATaskStore {
|
|
47
|
+
tasks = new Map();
|
|
48
|
+
filePath;
|
|
49
|
+
flushTimer = null;
|
|
50
|
+
flushInProgress = null;
|
|
51
|
+
dirty = false;
|
|
52
|
+
constructor(configDir) {
|
|
53
|
+
const dir = configDir ?? CLAWNEXUS_DIR;
|
|
54
|
+
this.filePath = path.join(dir, DEFAULT_FILE);
|
|
55
|
+
}
|
|
56
|
+
async init() {
|
|
57
|
+
await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
58
|
+
if (fs.existsSync(this.filePath)) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await fs.promises.readFile(this.filePath, "utf-8");
|
|
61
|
+
const data = JSON.parse(raw);
|
|
62
|
+
if (data.version === 1 && Array.isArray(data.tasks)) {
|
|
63
|
+
for (const t of data.tasks) {
|
|
64
|
+
this.tasks.set(t.id, t);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Corrupted file — start fresh
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
get(taskId) {
|
|
74
|
+
return this.tasks.get(taskId);
|
|
75
|
+
}
|
|
76
|
+
put(task) {
|
|
77
|
+
this.tasks.set(task.id, task);
|
|
78
|
+
this.evict();
|
|
79
|
+
this.scheduleDirtyFlush();
|
|
80
|
+
}
|
|
81
|
+
getAll() {
|
|
82
|
+
return Array.from(this.tasks.values());
|
|
83
|
+
}
|
|
84
|
+
async close() {
|
|
85
|
+
if (this.flushTimer) {
|
|
86
|
+
clearTimeout(this.flushTimer);
|
|
87
|
+
this.flushTimer = null;
|
|
88
|
+
}
|
|
89
|
+
// Wait for any in-progress timer-based flush before deciding whether to flush again
|
|
90
|
+
if (this.flushInProgress) {
|
|
91
|
+
await this.flushInProgress;
|
|
92
|
+
}
|
|
93
|
+
if (this.dirty) {
|
|
94
|
+
await this.flushNow();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
evict() {
|
|
98
|
+
if (this.tasks.size <= MAX_TASKS)
|
|
99
|
+
return;
|
|
100
|
+
// Map iteration order = insertion order; delete oldest entries
|
|
101
|
+
const excess = this.tasks.size - MAX_TASKS;
|
|
102
|
+
let removed = 0;
|
|
103
|
+
for (const key of this.tasks.keys()) {
|
|
104
|
+
if (removed >= excess)
|
|
105
|
+
break;
|
|
106
|
+
this.tasks.delete(key);
|
|
107
|
+
removed++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
scheduleDirtyFlush() {
|
|
111
|
+
this.dirty = true;
|
|
112
|
+
if (this.flushTimer)
|
|
113
|
+
return;
|
|
114
|
+
this.flushTimer = setTimeout(() => {
|
|
115
|
+
this.flushTimer = null;
|
|
116
|
+
this.flushInProgress = this.flushNow().finally(() => {
|
|
117
|
+
this.flushInProgress = null;
|
|
118
|
+
});
|
|
119
|
+
}, DEBOUNCE_MS);
|
|
120
|
+
}
|
|
121
|
+
async flushNow() {
|
|
122
|
+
const data = {
|
|
123
|
+
version: 1,
|
|
124
|
+
updated_at: new Date().toISOString(),
|
|
125
|
+
tasks: Array.from(this.tasks.values()),
|
|
126
|
+
};
|
|
127
|
+
const json = JSON.stringify(data, null, 2);
|
|
128
|
+
const tmpPath = this.filePath + ".tmp";
|
|
129
|
+
await fs.promises.writeFile(tmpPath, json, "utf-8");
|
|
130
|
+
await fs.promises.rename(tmpPath, this.filePath);
|
|
131
|
+
this.dirty = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.A2ATaskStore = A2ATaskStore;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface A2ARequest {
|
|
2
|
+
jsonrpc: "2.0";
|
|
3
|
+
method: string;
|
|
4
|
+
id: string | number;
|
|
5
|
+
params?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface A2AResponse {
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
id: string | number;
|
|
10
|
+
result?: unknown;
|
|
11
|
+
error?: A2AError;
|
|
12
|
+
}
|
|
13
|
+
export interface A2AError {
|
|
14
|
+
code: number;
|
|
15
|
+
message: string;
|
|
16
|
+
data?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface A2ATask {
|
|
19
|
+
id: string;
|
|
20
|
+
status: A2ATaskStatus;
|
|
21
|
+
artifacts?: A2AArtifact[];
|
|
22
|
+
history?: A2AMessage[];
|
|
23
|
+
}
|
|
24
|
+
export interface A2ATaskStatus {
|
|
25
|
+
state: "submitted" | "working" | "completed" | "failed" | "canceled";
|
|
26
|
+
message?: A2AMessage;
|
|
27
|
+
}
|
|
28
|
+
export interface A2AMessage {
|
|
29
|
+
role: "user" | "agent";
|
|
30
|
+
parts: A2APart[];
|
|
31
|
+
}
|
|
32
|
+
export type A2APart = {
|
|
33
|
+
type: "text";
|
|
34
|
+
text: string;
|
|
35
|
+
};
|
|
36
|
+
export interface A2AArtifact {
|
|
37
|
+
name?: string;
|
|
38
|
+
parts: A2APart[];
|
|
39
|
+
}
|
|
40
|
+
export declare const JSON_RPC_PARSE_ERROR = -32700;
|
|
41
|
+
export declare const JSON_RPC_INVALID_REQUEST = -32600;
|
|
42
|
+
export declare const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
43
|
+
export declare const JSON_RPC_INVALID_PARAMS = -32602;
|
|
44
|
+
export declare const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
45
|
+
export declare const JSON_RPC_TASK_LIMIT_EXCEEDED = -32005;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A2A JSON-RPC 2.0 types and Task model (spec v0.2.1)
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.JSON_RPC_TASK_LIMIT_EXCEEDED = exports.JSON_RPC_INTERNAL_ERROR = exports.JSON_RPC_INVALID_PARAMS = exports.JSON_RPC_METHOD_NOT_FOUND = exports.JSON_RPC_INVALID_REQUEST = exports.JSON_RPC_PARSE_ERROR = void 0;
|
|
5
|
+
// --- JSON-RPC Error Codes ---
|
|
6
|
+
exports.JSON_RPC_PARSE_ERROR = -32700;
|
|
7
|
+
exports.JSON_RPC_INVALID_REQUEST = -32600;
|
|
8
|
+
exports.JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
9
|
+
exports.JSON_RPC_INVALID_PARAMS = -32602;
|
|
10
|
+
exports.JSON_RPC_INTERNAL_ERROR = -32603;
|
|
11
|
+
exports.JSON_RPC_TASK_LIMIT_EXCEEDED = -32005;
|
package/dist/agent/router.js
CHANGED
|
@@ -47,7 +47,11 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
sendMessage(roomId, envelope) {
|
|
50
|
-
|
|
50
|
+
const result = this.connector.sendData(roomId, JSON.stringify(envelope));
|
|
51
|
+
if (!result) {
|
|
52
|
+
console.log(`[clawnexus] [Router] sendMessage FAILED — room=${roomId} (no room or no session_key)`);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
51
55
|
}
|
|
52
56
|
/** Initiate a propose to a peer (outbound task) */
|
|
53
57
|
propose(roomId, targetClawId, task) {
|
|
@@ -67,7 +71,8 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
67
71
|
room_id: roomId,
|
|
68
72
|
};
|
|
69
73
|
this.tasks.create(record);
|
|
70
|
-
this.sendMessage(roomId, envelope);
|
|
74
|
+
const sent = this.sendMessage(roomId, envelope);
|
|
75
|
+
console.log(`[clawnexus] [Router] propose sent=${sent} room=${roomId} target=${targetClawId}`);
|
|
71
76
|
this.emit("outbound", envelope);
|
|
72
77
|
return record;
|
|
73
78
|
}
|
package/dist/api/server.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { RegistryClient } from "../registry/client.js";
|
|
|
15
15
|
import { AutoRegister } from "../registry/auto-register.js";
|
|
16
16
|
import { RemoteDiscovery } from "../registry/discovery.js";
|
|
17
17
|
import { RelayConnector } from "../relay/connector.js";
|
|
18
|
+
import { CardFetcher } from "../a2a/fetcher.js";
|
|
19
|
+
import { A2AHandler } from "../a2a/handler.js";
|
|
18
20
|
import { SkillsRegistry } from "../agent/services.js";
|
|
19
21
|
export declare function registerRelayRoutes(app: FastifyInstance, getConnector: () => RelayConnector | null, getTokenRefresher?: () => (() => Promise<string>) | null): void;
|
|
20
22
|
export interface AgentDeps {
|
|
@@ -43,7 +45,7 @@ export interface DiagnosticsDeps {
|
|
|
43
45
|
unreachable: UnreachableInstance[];
|
|
44
46
|
}
|
|
45
47
|
export declare function registerDiagnosticsRoutes(app: FastifyInstance, deps: DiagnosticsDeps): void;
|
|
46
|
-
export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string, skillsRegistry?: SkillsRegistry): void;
|
|
48
|
+
export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string, skillsRegistry?: SkillsRegistry, a2aHandler?: A2AHandler): void;
|
|
47
49
|
export interface DaemonOptions {
|
|
48
50
|
port?: number;
|
|
49
51
|
host?: string;
|
|
@@ -62,6 +64,7 @@ export interface DaemonHandle {
|
|
|
62
64
|
tasks: TaskManager;
|
|
63
65
|
getRouter: () => AgentRouter | null;
|
|
64
66
|
skillsRegistry: SkillsRegistry;
|
|
67
|
+
cardFetcher: CardFetcher;
|
|
65
68
|
registryClient: RegistryClient | null;
|
|
66
69
|
autoRegister: AutoRegister | null;
|
|
67
70
|
remoteDiscovery: RemoteDiscovery | null;
|
package/dist/api/server.js
CHANGED
|
@@ -30,6 +30,10 @@ const auto_register_js_1 = require("../registry/auto-register.js");
|
|
|
30
30
|
const discovery_js_1 = require("../registry/discovery.js");
|
|
31
31
|
const connector_js_1 = require("../relay/connector.js");
|
|
32
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");
|
|
33
37
|
const services_js_1 = require("../agent/services.js");
|
|
34
38
|
const node_fs_1 = require("node:fs");
|
|
35
39
|
const node_path_1 = require("node:path");
|
|
@@ -344,7 +348,7 @@ function registerDiagnosticsRoutes(app, deps) {
|
|
|
344
348
|
};
|
|
345
349
|
});
|
|
346
350
|
}
|
|
347
|
-
function registerA2aRoutes(app, store, daemonVersion, skillsRegistry) {
|
|
351
|
+
function registerA2aRoutes(app, store, daemonVersion, skillsRegistry, a2aHandler) {
|
|
348
352
|
// A2A standard well-known endpoint — returns card for the local (is_self) instance
|
|
349
353
|
app.get("/.well-known/agent-card.json", async (_request, reply) => {
|
|
350
354
|
const self = store.getAll().find((i) => i.is_self);
|
|
@@ -356,8 +360,8 @@ function registerA2aRoutes(app, store, daemonVersion, skillsRegistry) {
|
|
|
356
360
|
// All instances as Agent Cards
|
|
357
361
|
app.get("/a2a/cards", async () => {
|
|
358
362
|
const instances = store.getAll();
|
|
359
|
-
const
|
|
360
|
-
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));
|
|
361
365
|
return { count: cards.length, cards };
|
|
362
366
|
});
|
|
363
367
|
// Single instance Agent Card by name
|
|
@@ -366,8 +370,69 @@ function registerA2aRoutes(app, store, daemonVersion, skillsRegistry) {
|
|
|
366
370
|
if (!inst) {
|
|
367
371
|
return reply.status(404).send({ error: "Instance not found" });
|
|
368
372
|
}
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
436
|
}
|
|
372
437
|
async function startDaemon(options = {}) {
|
|
373
438
|
const port = options.port ?? PORT;
|
|
@@ -402,6 +467,8 @@ async function startDaemon(options = {}) {
|
|
|
402
467
|
});
|
|
403
468
|
// 6c. Create SkillsRegistry
|
|
404
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);
|
|
405
472
|
// 7. Detect WireGuard interfaces
|
|
406
473
|
const wgInfo = await (0, wireguard_js_1.detectWireGuard)();
|
|
407
474
|
// 8. Create and configure Fastify app
|
|
@@ -421,11 +488,12 @@ async function startDaemon(options = {}) {
|
|
|
421
488
|
else {
|
|
422
489
|
console.log("[clawnexus] [WireGuard] No WireGuard interfaces detected");
|
|
423
490
|
}
|
|
491
|
+
const daemonPkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../../package.json"), "utf-8"));
|
|
424
492
|
// Health endpoint with component status
|
|
425
493
|
app.get("/health", async () => ({
|
|
426
494
|
status: "ok",
|
|
427
495
|
service: "clawnexus-daemon",
|
|
428
|
-
version:
|
|
496
|
+
version: daemonPkg.version,
|
|
429
497
|
timestamp: new Date().toISOString(),
|
|
430
498
|
components: {
|
|
431
499
|
registry: { instances: store.size },
|
|
@@ -476,9 +544,11 @@ async function startDaemon(options = {}) {
|
|
|
476
544
|
getAutoRegister: () => autoRegister,
|
|
477
545
|
unreachable,
|
|
478
546
|
});
|
|
479
|
-
// A2A Agent Card routes
|
|
480
|
-
const
|
|
481
|
-
|
|
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);
|
|
482
552
|
// 9. Initialize Registry integration (non-fatal — LAN must work without it)
|
|
483
553
|
let identityKeys = null;
|
|
484
554
|
let registryClient = null;
|
|
@@ -491,15 +561,27 @@ async function startDaemon(options = {}) {
|
|
|
491
561
|
broadcast.sendAnnounce();
|
|
492
562
|
// Start skills registry once we know the local Gateway is available
|
|
493
563
|
skillsRegistry.start();
|
|
564
|
+
// Start fetching remote Agent Cards
|
|
565
|
+
cardFetcher.start();
|
|
494
566
|
});
|
|
495
567
|
localProbe.on("local:unavailable", () => {
|
|
496
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();
|
|
497
571
|
});
|
|
498
572
|
// Initialize registry after LocalProbe (needs agentId for registration)
|
|
499
573
|
try {
|
|
500
574
|
identityKeys = await (0, keys_js_1.loadOrCreateKeys)();
|
|
501
575
|
registryClient = new client_js_1.RegistryClient(identityKeys);
|
|
502
|
-
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
|
+
});
|
|
503
585
|
remoteDiscovery = new discovery_js_1.RemoteDiscovery(registryClient, store);
|
|
504
586
|
registerRegistryRoutes(app, {
|
|
505
587
|
autoRegister,
|
|
@@ -632,6 +714,7 @@ async function startDaemon(options = {}) {
|
|
|
632
714
|
app.addHook("onClose", async () => {
|
|
633
715
|
if (tokenRefreshTimer)
|
|
634
716
|
clearInterval(tokenRefreshTimer);
|
|
717
|
+
cardFetcher.stop();
|
|
635
718
|
skillsRegistry.stop();
|
|
636
719
|
await taskExecutor.close();
|
|
637
720
|
autoRegister?.stop();
|
|
@@ -642,6 +725,8 @@ async function startDaemon(options = {}) {
|
|
|
642
725
|
mdns.stop();
|
|
643
726
|
await broadcast.stop();
|
|
644
727
|
connector?.disconnect();
|
|
728
|
+
a2aHandler.close();
|
|
729
|
+
await a2aTaskStore.close();
|
|
645
730
|
await store.close();
|
|
646
731
|
});
|
|
647
732
|
await app.listen({ port, host });
|
|
@@ -683,6 +768,7 @@ async function startDaemon(options = {}) {
|
|
|
683
768
|
tasks: taskManager,
|
|
684
769
|
getRouter: () => agentRouter,
|
|
685
770
|
skillsRegistry,
|
|
771
|
+
cardFetcher,
|
|
686
772
|
registryClient,
|
|
687
773
|
autoRegister,
|
|
688
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -65,6 +71,15 @@ class AutoRegister extends node_events_1.EventEmitter {
|
|
|
65
71
|
const bases = [agentId];
|
|
66
72
|
if (autoName && autoName !== agentId)
|
|
67
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;
|
|
68
83
|
// Try each base with suffixes -1, -2, ... up to MAX_SUFFIX if taken by another owner
|
|
69
84
|
const MAX_SUFFIX = 10;
|
|
70
85
|
let result = null;
|
|
@@ -72,7 +87,11 @@ class AutoRegister extends node_events_1.EventEmitter {
|
|
|
72
87
|
for (let i = 0; i <= MAX_SUFFIX; i++) {
|
|
73
88
|
const clawId = i === 0 ? base : `${base}-${i}`;
|
|
74
89
|
try {
|
|
75
|
-
result = await this.client.register({
|
|
90
|
+
result = await this.client.register({
|
|
91
|
+
claw_id: clawId,
|
|
92
|
+
metadata,
|
|
93
|
+
agent_card: cardSummary,
|
|
94
|
+
});
|
|
76
95
|
break outer;
|
|
77
96
|
}
|
|
78
97
|
catch (err) {
|
|
@@ -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;
|
package/dist/registry/client.js
CHANGED
|
@@ -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,
|
package/dist/registry/store.js
CHANGED
|
@@ -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:
|
|
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: "
|
|
353
|
+
schema_version: "6",
|
|
347
354
|
updated_at: new Date().toISOString(),
|
|
348
355
|
instances: Array.from(this.instances.values()),
|
|
349
356
|
};
|
package/dist/relay/connector.js
CHANGED
|
@@ -190,9 +190,13 @@ class RelayConnector extends node_events_1.EventEmitter {
|
|
|
190
190
|
this.emit("data", msg.room_id, plaintext);
|
|
191
191
|
}
|
|
192
192
|
catch {
|
|
193
|
-
|
|
193
|
+
// Decrypt failed — log and skip, do not emit "error" (would crash if no listener)
|
|
194
|
+
console.log(`[clawnexus] [Relay] Failed to decrypt message in room ${msg.room_id} — skipping`);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(`[clawnexus] [Relay] DATA in room ${msg.room_id} — no session_key yet, dropping`);
|
|
199
|
+
}
|
|
196
200
|
break;
|
|
197
201
|
}
|
|
198
202
|
case "PEER_LEFT": {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import type { AgentSkill } from "./a2a/card.js";
|
|
2
|
+
export interface RemoteCard {
|
|
3
|
+
skills: AgentSkill[];
|
|
4
|
+
capabilities?: {
|
|
5
|
+
streaming?: boolean;
|
|
6
|
+
pushNotifications?: boolean;
|
|
7
|
+
stateTransitionHistory?: boolean;
|
|
8
|
+
};
|
|
9
|
+
input_modes?: string[];
|
|
10
|
+
output_modes?: string[];
|
|
11
|
+
card_url: string;
|
|
12
|
+
fetched_at: string;
|
|
13
|
+
}
|
|
1
14
|
export type ClawImplementation = "openclaw" | "goclaw" | "zeroclaw" | "picoclaw" | "nanoclaw" | "nanobot" | "openfang" | "unknown";
|
|
2
15
|
export interface ClawInstance {
|
|
3
16
|
agent_id: string;
|
|
@@ -20,6 +33,7 @@ export interface ClawInstance {
|
|
|
20
33
|
claw_name?: string;
|
|
21
34
|
owner_pubkey?: string;
|
|
22
35
|
implementation?: ClawImplementation;
|
|
36
|
+
remote_card?: RemoteCard;
|
|
23
37
|
labels?: Record<string, string>;
|
|
24
38
|
}
|
|
25
39
|
export interface Connectivity {
|
|
@@ -39,7 +53,7 @@ export interface UnreachableInstance {
|
|
|
39
53
|
discovered_at: string;
|
|
40
54
|
}
|
|
41
55
|
export interface RegistryFile {
|
|
42
|
-
schema_version: "2" | "3" | "4" | "5";
|
|
56
|
+
schema_version: "2" | "3" | "4" | "5" | "6";
|
|
43
57
|
updated_at: string;
|
|
44
58
|
instances: ClawInstance[];
|
|
45
59
|
}
|