@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/.claude/settings.local.json +12 -0
- package/.github/workflows/fly-deploy.yml +18 -0
- package/Dockerfile +12 -0
- package/README.md +296 -0
- package/ROADMAP.md +88 -0
- package/SPEC.md +279 -0
- package/bin/agentchat.js +702 -0
- package/fly.toml +21 -0
- package/lib/client.js +362 -0
- package/lib/deploy/akash.js +811 -0
- package/lib/deploy/config.js +128 -0
- package/lib/deploy/index.js +149 -0
- package/lib/identity.js +166 -0
- package/lib/protocol.js +236 -0
- package/lib/server.js +526 -0
- package/package.json +44 -0
- package/quick-test.sh +45 -0
- package/test/integration.test.js +536 -0
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
|
+
}
|