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.
- package/dist/a2a/card.d.ts +29 -0
- package/dist/a2a/card.js +30 -0
- package/dist/adapter/index.js +4 -0
- package/dist/adapter/nanobot.d.ts +1 -0
- package/dist/adapter/nanobot.js +17 -2
- package/dist/adapter/nanoclaw.d.ts +21 -4
- package/dist/adapter/nanoclaw.js +229 -44
- package/dist/adapter/openclaw.d.ts +9 -0
- package/dist/adapter/openclaw.js +62 -0
- package/dist/adapter/openfang.d.ts +11 -0
- package/dist/adapter/openfang.js +88 -0
- package/dist/adapter/types.d.ts +3 -0
- package/dist/api/server.d.ts +1 -0
- package/dist/api/server.js +31 -0
- package/dist/discovery/broadcast.js +5 -4
- package/dist/health/checker.d.ts +4 -0
- package/dist/health/checker.js +84 -20
- package/dist/local/probe.d.ts +4 -0
- package/dist/local/probe.js +126 -12
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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;
|
package/dist/a2a/card.js
ADDED
|
@@ -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
|
+
}
|
package/dist/adapter/index.js
CHANGED
|
@@ -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;
|
package/dist/adapter/nanobot.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
}
|
package/dist/adapter/nanoclaw.js
CHANGED
|
@@ -1,74 +1,259 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// NanoClaw adapter —
|
|
3
|
-
//
|
|
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
|
|
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 = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,
|
|
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:
|
|
122
|
+
gateway_port: 0,
|
|
26
123
|
tls: false,
|
|
27
|
-
discovery_source: "
|
|
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
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
154
|
+
// /proc not available
|
|
51
155
|
}
|
|
156
|
+
return null;
|
|
52
157
|
}
|
|
53
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
package/dist/adapter/types.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/api/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/api/server.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
279
|
+
const res = await fetch(`http://${address}:${port}/`, {
|
|
280
280
|
signal: AbortSignal.timeout(TCP_PROBE_TIMEOUT),
|
|
281
281
|
});
|
|
282
|
-
|
|
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;
|
package/dist/health/checker.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/health/checker.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
79
|
+
catch (err) {
|
|
80
|
+
unreachableReason = err instanceof Error ? err.message : "Connection failed";
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
else {
|
|
68
|
-
|
|
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;
|
package/dist/local/probe.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/local/probe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// LocalProbe — detects
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
this.
|
|
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
|
-
|
|
98
|
-
|
|
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:
|
|
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
|
|
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;
|