clawnexus 0.2.6 → 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.
@@ -0,0 +1,4 @@
1
+ import type { FrameworkAdapter } from "./types.js";
2
+ export declare const ADAPTERS: readonly FrameworkAdapter[];
3
+ export declare function getAdapter(name: string): FrameworkAdapter | undefined;
4
+ export declare function getAllAdapterPorts(): number[];
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Framework adapter interface — pluggable probe logic for non-OpenClaw variants
3
+ // that don't share port 18789 or serve /__openclaw/control-ui-config.json
4
+ Object.defineProperty(exports, "__esModule", { value: true });
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 + 10));
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)
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  // Health Checker — periodically pings known instances and updates status
3
- // Enhanced with dual-channel connectivity detection (TICKET-021)
3
+ // Enhanced with dual-channel connectivity detection
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.HealthChecker = void 0;
6
6
  const node_events_1 = require("node:events");
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");
@@ -114,6 +114,7 @@ class LocalProbe extends node_events_1.EventEmitter {
114
114
  status: "online",
115
115
  last_seen: now,
116
116
  discovered_at: now,
117
+ implementation: "openclaw",
117
118
  connectivity: {
118
119
  lan_reachable: true,
119
120
  relay_available: false,
@@ -93,7 +93,7 @@ class MdnsListener extends node_events_1.EventEmitter {
93
93
  // Fetch agent_id from the OpenClaw instance
94
94
  const agentId = await this.fetchAgentId(address, port, txt["gatewayTls"] === "1");
95
95
  if (!agentId) {
96
- // Record unreachable instance for diagnostics (TICKET-021)
96
+ // Record unreachable instance for diagnostics
97
97
  this.emit("mdns:unreachable", {
98
98
  address,
99
99
  port,
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // RegistryClient — HTTP client for ClawNexus-Cloud public registry
2
+ // RegistryClient — HTTP client for the ClawNexus public registry
3
3
  // Handles register, resolve, getToken, checkName with signature and retry logic
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.RegistryClient = exports.RegistryError = void 0;
@@ -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 */
@@ -18,6 +18,7 @@ export declare class ActiveScanner extends EventEmitter {
18
18
  private generateIPs;
19
19
  private probeAll;
20
20
  private probeHost;
21
+ private probeConfigEndpoint;
21
22
  /**
22
23
  * Resolve a stable lan_host for scanned instances to enable multi-NIC dedup.
23
24
  *
@@ -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" | "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.6",
3
+ "version": "0.2.8",
4
4
  "description": "ClawNexus daemon and CLI — AI instance registry for OpenClaw",
5
5
  "license": "MIT",
6
6
  "author": "alan-silverstreams <alan@silverstream.tech>",
@@ -24,6 +24,12 @@
24
24
  ],
25
25
  "main": "dist/index.js",
26
26
  "types": "dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
27
33
  "bin": {
28
34
  "clawnexus": "dist/cli/index.js"
29
35
  },