clawnexus 0.2.7 → 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.
@@ -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 ?? [DEFAULT_PORT];
75
- const allPorts = [...new Set([DEFAULT_PORT, ...ports])];
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
- const url = `http://${host}:${port}${CONFIG_PATH}`;
164
- try {
165
- const res = await fetch(url, {
166
- signal: AbortSignal.timeout(TIMEOUT_PER_HOST),
167
- });
168
- if (!res.ok)
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: config.assistantAgentId,
179
- auto_name: "", // will be assigned by store.upsert()
180
- assistant_name: config.assistantName ?? "",
181
- display_name: config.displayName ?? config.assistantName ?? "",
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" | "openfang" | "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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawnexus",
3
- "version": "0.2.7",
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>",