@tjamescouch/agentchat 0.1.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/fly.toml ADDED
@@ -0,0 +1,21 @@
1
+ # fly.toml app configuration file generated for agentchat-server on 2026-02-02T19:57:15-07:00
2
+ #
3
+ # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4
+ #
5
+
6
+ app = 'agentchat-server'
7
+ primary_region = 'sjc'
8
+
9
+ [build]
10
+
11
+ [http_service]
12
+ internal_port = 6667
13
+ force_https = true
14
+ auto_stop_machines = 'off'
15
+ auto_start_machines = false
16
+ min_machines_running = 1
17
+ processes = ['app']
18
+
19
+ [[vm]]
20
+ size = 'shared-cpu-1x'
21
+ memory = '256mb'
package/lib/client.js ADDED
@@ -0,0 +1,362 @@
1
+ /**
2
+ * AgentChat Client
3
+ * Connect to agentchat servers from Node.js or CLI
4
+ */
5
+
6
+ import WebSocket from 'ws';
7
+ import { EventEmitter } from 'events';
8
+ import {
9
+ ClientMessageType,
10
+ ServerMessageType,
11
+ createMessage,
12
+ serialize,
13
+ parse
14
+ } from './protocol.js';
15
+ import { Identity } from './identity.js';
16
+
17
+ export class AgentChatClient extends EventEmitter {
18
+ constructor(options = {}) {
19
+ super();
20
+ this.server = options.server;
21
+ this.name = options.name || `agent-${Date.now()}`;
22
+ this.pubkey = options.pubkey || null;
23
+
24
+ // Identity support
25
+ this.identityPath = options.identity || null;
26
+ this._identity = null;
27
+
28
+ this.ws = null;
29
+ this.agentId = null;
30
+ this.connected = false;
31
+ this.channels = new Set();
32
+
33
+ this._pendingRequests = new Map();
34
+ this._requestId = 0;
35
+ }
36
+
37
+ /**
38
+ * Load identity from file
39
+ */
40
+ async _loadIdentity() {
41
+ if (this.identityPath) {
42
+ try {
43
+ this._identity = await Identity.load(this.identityPath);
44
+ this.name = this._identity.name;
45
+ this.pubkey = this._identity.pubkey;
46
+ } catch (err) {
47
+ throw new Error(`Failed to load identity from ${this.identityPath}: ${err.message}`);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Connect to the server and identify
54
+ */
55
+ async connect() {
56
+ // Load identity if path provided
57
+ await this._loadIdentity();
58
+
59
+ return new Promise((resolve, reject) => {
60
+ this.ws = new WebSocket(this.server);
61
+
62
+ this.ws.on('open', () => {
63
+ // Send identify
64
+ this._send({
65
+ type: ClientMessageType.IDENTIFY,
66
+ name: this.name,
67
+ pubkey: this.pubkey
68
+ });
69
+ });
70
+
71
+ this.ws.on('message', (data) => {
72
+ this._handleMessage(data.toString());
73
+ });
74
+
75
+ this.ws.on('close', () => {
76
+ this.connected = false;
77
+ this.emit('disconnect');
78
+ });
79
+
80
+ this.ws.on('error', (err) => {
81
+ this.emit('error', err);
82
+ if (!this.connected) {
83
+ reject(err);
84
+ }
85
+ });
86
+
87
+ // Wait for WELCOME
88
+ this.once('welcome', (info) => {
89
+ this.connected = true;
90
+ this.agentId = info.agent_id;
91
+ resolve(info);
92
+ });
93
+
94
+ // Handle connection error
95
+ this.once('error', (err) => {
96
+ if (!this.connected) {
97
+ reject(err);
98
+ }
99
+ });
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Disconnect from server
105
+ */
106
+ disconnect() {
107
+ if (this.ws) {
108
+ this.ws.close();
109
+ this.ws = null;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Join a channel
115
+ */
116
+ async join(channel) {
117
+ this._send({
118
+ type: ClientMessageType.JOIN,
119
+ channel
120
+ });
121
+
122
+ return new Promise((resolve, reject) => {
123
+ const onJoined = (msg) => {
124
+ if (msg.channel === channel) {
125
+ this.removeListener('error', onError);
126
+ this.channels.add(channel);
127
+ resolve(msg);
128
+ }
129
+ };
130
+
131
+ const onError = (msg) => {
132
+ this.removeListener('joined', onJoined);
133
+ reject(new Error(msg.message));
134
+ };
135
+
136
+ this.once('joined', onJoined);
137
+ this.once('error', onError);
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Leave a channel
143
+ */
144
+ async leave(channel) {
145
+ this._send({
146
+ type: ClientMessageType.LEAVE,
147
+ channel
148
+ });
149
+ this.channels.delete(channel);
150
+ }
151
+
152
+ /**
153
+ * Send a message to a channel or agent
154
+ */
155
+ async send(to, content) {
156
+ const msg = {
157
+ type: ClientMessageType.MSG,
158
+ to,
159
+ content
160
+ };
161
+
162
+ // Sign message if identity available
163
+ if (this._identity && this._identity.privkey) {
164
+ msg.ts = Date.now();
165
+ const dataToSign = JSON.stringify({
166
+ to: msg.to,
167
+ content: msg.content,
168
+ ts: msg.ts
169
+ });
170
+ msg.sig = this._identity.sign(dataToSign);
171
+ }
172
+
173
+ this._send(msg);
174
+ }
175
+
176
+ /**
177
+ * Send a direct message (alias for send with @target)
178
+ */
179
+ async dm(agent, content) {
180
+ const target = agent.startsWith('@') ? agent : `@${agent}`;
181
+ return this.send(target, content);
182
+ }
183
+
184
+ /**
185
+ * List available channels
186
+ */
187
+ async listChannels() {
188
+ this._send({
189
+ type: ClientMessageType.LIST_CHANNELS
190
+ });
191
+
192
+ return new Promise((resolve) => {
193
+ this.once('channels', (msg) => {
194
+ resolve(msg.list);
195
+ });
196
+ });
197
+ }
198
+
199
+ /**
200
+ * List agents in a channel
201
+ */
202
+ async listAgents(channel) {
203
+ this._send({
204
+ type: ClientMessageType.LIST_AGENTS,
205
+ channel
206
+ });
207
+
208
+ return new Promise((resolve) => {
209
+ this.once('agents', (msg) => {
210
+ resolve(msg.list);
211
+ });
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Create a new channel
217
+ */
218
+ async createChannel(channel, inviteOnly = false) {
219
+ this._send({
220
+ type: ClientMessageType.CREATE_CHANNEL,
221
+ channel,
222
+ invite_only: inviteOnly
223
+ });
224
+
225
+ return new Promise((resolve, reject) => {
226
+ const onJoined = (msg) => {
227
+ if (msg.channel === channel) {
228
+ this.removeListener('error', onError);
229
+ this.channels.add(channel);
230
+ resolve(msg);
231
+ }
232
+ };
233
+
234
+ const onError = (msg) => {
235
+ this.removeListener('joined', onJoined);
236
+ reject(new Error(msg.message));
237
+ };
238
+
239
+ this.once('joined', onJoined);
240
+ this.once('error', onError);
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Invite an agent to a channel
246
+ */
247
+ async invite(channel, agent) {
248
+ const target = agent.startsWith('@') ? agent : `@${agent}`;
249
+ this._send({
250
+ type: ClientMessageType.INVITE,
251
+ channel,
252
+ agent: target
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Send ping to server
258
+ */
259
+ ping() {
260
+ this._send({ type: ClientMessageType.PING });
261
+ }
262
+
263
+ _send(msg) {
264
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
265
+ this.ws.send(serialize(msg));
266
+ }
267
+ }
268
+
269
+ _handleMessage(data) {
270
+ let msg;
271
+ try {
272
+ msg = parse(data);
273
+ } catch (e) {
274
+ this.emit('error', { message: 'Invalid JSON from server' });
275
+ return;
276
+ }
277
+
278
+ // Emit raw message
279
+ this.emit('raw', msg);
280
+
281
+ // Handle by type
282
+ switch (msg.type) {
283
+ case ServerMessageType.WELCOME:
284
+ this.emit('welcome', msg);
285
+ break;
286
+
287
+ case ServerMessageType.MSG:
288
+ this.emit('message', msg);
289
+ break;
290
+
291
+ case ServerMessageType.JOINED:
292
+ this.emit('joined', msg);
293
+ break;
294
+
295
+ case ServerMessageType.LEFT:
296
+ this.emit('left', msg);
297
+ break;
298
+
299
+ case ServerMessageType.AGENT_JOINED:
300
+ this.emit('agent_joined', msg);
301
+ break;
302
+
303
+ case ServerMessageType.AGENT_LEFT:
304
+ this.emit('agent_left', msg);
305
+ break;
306
+
307
+ case ServerMessageType.CHANNELS:
308
+ this.emit('channels', msg);
309
+ break;
310
+
311
+ case ServerMessageType.AGENTS:
312
+ this.emit('agents', msg);
313
+ break;
314
+
315
+ case ServerMessageType.ERROR:
316
+ this.emit('error', msg);
317
+ break;
318
+
319
+ case ServerMessageType.PONG:
320
+ this.emit('pong', msg);
321
+ break;
322
+ }
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Quick send - connect, send message, disconnect
328
+ */
329
+ export async function quickSend(server, name, to, content, identityPath = null) {
330
+ const client = new AgentChatClient({ server, name, identity: identityPath });
331
+ await client.connect();
332
+
333
+ // Join channel if needed
334
+ if (to.startsWith('#')) {
335
+ await client.join(to);
336
+ }
337
+
338
+ await client.send(to, content);
339
+
340
+ // Small delay to ensure message is sent
341
+ await new Promise(r => setTimeout(r, 100));
342
+
343
+ client.disconnect();
344
+ }
345
+
346
+ /**
347
+ * Listen mode - connect, join channels, stream messages
348
+ */
349
+ export async function listen(server, name, channels, callback, identityPath = null) {
350
+ const client = new AgentChatClient({ server, name, identity: identityPath });
351
+ await client.connect();
352
+
353
+ for (const channel of channels) {
354
+ await client.join(channel);
355
+ }
356
+
357
+ client.on('message', callback);
358
+ client.on('agent_joined', callback);
359
+ client.on('agent_left', callback);
360
+
361
+ return client;
362
+ }