@tjamescouch/agentchat 0.10.0 → 0.12.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/bin/agentchat.js CHANGED
@@ -48,6 +48,10 @@ import {
48
48
  DEFAULT_RATINGS_PATH,
49
49
  DEFAULT_RATING
50
50
  } from '../lib/reputation.js';
51
+ import {
52
+ ServerDirectory,
53
+ DEFAULT_DIRECTORY_PATH
54
+ } from '../lib/server-directory.js';
51
55
 
52
56
  program
53
57
  .name('agentchat')
@@ -95,7 +99,11 @@ program
95
99
  console.log('Message sent');
96
100
  process.exit(0);
97
101
  } catch (err) {
98
- console.error('Error:', err.message);
102
+ if (err.code === 'ECONNREFUSED') {
103
+ console.error('Error: Connection refused. Is the server running?');
104
+ } else {
105
+ console.error('Error:', err.message || err.code || err);
106
+ }
99
107
  process.exit(1);
100
108
  }
101
109
  });
@@ -142,7 +150,12 @@ program
142
150
  });
143
151
 
144
152
  } catch (err) {
145
- console.error('Error:', err.message);
153
+ if (err.code === 'ECONNREFUSED') {
154
+ console.error('Error: Connection refused. Is the server running?');
155
+ console.error(` Try: agentchat serve --port 8080`);
156
+ } else {
157
+ console.error('Error:', err.message || err.code || err);
158
+ }
146
159
  process.exit(1);
147
160
  }
148
161
  });
@@ -358,6 +371,7 @@ program
358
371
  .option('-p, --payment-code <code>', 'Your payment code (BIP47, address)')
359
372
  .option('-e, --expires <seconds>', 'Expiration time in seconds', '300')
360
373
  .option('-t, --terms <terms>', 'Additional terms')
