@tjamescouch/agentchat 0.8.0 → 0.9.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
@@ -679,14 +679,22 @@ program
679
679
  console.log(` Instance: ${instanceName}`);
680
680
  console.log(` Server: ${server}`);
681
681
  console.log(` Identity: ${options.identity}`);
682
- console.log(` Channels: ${options.channels.join(', ')}`);
682
+
683
+ // Normalize channels: handle both comma-separated and space-separated formats
684
+ const normalizedChannels = options.channels
685
+ .flatMap(c => c.split(','))
686
+ .map(c => c.trim())
687
+ .filter(c => c.length > 0)
688
+ .map(c => c.startsWith('#') ? c : '#' + c);
689
+
690
+ console.log(` Channels: ${normalizedChannels.join(', ')}`);
683
691
  console.log('');
684
692
 
685
693
  const daemon = new AgentChatDaemon({
686
694
  server,
687
695
  name: instanceName,
688
696
  identity: options.identity,
689
- channels: options.channels,
697
+ channels: normalizedChannels,
690
698
  maxReconnectTime: parseInt(options.maxReconnectTime) * 60 * 1000 // Convert minutes to ms
691
699
  });
692
700
 
@@ -944,6 +952,155 @@ program
944
952
  }
945
953
  });
946
954
 
955
+ // Skills command - skill discovery and announcement
956
+ program
957
+ .command('skills <action> [server]')
958
+ .description('Manage skill discovery: announce, search, list')
959
+ .option('-c, --capability <capability>', 'Skill capability for announce/search')
960
+ .option('-r, --rate <rate>', 'Rate/price for the skill', parseFloat)
961
+ .option('--currency <currency>', 'Currency for rate (e.g., SOL, TEST)', 'TEST')
962
+ .option('--description <desc>', 'Description of skill')
963
+ .option('-f, --file <file>', 'YAML file with skill definitions')
964
+ .option('-i, --identity <file>', 'Path to identity file', DEFAULT_IDENTITY_PATH)
965
+ .option('--max-rate <rate>', 'Maximum rate for search', parseFloat)
966
+ .option('-l, --limit <n>', 'Limit search results', parseInt)
967
+ .option('--json', 'Output as JSON')
968
+ .action(async (action, server, options) => {
969
+ try {
970
+ if (action === 'announce') {
971
+ if (!server) {
972
+ console.error('Server URL required: agentchat skills announce <server>');
973
+ process.exit(1);
974
+ }
975
+
976
+ let skills = [];
977
+
978
+ // Load from file if provided
979
+ if (options.file) {
980
+ const yaml = await import('js-yaml');
981
+ const content = await fsp.readFile(options.file, 'utf-8');
982
+ const data = yaml.default.load(content);
983
+ skills = data.skills || [data];
984
+ } else if (options.capability) {
985
+ // Single skill from CLI args
986
+ skills = [{
987
+ capability: options.capability,
988
+ rate: options.rate,
989
+ currency: options.currency,
990
+ description: options.description
991
+ }];
992
+ } else {
993
+ console.error('Either --file or --capability required');
994
+ process.exit(1);
995
+ }
996
+
997
+ // Load identity and sign
998
+ const identity = await Identity.load(options.identity);
999
+ const skillsContent = JSON.stringify(skills);
1000
+ const sig = identity.sign(skillsContent);
1001
+
1002
+ // Connect and announce
1003
+ const client = new AgentChatClient({ server, identity: options.identity });
1004
+ await client.connect();
1005
+
1006
+ await client.sendRaw({
1007
+ type: 'REGISTER_SKILLS',
1008
+ skills,
1009
+ sig: sig.toString('base64')
1010
+ });
1011
+
1012
+ // Wait for response
1013
+ const response = await new Promise((resolve, reject) => {
1014
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1015
+ client.on('message', (msg) => {
1016
+ if (msg.type === 'SKILLS_REGISTERED' || msg.type === 'ERROR') {
1017
+ clearTimeout(timeout);
1018
+ resolve(msg);
1019
+ }
1020
+ });
1021
+ });
1022
+
1023
+ client.disconnect();
1024
+
1025
+ if (response.type === 'ERROR') {
1026
+ console.error('Error:', response.message);
1027
+ process.exit(1);
1028
+ }
1029
+
1030
+ console.log(`Registered ${response.skills_count} skill(s) for ${response.agent_id}`);
1031
+
1032
+ } else if (action === 'search') {
1033
+ if (!server) {
1034
+ console.error('Server URL required: agentchat skills search <server>');
1035
+ process.exit(1);
1036
+ }
1037
+
1038
+ const query = {};
1039
+ if (options.capability) query.capability = options.capability;
1040
+ if (options.maxRate !== undefined) query.max_rate = options.maxRate;
1041
+ if (options.currency) query.currency = options.currency;
1042
+ if (options.limit) query.limit = options.limit;
1043
+
1044
+ // Connect and search
1045
+ const client = new AgentChatClient({ server });
1046
+ await client.connect();
1047
+
1048
+ const queryId = `q_${Date.now()}`;
1049
+ await client.sendRaw({
1050
+ type: 'SEARCH_SKILLS',
1051
+ query,
1052
+ query_id: queryId
1053
+ });
1054
+
1055
+ // Wait for response
1056
+ const response = await new Promise((resolve, reject) => {
1057
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1058
+ client.on('message', (msg) => {
1059
+ if (msg.type === 'SEARCH_RESULTS' || msg.type === 'ERROR') {
1060
+ clearTimeout(timeout);
1061
+ resolve(msg);
1062
+ }
1063
+ });
1064
+ });
1065
+
1066
+ client.disconnect();
1067
+
1068
+ if (response.type === 'ERROR') {
1069
+ console.error('Error:', response.message);
1070
+ process.exit(1);
1071
+ }
1072
+
1073
+ if (options.json) {
1074
+ console.log(JSON.stringify(response.results, null, 2));
1075
+ } else {
1076
+ console.log(`Found ${response.results.length} skill(s) (${response.total} total):\n`);
1077
+ for (const skill of response.results) {
1078
+ const rate = skill.rate !== undefined ? `${skill.rate} ${skill.currency || ''}` : 'negotiable';
1079
+ console.log(` ${skill.agent_id}`);
1080
+ console.log(` Capability: ${skill.capability}`);
1081
+ console.log(` Rate: ${rate}`);
1082
+ if (skill.description) console.log(` Description: ${skill.description}`);
1083
+ console.log('');
1084
+ }
1085
+ }
1086
+
1087
+ } else if (action === 'list') {
1088
+ // List own registered skills (if server supports it)
1089
+ console.error('List action not yet implemented');
1090
+ process.exit(1);
1091
+
1092
+ } else {
1093
+ console.error(`Unknown action: ${action}`);
1094
+ console.error('Valid actions: announce, search, list');
1095
+ process.exit(1);
1096
+ }
1097
+
1098
+ } catch (err) {
1099
+ console.error('Error:', err.message);
1100
+ process.exit(1);
1101
+ }
1102
+ });
1103
+
947
1104
  // Deploy command
