clawnexus 0.2.5 → 0.2.6
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/LICENSE +21 -0
- package/README.md +129 -108
- package/dist/cli/index.js +0 -0
- package/dist/registry/store.d.ts +15 -0
- package/dist/registry/store.js +86 -3
- package/dist/scanner/active.d.ts +9 -0
- package/dist/scanner/active.js +27 -1
- package/package.json +53 -54
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan Lan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,108 +1,129 @@
|
|
|
1
|
-
# clawnexus
|
|
2
|
-
|
|
3
|
-
ClawNexus daemon and CLI — AI instance registry for OpenClaw.
|
|
4
|
-
|
|
5
|
-
Discovers OpenClaw instances on your local network, assigns human-readable aliases, and exposes an HTTP API for querying and managing them.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install -g clawnexus
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Requires Node.js >= 22.
|
|
14
|
-
|
|
15
|
-
## CLI Usage
|
|
16
|
-
|
|
17
|
-
### Daemon Management
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
clawnexus start # Start the daemon (background process)
|
|
21
|
-
clawnexus stop # Stop the daemon
|
|
22
|
-
clawnexus restart # Restart the daemon
|
|
23
|
-
clawnexus status # Show daemon status
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Instance Discovery
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
clawnexus scan # Scan local network for OpenClaw instances
|
|
30
|
-
clawnexus list # List all known instances
|
|
31
|
-
clawnexus list --json # Machine-readable JSON output
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Instance Management
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
clawnexus alias <id|address> <name> # Set a friendly alias
|
|
38
|
-
clawnexus info <name|address> # Show instance details
|
|
39
|
-
clawnexus forget <name|address> # Remove from registry
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Connection
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
clawnexus connect <name> # Output ws:// address for an instance
|
|
46
|
-
clawnexus open <name> # Open WebChat UI in browser
|
|
47
|
-
clawnexus relay status # Show relay connection status
|
|
48
|
-
clawnexus connect <name.claw> # Connect via relay (v0.4)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Global Flags
|
|
52
|
-
|
|
53
|
-
| Flag | Description | Default |
|
|
54
|
-
|------|-------------|---------|
|
|
55
|
-
| `--json` | Machine-readable JSON output | `false` |
|
|
56
|
-
| `--timeout <ms>` | Request timeout | `5000` |
|
|
57
|
-
| `--api <url>` | Daemon API URL | `http://localhost:17890` |
|
|
58
|
-
|
|
59
|
-
## Daemon HTTP API
|
|
60
|
-
|
|
61
|
-
The daemon listens on `http://localhost:17890` by default.
|
|
62
|
-
|
|
63
|
-
| Method | Path | Description |
|
|
64
|
-
|--------|------|-------------|
|
|
65
|
-
| `GET` | `/health` | Daemon health status |
|
|
66
|
-
| `GET` | `/instances` | List all instances |
|
|
67
|
-
| `GET` | `/instances/:id` | Get a single instance |
|
|
68
|
-
| `PUT` | `/instances/:id/alias` | Set/update alias |
|
|
69
|
-
| `DELETE` | `/instances/:id` | Remove instance |
|
|
70
|
-
| `POST` | `/scan` | Trigger network scan |
|
|
71
|
-
| `POST` | `/
|
|
72
|
-
| `GET` | `/
|
|
73
|
-
| `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
# clawnexus
|
|
2
|
+
|
|
3
|
+
ClawNexus daemon and CLI — AI instance registry for OpenClaw.
|
|
4
|
+
|
|
5
|
+
Discovers OpenClaw instances on your local network, assigns human-readable aliases, and exposes an HTTP API for querying and managing them.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g clawnexus
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js >= 22.
|
|
14
|
+
|
|
15
|
+
## CLI Usage
|
|
16
|
+
|
|
17
|
+
### Daemon Management
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
clawnexus start # Start the daemon (background process)
|
|
21
|
+
clawnexus stop # Stop the daemon
|
|
22
|
+
clawnexus restart # Restart the daemon
|
|
23
|
+
clawnexus status # Show daemon status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Instance Discovery
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
clawnexus scan # Scan local network for OpenClaw instances
|
|
30
|
+
clawnexus list # List all known instances
|
|
31
|
+
clawnexus list --json # Machine-readable JSON output
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Instance Management
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
clawnexus alias <id|address> <name> # Set a friendly alias
|
|
38
|
+
clawnexus info <name|address> # Show instance details
|
|
39
|
+
clawnexus forget <name|address> # Remove from registry
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Connection
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
clawnexus connect <name> # Output ws:// address for an instance
|
|
46
|
+
clawnexus open <name> # Open WebChat UI in browser
|
|
47
|
+
clawnexus relay status # Show relay connection status
|
|
48
|
+
clawnexus connect <name.claw> # Connect via relay (v0.4)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Global Flags
|
|
52
|
+
|
|
53
|
+
| Flag | Description | Default |
|
|
54
|
+
|------|-------------|---------|
|
|
55
|
+
| `--json` | Machine-readable JSON output | `false` |
|
|
56
|
+
| `--timeout <ms>` | Request timeout | `5000` |
|
|
57
|
+
| `--api <url>` | Daemon API URL | `http://localhost:17890` |
|
|
58
|
+
|
|
59
|
+
## Daemon HTTP API
|
|
60
|
+
|
|
61
|
+
The daemon listens on `http://localhost:17890` by default.
|
|
62
|
+
|
|
63
|
+
| Method | Path | Description |
|
|
64
|
+
|--------|------|-------------|
|
|
65
|
+
| `GET` | `/health` | Daemon health status |
|
|
66
|
+
| `GET` | `/instances` | List all instances |
|
|
67
|
+
| `GET` | `/instances/:id` | Get a single instance |
|
|
68
|
+
| `PUT` | `/instances/:id/alias` | Set/update alias |
|
|
69
|
+
| `DELETE` | `/instances/:id` | Remove instance |
|
|
70
|
+
| `POST` | `/scan` | Trigger network scan |
|
|
71
|
+
| `POST` | `/registry/register` | Register with public Registry (v0.2) |
|
|
72
|
+
| `GET` | `/registry/status` | Registration status (v0.2) |
|
|
73
|
+
| `GET` | `/resolve/:name` | Resolve a `.claw` name (v0.2) |
|
|
74
|
+
| `GET` | `/whoami` | This instance's identity (v0.2) |
|
|
75
|
+
| `POST` | `/relay/connect` | Connect via relay (v0.4) |
|
|
76
|
+
| `GET` | `/relay/status` | Relay connection status (v0.4) |
|
|
77
|
+
| `DELETE` | `/relay/disconnect/:room_id` | Disconnect relay room (v0.4) |
|
|
78
|
+
| `GET` | `/agent/policy` | Get agent policy (v0.4) |
|
|
79
|
+
| `PUT` | `/agent/policy` | Replace agent policy (v0.4) |
|
|
80
|
+
| `PATCH` | `/agent/policy` | Partial policy update (v0.4) |
|
|
81
|
+
| `POST` | `/agent/policy/reset` | Reset policy to defaults (v0.4) |
|
|
82
|
+
| `GET` | `/agent/tasks` | List tasks (v0.4) |
|
|
83
|
+
| `GET` | `/agent/tasks/stats` | Task statistics (v0.4) |
|
|
84
|
+
| `GET` | `/agent/tasks/:id` | Get a single task (v0.4) |
|
|
85
|
+
| `POST` | `/agent/tasks/:id/cancel` | Cancel a task (v0.4) |
|
|
86
|
+
| `POST` | `/agent/propose` | Send task proposal to peer (v0.4) |
|
|
87
|
+
| `POST` | `/agent/query` | Query peer capabilities (v0.4) |
|
|
88
|
+
| `GET` | `/agent/inbox` | List queued inbound proposals (v0.4) |
|
|
89
|
+
| `POST` | `/agent/inbox/:id/approve` | Approve queued proposal (v0.4) |
|
|
90
|
+
| `POST` | `/agent/inbox/:id/deny` | Deny queued proposal (v0.4) |
|
|
91
|
+
| `GET` | `/diagnostics` | Full diagnostics summary |
|
|
92
|
+
| `GET` | `/diagnostics/unreachable` | mDNS-heard but unreachable instances |
|
|
93
|
+
|
|
94
|
+
See [docs/api.md](../../docs/api.md) for full request/response examples.
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
| Environment Variable | Description | Default |
|
|
99
|
+
|---------------------|-------------|---------|
|
|
100
|
+
| `CLAWNEXUS_PORT` | Daemon API port | `17890` |
|
|
101
|
+
| `CLAWNEXUS_HOST` | Daemon bind address | `127.0.0.1` |
|
|
102
|
+
| `CLAWNEXUS_API` | CLI target API URL | `http://localhost:17890` |
|
|
103
|
+
| `CLAWNEXUS_RELAY_URL` | Override relay WebSocket URL | _(from Registry token)_ |
|
|
104
|
+
|
|
105
|
+
Data is stored in `~/.clawnexus/`:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
~/.clawnexus/
|
|
109
|
+
├── registry.json # Instance registry
|
|
110
|
+
├── daemon.pid # PID file
|
|
111
|
+
├── identity.json # Ed25519 identity keys (v0.2)
|
|
112
|
+
└── policy.json # Agent policy configuration (v0.4)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Programmatic Usage
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { startDaemon } from "clawnexus";
|
|
119
|
+
|
|
120
|
+
const handle = await startDaemon({ port: 17890, host: "127.0.0.1" });
|
|
121
|
+
|
|
122
|
+
// Access components
|
|
123
|
+
console.log(handle.store.getAll()); // List instances
|
|
124
|
+
await handle.app.close(); // Graceful shutdown
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/registry/store.d.ts
CHANGED
|
@@ -26,6 +26,21 @@ export declare class RegistryStore extends EventEmitter {
|
|
|
26
26
|
remove(networkKey: string): boolean;
|
|
27
27
|
setAlias(networkKey: string, alias: string): void;
|
|
28
28
|
get size(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Find an existing instance that represents the same physical machine
|
|
31
|
+
* discovered via a different network interface. Matches on agent_id + lan_host.
|
|
32
|
+
*/
|
|
33
|
+
private _findDuplicate;
|
|
34
|
+
/**
|
|
35
|
+
* Merge two instances representing the same machine on different NICs.
|
|
36
|
+
* Keeps user-set fields (alias, labels, auto_name, claw_name, owner_pubkey)
|
|
37
|
+
* from the existing entry. Picks the best address by network scope priority.
|
|
38
|
+
*/
|
|
39
|
+
private _mergeInstances;
|
|
40
|
+
/** Numeric priority for network scope: local > vpn > public */
|
|
41
|
+
private _addressPriority;
|
|
42
|
+
/** Normalize hostname for comparison: lowercase, strip trailing .local */
|
|
43
|
+
private _normalizeHost;
|
|
29
44
|
private scheduleDirtyFlush;
|
|
30
45
|
private flushNow;
|
|
31
46
|
}
|
package/dist/registry/store.js
CHANGED
|
@@ -201,8 +201,25 @@ 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
|
+
this.instances.set(key, instance);
|
|
205
|
+
this.scheduleDirtyFlush();
|
|
206
|
+
this.emit("upsert", instance);
|
|
204
207
|
}
|
|
205
208
|
else {
|
|
209
|
+
// Check for duplicate from different NIC (multi-NIC deduplication)
|
|
210
|
+
const duplicate = this._findDuplicate(instance);
|
|
211
|
+
if (duplicate) {
|
|
212
|
+
const merged = this._mergeInstances(duplicate, instance);
|
|
213
|
+
const oldKey = this.networkKey(duplicate.address, duplicate.gateway_port);
|
|
214
|
+
const newKey = this.networkKey(merged.address, merged.gateway_port);
|
|
215
|
+
if (oldKey !== newKey) {
|
|
216
|
+
this.instances.delete(oldKey);
|
|
217
|
+
}
|
|
218
|
+
this.instances.set(newKey, merged);
|
|
219
|
+
this.scheduleDirtyFlush();
|
|
220
|
+
this.emit("merge", { kept: merged, removed_key: oldKey !== newKey ? oldKey : null });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
206
223
|
// First discovery: generate auto_name if not already set
|
|
207
224
|
if (!instance.auto_name) {
|
|
208
225
|
const baseName = (0, auto_name_js_1.generateAutoName)(instance.lan_host, instance.display_name, instance.address);
|
|
@@ -212,10 +229,10 @@ class RegistryStore extends node_events_1.EventEmitter {
|
|
|
212
229
|
}
|
|
213
230
|
instance.auto_name = (0, auto_name_js_1.ensureUnique)(baseName, usedNames);
|
|
214
231
|
}
|
|
232
|
+
this.instances.set(key, instance);
|
|
233
|
+
this.scheduleDirtyFlush();
|
|
234
|
+
this.emit("upsert", instance);
|
|
215
235
|
}
|
|
216
|
-
this.instances.set(key, instance);
|
|
217
|
-
this.scheduleDirtyFlush();
|
|
218
|
-
this.emit("upsert", instance);
|
|
219
236
|
}
|
|
220
237
|
remove(networkKey) {
|
|
221
238
|
const deleted = this.instances.delete(networkKey);
|
|
@@ -246,6 +263,72 @@ class RegistryStore extends node_events_1.EventEmitter {
|
|
|
246
263
|
get size() {
|
|
247
264
|
return this.instances.size;
|
|
248
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Find an existing instance that represents the same physical machine
|
|
268
|
+
* discovered via a different network interface. Matches on agent_id + lan_host.
|
|
269
|
+
*/
|
|
270
|
+
_findDuplicate(instance) {
|
|
271
|
+
const incomingHost = this._normalizeHost(instance.lan_host);
|
|
272
|
+
const incomingAgent = instance.agent_id.toLowerCase();
|
|
273
|
+
for (const existing of this.instances.values()) {
|
|
274
|
+
if (existing.agent_id.toLowerCase() === incomingAgent &&
|
|
275
|
+
this._normalizeHost(existing.lan_host) === incomingHost) {
|
|
276
|
+
return existing;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Merge two instances representing the same machine on different NICs.
|
|
283
|
+
* Keeps user-set fields (alias, labels, auto_name, claw_name, owner_pubkey)
|
|
284
|
+
* from the existing entry. Picks the best address by network scope priority.
|
|
285
|
+
*/
|
|
286
|
+
_mergeInstances(existing, incoming) {
|
|
287
|
+
const existingPriority = this._addressPriority(existing.network_scope);
|
|
288
|
+
const incomingPriority = this._addressPriority(incoming.network_scope);
|
|
289
|
+
// Pick address: higher priority wins; if equal, keep existing (first-seen)
|
|
290
|
+
const keepExistingAddress = existingPriority >= incomingPriority;
|
|
291
|
+
return {
|
|
292
|
+
// Network identity: use the better address
|
|
293
|
+
address: keepExistingAddress ? existing.address : incoming.address,
|
|
294
|
+
gateway_port: keepExistingAddress ? existing.gateway_port : incoming.gateway_port,
|
|
295
|
+
network_scope: keepExistingAddress ? existing.network_scope : incoming.network_scope,
|
|
296
|
+
tls: keepExistingAddress ? existing.tls : incoming.tls,
|
|
297
|
+
tls_fingerprint: keepExistingAddress ? existing.tls_fingerprint : incoming.tls_fingerprint,
|
|
298
|
+
// Identity fields from existing (stable)
|
|
299
|
+
agent_id: existing.agent_id,
|
|
300
|
+
auto_name: existing.auto_name,
|
|
301
|
+
alias: existing.alias,
|
|
302
|
+
lan_host: existing.lan_host,
|
|
303
|
+
assistant_name: incoming.assistant_name,
|
|
304
|
+
display_name: incoming.display_name,
|
|
305
|
+
// Timestamps: keep original discovered_at, use latest last_seen
|
|
306
|
+
discovered_at: existing.discovered_at,
|
|
307
|
+
last_seen: incoming.last_seen,
|
|
308
|
+
// Discovery source: use the incoming one (most recent discovery)
|
|
309
|
+
discovery_source: incoming.discovery_source,
|
|
310
|
+
// Status from incoming (freshest)
|
|
311
|
+
status: incoming.status,
|
|
312
|
+
// Preserve user-set and registry fields from existing
|
|
313
|
+
claw_name: existing.claw_name ?? incoming.claw_name,
|
|
314
|
+
owner_pubkey: existing.owner_pubkey ?? incoming.owner_pubkey,
|
|
315
|
+
labels: existing.labels ?? incoming.labels,
|
|
316
|
+
connectivity: incoming.connectivity ?? existing.connectivity,
|
|
317
|
+
is_self: existing.is_self || incoming.is_self,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** Numeric priority for network scope: local > vpn > public */
|
|
321
|
+
_addressPriority(scope) {
|
|
322
|
+
switch (scope) {
|
|
323
|
+
case "local": return 2;
|
|
324
|
+
case "vpn": return 1;
|
|
325
|
+
case "public": return 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Normalize hostname for comparison: lowercase, strip trailing .local */
|
|
329
|
+
_normalizeHost(host) {
|
|
330
|
+
return host.toLowerCase().replace(/\.local$/, "");
|
|
331
|
+
}
|
|
249
332
|
scheduleDirtyFlush() {
|
|
250
333
|
this.dirty = true;
|
|
251
334
|
if (this.flushTimer)
|
package/dist/scanner/active.d.ts
CHANGED
|
@@ -18,4 +18,13 @@ export declare class ActiveScanner extends EventEmitter {
|
|
|
18
18
|
private generateIPs;
|
|
19
19
|
private probeAll;
|
|
20
20
|
private probeHost;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a stable lan_host for scanned instances to enable multi-NIC dedup.
|
|
23
|
+
*
|
|
24
|
+
* When scanning, we only have the target IP as lan_host. If the same instance
|
|
25
|
+
* was already discovered via mDNS or LocalProbe (which provide real hostnames),
|
|
26
|
+
* reuse that hostname so _findDuplicate() can match them as the same machine.
|
|
27
|
+
*/
|
|
28
|
+
private resolveHostForDedup;
|
|
29
|
+
private isIPAddress;
|
|
21
30
|
}
|
package/dist/scanner/active.js
CHANGED
|
@@ -170,13 +170,16 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
170
170
|
const config = (await res.json());
|
|
171
171
|
if (!config.assistantAgentId)
|
|
172
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);
|
|
173
176
|
const now = new Date().toISOString();
|
|
174
177
|
return {
|
|
175
178
|
agent_id: config.assistantAgentId,
|
|
176
179
|
auto_name: "", // will be assigned by store.upsert()
|
|
177
180
|
assistant_name: config.assistantName ?? "",
|
|
178
181
|
display_name: config.displayName ?? config.assistantName ?? "",
|
|
179
|
-
lan_host
|
|
182
|
+
lan_host,
|
|
180
183
|
address: host,
|
|
181
184
|
gateway_port: port,
|
|
182
185
|
tls: false,
|
|
@@ -191,6 +194,29 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
191
194
|
return null;
|
|
192
195
|
}
|
|
193
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve a stable lan_host for scanned instances to enable multi-NIC dedup.
|
|
199
|
+
*
|
|
200
|
+
* When scanning, we only have the target IP as lan_host. If the same instance
|
|
201
|
+
* was already discovered via mDNS or LocalProbe (which provide real hostnames),
|
|
202
|
+
* reuse that hostname so _findDuplicate() can match them as the same machine.
|
|
203
|
+
*/
|
|
204
|
+
resolveHostForDedup(host, port, agentId) {
|
|
205
|
+
if (!this.isIPAddress(host))
|
|
206
|
+
return host;
|
|
207
|
+
const agentLower = agentId.toLowerCase();
|
|
208
|
+
for (const existing of this.store.getAll()) {
|
|
209
|
+
if (existing.agent_id.toLowerCase() === agentLower &&
|
|
210
|
+
existing.gateway_port === port &&
|
|
211
|
+
!this.isIPAddress(existing.lan_host)) {
|
|
212
|
+
return existing.lan_host;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return host;
|
|
216
|
+
}
|
|
217
|
+
isIPAddress(value) {
|
|
218
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(value);
|
|
219
|
+
}
|
|
194
220
|
}
|
|
195
221
|
exports.ActiveScanner = ActiveScanner;
|
|
196
222
|
/** Parse "host:port" or "host" into { host, port } */
|
package/package.json
CHANGED
|
@@ -1,54 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "clawnexus",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "ClawNexus daemon and CLI — AI instance registry for OpenClaw",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "alan-silverstreams <alan@silverstream.tech>",
|
|
7
|
-
"homepage": "https://github.com/
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
11
|
-
"directory": "packages/daemon"
|
|
12
|
-
},
|
|
13
|
-
"bugs": "https://github.com/
|
|
14
|
-
"keywords": [
|
|
15
|
-
"openclaw",
|
|
16
|
-
"ai",
|
|
17
|
-
"agent",
|
|
18
|
-
"registry",
|
|
19
|
-
"mdns",
|
|
20
|
-
"discovery",
|
|
21
|
-
"daemon",
|
|
22
|
-
"cli",
|
|
23
|
-
"clawnexus"
|
|
24
|
-
],
|
|
25
|
-
"main": "dist/index.js",
|
|
26
|
-
"types": "dist/index.d.ts",
|
|
27
|
-
"bin": {
|
|
28
|
-
"clawnexus": "dist/cli/index.js"
|
|
29
|
-
},
|
|
30
|
-
"files": [
|
|
31
|
-
"dist",
|
|
32
|
-
"README.md"
|
|
33
|
-
],
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "clawnexus",
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"description": "ClawNexus daemon and CLI — AI instance registry for OpenClaw",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "alan-silverstreams <alan@silverstream.tech>",
|
|
7
|
+
"homepage": "https://github.com/SilverstreamsAI/ClawNexus",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/SilverstreamsAI/ClawNexus.git",
|
|
11
|
+
"directory": "packages/daemon"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/SilverstreamsAI/ClawNexus/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"openclaw",
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"registry",
|
|
19
|
+
"mdns",
|
|
20
|
+
"discovery",
|
|
21
|
+
"daemon",
|
|
22
|
+
"cli",
|
|
23
|
+
"clawnexus"
|
|
24
|
+
],
|
|
25
|
+
"main": "dist/index.js",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"bin": {
|
|
28
|
+
"clawnexus": "dist/cli/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"fastify": "^5.0.0",
|
|
39
|
+
"multicast-dns": "^7.2.5",
|
|
40
|
+
"ws": "^8.18.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"typescript": "^5.7.0",
|
|
44
|
+
"@types/node": "^22.0.0",
|
|
45
|
+
"@types/ws": "^8.5.14"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
50
|
+
"start": "node dist/index.js",
|
|
51
|
+
"test": "vitest run"
|
|
52
|
+
}
|
|
53
|
+
}
|