374
+ .option('-s, --elo-stake <n>', 'ELO points to stake on this proposal')
361
375
  .action(async (server, agent, task, options) => {
362
376
  try {
363
377
  const client = new AgentChatClient({ server, identity: options.identity });
@@ -369,7 +383,8 @@ program
369
383
  currency: options.currency,
370
384
  payment_code: options.paymentCode,
371
385
  terms: options.terms,
372
- expires: parseInt(options.expires)
386
+ expires: parseInt(options.expires),
387
+ elo_stake: options.eloStake ? parseInt(options.eloStake) : undefined
373
388
  });
374
389
 
375
390
  console.log('Proposal sent:');
@@ -377,6 +392,7 @@ program
377
392
  console.log(` To: ${proposal.to}`);
378
393
  console.log(` Task: ${proposal.task}`);
379
394
  if (proposal.amount) console.log(` Amount: ${proposal.amount} ${proposal.currency || ''}`);
395
+ if (proposal.elo_stake) console.log(` ELO Stake: ${proposal.elo_stake}`);
380
396
  if (proposal.expires) console.log(` Expires: ${new Date(proposal.expires).toISOString()}`);
381
397
  console.log(`\nUse this ID to track responses.`);
382
398
 
@@ -394,16 +410,20 @@ program
394
410
  .description('Accept a proposal')
395
411
  .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
396
412
  .option('-p, --payment-code <code>', 'Your payment code for receiving payment')
413
+ .option('-s, --elo-stake <n>', 'ELO points to stake (as acceptor)')
397
414
  .action(async (server, proposalId, options) => {
398
415
  try {
399
416
  const client = new AgentChatClient({ server, identity: options.identity });
400
417
  await client.connect();
401
418
 
402
- const response = await client.accept(proposalId, options.paymentCode);
419
+ const eloStake = options.eloStake ? parseInt(options.eloStake) : undefined;
420
+ const response = await client.accept(proposalId, options.paymentCode, eloStake);
403
421
 
404
422
  console.log('Proposal accepted:');
405
423
  console.log(` Proposal ID: ${response.proposal_id}`);
406
424
  console.log(` Status: ${response.status}`);
425
+ if (response.proposer_stake) console.log(` Proposer Stake: ${response.proposer_stake} ELO`);
426
+ if (response.acceptor_stake) console.log(` Your Stake: ${response.acceptor_stake} ELO`);
407
427
 
408
428
  client.disconnect();
409
429
  process.exit(0);
@@ -488,6 +508,39 @@ program
488
508
  }
489
509
  });
490
510
 
511
+ // Verify agent identity command
512
+ program
513
+ .command('verify <server> <agent>')
514
+ .description('Verify another agent\'s identity via challenge-response')
515
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
516
+ .action(async (server, agent, options) => {
517
+ try {
518
+ const client = new AgentChatClient({ server, identity: options.identity });
519
+ await client.connect();
520
+
521
+ console.log(`Verifying identity of ${agent}...`);
522
+
523
+ const result = await client.verify(agent);
524
+
525
+ if (result.verified) {
526
+ console.log('Identity verified!');
527
+ console.log(` Agent: ${result.agent}`);
528
+ console.log(` Public Key:`);
529
+ console.log(result.pubkey.split('\n').map(line => ` ${line}`).join('\n'));
530
+ } else {
531
+ console.log('Verification failed!');
532
+ console.log(` Target: ${result.target}`);
533
+ console.log(` Reason: ${result.reason}`);
534
+ }
535
+
536
+ client.disconnect();
537
+ process.exit(result.verified ? 0 : 1);
538
+ } catch (err) {
539
+ console.error('Error:', err.message);
540
+ process.exit(1);
541
+ }
542
+ });
543
+
491
544
  // Identity management command
492
545
  program
493
546
  .command('identity')
@@ -495,6 +548,8 @@ program
495
548
  .option('-g, --generate', 'Generate new keypair')
496
549
  .option('-s, --show', 'Show current identity')
497
550
  .option('-e, --export', 'Export public key for sharing (JSON to stdout)')
551
+ .option('-r, --rotate', 'Rotate to new keypair (signs new key with old key)')
552
+ .option('--verify-chain', 'Verify the rotation chain')
498
553
  .option('-f, --file <path>', 'Identity file path', DEFAULT_IDENTITY_PATH)
499
554
  .option('-n, --name <name>', 'Agent name (for --generate)', `agent-${process.pid}`)
500
555
  .option('--force', 'Overwrite existing identity')
@@ -535,6 +590,54 @@ program
535
590
  const identity = await Identity.load(options.file);
536
591
  console.log(JSON.stringify(identity.export(), null, 2));
537
592
 
593
+ } else if (options.rotate) {
594
+ // Rotate to new keypair
595
+ const identity = await Identity.load(options.file);
596
+ const oldAgentId = identity.getAgentId();
597
+ const oldFingerprint = identity.getFingerprint();
598
+
599
+ console.log('Rotating identity...');
600
+ console.log(` Old Agent ID: ${oldAgentId}`);
601
+ console.log(` Old Fingerprint: ${oldFingerprint}`);
602
+
603
+ const record = identity.rotate();
604
+ await identity.save(options.file);
605
+
606
+ console.log('');
607
+ console.log('Rotation complete:');
608
+ console.log(` New Agent ID: ${identity.getAgentId()}`);
609
+ console.log(` New Fingerprint: ${identity.getFingerprint()}`);
610
+ console.log(` Total rotations: ${identity.rotations.length}`);
611
+ console.log('');
612
+ console.log('The new key has been signed by the old key for chain of custody.');
613
+ console.log('Share the rotation record to prove key continuity.');
614
+
615
+ } else if (options.verifyChain) {
616
+ // Verify rotation chain
617
+ const identity = await Identity.load(options.file);
618
+
619
+ if (identity.rotations.length === 0) {
620
+ console.log('No rotations to verify (original identity).');
621
+ console.log(` Agent ID: ${identity.getAgentId()}`);
622
+ process.exit(0);
623
+ }
624
+
625
+ console.log(`Verifying rotation chain (${identity.rotations.length} rotation(s))...`);
626
+ const result = identity.verifyRotationChain();
627
+
628
+ if (result.valid) {
629
+ console.log('Chain verified successfully!');
630
+ console.log(` Original Agent ID: ${identity.getOriginalAgentId()}`);
631
+ console.log(` Current Agent ID: ${identity.getAgentId()}`);
632
+ console.log(` Rotations: ${identity.rotations.length}`);
633
+ } else {
634
+ console.error('Chain verification FAILED:');
635
+ for (const error of result.errors) {
636
+ console.error(` - ${error}`);
637
+ }
638
+ process.exit(1);
639
+ }
640
+
538
641
  } else {
539
642
  // Default: show if exists, otherwise show help
540
643
  const exists = await Identity.exists(options.file);
@@ -1101,6 +1204,91 @@ program
1101
1204
  }