948
1105
  program
949
1106
  .command('deploy')
package/lib/client.js CHANGED
@@ -534,7 +534,14 @@ export class AgentChatClient extends EventEmitter {
534
534
  this.ws.send(serialize(msg));
535
535
  }
536
536
  }
537
-
537
+
538
+ /**
539
+ * Send a raw message (for protocol extensions)
540
+ */
541
+ sendRaw(msg) {
542
+ this._send(msg);
543
+ }
544
+
538
545
  _handleMessage(data) {
539
546
  let msg;
540
547
  try {
@@ -609,6 +616,17 @@ export class AgentChatClient extends EventEmitter {
609
616
  case ServerMessageType.DISPUTE:
610
617
  this.emit('dispute', msg);
611
618
  break;
619
+
620
+ // Skills discovery messages
621
+ case ServerMessageType.SKILLS_REGISTERED:
622
+ this.emit('skills_registered', msg);
623
+ this.emit('message', msg);
624
+ break;
625
+
626
+ case ServerMessageType.SEARCH_RESULTS:
627
+ this.emit('search_results', msg);
628
+ this.emit('message', msg);
629
+ break;
612
630
  }
613
631
  }
614
632
  }
package/lib/protocol.js CHANGED
@@ -21,7 +21,10 @@ export const ClientMessageType = {
21
21
  ACCEPT: 'ACCEPT',
22
22
  REJECT: 'REJECT',
23
23
  COMPLETE: 'COMPLETE',
24
- DISPUTE: 'DISPUTE'
24
+ DISPUTE: 'DISPUTE',
25
+ // Skill discovery message types
26
+ REGISTER_SKILLS: 'REGISTER_SKILLS',
27
+ SEARCH_SKILLS: 'SEARCH_SKILLS'
25
28
  };
