clawnexus 0.2.8 → 0.3.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.
@@ -0,0 +1,29 @@
1
+ import type { ClawInstance } from "../types.js";
2
+ export interface AgentSkill {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ tags: string[];
7
+ examples?: string[];
8
+ inputModes?: string[];
9
+ outputModes?: string[];
10
+ }
11
+ export interface AgentCard {
12
+ name: string;
13
+ description: string;
14
+ url: string;
15
+ version: string;
16
+ capabilities: {
17
+ streaming: boolean;
18
+ pushNotifications: boolean;
19
+ stateTransitionHistory: boolean;
20
+ };
21
+ skills: AgentSkill[];
22
+ defaultInputModes: string[];
23
+ defaultOutputModes: string[];
24
+ provider: {
25
+ name: string;
26
+ url?: string;
27
+ };
28
+ }
29
+ export declare function buildAgentCard(instance: ClawInstance, daemonVersion: string): AgentCard;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // A2A Agent Card generation — maps ClawInstance to A2A v0.3.0 Agent Card
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.buildAgentCard = buildAgentCard;
5
+ const DEFAULT_SKILL = {
6
+ id: "general-assistant",
7
+ name: "General Assistant",
8
+ description: "General-purpose AI assistant",
9
+ tags: ["general"],
10
+ };
11
+ function buildAgentCard(instance, daemonVersion) {
12
+ return {
13
+ name: instance.alias ?? instance.auto_name,
14
+ description: instance.display_name || instance.assistant_name,
15
+ url: `http://${instance.address}:17890`,
16
+ version: daemonVersion,
17
+ capabilities: {
18
+ streaming: false,
19
+ pushNotifications: false,
20
+ stateTransitionHistory: false,
21
+ },
22
+ skills: [DEFAULT_SKILL],
23
+ defaultInputModes: ["text/plain"],
24
+ defaultOutputModes: ["text/plain"],
25
+ provider: {
26
+ name: "ClawNexus",
27
+ url: "https://github.com/SilverstreamsAI/ClawNexus",
28
+ },
29
+ };
30
+ }
@@ -4,11 +4,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.ADAPTERS = void 0;
5
5
  exports.getAdapter = getAdapter;
6
6
  exports.getAllAdapterPorts = getAllAdapterPorts;
7
+ const openclaw_js_1 = require("./openclaw.js");
7
8
  const nanoclaw_js_1 = require("./nanoclaw.js");
8
9
  const nanobot_js_1 = require("./nanobot.js");
10
+ const openfang_js_1 = require("./openfang.js");
9
11
  exports.ADAPTERS = [
12
+ new openclaw_js_1.OpenClawAdapter(),
10
13
  new nanoclaw_js_1.NanoClawAdapter(),
11
14
  new nanobot_js_1.NanoBotAdapter(),
15
+ new openfang_js_1.OpenFangAdapter(),
12
16
  ];
