@rookdaemon/agora 0.1.2 → 0.2.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.
- package/README.md +457 -1
- package/dist/cli.js +627 -37
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery/bootstrap.d.ts +32 -0
- package/dist/discovery/bootstrap.d.ts.map +1 -0
- package/dist/discovery/bootstrap.js +36 -0
- package/dist/discovery/bootstrap.js.map +1 -0
- package/dist/discovery/peer-discovery.d.ts +59 -0
- package/dist/discovery/peer-discovery.d.ts.map +1 -0
- package/dist/discovery/peer-discovery.js +108 -0
- package/dist/discovery/peer-discovery.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/message/envelope.d.ts +1 -1
- package/dist/message/envelope.d.ts.map +1 -1
- package/dist/message/envelope.js.map +1 -1
- package/dist/message/types/paper-discovery.d.ts +28 -0
- package/dist/message/types/paper-discovery.d.ts.map +1 -0
- package/dist/message/types/paper-discovery.js +2 -0
- package/dist/message/types/paper-discovery.js.map +1 -0
- package/dist/message/types/peer-discovery.d.ts +78 -0
- package/dist/message/types/peer-discovery.d.ts.map +1 -0
- package/dist/message/types/peer-discovery.js +90 -0
- package/dist/message/types/peer-discovery.js.map +1 -0
- package/dist/peer/client.d.ts +50 -0
- package/dist/peer/client.d.ts.map +1 -0
- package/dist/peer/client.js +138 -0
- package/dist/peer/client.js.map +1 -0
- package/dist/peer/manager.d.ts +65 -0
- package/dist/peer/manager.d.ts.map +1 -0
- package/dist/peer/manager.js +153 -0
- package/dist/peer/manager.js.map +1 -0
- package/dist/peer/server.d.ts +65 -0
- package/dist/peer/server.d.ts.map +1 -0
- package/dist/peer/server.js +154 -0
- package/dist/peer/server.js.map +1 -0
- package/dist/registry/discovery-service.d.ts +64 -0
- package/dist/registry/discovery-service.d.ts.map +1 -0
- package/dist/registry/discovery-service.js +129 -0
- package/dist/registry/discovery-service.js.map +1 -0
- package/dist/registry/messages.d.ts +55 -0
- package/dist/registry/messages.d.ts.map +1 -1
- package/dist/relay/client.d.ts +112 -0
- package/dist/relay/client.d.ts.map +1 -0
- package/dist/relay/client.js +281 -0
- package/dist/relay/client.js.map +1 -0
- package/dist/relay/server.d.ts +76 -0
- package/dist/relay/server.d.ts.map +1 -0
- package/dist/relay/server.js +338 -0
- package/dist/relay/server.js.map +1 -0
- package/dist/relay/types.d.ts +35 -0
- package/dist/relay/types.d.ts.map +1 -0
- package/dist/relay/types.js +2 -0
- package/dist/relay/types.js.map +1 -0
- package/dist/transport/peer-config.d.ts +3 -2
- package/dist/transport/peer-config.d.ts.map +1 -1
- package/dist/transport/peer-config.js.map +1 -1
- package/dist/transport/relay.d.ts +23 -0
- package/dist/transport/relay.d.ts.map +1 -0
- package/dist/transport/relay.js +85 -0
- package/dist/transport/relay.js.map +1 -0
- package/package.json +7 -2
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,12 @@ import { dirname, resolve } from 'node:path';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { loadPeerConfig, savePeerConfig, initPeerConfig } from './transport/peer-config.js';
|
|
7
7
|
import { sendToPeer, decodeInboundEnvelope } from './transport/http.js';
|
|
8
|
+
import { sendViaRelay } from './transport/relay.js';
|
|
9
|
+
import { PeerServer } from './peer/server.js';
|
|
10
|
+
import { RelayServer } from './relay/server.js';
|
|
11
|
+
import { RelayClient } from './relay/client.js';
|
|
12
|
+
import { PeerDiscoveryService } from './discovery/peer-discovery.js';
|
|
13
|
+
import { getDefaultBootstrapRelay } from './discovery/bootstrap.js';
|
|
8
14
|
/**
|
|
9
15
|
* Get the config file path from CLI options, environment, or default.
|
|
10
16
|
*/
|
|
@@ -108,7 +114,7 @@ function handleWhoami(options) {
|
|
|
108
114
|
*/
|
|
109
115
|
function handlePeersAdd(args, options) {
|
|
110
116
|
if (args.length < 1) {
|
|
111
|
-
console.error('Error: Missing peer name. Usage: agora peers add <name> --
|
|
117
|
+
console.error('Error: Missing peer name. Usage: agora peers add <name> --pubkey <pubkey> [--url <url> --token <token>]');
|
|
112
118
|
process.exit(1);
|
|
113
119
|
}
|
|
114
120
|
const name = args[0];
|
|
@@ -120,25 +126,42 @@ function handlePeersAdd(args, options) {
|
|
|
120
126
|
const url = options.url;
|
|
121
127
|
const token = options.token;
|
|
122
128
|
const pubkey = options.pubkey;
|
|
123
|
-
if (!
|
|
124
|
-
console.error('Error: Missing required
|
|
129
|
+
if (!pubkey) {
|
|
130
|
+
console.error('Error: Missing required --pubkey option.');
|
|
125
131
|
process.exit(1);
|
|
126
132
|
}
|
|
133
|
+
// Validate that if one of url/token is provided, both must be
|
|
134
|
+
if ((url && !token) || (!url && token)) {
|
|
135
|
+
console.error('Error: Both --url and --token must be provided together.');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
// Check if we have HTTP transport or relay
|
|
127
139
|
const config = loadPeerConfig(configPath);
|
|
128
|
-
|
|
140
|
+
const hasHttpConfig = url && token;
|
|
141
|
+
const hasRelay = config.relay;
|
|
142
|
+
if (!hasHttpConfig && !hasRelay) {
|
|
143
|
+
console.error('Error: Either (--url and --token) must be provided, or relay must be configured in config file.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
// Add the peer
|
|
129
147
|
config.peers[name] = {
|
|
130
|
-
url,
|
|
131
|
-
token,
|
|
132
148
|
publicKey: pubkey,
|
|
133
149
|
name, // Set name to match the key for consistency
|
|
134
150
|
};
|
|
151
|
+
if (url && token) {
|
|
152
|
+
config.peers[name].url = url;
|
|
153
|
+
config.peers[name].token = token;
|
|
154
|
+
}
|
|
135
155
|
savePeerConfig(configPath, config);
|
|
136
|
-
|
|
156
|
+
const outputData = {
|
|
137
157
|
status: 'added',
|
|
138
158
|
name,
|
|
139
|
-
url,
|
|
140
159
|
publicKey: pubkey
|
|
141
|
-
}
|
|
160
|
+
};
|
|
161
|
+
if (url) {
|
|
162
|
+
outputData.url = url;
|
|
163
|
+
}
|
|
164
|
+
output(outputData, options.pretty || false);
|
|
142
165
|
}
|
|
143
166
|
/**
|
|
144
167
|
* Handle the `agora peers list` command.
|
|
@@ -183,6 +206,134 @@ function handlePeersRemove(args, options) {
|
|
|
183
206
|
name
|
|
184
207
|
}, options.pretty || false);
|
|
185
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Handle the `agora peers discover` command.
|
|
211
|
+
*/
|
|
212
|
+
async function handlePeersDiscover(options) {
|
|
213
|
+
const configPath = getConfigPath(options);
|
|
214
|
+
if (!existsSync(configPath)) {
|
|
215
|
+
console.error('Error: Config file not found. Run `agora init` first.');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const config = loadPeerConfig(configPath);
|
|
219
|
+
// Determine relay configuration
|
|
220
|
+
let relayUrl;
|
|
221
|
+
let relayPublicKey;
|
|
222
|
+
if (options.relay) {
|
|
223
|
+
// Use custom relay from command line
|
|
224
|
+
relayUrl = options.relay;
|
|
225
|
+
relayPublicKey = options['relay-pubkey'];
|
|
226
|
+
}
|
|
227
|
+
else if (config.relay) {
|
|
228
|
+
// Use relay from config
|
|
229
|
+
relayUrl = config.relay;
|
|
230
|
+
// TODO: Add relayPublicKey to config schema in future
|
|
231
|
+
relayPublicKey = undefined;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Use default bootstrap relay
|
|
235
|
+
const bootstrap = getDefaultBootstrapRelay();
|
|
236
|
+
relayUrl = bootstrap.relayUrl;
|
|
237
|
+
relayPublicKey = bootstrap.relayPublicKey;
|
|
238
|
+
}
|
|
239
|
+
// Parse filters
|
|
240
|
+
const filters = {};
|
|
241
|
+
if (options['active-within']) {
|
|
242
|
+
const ms = parseInt(options['active-within'], 10);
|
|
243
|
+
if (isNaN(ms) || ms <= 0) {
|
|
244
|
+
console.error('Error: --active-within must be a positive number (milliseconds)');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
filters.activeWithin = ms;
|
|
248
|
+
}
|
|
249
|
+
if (options.limit) {
|
|
250
|
+
const limit = parseInt(options.limit, 10);
|
|
251
|
+
if (isNaN(limit) || limit <= 0) {
|
|
252
|
+
console.error('Error: --limit must be a positive number');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
filters.limit = limit;
|
|
256
|
+
}
|
|
257
|
+
// Connect to relay
|
|
258
|
+
const relayClient = new RelayClient({
|
|
259
|
+
relayUrl,
|
|
260
|
+
publicKey: config.identity.publicKey,
|
|
261
|
+
privateKey: config.identity.privateKey,
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
// Connect
|
|
265
|
+
await relayClient.connect();
|
|
266
|
+
// Create discovery service
|
|
267
|
+
const discoveryService = new PeerDiscoveryService({
|
|
268
|
+
publicKey: config.identity.publicKey,
|
|
269
|
+
privateKey: config.identity.privateKey,
|
|
270
|
+
relayClient,
|
|
271
|
+
relayPublicKey,
|
|
272
|
+
});
|
|
273
|
+
// Discover peers
|
|
274
|
+
const peerList = await discoveryService.discoverViaRelay(Object.keys(filters).length > 0 ? filters : undefined);
|
|
275
|
+
if (!peerList) {
|
|
276
|
+
output({
|
|
277
|
+
status: 'no_response',
|
|
278
|
+
message: 'No response from relay',
|
|
279
|
+
}, options.pretty || false);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
// Save to config if requested
|
|
283
|
+
if (options.save) {
|
|
284
|
+
let savedCount = 0;
|
|
285
|
+
for (const peer of peerList.peers) {
|
|
286
|
+
// Only save if not already in config
|
|
287
|
+
const existing = Object.values(config.peers).find(p => p.publicKey === peer.publicKey);
|
|
288
|
+
if (!existing) {
|
|
289
|
+
const peerName = peer.metadata?.name || `peer-${peer.publicKey.substring(0, 8)}`;
|
|
290
|
+
config.peers[peerName] = {
|
|
291
|
+
publicKey: peer.publicKey,
|
|
292
|
+
name: peerName,
|
|
293
|
+
};
|
|
294
|
+
savedCount++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (savedCount > 0) {
|
|
298
|
+
savePeerConfig(configPath, config);
|
|
299
|
+
}
|
|
300
|
+
output({
|
|
301
|
+
status: 'discovered',
|
|
302
|
+
totalPeers: peerList.totalPeers,
|
|
303
|
+
peersReturned: peerList.peers.length,
|
|
304
|
+
peersSaved: savedCount,
|
|
305
|
+
relayPublicKey: peerList.relayPublicKey,
|
|
306
|
+
peers: peerList.peers.map(p => ({
|
|
307
|
+
publicKey: p.publicKey,
|
|
308
|
+
name: p.metadata?.name,
|
|
309
|
+
version: p.metadata?.version,
|
|
310
|
+
lastSeen: p.lastSeen,
|
|
311
|
+
})),
|
|
312
|
+
}, options.pretty || false);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
output({
|
|
316
|
+
status: 'discovered',
|
|
317
|
+
totalPeers: peerList.totalPeers,
|
|
318
|
+
peersReturned: peerList.peers.length,
|
|
319
|
+
relayPublicKey: peerList.relayPublicKey,
|
|
320
|
+
peers: peerList.peers.map(p => ({
|
|
321
|
+
publicKey: p.publicKey,
|
|
322
|
+
name: p.metadata?.name,
|
|
323
|
+
version: p.metadata?.version,
|
|
324
|
+
lastSeen: p.lastSeen,
|
|
325
|
+
})),
|
|
326
|
+
}, options.pretty || false);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
console.error('Error discovering peers:', e instanceof Error ? e.message : String(e));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
relayClient.disconnect();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
186
337
|
/**
|
|
187
338
|
* Handle the `agora send` command.
|
|
188
339
|
*/
|
|
@@ -230,34 +381,74 @@ async function handleSend(args, options) {
|
|
|
230
381
|
messageType = 'publish';
|
|
231
382
|
messagePayload = { text: args.slice(1).join(' ') };
|
|
232
383
|
}
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
peers: new Map([[peer.publicKey, {
|
|
237
|
-
url: peer.url,
|
|
238
|
-
token: peer.token,
|
|
239
|
-
publicKey: peer.publicKey,
|
|
240
|
-
}]]),
|
|
241
|
-
};
|
|
384
|
+
// Determine transport method: HTTP or relay
|
|
385
|
+
const hasHttpTransport = peer.url && peer.token;
|
|
386
|
+
const hasRelay = config.relay;
|
|
242
387
|
// Send the message
|
|
243
388
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
389
|
+
if (hasHttpTransport) {
|
|
390
|
+
// Use HTTP transport (existing behavior)
|
|
391
|
+
// Non-null assertion: we know url and token are strings here
|
|
392
|
+
const transportConfig = {
|
|
393
|
+
identity: config.identity,
|
|
394
|
+
peers: new Map([[peer.publicKey, {
|
|
395
|
+
url: peer.url,
|
|
396
|
+
token: peer.token,
|
|
397
|
+
publicKey: peer.publicKey,
|
|
398
|
+
}]]),
|
|
399
|
+
};
|
|
400
|
+
const result = await sendToPeer(transportConfig, peer.publicKey, messageType, messagePayload);
|
|
401
|
+
if (result.ok) {
|
|
402
|
+
output({
|
|
403
|
+
status: 'sent',
|
|
404
|
+
peer: name,
|
|
405
|
+
type: messageType,
|
|
406
|
+
transport: 'http',
|
|
407
|
+
httpStatus: result.status
|
|
408
|
+
}, options.pretty || false);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
output({
|
|
412
|
+
status: 'failed',
|
|
413
|
+
peer: name,
|
|
414
|
+
type: messageType,
|
|
415
|
+
transport: 'http',
|
|
416
|
+
httpStatus: result.status,
|
|
417
|
+
error: result.error
|
|
418
|
+
}, options.pretty || false);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (hasRelay) {
|
|
423
|
+
// Use relay transport
|
|
424
|
+
// Non-null assertion: we know relay is a string here
|
|
425
|
+
const relayConfig = {
|
|
426
|
+
identity: config.identity,
|
|
427
|
+
relayUrl: config.relay,
|
|
428
|
+
};
|
|
429
|
+
const result = await sendViaRelay(relayConfig, peer.publicKey, messageType, messagePayload);
|
|
430
|
+
if (result.ok) {
|
|
431
|
+
output({
|
|
432
|
+
status: 'sent',
|
|
433
|
+
peer: name,
|
|
434
|
+
type: messageType,
|
|
435
|
+
transport: 'relay'
|
|
436
|
+
}, options.pretty || false);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
output({
|
|
440
|
+
status: 'failed',
|
|
441
|
+
peer: name,
|
|
442
|
+
type: messageType,
|
|
443
|
+
transport: 'relay',
|
|
444
|
+
error: result.error
|
|
445
|
+
}, options.pretty || false);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
252
448
|
}
|
|
253
449
|
else {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
peer: name,
|
|
257
|
-
type: messageType,
|
|
258
|
-
httpStatus: result.status,
|
|
259
|
-
error: result.error
|
|
260
|
-
}, options.pretty || false);
|
|
450
|
+
// Neither HTTP nor relay available
|
|
451
|
+
console.error(`Error: Peer '${name}' unreachable. No HTTP endpoint and no relay configured.`);
|
|
261
452
|
process.exit(1);
|
|
262
453
|
}
|
|
263
454
|
}
|
|
@@ -282,7 +473,14 @@ function handleDecode(args, options) {
|
|
|
282
473
|
const config = loadPeerConfig(configPath);
|
|
283
474
|
const peers = new Map();
|
|
284
475
|
for (const [, val] of Object.entries(config.peers)) {
|
|
285
|
-
peers
|
|
476
|
+
// Only add peers with HTTP config to the map for decoding
|
|
477
|
+
if (val.url && val.token) {
|
|
478
|
+
peers.set(val.publicKey, {
|
|
479
|
+
url: val.url,
|
|
480
|
+
token: val.token,
|
|
481
|
+
publicKey: val.publicKey,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
286
484
|
}
|
|
287
485
|
const message = args.join(' ');
|
|
288
486
|
const result = decodeInboundEnvelope(message, peers);
|
|
@@ -305,6 +503,359 @@ function handleDecode(args, options) {
|
|
|
305
503
|
process.exit(1);
|
|
306
504
|
}
|
|
307
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Handle the `agora status` command.
|
|
508
|
+
*/
|
|
509
|
+
function handleStatus(options) {
|
|
510
|
+
const configPath = getConfigPath(options);
|
|
511
|
+
if (!existsSync(configPath)) {
|
|
512
|
+
console.error('Error: Config file not found. Run `agora init` first.');
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
const config = loadPeerConfig(configPath);
|
|
516
|
+
const peerCount = Object.keys(config.peers).length;
|
|
517
|
+
output({
|
|
518
|
+
identity: config.identity.publicKey,
|
|
519
|
+
configPath,
|
|
520
|
+
relay: config.relay || 'not configured',
|
|
521
|
+
peerCount,
|
|
522
|
+
peers: Object.keys(config.peers),
|
|
523
|
+
}, options.pretty || false);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Handle the `agora announce` command.
|
|
527
|
+
* Broadcasts an announce message to all configured peers.
|
|
528
|
+
*/
|
|
529
|
+
async function handleAnnounce(options) {
|
|
530
|
+
const configPath = getConfigPath(options);
|
|
531
|
+
if (!existsSync(configPath)) {
|
|
532
|
+
console.error('Error: Config file not found. Run `agora init` first.');
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
const config = loadPeerConfig(configPath);
|
|
536
|
+
const peerCount = Object.keys(config.peers).length;
|
|
537
|
+
if (peerCount === 0) {
|
|
538
|
+
console.error('Error: No peers configured. Use `agora peers add` to add peers first.');
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
// Create announce payload
|
|
542
|
+
const announcePayload = {
|
|
543
|
+
capabilities: [],
|
|
544
|
+
metadata: {
|
|
545
|
+
name: options.name || 'agora-node',
|
|
546
|
+
version: options.version || '0.1.0',
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
// Send announce to all peers
|
|
550
|
+
const results = [];
|
|
551
|
+
for (const [name, peer] of Object.entries(config.peers)) {
|
|
552
|
+
const hasHttpTransport = peer.url && peer.token;
|
|
553
|
+
const hasRelay = config.relay;
|
|
554
|
+
try {
|
|
555
|
+
if (hasHttpTransport) {
|
|
556
|
+
// Use HTTP transport
|
|
557
|
+
const peers = new Map();
|
|
558
|
+
peers.set(peer.publicKey, {
|
|
559
|
+
url: peer.url,
|
|
560
|
+
token: peer.token,
|
|
561
|
+
publicKey: peer.publicKey,
|
|
562
|
+
});
|
|
563
|
+
const transportConfig = {
|
|
564
|
+
identity: config.identity,
|
|
565
|
+
peers,
|
|
566
|
+
};
|
|
567
|
+
const result = await sendToPeer(transportConfig, peer.publicKey, 'announce', announcePayload);
|
|
568
|
+
if (result.ok) {
|
|
569
|
+
results.push({
|
|
570
|
+
peer: name,
|
|
571
|
+
status: 'sent',
|
|
572
|
+
transport: 'http',
|
|
573
|
+
httpStatus: result.status,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
results.push({
|
|
578
|
+
peer: name,
|
|
579
|
+
status: 'failed',
|
|
580
|
+
transport: 'http',
|
|
581
|
+
httpStatus: result.status,
|
|
582
|
+
error: result.error,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else if (hasRelay) {
|
|
587
|
+
// Use relay transport
|
|
588
|
+
const relayConfig = {
|
|
589
|
+
identity: config.identity,
|
|
590
|
+
relayUrl: config.relay,
|
|
591
|
+
};
|
|
592
|
+
const result = await sendViaRelay(relayConfig, peer.publicKey, 'announce', announcePayload);
|
|
593
|
+
if (result.ok) {
|
|
594
|
+
results.push({
|
|
595
|
+
peer: name,
|
|
596
|
+
status: 'sent',
|
|
597
|
+
transport: 'relay',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
results.push({
|
|
602
|
+
peer: name,
|
|
603
|
+
status: 'failed',
|
|
604
|
+
transport: 'relay',
|
|
605
|
+
error: result.error,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
results.push({
|
|
611
|
+
peer: name,
|
|
612
|
+
status: 'unreachable',
|
|
613
|
+
error: 'No HTTP endpoint and no relay configured',
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (e) {
|
|
618
|
+
results.push({
|
|
619
|
+
peer: name,
|
|
620
|
+
status: 'error',
|
|
621
|
+
error: e instanceof Error ? e.message : String(e),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
output({ results }, options.pretty || false);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Handle the `agora diagnose` command.
|
|
629
|
+
* Run diagnostic checks on a peer (ping, workspace, tools).
|
|
630
|
+
*/
|
|
631
|
+
async function handleDiagnose(args, options) {
|
|
632
|
+
if (args.length < 1) {
|
|
633
|
+
console.error('Error: Missing peer name. Usage: agora diagnose <name> [--checks <comma-separated-list>]');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
const name = args[0];
|
|
637
|
+
const configPath = getConfigPath(options);
|
|
638
|
+
if (!existsSync(configPath)) {
|
|
639
|
+
console.error('Error: Config file not found. Run `agora init` first.');
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
const config = loadPeerConfig(configPath);
|
|
643
|
+
if (!config.peers[name]) {
|
|
644
|
+
console.error(`Error: Peer '${name}' not found.`);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
const peer = config.peers[name];
|
|
648
|
+
if (!peer.url) {
|
|
649
|
+
console.error(`Error: Peer '${name}' has no URL configured. Cannot diagnose.`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
// Parse checks parameter
|
|
653
|
+
const checksParam = options.checks || 'ping';
|
|
654
|
+
const requestedChecks = checksParam.split(',').map(c => c.trim());
|
|
655
|
+
// Validate check types
|
|
656
|
+
const validChecks = ['ping', 'workspace', 'tools'];
|
|
657
|
+
for (const check of requestedChecks) {
|
|
658
|
+
if (!validChecks.includes(check)) {
|
|
659
|
+
console.error(`Error: Invalid check type '${check}'. Valid checks: ${validChecks.join(', ')}`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const result = {
|
|
664
|
+
peer: name,
|
|
665
|
+
status: 'unknown',
|
|
666
|
+
checks: {},
|
|
667
|
+
timestamp: new Date().toISOString(),
|
|
668
|
+
};
|
|
669
|
+
// Run ping check
|
|
670
|
+
if (requestedChecks.includes('ping')) {
|
|
671
|
+
const startTime = Date.now();
|
|
672
|
+
try {
|
|
673
|
+
// Add timeout to prevent hanging on unreachable peers
|
|
674
|
+
const controller = new AbortController();
|
|
675
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
676
|
+
const response = await fetch(peer.url, {
|
|
677
|
+
method: 'GET',
|
|
678
|
+
headers: peer.token ? { 'Authorization': `Bearer ${peer.token}` } : {},
|
|
679
|
+
signal: controller.signal,
|
|
680
|
+
});
|
|
681
|
+
clearTimeout(timeout);
|
|
682
|
+
const latency = Date.now() - startTime;
|
|
683
|
+
if (response.ok || response.status === 404 || response.status === 405) {
|
|
684
|
+
// 404 or 405 means the endpoint exists but GET isn't supported - that's OK for a ping
|
|
685
|
+
result.checks.ping = { ok: true, latency_ms: latency };
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
result.checks.ping = { ok: false, latency_ms: latency, error: `HTTP ${response.status}` };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
const latency = Date.now() - startTime;
|
|
693
|
+
result.checks.ping = {
|
|
694
|
+
ok: false,
|
|
695
|
+
latency_ms: latency,
|
|
696
|
+
error: err instanceof Error ? err.message : String(err)
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Run workspace check
|
|
701
|
+
if (requestedChecks.includes('workspace')) {
|
|
702
|
+
// This is a placeholder - actual implementation would depend on peer's diagnostic protocol
|
|
703
|
+
result.checks.workspace = {
|
|
704
|
+
ok: false,
|
|
705
|
+
implemented: false,
|
|
706
|
+
error: 'Workspace check requires peer diagnostic protocol support'
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
// Run tools check
|
|
710
|
+
if (requestedChecks.includes('tools')) {
|
|
711
|
+
// This is a placeholder - actual implementation would depend on peer's diagnostic protocol
|
|
712
|
+
result.checks.tools = {
|
|
713
|
+
ok: false,
|
|
714
|
+
implemented: false,
|
|
715
|
+
error: 'Tools check requires peer diagnostic protocol support'
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
// Determine overall status - only consider implemented checks
|
|
719
|
+
const implementedChecks = Object.values(result.checks).filter(check => check.implemented !== false);
|
|
720
|
+
if (implementedChecks.length === 0) {
|
|
721
|
+
result.status = 'unknown';
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const allOk = implementedChecks.every(check => check.ok);
|
|
725
|
+
const anyOk = implementedChecks.some(check => check.ok);
|
|
726
|
+
result.status = allOk ? 'healthy' : anyOk ? 'degraded' : 'unhealthy';
|
|
727
|
+
}
|
|
728
|
+
output(result, options.pretty || false);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Handle the `agora serve` command.
|
|
732
|
+
* Starts a persistent WebSocket server for incoming peer connections.
|
|
733
|
+
*/
|
|
734
|
+
async function handleServe(options) {
|
|
735
|
+
const configPath = getConfigPath(options);
|
|
736
|
+
if (!existsSync(configPath)) {
|
|
737
|
+
console.error('Error: Config file not found. Run `agora init` first.');
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
const config = loadPeerConfig(configPath);
|
|
741
|
+
const port = parseInt(options.port || '9473', 10);
|
|
742
|
+
// Validate port
|
|
743
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
744
|
+
console.error(`Error: Invalid port number '${options.port}'. Port must be between 1 and 65535.`);
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
const serverName = options.name || 'agora-server';
|
|
748
|
+
// Create announce payload
|
|
749
|
+
const announcePayload = {
|
|
750
|
+
capabilities: [],
|
|
751
|
+
metadata: {
|
|
752
|
+
name: serverName,
|
|
753
|
+
version: '0.1.0',
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
// Create and configure PeerServer
|
|
757
|
+
const server = new PeerServer(config.identity, announcePayload);
|
|
758
|
+
// Setup event listeners
|
|
759
|
+
server.on('peer-connected', (publicKey, peer) => {
|
|
760
|
+
const peerName = peer.metadata?.name || publicKey.substring(0, 16);
|
|
761
|
+
console.log(`[${new Date().toISOString()}] Peer connected: ${peerName} (${publicKey})`);
|
|
762
|
+
});
|
|
763
|
+
server.on('peer-disconnected', (publicKey) => {
|
|
764
|
+
console.log(`[${new Date().toISOString()}] Peer disconnected: ${publicKey}`);
|
|
765
|
+
});
|
|
766
|
+
server.on('message-received', (envelope, fromPublicKey) => {
|
|
767
|
+
console.log(`[${new Date().toISOString()}] Message from ${fromPublicKey}:`);
|
|
768
|
+
console.log(JSON.stringify({
|
|
769
|
+
id: envelope.id,
|
|
770
|
+
type: envelope.type,
|
|
771
|
+
sender: envelope.sender,
|
|
772
|
+
timestamp: envelope.timestamp,
|
|
773
|
+
payload: envelope.payload,
|
|
774
|
+
}, null, 2));
|
|
775
|
+
});
|
|
776
|
+
server.on('error', (error) => {
|
|
777
|
+
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
|
778
|
+
});
|
|
779
|
+
// Start the server
|
|
780
|
+
try {
|
|
781
|
+
await server.start(port);
|
|
782
|
+
console.log(`[${new Date().toISOString()}] Agora server started`);
|
|
783
|
+
console.log(` Name: ${serverName}`);
|
|
784
|
+
console.log(` Public Key: ${config.identity.publicKey}`);
|
|
785
|
+
console.log(` WebSocket Port: ${port}`);
|
|
786
|
+
console.log(` Listening for peer connections...`);
|
|
787
|
+
console.log('');
|
|
788
|
+
console.log('Press Ctrl+C to stop the server');
|
|
789
|
+
// Keep the process alive
|
|
790
|
+
process.on('SIGINT', async () => {
|
|
791
|
+
console.log(`\n[${new Date().toISOString()}] Shutting down server...`);
|
|
792
|
+
await server.stop();
|
|
793
|
+
console.log('Server stopped');
|
|
794
|
+
process.exit(0);
|
|
795
|
+
});
|
|
796
|
+
process.on('SIGTERM', async () => {
|
|
797
|
+
console.log(`\n[${new Date().toISOString()}] Shutting down server...`);
|
|
798
|
+
await server.stop();
|
|
799
|
+
console.log('Server stopped');
|
|
800
|
+
process.exit(0);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
console.error('Failed to start server:', error instanceof Error ? error.message : String(error));
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Handle the `agora relay` command.
|
|
810
|
+
* Starts a WebSocket relay server for routing messages between agents.
|
|
811
|
+
*/
|
|
812
|
+
async function handleRelay(options) {
|
|
813
|
+
const port = parseInt(options.port || '9474', 10);
|
|
814
|
+
// Validate port
|
|
815
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
816
|
+
console.error(`Error: Invalid port number '${options.port}'. Port must be between 1 and 65535.`);
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
// Create and configure RelayServer
|
|
820
|
+
const server = new RelayServer();
|
|
821
|
+
// Setup event listeners
|
|
822
|
+
server.on('agent-registered', (publicKey) => {
|
|
823
|
+
console.log(`[${new Date().toISOString()}] Agent registered: ${publicKey}`);
|
|
824
|
+
});
|
|
825
|
+
server.on('agent-disconnected', (publicKey) => {
|
|
826
|
+
console.log(`[${new Date().toISOString()}] Agent disconnected: ${publicKey}`);
|
|
827
|
+
});
|
|
828
|
+
server.on('message-relayed', (from, to, envelope) => {
|
|
829
|
+
console.log(`[${new Date().toISOString()}] Message relayed: ${from.substring(0, 16)}... → ${to.substring(0, 16)}... (type: ${envelope.type})`);
|
|
830
|
+
});
|
|
831
|
+
server.on('error', (error) => {
|
|
832
|
+
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
|
833
|
+
});
|
|
834
|
+
// Start the server
|
|
835
|
+
try {
|
|
836
|
+
await server.start(port);
|
|
837
|
+
console.log(`[${new Date().toISOString()}] Agora relay server started`);
|
|
838
|
+
console.log(` WebSocket Port: ${port}`);
|
|
839
|
+
console.log(` Connected agents: 0`);
|
|
840
|
+
console.log(` Listening for agent connections...`);
|
|
841
|
+
console.log('');
|
|
842
|
+
console.log('Press Ctrl+C to stop the relay');
|
|
843
|
+
// Shared shutdown handler
|
|
844
|
+
const shutdown = async () => {
|
|
845
|
+
console.log(`\n[${new Date().toISOString()}] Shutting down relay...`);
|
|
846
|
+
await server.stop();
|
|
847
|
+
console.log('Relay stopped');
|
|
848
|
+
process.exit(0);
|
|
849
|
+
};
|
|
850
|
+
// Keep the process alive
|
|
851
|
+
process.on('SIGINT', shutdown);
|
|
852
|
+
process.on('SIGTERM', shutdown);
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
console.error('Failed to start relay:', error instanceof Error ? error.message : String(error));
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
308
859
|
/**
|
|
309
860
|
* Parse CLI arguments and route to appropriate handler.
|
|
310
861
|
*/
|
|
@@ -312,7 +863,8 @@ async function main() {
|
|
|
312
863
|
const args = process.argv.slice(2);
|
|
313
864
|
if (args.length === 0) {
|
|
314
865
|
console.error('Usage: agora <command> [options]');
|
|
315
|
-
console.error('Commands: init, whoami, peers, send');
|
|
866
|
+
console.error('Commands: init, whoami, status, peers, announce, send, decode, serve, diagnose, relay');
|
|
867
|
+
console.error(' peers subcommands: add, list, remove, discover');
|
|
316
868
|
process.exit(1);
|
|
317
869
|
}
|
|
318
870
|
// Parse global options
|
|
@@ -326,6 +878,15 @@ async function main() {
|
|
|
326
878
|
pubkey: { type: 'string' },
|
|
327
879
|
type: { type: 'string' },
|
|
328
880
|
payload: { type: 'string' },
|
|
881
|
+
name: { type: 'string' },
|
|
882
|
+
version: { type: 'string' },
|
|
883
|
+
port: { type: 'string' },
|
|
884
|
+
checks: { type: 'string' },
|
|
885
|
+
relay: { type: 'string' },
|
|
886
|
+
'relay-pubkey': { type: 'string' },
|
|
887
|
+
limit: { type: 'string' },
|
|
888
|
+
'active-within': { type: 'string' },
|
|
889
|
+
save: { type: 'boolean' },
|
|
329
890
|
},
|
|
330
891
|
strict: false,
|
|
331
892
|
allowPositionals: true,
|
|
@@ -341,6 +902,15 @@ async function main() {
|
|
|
341
902
|
url: typeof parsed.values.url === 'string' ? parsed.values.url : undefined,
|
|
342
903
|
token: typeof parsed.values.token === 'string' ? parsed.values.token : undefined,
|
|
343
904
|
pubkey: typeof parsed.values.pubkey === 'string' ? parsed.values.pubkey : undefined,
|
|
905
|
+
name: typeof parsed.values.name === 'string' ? parsed.values.name : undefined,
|
|
906
|
+
version: typeof parsed.values.version === 'string' ? parsed.values.version : undefined,
|
|
907
|
+
port: typeof parsed.values.port === 'string' ? parsed.values.port : undefined,
|
|
908
|
+
checks: typeof parsed.values.checks === 'string' ? parsed.values.checks : undefined,
|
|
909
|
+
relay: typeof parsed.values.relay === 'string' ? parsed.values.relay : undefined,
|
|
910
|
+
'relay-pubkey': typeof parsed.values['relay-pubkey'] === 'string' ? parsed.values['relay-pubkey'] : undefined,
|
|
911
|
+
limit: typeof parsed.values.limit === 'string' ? parsed.values.limit : undefined,
|
|
912
|
+
'active-within': typeof parsed.values['active-within'] === 'string' ? parsed.values['active-within'] : undefined,
|
|
913
|
+
save: typeof parsed.values.save === 'boolean' ? parsed.values.save : undefined,
|
|
344
914
|
};
|
|
345
915
|
try {
|
|
346
916
|
switch (command) {
|
|
@@ -350,19 +920,33 @@ async function main() {
|
|
|
350
920
|
case 'whoami':
|
|
351
921
|
handleWhoami(options);
|
|
352
922
|
break;
|
|
923
|
+
case 'status':
|
|
924
|
+
handleStatus(options);
|
|
925
|
+
break;
|
|
926
|
+
case 'announce':
|
|
927
|
+
await handleAnnounce(options);
|
|
928
|
+
break;
|
|
929
|
+
case 'diagnose':
|
|
930
|
+
await handleDiagnose([subcommand, ...remainingArgs].filter(Boolean), options);
|
|
931
|
+
break;
|
|
353
932
|
case 'peers':
|
|
354
933
|
switch (subcommand) {
|
|
355
934
|
case 'add':
|
|
356
935
|
handlePeersAdd(remainingArgs, options);
|
|
357
936
|
break;
|
|
358
937
|
case 'list':
|
|
938
|
+
case undefined:
|
|
939
|
+
// Allow 'agora peers' to work like 'agora peers list'
|
|
359
940
|
handlePeersList(options);
|
|
360
941
|
break;
|
|
361
942
|
case 'remove':
|
|
362
943
|
handlePeersRemove(remainingArgs, options);
|
|
363
944
|
break;
|
|
945
|
+
case 'discover':
|
|
946
|
+
await handlePeersDiscover(options);
|
|
947
|
+
break;
|
|
364
948
|
default:
|
|
365
|
-
console.error('Error: Unknown peers subcommand. Use: add, list, remove');
|
|
949
|
+
console.error('Error: Unknown peers subcommand. Use: add, list, remove, discover');
|
|
366
950
|
process.exit(1);
|
|
367
951
|
}
|
|
368
952
|
break;
|
|
@@ -372,8 +956,14 @@ async function main() {
|
|
|
372
956
|
case 'decode':
|
|
373
957
|
handleDecode([subcommand, ...remainingArgs].filter(Boolean), options);
|
|
374
958
|
break;
|
|
959
|
+
case 'serve':
|
|
960
|
+
await handleServe(options);
|
|
961
|
+
break;
|
|
962
|
+
case 'relay':
|
|
963
|
+
await handleRelay(options);
|
|
964
|
+
break;
|
|
375
965
|
default:
|
|
376
|
-
console.error(`Error: Unknown command '${command}'. Use: init, whoami, peers, send, decode`);
|
|
966
|
+
console.error(`Error: Unknown command '${command}'. Use: init, whoami, status, peers, announce, send, decode, serve, diagnose, relay`);
|
|
377
967
|
process.exit(1);
|
|
378
968
|
}
|
|
379
969
|
}
|