26
29
 
27
30
  // Server -> Client message types
@@ -41,7 +44,10 @@ export const ServerMessageType = {
41
44
  ACCEPT: 'ACCEPT',
42
45
  REJECT: 'REJECT',
43
46
  COMPLETE: 'COMPLETE',
44
- DISPUTE: 'DISPUTE'
47
+ DISPUTE: 'DISPUTE',
48
+ // Skill discovery message types
49
+ SKILLS_REGISTERED: 'SKILLS_REGISTERED',
50
+ SEARCH_RESULTS: 'SEARCH_RESULTS'
45
51
  };
46
52
 
47
53
  // Error codes
@@ -284,6 +290,33 @@ export function validateClientMessage(raw) {
284
290
  }
285
291
  break;
286
292
 
293
+ case ClientMessageType.REGISTER_SKILLS:
294
+ // Register skills requires: skills array and signature
295
+ if (!msg.skills || !Array.isArray(msg.skills)) {
296
+ return { valid: false, error: 'Missing or invalid skills array' };
297
+ }
298
+ if (msg.skills.length === 0) {
299
+ return { valid: false, error: 'Skills array cannot be empty' };
300
+ }
301
+ // Validate each skill has at least a capability
302
+ for (const skill of msg.skills) {
303
+ if (!skill.capability || typeof skill.capability !== 'string') {
304
+ return { valid: false, error: 'Each skill must have a capability string' };
305
+ }
306
+ }
307
+ if (!msg.sig) {
308
+ return { valid: false, error: 'Skill registration must be signed' };
309
+ }
310
+ break;
311
+
312
+ case ClientMessageType.SEARCH_SKILLS:
313
+ // Search skills requires: query object
314
+ if (!msg.query || typeof msg.query !== 'object') {
315
+ return { valid: false, error: 'Missing or invalid query object' };
316
+ }
317
+ // query_id is optional but useful for tracking responses
318
+ break;
319
+
287
320
  default:
288
321
  return { valid: false, error: `Unknown message type: ${msg.type}` };
289
322
  }
package/lib/server.js CHANGED
@@ -67,10 +67,14 @@ export class AgentChatServer {
67
67
  // Create default channels
68
68
  this._createChannel('#general', false);
69
69
  this._createChannel('#agents', false);
70
+ this._createChannel('#discovery', false); // For skill announcements
70
71
 
71
72
  // Proposal store for structured negotiations
72
73
  this.proposals = new ProposalStore();
73
74
 
75
+ // Skills registry: agentId -> { skills: [], registered_at, sig }
76
+ this.skillsRegistry = new Map();
77
+
74
78
  this.wss = null;
75
79
  this.httpServer = null; // For TLS mode
76
80
  }
@@ -315,6 +319,13 @@ export class AgentChatServer {
315
319
  case ClientMessageType.DISPUTE:
316
320
  this._handleDispute(ws, msg);
317
321
  break;
322
+ // Skill discovery messages
323
+ case ClientMessageType.REGISTER_SKILLS:
324
+ this._handleRegisterSkills(ws, msg);
325
+ break;
326
+ case ClientMessageType.SEARCH_SKILLS:
327
+ this._handleSearchSkills(ws, msg);
328
+ break;
318
329
  }
