clawnexus 0.2.7 → 0.2.8
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/adapter/index.d.ts +4 -0
- package/dist/adapter/index.js +24 -0
- package/dist/adapter/nanobot.d.ts +11 -0
- package/dist/adapter/nanobot.js +82 -0
- package/dist/adapter/nanoclaw.d.ts +10 -0
- package/dist/adapter/nanoclaw.js +75 -0
- package/dist/adapter/types.d.ts +13 -0
- package/dist/adapter/types.js +4 -0
- package/dist/cli/index.js +7 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -1
- package/dist/local/probe.js +1 -0
- package/dist/registry/store.js +3 -0
- package/dist/scanner/active.d.ts +1 -0
- package/dist/scanner/active.js +73 -19
- package/dist/scanner/fingerprint.d.ts +22 -0
- package/dist/scanner/fingerprint.js +135 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Adapter registry — central place to register framework adapters
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ADAPTERS = void 0;
|
|
5
|
+
exports.getAdapter = getAdapter;
|
|
6
|
+
exports.getAllAdapterPorts = getAllAdapterPorts;
|
|
7
|
+
const nanoclaw_js_1 = require("./nanoclaw.js");
|
|
8
|
+
const nanobot_js_1 = require("./nanobot.js");
|
|
9
|
+
exports.ADAPTERS = [
|
|
10
|
+
new nanoclaw_js_1.NanoClawAdapter(),
|
|
11
|
+
new nanobot_js_1.NanoBotAdapter(),
|
|
12
|
+
];
|
|
13
|
+
function getAdapter(name) {
|
|
14
|
+
return exports.ADAPTERS.find((a) => a.name === name);
|
|
15
|
+
}
|
|
16
|
+
function getAllAdapterPorts() {
|
|
17
|
+
const ports = new Set();
|
|
18
|
+
for (const adapter of exports.ADAPTERS) {
|
|
19
|
+
for (const port of adapter.defaultPorts) {
|
|
20
|
+
ports.add(port);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...ports].sort((a, b) => a - b);
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClawInstance } from "../types.js";
|
|
2
|
+
import type { FrameworkAdapter, ProbeResult } from "./types.js";
|
|
3
|
+
export declare class NanoBotAdapter implements FrameworkAdapter {
|
|
4
|
+
readonly name = "nanobot";
|
|
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 probeApiHealth;
|
|
10
|
+
private extractProbeResult;
|
|
11
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// NanoBot adapter — Python variant, default ports 8000/8080
|
|
3
|
+
// Probe: /health → check for framework/app: "nanobot", fallback /api/health
|
|
4
|
+
// Heuristic: python_version field on expected port → infer nanobot
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NanoBotAdapter = void 0;
|
|
7
|
+
const PROBE_TIMEOUT = 2_000;
|
|
8
|
+
class NanoBotAdapter {
|
|
9
|
+
name = "nanobot";
|
|
10
|
+
defaultPorts = [8000, 8080];
|
|
11
|
+
async probe(host, port) {
|
|
12
|
+
// Try /health first
|
|
13
|
+
const healthResult = await this.probeHealth(host, port);
|
|
14
|
+
if (healthResult)
|
|
15
|
+
return healthResult;
|
|
16
|
+
// Fallback: /api/health
|
|
17
|
+
return this.probeApiHealth(host, port);
|
|
18
|
+
}
|
|
19
|
+
toClawInstance(host, port, probe) {
|
|
20
|
+
return {
|
|
21
|
+
agent_id: `nanobot@${host}`,
|
|
22
|
+
assistant_name: probe.display_name ?? "",
|
|
23
|
+
display_name: probe.display_name ?? "nanobot",
|
|
24
|
+
lan_host: host,
|
|
25
|
+
address: host,
|
|
26
|
+
gateway_port: port,
|
|
27
|
+
tls: false,
|
|
28
|
+
discovery_source: "scan",
|
|
29
|
+
status: "online",
|
|
30
|
+
implementation: "nanobot",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async probeHealth(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 null;
|
|
40
|
+
const data = (await res.json());
|
|
41
|
+
if (data.framework === "nanobot" || data.app === "nanobot") {
|
|
42
|
+
return this.extractProbeResult(data);
|
|
43
|
+
}
|
|
44
|
+
// Heuristic: python_version on expected port → infer nanobot
|
|
45
|
+
if (typeof data.python_version === "string" && this.defaultPorts.includes(port)) {
|
|
46
|
+
return this.extractProbeResult(data);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async probeApiHealth(host, port) {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`http://${host}:${port}/api/health`, {
|
|
57
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT),
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok)
|
|
60
|
+
return null;
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
if (data.framework === "nanobot" || data.app === "nanobot") {
|
|
63
|
+
return this.extractProbeResult(data);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
extractProbeResult(data) {
|
|
72
|
+
return {
|
|
73
|
+
name: "nanobot",
|
|
74
|
+
version: typeof data.version === "string" ? data.version : undefined,
|
|
75
|
+
display_name: typeof data.name === "string" ? data.name : undefined,
|
|
76
|
+
metadata: typeof data.python_version === "string"
|
|
77
|
+
? { python_version: data.python_version }
|
|
78
|
+
: undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.NanoBotAdapter = NanoBotAdapter;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ClawInstance } from "../types.js";
|
|
2
|
+
import type { FrameworkAdapter, ProbeResult } from "./types.js";
|
|
3
|
+
export declare class NanoClawAdapter implements FrameworkAdapter {
|
|
4
|
+
readonly name = "nanoclaw";
|
|
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;
|
|
10
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// NanoClaw adapter — TypeScript variant, default ports 3100/3101
|
|
3
|
+
// Probe: /health → check for framework: "nanoclaw", fallback /api/info
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.NanoClawAdapter = void 0;
|
|
6
|
+
const PROBE_TIMEOUT = 2_000;
|
|
7
|
+
class NanoClawAdapter {
|
|
8
|
+
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);
|
|
17
|
+
}
|
|
18
|
+
toClawInstance(host, port, probe) {
|
|
19
|
+
return {
|
|
20
|
+
agent_id: `nanoclaw@${host}`,
|
|
21
|
+
assistant_name: probe.display_name ?? "",
|
|
22
|
+
display_name: probe.display_name ?? "nanoclaw",
|
|
23
|
+
lan_host: host,
|
|
24
|
+
address: host,
|
|
25
|
+
gateway_port: port,
|
|
26
|
+
tls: false,
|
|
27
|
+
discovery_source: "scan",
|
|
28
|
+
status: "online",
|
|
29
|
+
implementation: "nanoclaw",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async probeHealth(host, port) {
|
|
33
|
+
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
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async probeApiInfo(host, port) {
|
|
54
|
+
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
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.NanoClawAdapter = NanoClawAdapter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClawInstance } from "../types.js";
|
|
2
|
+
export interface ProbeResult {
|
|
3
|
+
name: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
display_name?: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface FrameworkAdapter {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly defaultPorts: number[];
|
|
11
|
+
probe(host: string, port: number): Promise<ProbeResult | null>;
|
|
12
|
+
toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
|
|
13
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -156,6 +156,7 @@ function printTable(instances) {
|
|
|
156
156
|
}
|
|
157
157
|
const header = {
|
|
158
158
|
name: "NAME",
|
|
159
|
+
impl: "IMPL",
|
|
159
160
|
address: "ADDRESS",
|
|
160
161
|
status: "STATUS",
|
|
161
162
|
channel: "CHANNEL",
|
|
@@ -166,6 +167,7 @@ function printTable(instances) {
|
|
|
166
167
|
const baseName = i.alias ?? i.auto_name;
|
|
167
168
|
return {
|
|
168
169
|
name: i.is_self ? `${baseName} (self)` : baseName,
|
|
170
|
+
impl: i.implementation ?? "-",
|
|
169
171
|
address: `${i.address}:${i.gateway_port}`,
|
|
170
172
|
status: i.status,
|
|
171
173
|
channel: getChannel(i),
|
|
@@ -175,15 +177,16 @@ function printTable(instances) {
|
|
|
175
177
|
});
|
|
176
178
|
const colWidths = {
|
|
177
179
|
name: Math.max(header.name.length, ...rows.map((r) => r.name.length)),
|
|
180
|
+
impl: Math.max(header.impl.length, ...rows.map((r) => r.impl.length)),
|
|
178
181
|
address: Math.max(header.address.length, ...rows.map((r) => r.address.length)),
|
|
179
182
|
status: Math.max(header.status.length, ...rows.map((r) => r.status.length)),
|
|
180
183
|
channel: Math.max(header.channel.length, ...rows.map((r) => r.channel.length)),
|
|
181
184
|
source: Math.max(header.source.length, ...rows.map((r) => r.source.length)),
|
|
182
185
|
lastSeen: Math.max(header.lastSeen.length, ...rows.map((r) => r.lastSeen.length)),
|
|
183
186
|
};
|
|
184
|
-
const line = (r) => `${r.name.padEnd(colWidths.name)} ${r.address.padEnd(colWidths.address)} ${r.status.padEnd(colWidths.status)} ${r.channel.padEnd(colWidths.channel)} ${r.source.padEnd(colWidths.source)} ${r.lastSeen}`;
|
|
187
|
+
const line = (r) => `${r.name.padEnd(colWidths.name)} ${r.impl.padEnd(colWidths.impl)} ${r.address.padEnd(colWidths.address)} ${r.status.padEnd(colWidths.status)} ${r.channel.padEnd(colWidths.channel)} ${r.source.padEnd(colWidths.source)} ${r.lastSeen}`;
|
|
185
188
|
console.log(line(header));
|
|
186
|
-
console.log("-".repeat(colWidths.name + colWidths.address + colWidths.status + colWidths.channel + colWidths.source + colWidths.lastSeen +
|
|
189
|
+
console.log("-".repeat(colWidths.name + colWidths.impl + colWidths.address + colWidths.status + colWidths.channel + colWidths.source + colWidths.lastSeen + 12));
|
|
187
190
|
for (const row of rows) {
|
|
188
191
|
console.log(line(row));
|
|
189
192
|
}
|
|
@@ -404,6 +407,8 @@ async function cmdInfo(args) {
|
|
|
404
407
|
const inst = data;
|
|
405
408
|
console.log(`Auto Name: ${inst.auto_name}${inst.is_self ? " (self)" : ""}`);
|
|
406
409
|
console.log(`Agent ID: ${inst.agent_id}`);
|
|
410
|
+
if (inst.implementation)
|
|
411
|
+
console.log(`Implementation: ${inst.implementation}`);
|
|
407
412
|
console.log(`Display Name: ${inst.display_name}`);
|
|
408
413
|
console.log(`Assistant: ${inst.assistant_name}`);
|
|
409
414
|
if (inst.alias)
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export { HealthChecker } from './health/checker.js';
|
|
|
6
6
|
export { LocalProbe } from './local/probe.js';
|
|
7
7
|
export { ActiveScanner } from './scanner/active.js';
|
|
8
8
|
export type { ScanOptions } from './scanner/active.js';
|
|
9
|
+
export { ADAPTERS, getAdapter, getAllAdapterPorts } from './adapter/index.js';
|
|
10
|
+
export { NanoClawAdapter } from './adapter/nanoclaw.js';
|
|
11
|
+
export { NanoBotAdapter } from './adapter/nanobot.js';
|
|
12
|
+
export type { FrameworkAdapter, ProbeResult } from './adapter/types.js';
|
|
9
13
|
export { BroadcastDiscovery } from './discovery/broadcast.js';
|
|
10
14
|
export { registerRelayRoutes, registerInstanceRoutes, registerAgentRoutes, registerDiagnosticsRoutes, registerRegistryRoutes, startDaemon } from './api/server.js';
|
|
11
15
|
export type { DaemonOptions, DaemonHandle, AgentDeps, DiagnosticsDeps, RegistryDeps } from './api/server.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProtocolError = exports.isExpired = exports.validatePayload = exports.parseEnvelope = exports.createEnvelope = exports.AgentRouter = exports.TaskManager = exports.PolicyEngine = exports.RelayConnector = exports.RemoteDiscovery = exports.AutoRegister = exports.RegistryError = exports.RegistryClient = exports.getPublicKeyString = exports.sign = exports.loadOrCreateKeys = exports.startDaemon = exports.registerRegistryRoutes = exports.registerDiagnosticsRoutes = exports.registerAgentRoutes = exports.registerInstanceRoutes = exports.registerRelayRoutes = exports.BroadcastDiscovery = exports.ActiveScanner = exports.LocalProbe = exports.HealthChecker = exports.MdnsListener = exports.NotFoundError = exports.AliasConflictError = exports.AliasError = exports.RegistryStore = void 0;
|
|
3
|
+
exports.ProtocolError = exports.isExpired = exports.validatePayload = exports.parseEnvelope = exports.createEnvelope = exports.AgentRouter = exports.TaskManager = exports.PolicyEngine = exports.RelayConnector = exports.RemoteDiscovery = exports.AutoRegister = exports.RegistryError = exports.RegistryClient = exports.getPublicKeyString = exports.sign = exports.loadOrCreateKeys = exports.startDaemon = exports.registerRegistryRoutes = exports.registerDiagnosticsRoutes = exports.registerAgentRoutes = exports.registerInstanceRoutes = exports.registerRelayRoutes = exports.BroadcastDiscovery = exports.NanoBotAdapter = exports.NanoClawAdapter = exports.getAllAdapterPorts = exports.getAdapter = exports.ADAPTERS = exports.ActiveScanner = exports.LocalProbe = exports.HealthChecker = exports.MdnsListener = exports.NotFoundError = exports.AliasConflictError = exports.AliasError = exports.RegistryStore = void 0;
|
|
4
4
|
var store_js_1 = require("./registry/store.js");
|
|
5
5
|
Object.defineProperty(exports, "RegistryStore", { enumerable: true, get: function () { return store_js_1.RegistryStore; } });
|
|
6
6
|
Object.defineProperty(exports, "AliasError", { enumerable: true, get: function () { return store_js_1.AliasError; } });
|
|
@@ -14,6 +14,14 @@ var probe_js_1 = require("./local/probe.js");
|
|
|
14
14
|
Object.defineProperty(exports, "LocalProbe", { enumerable: true, get: function () { return probe_js_1.LocalProbe; } });
|
|
15
15
|
var active_js_1 = require("./scanner/active.js");
|
|
16
16
|
Object.defineProperty(exports, "ActiveScanner", { enumerable: true, get: function () { return active_js_1.ActiveScanner; } });
|
|
17
|
+
var index_js_1 = require("./adapter/index.js");
|
|
18
|
+
Object.defineProperty(exports, "ADAPTERS", { enumerable: true, get: function () { return index_js_1.ADAPTERS; } });
|
|
19
|
+
Object.defineProperty(exports, "getAdapter", { enumerable: true, get: function () { return index_js_1.getAdapter; } });
|
|
20
|
+
Object.defineProperty(exports, "getAllAdapterPorts", { enumerable: true, get: function () { return index_js_1.getAllAdapterPorts; } });
|
|
21
|
+
var nanoclaw_js_1 = require("./adapter/nanoclaw.js");
|
|
22
|
+
Object.defineProperty(exports, "NanoClawAdapter", { enumerable: true, get: function () { return nanoclaw_js_1.NanoClawAdapter; } });
|
|
23
|
+
var nanobot_js_1 = require("./adapter/nanobot.js");
|
|
24
|
+
Object.defineProperty(exports, "NanoBotAdapter", { enumerable: true, get: function () { return nanobot_js_1.NanoBotAdapter; } });
|
|
17
25
|
var broadcast_js_1 = require("./discovery/broadcast.js");
|
|
18
26
|
Object.defineProperty(exports, "BroadcastDiscovery", { enumerable: true, get: function () { return broadcast_js_1.BroadcastDiscovery; } });
|
|
19
27
|
var server_js_1 = require("./api/server.js");
|
package/dist/local/probe.js
CHANGED
package/dist/registry/store.js
CHANGED
|
@@ -201,6 +201,8 @@ class RegistryStore extends node_events_1.EventEmitter {
|
|
|
201
201
|
// Preserve registry fields
|
|
202
202
|
instance.claw_name = instance.claw_name ?? existing.claw_name;
|
|
203
203
|
instance.owner_pubkey = instance.owner_pubkey ?? existing.owner_pubkey;
|
|
204
|
+
// Preserve implementation (prefer new value if provided)
|
|
205
|
+
instance.implementation = instance.implementation ?? existing.implementation;
|
|
204
206
|
this.instances.set(key, instance);
|
|
205
207
|
this.scheduleDirtyFlush();
|
|
206
208
|
this.emit("upsert", instance);
|
|
@@ -315,6 +317,7 @@ class RegistryStore extends node_events_1.EventEmitter {
|
|
|
315
317
|
labels: existing.labels ?? incoming.labels,
|
|
316
318
|
connectivity: incoming.connectivity ?? existing.connectivity,
|
|
317
319
|
is_self: existing.is_self || incoming.is_self,
|
|
320
|
+
implementation: incoming.implementation ?? existing.implementation,
|
|
318
321
|
};
|
|
319
322
|
}
|
|
320
323
|
/** Numeric priority for network scope: local > vpn > public */
|
package/dist/scanner/active.d.ts
CHANGED
package/dist/scanner/active.js
CHANGED
|
@@ -38,11 +38,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
38
38
|
exports.ActiveScanner = void 0;
|
|
39
39
|
const node_events_1 = require("node:events");
|
|
40
40
|
const os = __importStar(require("node:os"));
|
|
41
|
+
const fingerprint_js_1 = require("./fingerprint.js");
|
|
41
42
|
const wireguard_js_1 = require("./wireguard.js");
|
|
43
|
+
const index_js_1 = require("../adapter/index.js");
|
|
42
44
|
const CONCURRENCY = 50;
|
|
43
45
|
const TIMEOUT_PER_HOST = 2_000;
|
|
44
46
|
const DEFAULT_PORT = 18789;
|
|
45
47
|
const CONFIG_PATH = "/__openclaw/control-ui-config.json";
|
|
48
|
+
// Default ports for all known implementations
|
|
49
|
+
const BUILTIN_PORTS = [
|
|
50
|
+
18789, // OpenClaw, GoClaw
|
|
51
|
+
42617, // ZeroClaw
|
|
52
|
+
18790, // PicoClaw
|
|
53
|
+
];
|
|
54
|
+
const DEFAULT_SCAN_PORTS = [...new Set([...BUILTIN_PORTS, ...(0, index_js_1.getAllAdapterPorts)()])];
|
|
46
55
|
class ActiveScanner extends node_events_1.EventEmitter {
|
|
47
56
|
store;
|
|
48
57
|
scanning = false;
|
|
@@ -71,8 +80,8 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
71
80
|
}
|
|
72
81
|
// 2. Scan subnets (skip if only explicit targets were given)
|
|
73
82
|
if (!hasExplicitTargets) {
|
|
74
|
-
const ports = options?.ports ??
|
|
75
|
-
const allPorts = [...new Set([
|
|
83
|
+
const ports = options?.ports ?? DEFAULT_SCAN_PORTS;
|
|
84
|
+
const allPorts = [...new Set([...DEFAULT_SCAN_PORTS, ...ports])];
|
|
76
85
|
const wgInfo = await (0, wireguard_js_1.detectWireGuard)();
|
|
77
86
|
const subnets = this.detectSubnets(wgInfo);
|
|
78
87
|
for (const si of subnets) {
|
|
@@ -160,25 +169,18 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
160
169
|
return discovered;
|
|
161
170
|
}
|
|
162
171
|
async probeHost(host, port = DEFAULT_PORT, networkScope = "local") {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return null;
|
|
170
|
-
const config = (await res.json());
|
|
171
|
-
if (!config.assistantAgentId)
|
|
172
|
-
return null;
|
|
173
|
-
// Resolve lan_host: prefer known hostname from existing registry entries
|
|
174
|
-
// over raw IP to enable multi-NIC deduplication
|
|
175
|
-
const lan_host = this.resolveHostForDedup(host, port, config.assistantAgentId);
|
|
172
|
+
// Try OpenClaw-compatible config endpoint first
|
|
173
|
+
const configResult = await this.probeConfigEndpoint(host, port);
|
|
174
|
+
if (configResult) {
|
|
175
|
+
// Got config — identify variant (openclaw vs goclaw)
|
|
176
|
+
const fp = await (0, fingerprint_js_1.identifyImplementation)(host, port, configResult);
|
|
177
|
+
const lan_host = this.resolveHostForDedup(host, port, configResult.assistantAgentId);
|
|
176
178
|
const now = new Date().toISOString();
|
|
177
179
|
return {
|
|
178
|
-
agent_id:
|
|
179
|
-
auto_name: "",
|
|
180
|
-
assistant_name:
|
|
181
|
-
display_name:
|
|
180
|
+
agent_id: configResult.assistantAgentId,
|
|
181
|
+
auto_name: "",
|
|
182
|
+
assistant_name: configResult.assistantName ?? "",
|
|
183
|
+
display_name: configResult.displayName ?? configResult.assistantName ?? "",
|
|
182
184
|
lan_host,
|
|
183
185
|
address: host,
|
|
184
186
|
gateway_port: port,
|
|
@@ -188,8 +190,60 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
188
190
|
status: "online",
|
|
189
191
|
last_seen: now,
|
|
190
192
|
discovered_at: now,
|
|
193
|
+
implementation: fp.implementation,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// No config endpoint — try fingerprint-only identification (zeroclaw, picoclaw)
|
|
197
|
+
const fp = await (0, fingerprint_js_1.identifyImplementation)(host, port, null);
|
|
198
|
+
if (fp.implementation !== "unknown") {
|
|
199
|
+
const now = new Date().toISOString();
|
|
200
|
+
return {
|
|
201
|
+
agent_id: `${fp.implementation}@${host}`,
|
|
202
|
+
auto_name: "",
|
|
203
|
+
assistant_name: "",
|
|
204
|
+
display_name: fp.implementation,
|
|
205
|
+
lan_host: host,
|
|
206
|
+
address: host,
|
|
207
|
+
gateway_port: port,
|
|
208
|
+
tls: false,
|
|
209
|
+
discovery_source: "scan",
|
|
210
|
+
network_scope: networkScope,
|
|
211
|
+
status: "online",
|
|
212
|
+
last_seen: now,
|
|
213
|
+
discovered_at: now,
|
|
214
|
+
implementation: fp.implementation,
|
|
191
215
|
};
|
|
192
216
|
}
|
|
217
|
+
// Fingerprint unknown — try framework adapters as final fallback
|
|
218
|
+
for (const adapter of index_js_1.ADAPTERS) {
|
|
219
|
+
const probe = await adapter.probe(host, port);
|
|
220
|
+
if (probe) {
|
|
221
|
+
const now = new Date().toISOString();
|
|
222
|
+
const partial = adapter.toClawInstance(host, port, probe);
|
|
223
|
+
return {
|
|
224
|
+
...partial,
|
|
225
|
+
auto_name: "",
|
|
226
|
+
network_scope: networkScope,
|
|
227
|
+
last_seen: now,
|
|
228
|
+
discovered_at: now,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
async probeConfigEndpoint(host, port) {
|
|
235
|
+
const url = `http://${host}:${port}${CONFIG_PATH}`;
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(url, {
|
|
238
|
+
signal: AbortSignal.timeout(TIMEOUT_PER_HOST),
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
return null;
|
|
242
|
+
const config = (await res.json());
|
|
243
|
+
if (!config.assistantAgentId)
|
|
244
|
+
return null;
|
|
245
|
+
return config;
|
|
246
|
+
}
|
|
193
247
|
catch {
|
|
194
248
|
return null;
|
|
195
249
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClawImplementation, ControlUiConfig } from "../types.js";
|
|
2
|
+
export interface FingerprintResult {
|
|
3
|
+
implementation: ClawImplementation;
|
|
4
|
+
confidence: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Identify the implementation variant of a host.
|
|
8
|
+
*
|
|
9
|
+
* Probe order (highest to lowest confidence):
|
|
10
|
+
* 1. /.well-known/claw-identity.json — ClawLink Protocol (self-declared)
|
|
11
|
+
* 2. /__openclaw/control-ui-config.json analysis — field count heuristic
|
|
12
|
+
* 3. /health endpoint — ZeroClaw vs PicoClaw differentiation
|
|
13
|
+
*/
|
|
14
|
+
export declare function identifyImplementation(host: string, port: number, config?: ControlUiConfig | null): Promise<FingerprintResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Detect implementation from a 404 error response body.
|
|
17
|
+
* Auxiliary signal — low confidence, used as tiebreaker.
|
|
18
|
+
*
|
|
19
|
+
* Go net/http: "404 page not found\n" (plain text)
|
|
20
|
+
* Fastify (Node.js): JSON {"message":"Route ... not found","error":"Not Found","statusCode":404}
|
|
21
|
+
*/
|
|
22
|
+
export declare function detect404Format(body: string): "go" | "fastify" | "unknown";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Fingerprint identification — multi-signal cascade to identify OpenClaw variants
|
|
3
|
+
//
|
|
4
|
+
// Supported implementations:
|
|
5
|
+
// openclaw — Node.js, port 18789, serves /__openclaw/control-ui-config.json
|
|
6
|
+
// goclaw — Go (chi), port 18789, serves /__openclaw/control-ui-config.json (compat mode)
|
|
7
|
+
// zeroclaw — Rust (Axum), port 42617, /health with "paired" field
|
|
8
|
+
// picoclaw — Go (net/http), port 18790, /health + /ready (K8s-style probes)
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.identifyImplementation = identifyImplementation;
|
|
11
|
+
exports.detect404Format = detect404Format;
|
|
12
|
+
const FINGERPRINT_TIMEOUT = 2_000;
|
|
13
|
+
/**
|
|
14
|
+
* Identify the implementation variant of a host.
|
|
15
|
+
*
|
|
16
|
+
* Probe order (highest to lowest confidence):
|
|
17
|
+
* 1. /.well-known/claw-identity.json — ClawLink Protocol (self-declared)
|
|
18
|
+
* 2. /__openclaw/control-ui-config.json analysis — field count heuristic
|
|
19
|
+
* 3. /health endpoint — ZeroClaw vs PicoClaw differentiation
|
|
20
|
+
*/
|
|
21
|
+
async function identifyImplementation(host, port, config) {
|
|
22
|
+
// Signal 1: ClawLink identity endpoint (highest priority)
|
|
23
|
+
const clawlink = await probeClawIdentity(host, port);
|
|
24
|
+
if (clawlink)
|
|
25
|
+
return clawlink;
|
|
26
|
+
// Signal 2: If we got a control-ui-config, analyze it
|
|
27
|
+
if (config) {
|
|
28
|
+
return analyzeConfig(config);
|
|
29
|
+
}
|
|
30
|
+
// Signal 3: No config → check /health for non-OpenClaw variants
|
|
31
|
+
const healthResult = await probeHealth(host, port);
|
|
32
|
+
if (healthResult)
|
|
33
|
+
return healthResult;
|
|
34
|
+
return { implementation: "unknown", confidence: 0.1 };
|
|
35
|
+
}
|
|
36
|
+
async function probeClawIdentity(host, port) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`http://${host}:${port}/.well-known/claw-identity.json`, { signal: AbortSignal.timeout(FINGERPRINT_TIMEOUT) });
|
|
39
|
+
if (!res.ok)
|
|
40
|
+
return null;
|
|
41
|
+
const data = (await res.json());
|
|
42
|
+
if (data.implementation && typeof data.implementation === "string") {
|
|
43
|
+
const impl = data.implementation.toLowerCase();
|
|
44
|
+
const known = [
|
|
45
|
+
"openclaw", "goclaw", "zeroclaw", "picoclaw", "nanoclaw", "nanobot",
|
|
46
|
+
];
|
|
47
|
+
return {
|
|
48
|
+
implementation: known.includes(impl) ? impl : "unknown",
|
|
49
|
+
confidence: 1.0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Analyze control-ui-config.json to distinguish OpenClaw from GoClaw.
|
|
60
|
+
*
|
|
61
|
+
* OpenClaw's config is feature-rich (>8 fields), while GoClaw's compat mode
|
|
62
|
+
* returns a minimal subset (<6 fields, missing UI-specific keys).
|
|
63
|
+
*/
|
|
64
|
+
function analyzeConfig(config) {
|
|
65
|
+
const knownFields = Object.keys(config);
|
|
66
|
+
const fieldCount = knownFields.length;
|
|
67
|
+
// GoClaw compat mode: minimal config, typically <6 fields
|
|
68
|
+
// and missing OpenClaw-specific UI fields
|
|
69
|
+
const uiFields = [
|
|
70
|
+
"controlUi", "assistantUrl", "webSearchEnabled",
|
|
71
|
+
"customInstructions", "tools",
|
|
72
|
+
];
|
|
73
|
+
const hasUiFields = uiFields.some((f) => f in config);
|
|
74
|
+
if (fieldCount < 6 && !hasUiFields) {
|
|
75
|
+
return { implementation: "goclaw", confidence: 0.7 };
|
|
76
|
+
}
|
|
77
|
+
return { implementation: "openclaw", confidence: 0.8 };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Probe /health to identify ZeroClaw or PicoClaw.
|
|
81
|
+
*
|
|
82
|
+
* ZeroClaw: /health returns JSON with "paired" + "runtime" fields
|
|
83
|
+
* PicoClaw: /health + /ready both return 200 (K8s-style)
|
|
84
|
+
*/
|
|
85
|
+
async function probeHealth(host, port) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`http://${host}:${port}/health`, {
|
|
88
|
+
signal: AbortSignal.timeout(FINGERPRINT_TIMEOUT),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
return null;
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
// ZeroClaw: JSON with "paired" field
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(text);
|
|
96
|
+
if ("paired" in data) {
|
|
97
|
+
return { implementation: "zeroclaw", confidence: 0.9 };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Not JSON, continue checking
|
|
102
|
+
}
|
|
103
|
+
// PicoClaw: /health OK + /ready also OK
|
|
104
|
+
const readyRes = await fetch(`http://${host}:${port}/ready`, {
|
|
105
|
+
signal: AbortSignal.timeout(FINGERPRINT_TIMEOUT),
|
|
106
|
+
});
|
|
107
|
+
if (readyRes.ok) {
|
|
108
|
+
return { implementation: "picoclaw", confidence: 0.8 };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Detect implementation from a 404 error response body.
|
|
118
|
+
* Auxiliary signal — low confidence, used as tiebreaker.
|
|
119
|
+
*
|
|
120
|
+
* Go net/http: "404 page not found\n" (plain text)
|
|
121
|
+
* Fastify (Node.js): JSON {"message":"Route ... not found","error":"Not Found","statusCode":404}
|
|
122
|
+
*/
|
|
123
|
+
function detect404Format(body) {
|
|
124
|
+
if (body.trim() === "404 page not found")
|
|
125
|
+
return "go";
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(body);
|
|
128
|
+
if (data.statusCode === 404 && typeof data.error === "string")
|
|
129
|
+
return "fastify";
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Not JSON
|
|
133
|
+
}
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type ClawImplementation = "openclaw" | "goclaw" | "zeroclaw" | "picoclaw" | "nanoclaw" | "nanobot" | "unknown";
|
|
1
2
|
export interface ClawInstance {
|
|
2
3
|
agent_id: string;
|
|
3
4
|
auto_name: string;
|
|
@@ -18,6 +19,7 @@ export interface ClawInstance {
|
|
|
18
19
|
is_self?: boolean;
|
|
19
20
|
claw_name?: string;
|
|
20
21
|
owner_pubkey?: string;
|
|
22
|
+
implementation?: ClawImplementation;
|
|
21
23
|
labels?: Record<string, string>;
|
|
22
24
|
}
|
|
23
25
|
export interface Connectivity {
|