1102
1205
  });
1103
1206
 
1207
+ // Discover command - find public AgentChat servers
1208
+ program
1209
+ .command('discover')
1210
+ .description('Discover available AgentChat servers')
1211
+ .option('--add <url>', 'Add a server to the directory')
1212
+ .option('--remove <url>', 'Remove a server from the directory')
1213
+ .option('--name <name>', 'Server name (for --add)')
1214
+ .option('--description <desc>', 'Server description (for --add)')
1215
+ .option('--region <region>', 'Server region (for --add)')
1216
+ .option('--online', 'Only show online servers')
1217
+ .option('--json', 'Output as JSON')
1218
+ .option('--no-check', 'List servers without health check')
1219
+ .option('--directory <path>', 'Custom directory file path', DEFAULT_DIRECTORY_PATH)
1220
+ .action(async (options) => {
1221
+ try {
1222
+ const directory = new ServerDirectory({ directoryPath: options.directory });
1223
+ await directory.load();
1224
+
1225
+ // Add server
1226
+ if (options.add) {
1227
+ await directory.addServer({
1228
+ url: options.add,
1229
+ name: options.name || options.add,
1230
+ description: options.description || '',
1231
+ region: options.region || 'unknown'
1232
+ });
1233
+ console.log(`Added server: ${options.add}`);
1234
+ process.exit(0);
1235
+ }
1236
+
1237
+ // Remove server
1238
+ if (options.remove) {
1239
+ await directory.removeServer(options.remove);
1240
+ console.log(`Removed server: ${options.remove}`);
1241
+ process.exit(0);
1242
+ }
1243
+
1244
+ // List/discover servers
1245
+ let servers;
1246
+ if (options.check === false) {
1247
+ servers = directory.list().map(s => ({ ...s, status: 'unknown' }));
1248
+ } else {
1249
+ console.error('Checking server status...');
1250
+ servers = await directory.discover({ onlineOnly: options.online });
1251
+ }
1252
+
1253
+ if (options.json) {
1254
+ console.log(JSON.stringify(servers, null, 2));
1255
+ } else {
1256
+ if (servers.length === 0) {
1257
+ console.log('No servers found.');
1258
+ } else {
1259
+ console.log(`\nFound ${servers.length} server(s):\n`);
1260
+ for (const server of servers) {
1261
+ const statusIcon = server.status === 'online' ? '\u2713' :
1262
+ server.status === 'offline' ? '\u2717' : '?';
1263
+ console.log(` ${statusIcon} ${server.name}`);
1264
+ console.log(` URL: ${server.url}`);
1265
+ console.log(` Status: ${server.status}`);
1266
+ if (server.description) {
1267
+ console.log(` Description: ${server.description}`);
1268
+ }
1269
+ if (server.region) {
1270
+ console.log(` Region: ${server.region}`);
1271
+ }
1272
+ if (server.health) {
1273
+ console.log(` Agents: ${server.health.agents?.connected || 0}`);
1274
+ console.log(` Uptime: ${server.health.uptime_seconds || 0}s`);
1275
+ }
1276
+ if (server.error) {
1277
+ console.log(` Error: ${server.error}`);
1278
+ }
1279
+ console.log('');
1280
+ }
1281
+ }
1282
+ console.log(`Directory: ${options.directory}`);
1283
+ }
1284
+
1285
+ process.exit(0);
1286
+ } catch (err) {
1287
+ console.error('Error:', err.message);
1288
+ process.exit(1);
1289
+ }
1290
+ });
1291
+
1104
1292
  // Deploy command
