@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.
Files changed (68) hide show
  1. package/README.md +457 -1
  2. package/dist/cli.js +627 -37
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +44 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +74 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/discovery/bootstrap.d.ts +32 -0
  9. package/dist/discovery/bootstrap.d.ts.map +1 -0
  10. package/dist/discovery/bootstrap.js +36 -0
  11. package/dist/discovery/bootstrap.js.map +1 -0
  12. package/dist/discovery/peer-discovery.d.ts +59 -0
  13. package/dist/discovery/peer-discovery.d.ts.map +1 -0
  14. package/dist/discovery/peer-discovery.js +108 -0
  15. package/dist/discovery/peer-discovery.js.map +1 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +9 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/message/envelope.d.ts +1 -1
  21. package/dist/message/envelope.d.ts.map +1 -1
  22. package/dist/message/envelope.js.map +1 -1
  23. package/dist/message/types/paper-discovery.d.ts +28 -0
  24. package/dist/message/types/paper-discovery.d.ts.map +1 -0
  25. package/dist/message/types/paper-discovery.js +2 -0
  26. package/dist/message/types/paper-discovery.js.map +1 -0
  27. package/dist/message/types/peer-discovery.d.ts +78 -0
  28. package/dist/message/types/peer-discovery.d.ts.map +1 -0
  29. package/dist/message/types/peer-discovery.js +90 -0
  30. package/dist/message/types/peer-discovery.js.map +1 -0
  31. package/dist/peer/client.d.ts +50 -0
  32. package/dist/peer/client.d.ts.map +1 -0
  33. package/dist/peer/client.js +138 -0
  34. package/dist/peer/client.js.map +1 -0
  35. package/dist/peer/manager.d.ts +65 -0
  36. package/dist/peer/manager.d.ts.map +1 -0
  37. package/dist/peer/manager.js +153 -0
  38. package/dist/peer/manager.js.map +1 -0
  39. package/dist/peer/server.d.ts +65 -0
  40. package/dist/peer/server.d.ts.map +1 -0
  41. package/dist/peer/server.js +154 -0
  42. package/dist/peer/server.js.map +1 -0
  43. package/dist/registry/discovery-service.d.ts +64 -0
  44. package/dist/registry/discovery-service.d.ts.map +1 -0
  45. package/dist/registry/discovery-service.js +129 -0
  46. package/dist/registry/discovery-service.js.map +1 -0
  47. package/dist/registry/messages.d.ts +55 -0
  48. package/dist/registry/messages.d.ts.map +1 -1
  49. package/dist/relay/client.d.ts +112 -0
  50. package/dist/relay/client.d.ts.map +1 -0
  51. package/dist/relay/client.js +281 -0
  52. package/dist/relay/client.js.map +1 -0
  53. package/dist/relay/server.d.ts +76 -0
  54. package/dist/relay/server.d.ts.map +1 -0
  55. package/dist/relay/server.js +338 -0
  56. package/dist/relay/server.js.map +1 -0
  57. package/dist/relay/types.d.ts +35 -0
  58. package/dist/relay/types.d.ts.map +1 -0
  59. package/dist/relay/types.js +2 -0
  60. package/dist/relay/types.js.map +1 -0
  61. package/dist/transport/peer-config.d.ts +3 -2
  62. package/dist/transport/peer-config.d.ts.map +1 -1
  63. package/dist/transport/peer-config.js.map +1 -1
  64. package/dist/transport/relay.d.ts +23 -0
  65. package/dist/transport/relay.d.ts.map +1 -0
  66. package/dist/transport/relay.js +85 -0
  67. package/dist/transport/relay.js.map +1 -0
  68. 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> --url <url> --token <token> --pubkey <pubkey>');
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 (!url || !token || !pubkey) {
124
- console.error('Error: Missing required options. Usage: agora peers add <name> --url <url> --token <token> --pubkey <pubkey>');
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
- // Add the peer (name is optional but set for clarity)
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
- output({
156
+ const outputData = {
137
157
  status: 'added',
138
158
  name,
139
- url,
140
159
  publicKey: pubkey
141
- }, options.pretty || false);
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
- // Create transport config
234
- const transportConfig = {
235
- identity: config.identity,
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
- const result = await sendToPeer(transportConfig, peer.publicKey, messageType, messagePayload);
245
- if (result.ok) {
246
- output({
247
- status: 'sent',
248
- peer: name,
249
- type: messageType,
250
- httpStatus: result.status
251
- }, options.pretty || false);
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
- output({
255
- status: 'failed',
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.set(val.publicKey, val);
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
  }