13
17
  function getAdapter(name) {
14
18
  return exports.ADAPTERS.find((a) => a.name === name);
@@ -5,6 +5,7 @@ export declare class NanoBotAdapter implements FrameworkAdapter {
5
5
  readonly defaultPorts: number[];
6
6
  probe(host: string, port: number): Promise<ProbeResult | null>;
7
7
  toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
+ healthCheck(host: string, port: number): Promise<boolean>;
8
9
  private probeHealth;
9
10
  private probeApiHealth;
10
11
  private extractProbeResult;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // NanoBot adapter — Python variant, default ports 8000/8080
2
+ // NanoBot adapter — Python variant, default ports 18790/8000/8080
3
3
  // Probe: /health → check for framework/app: "nanobot", fallback /api/health
4
4
  // Heuristic: python_version field on expected port → infer nanobot
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -7,7 +7,7 @@ exports.NanoBotAdapter = void 0;
7
7
  const PROBE_TIMEOUT = 2_000;
8
8
  class NanoBotAdapter {
9
9
  name = "nanobot";
10
- defaultPorts = [8000, 8080];
10
+ defaultPorts = [18790, 8000, 8080];
11
11
  async probe(host, port) {
12
12
  // Try /health first
13
13
  const healthResult = await this.probeHealth(host, port);
@@ -30,6 +30,21 @@ class NanoBotAdapter {
30
30
  implementation: "nanobot",
31
31
  };
32
32
  }
33
+ async healthCheck(host, port) {
34
+ try {
35
+ const res = await fetch(`http://${host}:${port}/health`, {
36
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
37
+ });
38
+ if (!res.ok)
39
+ return false;
40
+ const data = (await res.json());
41
+ return data.framework === "nanobot" || data.app === "nanobot" ||
42
+ (typeof data.python_version === "string" && this.defaultPorts.includes(port));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
33
48
  async probeHealth(host, port) {
34
49
  try {
35
50
  const res = await fetch(`http://${host}:${port}/health`, {
@@ -3,8 +3,25 @@ import type { FrameworkAdapter, ProbeResult } from "./types.js";
3
3
  export declare class NanoClawAdapter implements FrameworkAdapter {
4
4
  readonly name = "nanoclaw";
5
5
  readonly defaultPorts: number[];
6
- probe(host: string, port: number): Promise<ProbeResult | null>;
7
- toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
- private probeHealth;
9
- private probeApiInfo;
6
+ private _lastProjectDir;
7
+ probe(_host: string, _port: number): Promise<ProbeResult | null>;
8
+ probeLocal(): Promise<ProbeResult | null>;
9
+ healthCheckLocal(): Promise<boolean>;
10
+ toClawInstance(host: string, _port: number, probe: ProbeResult): Partial<ClawInstance>;
11
+ healthCheck(_host: string, _port: number): Promise<boolean>;
12
+ /** Scan /proc for Node.js processes whose cwd contains a nanoclaw package.json */
13
+ private _findFromProc;
14
+ /** Check common paths under home directory */
15
+ private _findFromCandidateDirs;
16
+ /** Check if a directory is a nanoclaw project */
17
+ private _isNanoClawDir;
18
+ /** Read package.json and verify it's nanoclaw */
19
+ private _readPackageJson;
20
+ /** Extract ASSISTANT_NAME from .env file */
21
+ private _readAssistantName;
22
+ /** Find a running process with cwd matching the project dir */
23
+ private _findRunningPid;
24
+ /** Count JSON files in data/ipc/{channel}/messages/ as a proxy for active tasks */
25
+ private _countIpcTasks;
26
+ private _fileExists;
10
27
  }
@@ -1,74 +1,259 @@
1
1
  "use strict";
2
- // NanoClaw adapter — TypeScript variant, default ports 3100/3101
3
- // Probe: /health check for framework: "nanoclaw", fallback /api/info
2
+ // NanoClaw adapter — ProjectProbe implementation
3
+ // NanoClaw has no HTTP server, so discovery uses local filesystem probing:
4
+ // /proc scan for running processes, package.json, .env, data/ipc/ directory.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
4
38
  Object.defineProperty(exports, "__esModule", { value: true });
5
39
  exports.NanoClawAdapter = void 0;
6
- const PROBE_TIMEOUT = 2_000;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const os = __importStar(require("node:os"));
43
+ const NANOCLAW_PACKAGE_NAME = "nanoclaw";
44
+ // Common paths where nanoclaw might be installed
45
+ const CANDIDATE_DIRS = [
46
+ "nanoclaw",
47
+ "NanoClaw",
48
+ "projects/nanoclaw",
49
+ "projects/NanoClaw",
50
+ ];
7
51
  class NanoClawAdapter {
8
52
  name = "nanoclaw";
9
- defaultPorts = [3100, 3101];
10
- async probe(host, port) {
11
- // Try /health first
12
- const healthResult = await this.probeHealth(host, port);
13
- if (healthResult)
14
- return healthResult;
15
- // Fallback: /api/info
16
- return this.probeApiInfo(host, port);
53
+ defaultPorts = [];
54
+ // Cached project dir from last successful probe
55
+ _lastProjectDir = null;
56
+ async probe(_host, _port) {
57
+ return null;
58
+ }
59
+ async probeLocal() {
60
+ // 1. Try to find via running Node.js processes (/proc scan)
61
+ let projectDir = await this._findFromProc();
62
+ // 2. Fallback: scan common home directory paths
63
+ if (!projectDir) {
64
+ projectDir = await this._findFromCandidateDirs();
65
+ }
66
+ if (!projectDir)
67
+ return null;
68
+ this._lastProjectDir = projectDir;
69
+ // Read package.json for version
70
+ const pkgInfo = await this._readPackageJson(projectDir);
71
+ if (!pkgInfo)
72
+ return null; // not actually nanoclaw
73
+ // Read .env for ASSISTANT_NAME (optional)
74
+ const assistantName = await this._readAssistantName(projectDir);
75
+ // Check if process is running
76
+ const procInfo = await this._findRunningPid(projectDir);
77
+ // Count active IPC tasks (data/ipc/*/messages/*.json)
78
+ const activeTasks = await this._countIpcTasks(projectDir);
79
+ // Check if store/messages.db exists (has been run before)
80
+ const hasMessageDb = await this._fileExists(path.join(projectDir, "store", "messages.db"));
81
+ return {
82
+ name: NANOCLAW_PACKAGE_NAME,
83
+ version: pkgInfo.version,
84
+ display_name: assistantName ?? undefined,
85
+ metadata: {
86
+ project_dir: projectDir,
87
+ is_running: procInfo !== null,
88
+ pid: procInfo,
89
+ active_tasks: activeTasks,
90
+ has_message_db: hasMessageDb,
91
+ },
92
+ };
93
+ }
94
+ async healthCheckLocal() {
95
+ const projectDir = this._lastProjectDir;
96
+ if (!projectDir)
97
+ return false;
98
+ // Check if a nanoclaw process is running with this project dir
99
+ const pid = await this._findRunningPid(projectDir);
100
+ if (pid !== null)
101
+ return true;
102
+ // Fallback: check store/messages.db mtime (recent activity = likely alive)
103
+ try {
104
+ const dbPath = path.join(projectDir, "store", "messages.db");
105
+ const stat = await fs.promises.stat(dbPath);
106
+ const ageMs = Date.now() - stat.mtimeMs;
107
+ // Consider "healthy" if db was modified in last 5 minutes
108
+ return ageMs < 5 * 60 * 1000;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
17
113
  }
18
- toClawInstance(host, port, probe) {
114
+ toClawInstance(host, _port, probe) {
115
+ const meta = probe.metadata;
19
116
  return {
20
117
  agent_id: `nanoclaw@${host}`,
21
118
  assistant_name: probe.display_name ?? "",
22
119
  display_name: probe.display_name ?? "nanoclaw",
23
120
  lan_host: host,
24
121
  address: host,
25
- gateway_port: port,
122
+ gateway_port: 0,
26
123
  tls: false,
27
- discovery_source: "scan",
28
- status: "online",
124
+ discovery_source: "local",
29
125
  implementation: "nanoclaw",
126
+ labels: meta?.project_dir ? { project_dir: meta.project_dir } : undefined,
30
127
  };
31
128
  }
32
- async probeHealth(host, port) {
129
+ async healthCheck(_host, _port) {
130
+ return false;
131
+ }
132
+ /** Scan /proc for Node.js processes whose cwd contains a nanoclaw package.json */
133
+ async _findFromProc() {
134
+ if (process.platform !== "linux")
135
+ return null;
33
136
  try {
34
- const res = await fetch(`http://${host}:${port}/health`, {
35
- signal: AbortSignal.timeout(PROBE_TIMEOUT),
36
- });
37
- if (!res.ok)
38
- return null;
39
- const data = (await res.json());
40
- if (data.framework === "nanoclaw") {
41
- return {
42
- name: "nanoclaw",
43
- version: typeof data.version === "string" ? data.version : undefined,
44
- display_name: typeof data.name === "string" ? data.name : undefined,
45
- };
137
+ const entries = await fs.promises.readdir("/proc");
138
+ for (const entry of entries) {
139
+ // Only numeric dirs (PIDs)
140
+ if (!/^\d+$/.test(entry))
141
+ continue;
142
+ try {
143
+ const cwd = await fs.promises.readlink(`/proc/${entry}/cwd`);
144
+ if (await this._isNanoClawDir(cwd)) {
145
+ return cwd;
146
+ }
147
+ }
148
+ catch {
149
+ // Permission denied or process gone — skip
150
+ }
46
151
  }
47
- return null;
48
152
  }
49
153
  catch {
50
- return null;
154
+ // /proc not available
51
155
  }
156
+ return null;
52
157
  }
53
- async probeApiInfo(host, port) {
158
+ /** Check common paths under home directory */
159
+ async _findFromCandidateDirs() {
160
+ const home = os.homedir();
161
+ for (const rel of CANDIDATE_DIRS) {
162
+ const dir = path.join(home, rel);
163
+ if (await this._isNanoClawDir(dir)) {
164
+ return dir;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ /** Check if a directory is a nanoclaw project */
170
+ async _isNanoClawDir(dir) {
171
+ const info = await this._readPackageJson(dir);
172
+ return info !== null;
173
+ }
174
+ /** Read package.json and verify it's nanoclaw */
175
+ async _readPackageJson(dir) {
54
176
  try {
55
- const res = await fetch(`http://${host}:${port}/api/info`, {
56
- signal: AbortSignal.timeout(PROBE_TIMEOUT),
57
- });
58
- if (!res.ok)
59
- return null;
60
- const data = (await res.json());
61
- if (data.framework === "nanoclaw") {
62
- return {
63
- name: "nanoclaw",
64
- version: typeof data.version === "string" ? data.version : undefined,
65
- display_name: typeof data.name === "string" ? data.name : undefined,
66
- };
177
+ const raw = await fs.promises.readFile(path.join(dir, "package.json"), "utf-8");
178
+ const pkg = JSON.parse(raw);
179
+ if (pkg.name === NANOCLAW_PACKAGE_NAME) {
180
+ return { version: pkg.version };
181
+ }
182
+ }
183
+ catch {
184
+ // Not found or invalid
185
+ }
186
+ return null;
187
+ }
188
+ /** Extract ASSISTANT_NAME from .env file */
189
+ async _readAssistantName(dir) {
190
+ try {
191
+ const raw = await fs.promises.readFile(path.join(dir, ".env"), "utf-8");
192
+ const match = raw.match(/^ASSISTANT_NAME=(.*)$/m);
193
+ if (match?.[1]) {
194
+ return match[1].trim().replace(/^["']|["']$/g, "");
67
195
  }
68
- return null;
69
196
  }
70
197
  catch {
198
+ // .env not found
199
+ }
200
+ return null;
201
+ }
202
+ /** Find a running process with cwd matching the project dir */
203
+ async _findRunningPid(dir) {
204
+ if (process.platform !== "linux")
71
205
  return null;
206
+ try {
207
+ const entries = await fs.promises.readdir("/proc");
208
+ for (const entry of entries) {
209
+ if (!/^\d+$/.test(entry))
210
+ continue;
211
+ try {
212
+ const cwd = await fs.promises.readlink(`/proc/${entry}/cwd`);
213
+ if (cwd === dir) {
214
+ return parseInt(entry, 10);
215
+ }
216
+ }
217
+ catch {
218
+ // skip
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ // /proc not available
224
+ }
225
+ return null;
226
+ }
227
+ /** Count JSON files in data/ipc/{channel}/messages/ as a proxy for active tasks */
228
+ async _countIpcTasks(dir) {
229
+ const ipcDir = path.join(dir, "data", "ipc");
230
+ try {
231
+ const channels = await fs.promises.readdir(ipcDir);
232
+ let count = 0;
233
+ for (const ch of channels) {
234
+ const messagesDir = path.join(ipcDir, ch, "messages");
235
+ try {
236
+ const files = await fs.promises.readdir(messagesDir);
237
+ count += files.filter((f) => f.endsWith(".json")).length;
238
+ }
239
+ catch {
240
+ // messages dir doesn't exist for this channel
241
+ }
242
+ }
243
+ return count;
244
+ }
245
+ catch {
246
+ // data/ipc doesn't exist
247
+ return 0;
248
+ }
249
+ }
250
+ async _fileExists(filePath) {
251
+ try {
252
+ await fs.promises.access(filePath);
253
+ return true;
254
+ }
255
+ catch {
256
+ return false;
72
257
  }
73
258
  }
74
259
  }
@@ -0,0 +1,9 @@
1
+ import type { ClawInstance } from "../types.js";
2
+ import type { FrameworkAdapter, ProbeResult } from "./types.js";
3
+ export declare class OpenClawAdapter implements FrameworkAdapter {
4
+ readonly name = "openclaw";
5
+ readonly defaultPorts: number[];
6
+ probe(host: string, port: number): Promise<ProbeResult | null>;
7
+ toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
+ healthCheck(host: string, port: number): Promise<boolean>;
9
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ // OpenClaw adapter — canonical OpenClaw (and compatible variants like GoClaw)
3
+ // Probe: /__openclaw/control-ui-config.json → extract agentId, assistantName, displayName
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.OpenClawAdapter = void 0;
6
+ const PROBE_TIMEOUT = 2_000;
7
+ const CONFIG_PATH = "/__openclaw/control-ui-config.json";
8
+ class OpenClawAdapter {
9
+ name = "openclaw";
10
+ defaultPorts = [18789];
11
+ async probe(host, port) {
12
+ try {
13
+ const res = await fetch(`http://${host}:${port}${CONFIG_PATH}`, {
14
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
15
+ });
16
+ if (!res.ok)
17
+ return null;
18
+ const config = (await res.json());
19
+ if (!config.assistantAgentId)
20
+ return null;
21
+ return {
22
+ name: "openclaw",
23
+ display_name: config.displayName ?? config.assistantName ?? undefined,
24
+ metadata: {
25
+ assistantAgentId: config.assistantAgentId,
26
+ assistantName: config.assistantName,
27
+ displayName: config.displayName,
28
+ },
29
+ };
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ toClawInstance(host, port, probe) {
36
+ const meta = probe.metadata;
37
+ return {
38
+ agent_id: meta?.assistantAgentId ?? `openclaw@${host}`,
39
+ assistant_name: meta?.assistantName ?? "",
40
+ display_name: meta?.displayName ?? meta?.assistantName ?? "",
41
+ lan_host: host,
42
+ address: host,
43
+ gateway_port: port,
44
+ tls: false,
45
+ discovery_source: "scan",
46
+ status: "online",
47
+ implementation: "openclaw",
48
+ };
49
+ }
50
+ async healthCheck(host, port) {
51
+ try {
52
+ const res = await fetch(`http://${host}:${port}${CONFIG_PATH}`, {
53
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
54
+ });
55
+ return res.ok;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ }
62
+ exports.OpenClawAdapter = OpenClawAdapter;
@@ -0,0 +1,11 @@
1
+ import type { ClawInstance } from "../types.js";
2
+ import type { FrameworkAdapter, ProbeResult } from "./types.js";
3
+ export declare class OpenFangAdapter implements FrameworkAdapter {
4
+ readonly name = "openfang";
5
+ readonly defaultPorts: number[];
6
+ probe(host: string, port: number): Promise<ProbeResult | null>;
7
+ toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
+ healthCheck(host: string, port: number): Promise<boolean>;
9
+ private isOpenFang;
10
+ private fetchAgentJson;
11
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ // OpenFang adapter — port 4200, REST API
3
+ // Probe: /api/health → confirm OpenFang, identity from /.well-known/agent.json
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.OpenFangAdapter = void 0;
6
+ const PROBE_TIMEOUT = 2_000;
7
+ class OpenFangAdapter {
8
+ name = "openfang";
9
+ defaultPorts = [4200];
10
+ async probe(host, port) {
11
+ try {
12
+ const res = await fetch(`http://${host}:${port}/api/health`, {
13
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
14
+ });
15
+ if (!res.ok)
16
+ return null;
17
+ const data = (await res.json());
18
+ if (!this.isOpenFang(data))
19
+ return null;
20
+ // Try to get identity from /.well-known/agent.json
21
+ const agent = await this.fetchAgentJson(host, port);
22
+ return {
23
+ name: "openfang",
24
+ version: data.version ?? agent?.version,
25
+ display_name: agent?.display_name ?? agent?.name,
26
+ metadata: {
27
+ agent_id: agent?.agent_id,
28
+ agent_name: agent?.name,
29
+ },
30
+ };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ toClawInstance(host, port, probe) {
37
+ const meta = probe.metadata;
38
+ return {
39
+ agent_id: meta?.agent_id ?? `openfang@${host}`,
40
+ assistant_name: meta?.agent_name ?? "",
41
+ display_name: probe.display_name ?? "openfang",
42
+ lan_host: host,
43
+ address: host,
44
+ gateway_port: port,
45
+ tls: false,
46
+ discovery_source: "scan",
47
+ status: "online",
48
+ implementation: "openfang",
49
+ };
50
+ }
51
+ async healthCheck(host, port) {
52
+ try {
53
+ const res = await fetch(`http://${host}:${port}/api/health`, {
54
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
55
+ });
56
+ if (!res.ok)
57
+ return false;
58
+ const data = (await res.json());
59
+ return data.status === "ok";
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ isOpenFang(data) {
66
+ // Confirm OpenFang by framework field or status shape
67
+ if (data.framework === "openfang")
68
+ return true;
69
+ // Heuristic: /api/health with status: "ok" on default port
70
+ if (data.status === "ok")
71
+ return true;
72
+ return false;
73
+ }
74
+ async fetchAgentJson(host, port) {
75
+ try {
76
+ const res = await fetch(`http://${host}:${port}/.well-known/agent.json`, {
77
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
78
+ });
79
+ if (!res.ok)
80
+ return null;
81
+ return (await res.json());
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ }
88
+ exports.OpenFangAdapter = OpenFangAdapter;
@@ -10,4 +10,7 @@ export interface FrameworkAdapter {
10
10
  readonly defaultPorts: number[];
11
11
  probe(host: string, port: number): Promise<ProbeResult | null>;
12
12
  toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
13
+ healthCheck(host: string, port: number): Promise<boolean>;
14
+ probeLocal?(): Promise<ProbeResult | null>;
15
+ healthCheckLocal?(): Promise<boolean>;
13
16
  }
@@ -39,6 +39,7 @@ export interface DiagnosticsDeps {
39
39
  unreachable: UnreachableInstance[];
40
40
  }
41
41
  export declare function registerDiagnosticsRoutes(app: FastifyInstance, deps: DiagnosticsDeps): void;
42
+ export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string): void;
42
43
  export interface DaemonOptions {
43
44
  port?: number;
44
45
  host?: string;
@@ -10,6 +10,7 @@ exports.registerAgentRoutes = registerAgentRoutes;
10
10
  exports.registerRegistryRoutes = registerRegistryRoutes;
11
11
  exports.registerInstanceRoutes = registerInstanceRoutes;
12
12
  exports.registerDiagnosticsRoutes = registerDiagnosticsRoutes;
13
+ exports.registerA2aRoutes = registerA2aRoutes;
13
14
  exports.startDaemon = startDaemon;
14
15
  const fastify_1 = __importDefault(require("fastify"));
15
16
  const store_js_1 = require("../registry/store.js");
@@ -27,6 +28,9 @@ const client_js_1 = require("../registry/client.js");
27
28
  const auto_register_js_1 = require("../registry/auto-register.js");
28
29
  const discovery_js_1 = require("../registry/discovery.js");
29
30
  const connector_js_1 = require("../relay/connector.js");
31
+ const card_js_1 = require("../a2a/card.js");
32
+ const node_fs_1 = require("node:fs");
33
+ const node_path_1 = require("node:path");
30
34
  const PORT = parseInt(process.env.CLAWNEXUS_PORT ?? "17890", 10);
31
35
  const HOST = process.env.CLAWNEXUS_HOST ?? "127.0.0.1";
32
36
  function registerRelayRoutes(app, getConnector) {
@@ -301,6 +305,30 @@ function registerDiagnosticsRoutes(app, deps) {
301
305
  };
302
306
  });
303
307
  }
308
+ function registerA2aRoutes(app, store, daemonVersion) {
309
+ // A2A standard well-known endpoint — returns card for the local (is_self) instance
310
+ app.get("/.well-known/agent-card.json", async (_request, reply) => {
311
+ const self = store.getAll().find((i) => i.is_self);
312
+ if (!self) {
313
+ return reply.status(404).send({ error: "No local instance discovered" });
314
+ }
315
+ return (0, card_js_1.buildAgentCard)(self, daemonVersion);
316
+ });
317
+ // All instances as Agent Cards
318
+ app.get("/a2a/cards", async () => {
319
+ const instances = store.getAll();
320
+ const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion));
321
+ return { count: cards.length, cards };
322
+ });
323
+ // Single instance Agent Card by name
324
+ app.get("/a2a/cards/:name", async (request, reply) => {
325
+ const inst = store.resolve(request.params.name);
326
+ if (!inst) {
327
+ return reply.status(404).send({ error: "Instance not found" });
328
+ }
329
+ return (0, card_js_1.buildAgentCard)(inst, daemonVersion);
330
+ });
331
+ }
304
332
  async function startDaemon(options = {}) {
305
333
  const port = options.port ?? PORT;
306
334
  const host = options.host ?? HOST;
@@ -390,6 +418,9 @@ async function startDaemon(options = {}) {
390
418
  getAutoRegister: () => autoRegister,
391
419
  unreachable,
392
420
  });
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);
393
424
  // 9. Initialize Registry integration (non-fatal — LAN must work without it)
394
425
  let identityKeys = null;
395
426
  let registryClient = null;
@@ -46,7 +46,6 @@ const CDP_PORT = 17891;
46
46
  const TCP_PROBE_TIMEOUT = 2_000;
47
47
  const ANNOUNCE_INTERVAL_BASE = 60_000;
48
48
  const ANNOUNCE_JITTER = 10_000;
49
- const CONFIG_PATH = "/__openclaw/control-ui-config.json";
50
49
  class BroadcastDiscovery extends node_events_1.EventEmitter {
51
50
  store;
52
51
  getLocalInstance;
@@ -274,12 +273,14 @@ class BroadcastDiscovery extends node_events_1.EventEmitter {
274
273
  });
275
274
  }
276
275
  async _tcpProbe(address, port) {
277
- const url = `http://${address}:${port}${CONFIG_PATH}`;
276
+ // Quick HTTP liveness check — just confirm the port is alive.
277
+ // Framework identification is left to HealthChecker / subsequent scans.
278
278
  try {
279
- const res = await fetch(url, {
279
+ const res = await fetch(`http://${address}:${port}/`, {
280
280
  signal: AbortSignal.timeout(TCP_PROBE_TIMEOUT),
281
281
  });
282
- return res.ok;
282
+ // Any HTTP response (including 404, 500) means the port is alive
283
+ return res.status > 0;
283
284
  }
284
285
  catch {
285
286
  return false;
@@ -11,4 +11,8 @@ export declare class HealthChecker extends EventEmitter {
11
11
  stop(): void;
12
12
  checkAll(): Promise<void>;
13
13
  private checkOne;
14
+ /** OpenClaw-specific health check with name updates */
15
+ private checkOpenClaw;
16
+ /** Fallback health check for instances without a known adapter */
17
+ private genericHttpCheck;
14
18
  }
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  // Health Checker — periodically pings known instances and updates status
3
3
  // Enhanced with dual-channel connectivity detection
4
+ // Adapter-aware: uses framework-specific health checks for non-OpenClaw instances
4
5
  Object.defineProperty(exports, "__esModule", { value: true });
5
6
  exports.HealthChecker = void 0;
6
7
  const node_events_1 = require("node:events");
8
+ const index_js_1 = require("../adapter/index.js");
7
9
  const CHECK_INTERVAL = 30_000;
8
10
  const PING_TIMEOUT = 5_000;
9
11
  const CONFIG_PATH = "/__openclaw/control-ui-config.json";
@@ -41,36 +43,51 @@ class HealthChecker extends node_events_1.EventEmitter {
41
43
  if (inst.is_self)
42
44
  return;
43
45
  const networkKey = this.store.networkKey(inst.address, inst.gateway_port);
44
- const protocol = inst.tls ? "https" : "http";
45
- const url = `${protocol}://${inst.address}:${inst.gateway_port}${CONFIG_PATH}`;
46
+ const impl = inst.implementation ?? "openclaw";
46
47
  const now = new Date().toISOString();
47
48
  let lanOk = false;
48
49
  let lanLatency;
49
50
  let unreachableReason;
50
- try {
51
- const start = performance.now();
52
- const res = await fetch(url, {
53
- signal: AbortSignal.timeout(PING_TIMEOUT),
54
- });
55
- lanLatency = Math.round(performance.now() - start);
56
- if (res.ok) {
57
- lanOk = true;
58
- const config = (await res.json());
59
- inst.last_seen = now;
60
- if (config.assistantName) {
61
- inst.assistant_name = config.assistantName;
51
+ if (impl === "openclaw" || impl === "goclaw") {
52
+ // OpenClaw path: detailed check with name updates (original behavior)
53
+ const result = await this.checkOpenClaw(inst, now);
54
+ lanOk = result.ok;
55
+ lanLatency = result.latency;
56
+ unreachableReason = result.reason;
57
+ }
58
+ else {
59
+ // Non-OpenClaw: use adapter healthCheck
60
+ const adapter = (0, index_js_1.getAdapter)(impl);
61
+ if (adapter) {
62
+ try {
63
+ const start = performance.now();
64
+ // Use healthCheckLocal for port-0 instances (no HTTP server)
65
+ if (inst.gateway_port === 0 && adapter.healthCheckLocal) {
66
+ lanOk = await adapter.healthCheckLocal();
67
+ }
68
+ else {
69
+ lanOk = await adapter.healthCheck(inst.address, inst.gateway_port);
70
+ }
71
+ lanLatency = Math.round(performance.now() - start);
72
+ if (lanOk) {
73
+ inst.last_seen = now;
74
+ }
75
+ else {
76
+ unreachableReason = "Health check failed";
77
+ }
62
78
  }
63
- if (config.displayName) {
64
- inst.display_name = config.displayName;
79
+ catch (err) {
80
+ unreachableReason = err instanceof Error ? err.message : "Connection failed";
65
81
  }
66
82
  }
67
83
  else {
68
- unreachableReason = `HTTP ${res.status}`;
84
+ // Unknown implementation: generic HTTP check
85
+ const result = await this.genericHttpCheck(inst, now);
86
+ lanOk = result.ok;
87
+ lanLatency = result.latency;
88
+ unreachableReason = result.reason;
69
89
  }
70
90
  }
71
- catch (err) {
72
- unreachableReason = err instanceof Error ? err.message : "Connection failed";
73
- }
74
91
  // Check relay availability
75
92
  const relayAvailable = this.relayChecker?.(inst.agent_id) ?? false;
76
93
  // Update connectivity
@@ -104,5 +121,52 @@ class HealthChecker extends node_events_1.EventEmitter {
104
121
  });
105
122
  }
106
123
  }
124
+ /** OpenClaw-specific health check with name updates */
125
+ async checkOpenClaw(inst, now) {
126
+ const protocol = inst.tls ? "https" : "http";
127
+ const url = `${protocol}://${inst.address}:${inst.gateway_port}${CONFIG_PATH}`;
128
+ try {
129
+ const start = performance.now();
130
+ const res = await fetch(url, {
131
+ signal: AbortSignal.timeout(PING_TIMEOUT),
132
+ });
133
+ const latency = Math.round(performance.now() - start);
134
+ if (res.ok) {
135
+ const config = (await res.json());
136
+ inst.last_seen = now;
137
+ if (config.assistantName) {
138
+ inst.assistant_name = config.assistantName;
139
+ }
140
+ if (config.displayName) {
141
+ inst.display_name = config.displayName;
142
+ }
143
+ return { ok: true, latency };
144
+ }
145
+ return { ok: false, latency, reason: `HTTP ${res.status}` };
146
+ }
147
+ catch (err) {
148
+ return { ok: false, reason: err instanceof Error ? err.message : "Connection failed" };
149
+ }
150
+ }
151
+ /** Fallback health check for instances without a known adapter */
152
+ async genericHttpCheck(inst, now) {
153
+ const protocol = inst.tls ? "https" : "http";
154
+ const url = `${protocol}://${inst.address}:${inst.gateway_port}/`;
155
+ try {
156
+ const start = performance.now();
157
+ const res = await fetch(url, {
158
+ signal: AbortSignal.timeout(PING_TIMEOUT),
159
+ });
160
+ const latency = Math.round(performance.now() - start);
161
+ if (res.ok) {
162
+ inst.last_seen = now;
163
+ return { ok: true, latency };
164
+ }
165
+ return { ok: false, latency, reason: `HTTP ${res.status}` };
166
+ }
167
+ catch (err) {
168
+ return { ok: false, reason: err instanceof Error ? err.message : "Connection failed" };
169
+ }
170
+ }
107
171
  }
108
172
  exports.HealthChecker = HealthChecker;
@@ -11,5 +11,9 @@ export declare class LocalProbe extends EventEmitter {
11
11
  start(): Promise<void>;
12
12
  stop(): void;
13
13
  private _markOffline;
14
+ /** Mark offline any previously-known self instances that were not rediscovered this cycle */
15
+ private _markOfflineStaleSelf;
14
16
  probe(): Promise<ClawInstance | null>;
17
+ /** Original OpenClaw probe logic (kept as primary path for backward compatibility) */
18
+ private probeOpenClaw;
15
19
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
- // LocalProbe — detects OpenClaw instance on localhost and registers it
2
+ // LocalProbe — detects AI instances on localhost and registers them
3
3
  // Runs on daemon startup, then periodically re-checks
4
+ // Adapter-aware: tries all registered adapters on their default ports
4
5
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
6
  if (k2 === undefined) k2 = k;
6
7
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -38,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
39
  exports.LocalProbe = void 0;
39
40
  const node_events_1 = require("node:events");
40
41
  const os = __importStar(require("node:os"));
42
+ const index_js_1 = require("../adapter/index.js");
41
43
  const LOCAL_HOST = "127.0.0.1";
42
44
  const DEFAULT_PORT = 18789;
43
45
  const CONFIG_PATH = "/__openclaw/control-ui-config.json";
@@ -71,8 +73,8 @@ class LocalProbe extends node_events_1.EventEmitter {
71
73
  this.timer = null;
72
74
  }
73
75
  }
74
- _markOffline() {
75
- const existing = this.store.getByNetworkKey(LOCAL_HOST, this.port);
76
+ _markOffline(port) {
77
+ const existing = this.store.getByNetworkKey(LOCAL_HOST, port);
76
78
  if (existing && existing.status !== "offline") {
77
79
  this.store.upsert({
78
80
  ...existing,
@@ -81,21 +83,135 @@ class LocalProbe extends node_events_1.EventEmitter {
81
83
  });
82
84
  }
83
85
  }
86
+ /** Mark offline any previously-known self instances that were not rediscovered this cycle */
87
+ _markOfflineStaleSelf(staleKeys) {
88
+ for (const key of staleKeys) {
89
+ const [address, portStr] = key.split(":");
90
+ this._markOffline(Number(portStr));
91
+ }
92
+ }
84
93
  async probe() {
85
- const url = `http://${LOCAL_HOST}:${this.port}${CONFIG_PATH}`;
94
+ // Snapshot all current is_self instances so we can mark stale ones offline later
95
+ const previousSelfKeys = new Set(this.store
96
+ .getAll()
97
+ .filter((inst) => inst.is_self)
98
+ .map((inst) => `${inst.address}:${inst.gateway_port}`));
99
+ // 1. Try OpenClaw on the configured port (backward-compatible primary path)
100
+ const openClawResult = await this.probeOpenClaw(this.port);
101
+ if (openClawResult) {
102
+ previousSelfKeys.delete(`${LOCAL_HOST}:${this.port}`);
103
+ this._markOfflineStaleSelf(previousSelfKeys);
104
+ return openClawResult;
105
+ }
106
+ // 2. Try all adapters on their default ports (skip OpenClaw on this.port, already tried)
107
+ let found = null;
108
+ for (const adapter of index_js_1.ADAPTERS) {
109
+ if (adapter.name === "openclaw")
110
+ continue; // already tried above
111
+ // Try probeLocal first (for frameworks without HTTP servers, e.g. NanoClaw)
112
+ if (adapter.probeLocal) {
113
+ const probeResult = await adapter.probeLocal();
114
+ if (probeResult) {
115
+ const now = new Date().toISOString();
116
+ const partial = adapter.toClawInstance(LOCAL_HOST, 0, probeResult);
117
+ const instance = {
118
+ agent_id: partial.agent_id ?? `${adapter.name}@localhost`,
119
+ auto_name: "",
120
+ assistant_name: partial.assistant_name ?? "",
121
+ display_name: partial.display_name ?? adapter.name,
122
+ lan_host: os.hostname(),
123
+ address: LOCAL_HOST,
124
+ gateway_port: partial.gateway_port ?? 0,
125
+ tls: false,
126
+ discovery_source: "local",
127
+ network_scope: "local",
128
+ status: "online",
129
+ last_seen: now,
130
+ discovered_at: now,
131
+ implementation: partial.implementation,
132
+ connectivity: {
133
+ lan_reachable: false,
134
+ relay_available: false,
135
+ preferred_channel: "local",
136
+ last_lan_check: now,
137
+ },
138
+ is_self: true,
139
+ labels: partial.labels,
140
+ };
141
+ this.store.upsert(instance);
142
+ this.emit("local:discovered", instance);
143
+ previousSelfKeys.delete(`${LOCAL_HOST}:${instance.gateway_port}`);
144
+ found = instance;
145
+ break;
146
+ }
147
+ }
148
+ // Then try HTTP probe on default ports
149
+ if (!found) {
150
+ for (const port of adapter.defaultPorts) {
151
+ const probeResult = await adapter.probe(LOCAL_HOST, port);
152
+ if (probeResult) {
153
+ const now = new Date().toISOString();
154
+ const partial = adapter.toClawInstance(LOCAL_HOST, port, probeResult);
155
+ const instance = {
156
+ agent_id: partial.agent_id ?? `${adapter.name}@localhost`,
157
+ auto_name: "",
158
+ assistant_name: partial.assistant_name ?? "",
159
+ display_name: partial.display_name ?? adapter.name,
160
+ lan_host: os.hostname(),
161
+ address: LOCAL_HOST,
162
+ gateway_port: port,
163
+ tls: false,
164
+ discovery_source: "local",
165
+ network_scope: "local",
166
+ status: "online",
167
+ last_seen: now,
168
+ discovered_at: now,
169
+ implementation: partial.implementation,
170
+ connectivity: {
171
+ lan_reachable: true,
172
+ relay_available: false,
173
+ preferred_channel: "local",
174
+ last_lan_check: now,
175
+ },
176
+ is_self: true,
177
+ };
178
+ this.store.upsert(instance);
179
+ this.emit("local:discovered", instance);
180
+ previousSelfKeys.delete(`${LOCAL_HOST}:${port}`);
181
+ found = instance;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ if (found)
187
+ break;
188
+ }
189
+ if (found) {
190
+ this._markOfflineStaleSelf(previousSelfKeys);
191
+ return found;
192
+ }
193
+ // Nothing found
194
+ this.localAgentId = null;
195
+ this._markOfflineStaleSelf(previousSelfKeys);
196
+ this.emit("local:unavailable");
197
+ return null;
198
+ }
199
+ /** Original OpenClaw probe logic (kept as primary path for backward compatibility) */
200
+ async probeOpenClaw(port) {
201
+ const url = `http://${LOCAL_HOST}:${port}${CONFIG_PATH}`;
86
202
  try {
87
203
  const res = await fetch(url, {
88
204
  signal: AbortSignal.timeout(PROBE_TIMEOUT),
89
205
  });
90
206
  if (!res.ok) {
91
- this._markOffline();
92
- this.emit("local:unreachable", { reason: `HTTP ${res.status}` });
207
+ // Port responded but not valid OpenClaw — don't mark offline or emit unreachable,
208
+ // because another adapter may be occupying this port. Let the adapter loop try.
93
209
  return null;
94
210
  }
95
211
  const config = (await res.json());
96
212
  if (!config.assistantAgentId) {
97
- this._markOffline();
98
- this.emit("local:unreachable", { reason: "missing assistantAgentId" });
213
+ // Has an HTTP server but no assistantAgentId — not a valid OpenClaw instance.
214
+ // Don't mark offline; another adapter may match this endpoint.
99
215
  return null;
100
216
  }
101
217
  this.localAgentId = config.assistantAgentId;
@@ -107,7 +223,7 @@ class LocalProbe extends node_events_1.EventEmitter {
107
223
  display_name: config.displayName ?? config.assistantName ?? "",
108
224
  lan_host: os.hostname(),
109
225
  address: LOCAL_HOST,
110
- gateway_port: this.port,
226
+ gateway_port: port,
111
227
  tls: false,
112
228
  discovery_source: "local",
113
229
  network_scope: "local",
@@ -128,9 +244,7 @@ class LocalProbe extends node_events_1.EventEmitter {
128
244
  return instance;
129
245
  }
130
246
  catch {
131
- this.localAgentId = null;
132
- this._markOffline();
133
- this.emit("local:unavailable");
247
+ // OpenClaw not running on this port — don't emit yet, try adapters first
134
248
  return null;
135
249
  }
136
250
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type ClawImplementation = "openclaw" | "goclaw" | "zeroclaw" | "picoclaw" | "nanoclaw" | "nanobot" | "unknown";
1
+ export type ClawImplementation = "openclaw" | "goclaw" | "zeroclaw" | "picoclaw" | "nanoclaw" | "nanobot" | "openfang" | "unknown";
2
2
  export interface ClawInstance {
3
3
  agent_id: string;
4
4
  auto_name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawnexus",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "ClawNexus daemon and CLI — AI instance registry for OpenClaw",
5
5
  "license": "MIT",
6
6
  "author": "alan-silverstreams <alan@silverstream.tech>",