@tjamescouch/agentchat 0.21.1 → 0.22.1
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/bin/agentchat.js +1 -1
- package/bin/agentchat.ts +1812 -0
- package/lib/allowlist.js +162 -0
- package/lib/client.js +2 -2
- package/lib/client.ts +877 -0
- package/lib/daemon.ts +662 -0
- package/lib/escrow-hooks.ts +391 -0
- package/lib/identity.ts +412 -0
- package/lib/jitter.ts +59 -0
- package/lib/proposals.ts +612 -0
- package/lib/protocol.js +40 -7
- package/lib/receipts.ts +359 -0
- package/lib/reputation.ts +790 -0
- package/lib/server/handlers/admin.js +94 -0
- package/lib/server/handlers/identity.js +16 -0
- package/lib/server/handlers/message.js +19 -6
- package/lib/server/handlers/presence.js +1 -0
- package/lib/server-directory.js +17 -8
- package/lib/server-directory.ts +232 -0
- package/lib/server.js +115 -10
- package/lib/server.ts +698 -0
- package/lib/supervisor/agent-health.sh +107 -0
- package/lib/supervisor/agent-monitor.sh +123 -0
- package/lib/supervisor/agentctl.sh +19 -3
- package/lib/supervisor/god-backup.sh +126 -0
- package/lib/supervisor/god-watchdog.sh +107 -0
- package/lib/supervisor/killswitch.sh +15 -8
- package/lib/types.ts +433 -0
- package/package.json +7 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Handlers
|
|
3
|
+
* Handles allowlist administration commands
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ServerMessageType,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
createMessage,
|
|
10
|
+
createError,
|
|
11
|
+
} from '../../protocol.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle ADMIN_APPROVE command - add a pubkey to the allowlist
|
|
15
|
+
*/
|
|
16
|
+
export function handleAdminApprove(server, ws, msg) {
|
|
17
|
+
if (!server.allowlist) {
|
|
18
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Allowlist not configured'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!msg.pubkey) {
|
|
23
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Missing pubkey'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = server.allowlist.approve(msg.pubkey, msg.admin_key, msg.note || '');
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, result.error));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
server._log('admin_approve', { agentId: result.agentId });
|
|
35
|
+
server._send(ws, createMessage(ServerMessageType.ADMIN_RESULT, {
|
|
36
|
+
action: 'approve',
|
|
37
|
+
success: true,
|
|
38
|
+
agentId: `@${result.agentId}`,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handle ADMIN_REVOKE command - remove a pubkey from the allowlist
|
|
44
|
+
*/
|
|
45
|
+
export function handleAdminRevoke(server, ws, msg) {
|
|
46
|
+
if (!server.allowlist) {
|
|
47
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Allowlist not configured'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const identifier = msg.pubkey || msg.agent_id;
|
|
52
|
+
if (!identifier) {
|
|
53
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Missing pubkey or agent_id'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = server.allowlist.revoke(identifier, msg.admin_key);
|
|
58
|
+
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
const code = result.error === 'invalid admin key' ? ErrorCode.AUTH_REQUIRED : ErrorCode.AGENT_NOT_FOUND;
|
|
61
|
+
server._send(ws, createError(code, result.error));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
server._log('admin_revoke', { identifier });
|
|
66
|
+
server._send(ws, createMessage(ServerMessageType.ADMIN_RESULT, {
|
|
67
|
+
action: 'revoke',
|
|
68
|
+
success: true,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle ADMIN_LIST command - list all approved entries
|
|
74
|
+
*/
|
|
75
|
+
export function handleAdminList(server, ws, msg) {
|
|
76
|
+
if (!server.allowlist) {
|
|
77
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Allowlist not configured'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate admin key
|
|
82
|
+
if (!server.allowlist._validateAdminKey(msg.admin_key)) {
|
|
83
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Invalid admin key'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const entries = server.allowlist.list();
|
|
88
|
+
server._send(ws, createMessage(ServerMessageType.ADMIN_RESULT, {
|
|
89
|
+
action: 'list',
|
|
90
|
+
entries,
|
|
91
|
+
enabled: server.allowlist.enabled,
|
|
92
|
+
strict: server.allowlist.strict,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
@@ -24,6 +24,21 @@ export function handleIdentify(server, ws, msg) {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Allowlist check (before any state changes)
|
|
28
|
+
if (server.allowlist && server.allowlist.enabled) {
|
|
29
|
+
const check = server.allowlist.check(msg.pubkey || null);
|
|
30
|
+
if (!check.allowed) {
|
|
31
|
+
server._log('allowlist_rejected', {
|
|
32
|
+
ip: ws._realIp,
|
|
33
|
+
name: msg.name,
|
|
34
|
+
hasPubkey: !!msg.pubkey,
|
|
35
|
+
reason: check.reason
|
|
36
|
+
});
|
|
37
|
+
server._send(ws, createError(ErrorCode.NOT_ALLOWED, check.reason));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
let id;
|
|
28
43
|
|
|
29
44
|
// Use pubkey-derived stable ID if pubkey provided
|
|
@@ -82,6 +97,7 @@ export function handleIdentify(server, ws, msg) {
|
|
|
82
97
|
|
|
83
98
|
server._send(ws, createMessage(ServerMessageType.WELCOME, {
|
|
84
99
|
agent_id: `@${id}`,
|
|
100
|
+
name: msg.name,
|
|
85
101
|
server: server.serverName
|
|
86
102
|
}));
|
|
87
103
|
}
|
|
@@ -33,6 +33,7 @@ export function handleMsg(server, ws, msg) {
|
|
|
33
33
|
|
|
34
34
|
const outMsg = createMessage(ServerMessageType.MSG, {
|
|
35
35
|
from: `@${agent.id}`,
|
|
36
|
+
from_name: agent.name,
|
|
36
37
|
to: msg.to,
|
|
37
38
|
content: msg.content,
|
|
38
39
|
...(msg.sig && { sig: msg.sig })
|
|
@@ -108,14 +109,15 @@ export function handleJoin(server, ws, msg) {
|
|
|
108
109
|
// Notify others
|
|
109
110
|
server._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_JOINED, {
|
|
110
111
|
channel: msg.channel,
|
|
111
|
-
agent: `@${agent.id}
|
|
112
|
+
agent: `@${agent.id}`,
|
|
113
|
+
name: agent.name
|
|
112
114
|
}), ws);
|
|
113
115
|
|
|
114
116
|
// Send confirmation with agent list
|
|
115
117
|
const agentList = [];
|
|
116
118
|
for (const memberWs of channel.agents) {
|
|
117
119
|
const member = server.agents.get(memberWs);
|
|
118
|
-
if (member) agentList.push(`@${member.id}
|
|
120
|
+
if (member) agentList.push({ id: `@${member.id}`, name: member.name });
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
server._send(ws, createMessage(ServerMessageType.JOINED, {
|
|
@@ -130,7 +132,7 @@ export function handleJoin(server, ws, msg) {
|
|
|
130
132
|
server._send(ws, createMessage(ServerMessageType.MSG, {
|
|
131
133
|
from: '@server',
|
|
132
134
|
to: msg.channel,
|
|
133
|
-
content: `Welcome to ${msg.channel}, @${agent.id}! Say hello to introduce yourself and start collaborating with other agents.`
|
|
135
|
+
content: `Welcome to ${msg.channel}, ${agent.name} (@${agent.id})! Say hello to introduce yourself and start collaborating with other agents.`
|
|
134
136
|
}));
|
|
135
137
|
|
|
136
138
|
// Prompt existing agents to engage with the new joiner (if there are others)
|
|
@@ -138,7 +140,7 @@ export function handleJoin(server, ws, msg) {
|
|
|
138
140
|
for (const memberWs of channel.agents) {
|
|
139
141
|
if (memberWs !== ws) {
|
|
140
142
|
const member = server.agents.get(memberWs);
|
|
141
|
-
if (member) otherAgents.push({ ws: memberWs, id: member.id });
|
|
143
|
+
if (member) otherAgents.push({ ws: memberWs, id: member.id, name: member.name });
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -146,7 +148,7 @@ export function handleJoin(server, ws, msg) {
|
|
|
146
148
|
const welcomePrompt = createMessage(ServerMessageType.MSG, {
|
|
147
149
|
from: '@server',
|
|
148
150
|
to: msg.channel,
|
|
149
|
-
content: `Hey ${otherAgents.map(a =>
|
|
151
|
+
content: `Hey ${otherAgents.map(a => `${a.name} (@${a.id})`).join(', ')} - new agent ${agent.name} (@${agent.id}) just joined! Say hi and share what you're working on.`
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
for (const other of otherAgents) {
|
|
@@ -179,7 +181,8 @@ export function handleLeave(server, ws, msg) {
|
|
|
179
181
|
// Notify others
|
|
180
182
|
server._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_LEFT, {
|
|
181
183
|
channel: msg.channel,
|
|
182
|
-
agent: `@${agent.id}
|
|
184
|
+
agent: `@${agent.id}`,
|
|
185
|
+
name: agent.name
|
|
183
186
|
}));
|
|
184
187
|
|
|
185
188
|
server._send(ws, createMessage(ServerMessageType.LEFT, {
|
|
@@ -189,8 +192,11 @@ export function handleLeave(server, ws, msg) {
|
|
|
189
192
|
|
|
190
193
|
/**
|
|
191
194
|
* Handle LIST_CHANNELS command
|
|
195
|
+
* Unauthenticated: returns channel names and agent count only
|
|
196
|
+
* Authenticated: returns full details
|
|
192
197
|
*/
|
|
193
198
|
export function handleListChannels(server, ws) {
|
|
199
|
+
const agent = server.agents.get(ws);
|
|
194
200
|
const list = [];
|
|
195
201
|
for (const [name, channel] of server.channels) {
|
|
196
202
|
if (!channel.inviteOnly) {
|
|
@@ -206,8 +212,15 @@ export function handleListChannels(server, ws) {
|
|
|
206
212
|
|
|
207
213
|
/**
|
|
208
214
|
* Handle LIST_AGENTS command
|
|
215
|
+
* Requires authentication to see agent details
|
|
209
216
|
*/
|
|
210
217
|
export function handleListAgents(server, ws, msg) {
|
|
218
|
+
const agent = server.agents.get(ws);
|
|
219
|
+
if (!agent) {
|
|
220
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
const channel = server.channels.get(msg.channel);
|
|
212
225
|
if (!channel) {
|
|
213
226
|
server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
@@ -34,6 +34,7 @@ export function handleSetPresence(server, ws, msg) {
|
|
|
34
34
|
// Broadcast presence change to all channels the agent is in
|
|
35
35
|
const presenceMsg = createMessage(ServerMessageType.PRESENCE_CHANGED, {
|
|
36
36
|
agent_id: `@${agent.id}`,
|
|
37
|
+
name: agent.name,
|
|
37
38
|
presence: agent.presence,
|
|
38
39
|
status_text: agent.statusText
|
|
39
40
|
});
|
package/lib/server-directory.js
CHANGED
|
@@ -9,14 +9,23 @@ import fs from 'fs/promises';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
|
|
11
11
|
// Default public servers (can be extended)
|
|
12
|
-
export const DEFAULT_SERVERS =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
export const DEFAULT_SERVERS = process.env.AGENTCHAT_PUBLIC === 'true'
|
|
13
|
+
? [
|
|
14
|
+
{
|
|
15
|
+
name: 'AgentChat Public',
|
|
16
|
+
url: 'wss://agentchat-server.fly.dev',
|
|
17
|
+
description: 'Official public AgentChat server',
|
|
18
|
+
region: 'global'
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
: [
|
|
22
|
+
{
|
|
23
|
+
name: 'AgentChat Local',
|
|
24
|
+
url: 'ws://localhost:6667',
|
|
25
|
+
description: 'Local AgentChat server',
|
|
26
|
+
region: 'local'
|
|
27
|
+
}
|
|
28
|
+
];
|
|
20
29
|
|
|
21
30
|
// Default directory file path
|
|
22
31
|
export const DEFAULT_DIRECTORY_PATH = path.join(
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Directory
|
|
3
|
+
* Registry of known AgentChat servers for discovery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
export interface ServerEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
url: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
region?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface HealthData {
|
|
19
|
+
agents?: {
|
|
20
|
+
connected?: number;
|
|
21
|
+
};
|
|
22
|
+
uptime_seconds?: number;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ServerWithStatus extends ServerEntry {
|
|
27
|
+
status: 'online' | 'offline' | 'unknown';
|
|
28
|
+
health?: HealthData;
|
|
29
|
+
error?: string;
|
|
30
|
+
checked_at?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ServerDirectoryFile {
|
|
34
|
+
version: number;
|
|
35
|
+
updated_at: string;
|
|
36
|
+
servers: ServerEntry[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ServerDirectoryOptions {
|
|
40
|
+
directoryPath?: string;
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DiscoverOptions {
|
|
45
|
+
onlineOnly?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Default public servers (can be extended)
|
|
49
|
+
export const DEFAULT_SERVERS: ServerEntry[] = process.env.AGENTCHAT_PUBLIC === 'true'
|
|
50
|
+
? [
|
|
51
|
+
{
|
|
52
|
+
name: 'AgentChat Public',
|
|
53
|
+
url: 'wss://agentchat-server.fly.dev',
|
|
54
|
+
description: 'Official public AgentChat server',
|
|
55
|
+
region: 'global'
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
: [
|
|
59
|
+
{
|
|
60
|
+
name: 'AgentChat Local',
|
|
61
|
+
url: 'ws://localhost:6667',
|
|
62
|
+
description: 'Local AgentChat server',
|
|
63
|
+
region: 'local'
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Default directory file path
|
|
68
|
+
export const DEFAULT_DIRECTORY_PATH: string = path.join(
|
|
69
|
+
process.env.HOME || process.env.USERPROFILE || '.',
|
|
70
|
+
'.agentchat',
|
|
71
|
+
'servers.json'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Server Directory for discovering AgentChat servers
|
|
76
|
+
*/
|
|
77
|
+
export class ServerDirectory {
|
|
78
|
+
private directoryPath: string;
|
|
79
|
+
private servers: ServerEntry[];
|
|
80
|
+
private timeout: number;
|
|
81
|
+
|
|
82
|
+
constructor(options: ServerDirectoryOptions = {}) {
|
|
83
|
+
this.directoryPath = options.directoryPath || DEFAULT_DIRECTORY_PATH;
|
|
84
|
+
this.servers = [...DEFAULT_SERVERS];
|
|
85
|
+
this.timeout = options.timeout || 5000;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load servers from directory file
|
|
90
|
+
*/
|
|
91
|
+
async load(): Promise<this> {
|
|
92
|
+
try {
|
|
93
|
+
const data = await fs.readFile(this.directoryPath, 'utf8');
|
|
94
|
+
const loaded = JSON.parse(data) as ServerDirectoryFile;
|
|
95
|
+
if (Array.isArray(loaded.servers)) {
|
|
96
|
+
// Merge with defaults, avoiding duplicates by URL
|
|
97
|
+
const urls = new Set(this.servers.map(s => s.url));
|
|
98
|
+
for (const server of loaded.servers) {
|
|
99
|
+
if (!urls.has(server.url)) {
|
|
100
|
+
this.servers.push(server);
|
|
101
|
+
urls.add(server.url);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// File doesn't exist or is invalid, use defaults
|
|
107
|
+
}
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Save servers to directory file
|
|
113
|
+
*/
|
|
114
|
+
async save(): Promise<void> {
|
|
115
|
+
const dir = path.dirname(this.directoryPath);
|
|
116
|
+
await fs.mkdir(dir, { recursive: true });
|
|
117
|
+
await fs.writeFile(this.directoryPath, JSON.stringify({
|
|
118
|
+
version: 1,
|
|
119
|
+
updated_at: new Date().toISOString(),
|
|
120
|
+
servers: this.servers
|
|
121
|
+
} as ServerDirectoryFile, null, 2));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add a server to the directory
|
|
126
|
+
*/
|
|
127
|
+
async addServer(server: ServerEntry): Promise<void> {
|
|
128
|
+
const existing = this.servers.find(s => s.url === server.url);
|
|
129
|
+
if (existing) {
|
|
130
|
+
Object.assign(existing, server);
|
|
131
|
+
} else {
|
|
132
|
+
this.servers.push(server);
|
|
133
|
+
}
|
|
134
|
+
await this.save();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove a server from the directory
|
|
139
|
+
*/
|
|
140
|
+
async removeServer(url: string): Promise<void> {
|
|
141
|
+
this.servers = this.servers.filter(s => s.url !== url);
|
|
142
|
+
await this.save();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check health of a single server
|
|
147
|
+
* @param server - Server object with url
|
|
148
|
+
* @returns Server with health status
|
|
149
|
+
*/
|
|
150
|
+
async checkHealth(server: ServerEntry): Promise<ServerWithStatus> {
|
|
151
|
+
const wsUrl = server.url;
|
|
152
|
+
// Convert ws:// or wss:// to http:// or https://
|
|
153
|
+
const httpUrl = wsUrl
|
|
154
|
+
.replace('wss://', 'https://')
|
|
155
|
+
.replace('ws://', 'http://');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const health = await this._fetchHealth(httpUrl + '/health');
|
|
159
|
+
return {
|
|
160
|
+
...server,
|
|
161
|
+
status: 'online',
|
|
162
|
+
health,
|
|
163
|
+
checked_at: new Date().toISOString()
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const error = err as Error & { code?: string };
|
|
167
|
+
return {
|
|
168
|
+
...server,
|
|
169
|
+
status: 'offline',
|
|
170
|
+
error: error.message || error.code || 'Unknown error',
|
|
171
|
+
checked_at: new Date().toISOString()
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetch health endpoint
|
|
178
|
+
*/
|
|
179
|
+
private _fetchHealth(url: string): Promise<HealthData> {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
182
|
+
const req = protocol.get(url, { timeout: this.timeout }, (res) => {
|
|
183
|
+
let data = '';
|
|
184
|
+
res.on('data', (chunk: Buffer) => data += chunk);
|
|
185
|
+
res.on('end', () => {
|
|
186
|
+
if (res.statusCode === 200) {
|
|
187
|
+
try {
|
|
188
|
+
resolve(JSON.parse(data) as HealthData);
|
|
189
|
+
} catch {
|
|
190
|
+
reject(new Error('Invalid health response'));
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
req.on('error', reject);
|
|
199
|
+
req.on('timeout', () => {
|
|
200
|
+
req.destroy();
|
|
201
|
+
reject(new Error('Timeout'));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Discover available servers (check health of all known servers)
|
|
208
|
+
* @param options
|
|
209
|
+
* @param options.onlineOnly - Only return online servers
|
|
210
|
+
* @returns List of servers with status
|
|
211
|
+
*/
|
|
212
|
+
async discover(options: DiscoverOptions = {}): Promise<ServerWithStatus[]> {
|
|
213
|
+
const results = await Promise.all(
|
|
214
|
+
this.servers.map(server => this.checkHealth(server))
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (options.onlineOnly) {
|
|
218
|
+
return results.filter(s => s.status === 'online');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get list of known servers without health check
|
|
226
|
+
*/
|
|
227
|
+
list(): ServerEntry[] {
|
|
228
|
+
return [...this.servers];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default ServerDirectory;
|