319
330
  }
320
331
 
@@ -855,6 +866,112 @@ export class AgentChatServer {
855
866
  this._send(ws, outMsg);
856
867
  }
857
868
 
869
+ _handleRegisterSkills(ws, msg) {
870
+ const agent = this.agents.get(ws);
871
+ if (!agent) {
872
+ this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
873
+ return;
874
+ }
875
+
876
+ if (!agent.pubkey) {
877
+ this._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Skill registration requires persistent identity'));
878
+ return;
879
+ }
880
+
881
+ // Store skills for this agent
882
+ const registration = {
883
+ agent_id: `@${agent.id}`,
884
+ skills: msg.skills,
885
+ registered_at: Date.now(),
886
+ sig: msg.sig
887
+ };
888
+
889
+ this.skillsRegistry.set(agent.id, registration);
890
+
891
+ this._log('skills_registered', { agent: agent.id, count: msg.skills.length });
892
+
893
+ // Notify the registering agent
894
+ this._send(ws, createMessage(ServerMessageType.SKILLS_REGISTERED, {
895
+ agent_id: `@${agent.id}`,
896
+ skills_count: msg.skills.length,
897
+ registered_at: registration.registered_at
898
+ }));
899
+
900
+ // Optionally broadcast to #discovery channel if it exists
901
+ if (this.channels.has('#discovery')) {
902
+ this._broadcast('#discovery', createMessage(ServerMessageType.MSG, {
903
+ from: '@server',
904
+ to: '#discovery',
905
+ content: `Agent @${agent.id} registered ${msg.skills.length} skill(s): ${msg.skills.map(s => s.capability).join(', ')}`
906
+ }));
907
+ }
908
+ }
909
+
910
+ _handleSearchSkills(ws, msg) {
911
+ const agent = this.agents.get(ws);
912
+ if (!agent) {
913
+ this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
914
+ return;
915
+ }
916
+
917
+ const query = msg.query || {};
918
+ const results = [];
919
+
920
+ // Search through all registered skills
921
+ for (const [agentId, registration] of this.skillsRegistry) {
922
+ for (const skill of registration.skills) {
923
+ let matches = true;
924
+
925
+ // Filter by capability (substring match, case-insensitive)
926
+ if (query.capability) {
927
+ const cap = skill.capability.toLowerCase();
928
+ const search = query.capability.toLowerCase();
929
+ if (!cap.includes(search)) {
930
+ matches = false;
931
+ }
932
+ }
933
+
934
+ // Filter by max_rate
935
+ if (query.max_rate !== undefined && skill.rate !== undefined) {
936
+ if (skill.rate > query.max_rate) {
937
+ matches = false;
938
+ }
939
+ }
940
+
941
+ // Filter by currency
942
+ if (query.currency && skill.currency) {
943
+ if (skill.currency.toLowerCase() !== query.currency.toLowerCase()) {
944
+ matches = false;
945
+ }
946
+ }
947
+
948
+ if (matches) {
949
+ results.push({
950
+ agent_id: registration.agent_id,
951
+ ...skill,
952
+ registered_at: registration.registered_at
953
+ });
954
+ }
955
+ }
956
+ }
957
+
958
+ // Sort by registration time (newest first)
959
+ results.sort((a, b) => b.registered_at - a.registered_at);
960
+
961
+ // Limit results
962
+ const limit = query.limit || 50;
963
+ const limitedResults = results.slice(0, limit);
964
+
965
+ this._log('skills_search', { agent: agent.id, query, results_count: limitedResults.length });
966
+
967
+ this._send(ws, createMessage(ServerMessageType.SEARCH_RESULTS, {
968
+ query_id: msg.query_id || null,
969
+ query,
970
+ results: limitedResults,
971
+ total: results.length
972
+ }));
973
+ }
974
+
858
975
  _handleDisconnect(ws) {
859
976
  const agent = this.agents.get(ws);
860
977
  if (!agent) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/agentchat",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Real-time IRC-like communication protocol for AI agents",
5
5
  "main": "lib/client.js",
6
6
  "files": [