1105
1293
  program
1106
1294
  .command('deploy')
package/lib/client.js CHANGED
@@ -10,7 +10,8 @@ import {
10
10
  ServerMessageType,
11
11
  createMessage,
12
12
  serialize,
13
- parse
13
+ parse,
14
+ generateNonce
14
15
  } from './protocol.js';
15
16
  import { Identity } from './identity.js';
16
17
  import {
@@ -297,7 +298,8 @@ export class AgentChatClient extends EventEmitter {
297
298
  currency: proposal.currency,
298
299
  payment_code: proposal.payment_code,
299
300
  terms: proposal.terms,
300
- expires: proposal.expires
301
+ expires: proposal.expires,
302
+ elo_stake: proposal.elo_stake
301
303
  };
302
304
 
303
305
  // Sign the proposal
@@ -337,19 +339,21 @@ export class AgentChatClient extends EventEmitter {
337
339
  * Accept a proposal
338
340
  * @param {string} proposalId - The proposal ID to accept
339
341
  * @param {string} [payment_code] - Your payment code for receiving payment
342
+ * @param {number} [elo_stake] - ELO points to stake as acceptor
340
343
  */
341
- async accept(proposalId, payment_code = null) {
344
+ async accept(proposalId, payment_code = null, elo_stake = null) {
342
345
  if (!this._identity || !this._identity.privkey) {
343
346
  throw new Error('Accepting proposals requires persistent identity.');
344
347
  }
345
348
 
346
- const sigContent = getAcceptSigningContent(proposalId, payment_code || '');
349
+ const sigContent = getAcceptSigningContent(proposalId, payment_code || '', elo_stake || '');
347
350
  const sig = this._identity.sign(sigContent);
348
351
 
349
352
  const msg = {
350
353
  type: ClientMessageType.ACCEPT,
351
354
  proposal_id: proposalId,
352
355
  payment_code,
356
+ elo_stake,
353
357
  sig
354
358
  };
355
359
 
@@ -529,6 +533,118 @@ export class AgentChatClient extends EventEmitter {
529
533
  });
530
534
  }
531
535
 
536
+ // ===== IDENTITY VERIFICATION METHODS =====
537
+
538
+ /**
539
+ * Request identity verification from another agent
540
+ * Sends a challenge nonce that the target must sign to prove they control their identity
541
+ * @param {string} target - Target agent to verify (@id)
542
+ * @returns {Promise<object>} Verification result with pubkey if successful
543
+ */
544
+ async verify(target) {
545
+ const targetAgent = target.startsWith('@') ? target : `@${target}`;
546
+ const nonce = generateNonce();
547
+
548
+ const msg = {
549
+ type: ClientMessageType.VERIFY_REQUEST,
550
+ target: targetAgent,
551
+ nonce
552
+ };
553
+
554
+ this._send(msg);
555
+
556
+ return new Promise((resolve, reject) => {
557
+ const timeout = setTimeout(() => {
558
+ this.removeListener('verify_success', onSuccess);
559
+ this.removeListener('verify_failed', onFailed);
560
+ this.removeListener('error', onError);
561
+ reject(new Error('Verification timeout'));
562
+ }, 35000); // Slightly longer than server timeout
563
+
564
+ const onSuccess = (response) => {
565
+ if (response.agent === targetAgent || response.target === targetAgent) {
566
+ clearTimeout(timeout);
567
+ this.removeListener('verify_failed', onFailed);
568
+ this.removeListener('error', onError);
569
+ resolve({
570
+ verified: true,
571
+ agent: response.agent,
572
+ pubkey: response.pubkey,
573
+ request_id: response.request_id
574
+ });
575
+ }
576
+ };
577
+
578
+ const onFailed = (response) => {
579
+ if (response.target === targetAgent) {
580
+ clearTimeout(timeout);
581
+ this.removeListener('verify_success', onSuccess);
582
+ this.removeListener('error', onError);
583
+ resolve({
584
+ verified: false,
585
+ target: response.target,
586
+ reason: response.reason,
587
+ request_id: response.request_id
588
+ });
589
+ }
590
+ };
591
+
592
+ const onError = (err) => {
593
+ clearTimeout(timeout);
594
+ this.removeListener('verify_success', onSuccess);
595
+ this.removeListener('verify_failed', onFailed);
596
+ reject(new Error(err.message));
597
+ };
598
+
599
+ this.on('verify_success', onSuccess);
600
+ this.on('verify_failed', onFailed);
601
+ this.once('error', onError);
602
+ });
603
+ }
604
+
605
+ /**
606
+ * Respond to a verification request by signing the nonce
607
+ * This is typically called automatically when a VERIFY_REQUEST is received
608
+ * @param {string} requestId - The verification request ID
609
+ * @param {string} nonce - The nonce to sign
610
+ */
611
+ async respondToVerification(requestId, nonce) {
612
+ if (!this._identity || !this._identity.privkey) {
613
+ throw new Error('Responding to verification requires persistent identity.');
614
+ }
615
+
616
+ const sig = this._identity.sign(nonce);
617
+
618
+ const msg = {
619
+ type: ClientMessageType.VERIFY_RESPONSE,
620
+ request_id: requestId,
621
+ nonce,
622
+ sig
623
+ };
624
+
625
+ this._send(msg);
626
+ }
627
+
628
+ /**
629
+ * Enable automatic verification response
630
+ * When enabled, the client will automatically respond to VERIFY_REQUEST messages
631
+ * @param {boolean} enabled - Whether to enable auto-response
632
+ */
633
+ enableAutoVerification(enabled = true) {
634
+ if (enabled) {
635
+ this._autoVerifyHandler = (msg) => {
636
+ if (msg.request_id && msg.nonce && msg.from) {
637
+ this.respondToVerification(msg.request_id, msg.nonce)
638
+ .catch(err => this.emit('error', { message: `Auto-verification failed: ${err.message}` }));
639
+ }
640
+ };
641
+ this.on('verify_request', this._autoVerifyHandler);
642
+ } else if (this._autoVerifyHandler) {
643
+ this.removeListener('verify_request', this._autoVerifyHandler);
644
+ this._autoVerifyHandler = null;
645
+ }
646
+ }
647
+
532
648
  _send(msg) {
533
649
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
534
650
  this.ws.send(serialize(msg));
@@ -627,6 +743,23 @@ export class AgentChatClient extends EventEmitter {
627
743
  this.emit('search_results', msg);
628
744
  this.emit('message', msg);
629
745
  break;
746
+
747
+ // Identity verification messages
748
+ case ServerMessageType.VERIFY_REQUEST:
749
+ this.emit('verify_request', msg);
750
+ break;
751
+
752
+ case ServerMessageType.VERIFY_RESPONSE:
753
+ this.emit('verify_response', msg);
754
+ break;
755
+
756
+ case ServerMessageType.VERIFY_SUCCESS:
757
+ this.emit('verify_success', msg);
758
+ break;
759
+
760
+ case ServerMessageType.VERIFY_FAILED:
761
+ this.emit('verify_failed', msg);
762
+ break;
630
763
  }
631
764
  }
632
765
  }