@tjamescouch/agentchat 0.18.1 → 0.18.2
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/lib/server/handlers/identity.js +242 -0
- package/lib/server/handlers/index.js +42 -0
- package/lib/server/handlers/message.js +306 -0
- package/lib/server/handlers/presence.js +44 -0
- package/lib/server/handlers/proposal.js +358 -0
- package/lib/server/handlers/skills.js +141 -0
- package/lib/server.js +52 -1007
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Handlers
|
|
3
|
+
* Handles identify, verification request/response
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import {
|
|
8
|
+
ServerMessageType,
|
|
9
|
+
ErrorCode,
|
|
10
|
+
createMessage,
|
|
11
|
+
createError,
|
|
12
|
+
generateAgentId,
|
|
13
|
+
generateVerifyId,
|
|
14
|
+
pubkeyToAgentId,
|
|
15
|
+
} from '../../protocol.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle IDENTIFY command
|
|
19
|
+
*/
|
|
20
|
+
export function handleIdentify(server, ws, msg) {
|
|
21
|
+
// Check if already identified
|
|
22
|
+
if (server.agents.has(ws)) {
|
|
23
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Already identified'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let id;
|
|
28
|
+
|
|
29
|
+
// Use pubkey-derived stable ID if pubkey provided
|
|
30
|
+
if (msg.pubkey) {
|
|
31
|
+
// Check if this pubkey has connected before
|
|
32
|
+
const existingId = server.pubkeyToId.get(msg.pubkey);
|
|
33
|
+
if (existingId) {
|
|
34
|
+
// Returning agent - use their stable ID
|
|
35
|
+
id = existingId;
|
|
36
|
+
} else {
|
|
37
|
+
// New agent with pubkey - generate stable ID from pubkey
|
|
38
|
+
id = pubkeyToAgentId(msg.pubkey);
|
|
39
|
+
server.pubkeyToId.set(msg.pubkey, id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if this ID is currently in use by another connection
|
|
43
|
+
if (server.agentById.has(id)) {
|
|
44
|
+
// Kick the old connection instead of rejecting the new one
|
|
45
|
+
const oldWs = server.agentById.get(id);
|
|
46
|
+
server._log('identity-takeover', { id, reason: 'New connection with same identity' });
|
|
47
|
+
server._send(oldWs, createError(ErrorCode.INVALID_MSG, 'Disconnected: Another connection claimed this identity'));
|
|
48
|
+
server._handleDisconnect(oldWs);
|
|
49
|
+
oldWs.close(1000, 'Identity claimed by new connection');
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Ephemeral agent - generate random ID
|
|
53
|
+
id = generateAgentId();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const agent = {
|
|
57
|
+
id,
|
|
58
|
+
name: msg.name,
|
|
59
|
+
pubkey: msg.pubkey || null,
|
|
60
|
+
channels: new Set(),
|
|
61
|
+
connectedAt: Date.now(),
|
|
62
|
+
presence: 'online',
|
|
63
|
+
statusText: null
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
server.agents.set(ws, agent);
|
|
67
|
+
server.agentById.set(id, ws);
|
|
68
|
+
|
|
69
|
+
// Determine if this is a new or returning identity
|
|
70
|
+
const isReturning = msg.pubkey && server.pubkeyToId.has(msg.pubkey);
|
|
71
|
+
const isEphemeral = !msg.pubkey;
|
|
72
|
+
|
|
73
|
+
server._log('identify', {
|
|
74
|
+
id,
|
|
75
|
+
name: msg.name,
|
|
76
|
+
hasPubkey: !!msg.pubkey,
|
|
77
|
+
returning: isReturning,
|
|
78
|
+
ephemeral: isEphemeral,
|
|
79
|
+
ip: ws._realIp,
|
|
80
|
+
user_agent: ws._userAgent
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
server._send(ws, createMessage(ServerMessageType.WELCOME, {
|
|
84
|
+
agent_id: `@${id}`,
|
|
85
|
+
server: server.serverName
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle VERIFY_REQUEST command
|
|
91
|
+
*/
|
|
92
|
+
export function handleVerifyRequest(server, ws, msg) {
|
|
93
|
+
const agent = server.agents.get(ws);
|
|
94
|
+
if (!agent) {
|
|
95
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Find target agent
|
|
100
|
+
const targetId = msg.target.slice(1);
|
|
101
|
+
const targetWs = server.agentById.get(targetId);
|
|
102
|
+
|
|
103
|
+
if (!targetWs) {
|
|
104
|
+
server._send(ws, createError(ErrorCode.AGENT_NOT_FOUND, `Agent ${msg.target} not found`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const targetAgent = server.agents.get(targetWs);
|
|
109
|
+
|
|
110
|
+
// Target must have a pubkey for verification
|
|
111
|
+
if (!targetAgent.pubkey) {
|
|
112
|
+
server._send(ws, createError(ErrorCode.NO_PUBKEY, `Agent ${msg.target} has no persistent identity`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Create verification request
|
|
117
|
+
const requestId = generateVerifyId();
|
|
118
|
+
const expires = Date.now() + server.verificationTimeoutMs;
|
|
119
|
+
|
|
120
|
+
server.pendingVerifications.set(requestId, {
|
|
121
|
+
from: `@${agent.id}`,
|
|
122
|
+
fromWs: ws,
|
|
123
|
+
target: msg.target,
|
|
124
|
+
targetPubkey: targetAgent.pubkey,
|
|
125
|
+
nonce: msg.nonce,
|
|
126
|
+
expires
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Set timeout to clean up expired requests
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
const request = server.pendingVerifications.get(requestId);
|
|
132
|
+
if (request) {
|
|
133
|
+
server.pendingVerifications.delete(requestId);
|
|
134
|
+
// Notify requester of timeout
|
|
135
|
+
if (request.fromWs.readyState === 1) {
|
|
136
|
+
server._send(request.fromWs, createMessage(ServerMessageType.VERIFY_FAILED, {
|
|
137
|
+
request_id: requestId,
|
|
138
|
+
target: request.target,
|
|
139
|
+
reason: 'Verification timed out'
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}, server.verificationTimeoutMs);
|
|
144
|
+
|
|
145
|
+
server._log('verify_request', { id: requestId, from: agent.id, target: targetId });
|
|
146
|
+
|
|
147
|
+
// Forward to target agent
|
|
148
|
+
server._send(targetWs, createMessage(ServerMessageType.VERIFY_REQUEST, {
|
|
149
|
+
request_id: requestId,
|
|
150
|
+
from: `@${agent.id}`,
|
|
151
|
+
nonce: msg.nonce
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
// Acknowledge to requester
|
|
155
|
+
server._send(ws, createMessage(ServerMessageType.VERIFY_REQUEST, {
|
|
156
|
+
request_id: requestId,
|
|
157
|
+
target: msg.target,
|
|
158
|
+
status: 'pending'
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle VERIFY_RESPONSE command
|
|
164
|
+
*/
|
|
165
|
+
export function handleVerifyResponse(server, ws, msg) {
|
|
166
|
+
const agent = server.agents.get(ws);
|
|
167
|
+
if (!agent) {
|
|
168
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Must have identity to respond to verification
|
|
173
|
+
if (!agent.pubkey) {
|
|
174
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Responding to verification requires persistent identity'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Find the pending verification
|
|
179
|
+
const request = server.pendingVerifications.get(msg.request_id);
|
|
180
|
+
if (!request) {
|
|
181
|
+
server._send(ws, createError(ErrorCode.VERIFICATION_EXPIRED, 'Verification request not found or expired'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Verify the responder is the target
|
|
186
|
+
if (request.target !== `@${agent.id}`) {
|
|
187
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'You are not the target of this verification'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Verify the nonce matches
|
|
192
|
+
if (msg.nonce !== request.nonce) {
|
|
193
|
+
server._send(ws, createError(ErrorCode.INVALID_MSG, 'Nonce mismatch'));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Verify the signature
|
|
198
|
+
let verified = false;
|
|
199
|
+
try {
|
|
200
|
+
const keyObj = crypto.createPublicKey(request.targetPubkey);
|
|
201
|
+
verified = crypto.verify(
|
|
202
|
+
null,
|
|
203
|
+
Buffer.from(msg.nonce),
|
|
204
|
+
keyObj,
|
|
205
|
+
Buffer.from(msg.sig, 'base64')
|
|
206
|
+
);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
server._log('verify_error', { request_id: msg.request_id, error: err.message });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Clean up the pending request
|
|
212
|
+
server.pendingVerifications.delete(msg.request_id);
|
|
213
|
+
|
|
214
|
+
server._log('verify_response', {
|
|
215
|
+
request_id: msg.request_id,
|
|
216
|
+
from: agent.id,
|
|
217
|
+
verified
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Notify the original requester
|
|
221
|
+
if (request.fromWs.readyState === 1) {
|
|
222
|
+
if (verified) {
|
|
223
|
+
server._send(request.fromWs, createMessage(ServerMessageType.VERIFY_SUCCESS, {
|
|
224
|
+
request_id: msg.request_id,
|
|
225
|
+
agent: request.target,
|
|
226
|
+
pubkey: request.targetPubkey
|
|
227
|
+
}));
|
|
228
|
+
} else {
|
|
229
|
+
server._send(request.fromWs, createMessage(ServerMessageType.VERIFY_FAILED, {
|
|
230
|
+
request_id: msg.request_id,
|
|
231
|
+
target: request.target,
|
|
232
|
+
reason: 'Signature verification failed'
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Notify the responder
|
|
238
|
+
server._send(ws, createMessage(verified ? ServerMessageType.VERIFY_SUCCESS : ServerMessageType.VERIFY_FAILED, {
|
|
239
|
+
request_id: msg.request_id,
|
|
240
|
+
verified
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Handlers Index
|
|
3
|
+
* Exports all handler functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Message handlers
|
|
7
|
+
export {
|
|
8
|
+
handleMsg,
|
|
9
|
+
handleJoin,
|
|
10
|
+
handleLeave,
|
|
11
|
+
handleListChannels,
|
|
12
|
+
handleListAgents,
|
|
13
|
+
handleCreateChannel,
|
|
14
|
+
handleInvite,
|
|
15
|
+
} from './message.js';
|
|
16
|
+
|
|
17
|
+
// Proposal handlers
|
|
18
|
+
export {
|
|
19
|
+
handleProposal,
|
|
20
|
+
handleAccept,
|
|
21
|
+
handleReject,
|
|
22
|
+
handleComplete,
|
|
23
|
+
handleDispute,
|
|
24
|
+
} from './proposal.js';
|
|
25
|
+
|
|
26
|
+
// Identity handlers
|
|
27
|
+
export {
|
|
28
|
+
handleIdentify,
|
|
29
|
+
handleVerifyRequest,
|
|
30
|
+
handleVerifyResponse,
|
|
31
|
+
} from './identity.js';
|
|
32
|
+
|
|
33
|
+
// Skills handlers
|
|
34
|
+
export {
|
|
35
|
+
handleRegisterSkills,
|
|
36
|
+
handleSearchSkills,
|
|
37
|
+
} from './skills.js';
|
|
38
|
+
|
|
39
|
+
// Presence handlers
|
|
40
|
+
export {
|
|
41
|
+
handleSetPresence,
|
|
42
|
+
} from './presence.js';
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Handlers
|
|
3
|
+
* Handles message routing, join, leave, and channel operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ServerMessageType,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
createMessage,
|
|
10
|
+
createError,
|
|
11
|
+
isChannel,
|
|
12
|
+
isAgent,
|
|
13
|
+
} from '../../protocol.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handle MSG command - route messages to channels or agents
|
|
17
|
+
*/
|
|
18
|
+
export function handleMsg(server, ws, msg) {
|
|
19
|
+
const agent = server.agents.get(ws);
|
|
20
|
+
if (!agent) {
|
|
21
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Rate limiting: 1 message per second per agent
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const lastTime = server.lastMessageTime.get(ws) || 0;
|
|
28
|
+
if (now - lastTime < server.rateLimitMs) {
|
|
29
|
+
server._send(ws, createError(ErrorCode.RATE_LIMITED, 'Rate limit exceeded (max 1 message per second)'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
server.lastMessageTime.set(ws, now);
|
|
33
|
+
|
|
34
|
+
const outMsg = createMessage(ServerMessageType.MSG, {
|
|
35
|
+
from: `@${agent.id}`,
|
|
36
|
+
to: msg.to,
|
|
37
|
+
content: msg.content,
|
|
38
|
+
...(msg.sig && { sig: msg.sig })
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (isChannel(msg.to)) {
|
|
42
|
+
// Channel message
|
|
43
|
+
const channel = server.channels.get(msg.to);
|
|
44
|
+
if (!channel) {
|
|
45
|
+
server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.to} not found`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!agent.channels.has(msg.to)) {
|
|
50
|
+
server._send(ws, createError(ErrorCode.NOT_INVITED, `Not a member of ${msg.to}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Broadcast to channel including sender
|
|
55
|
+
server._broadcast(msg.to, outMsg);
|
|
56
|
+
|
|
57
|
+
// Buffer the message for replay to future joiners
|
|
58
|
+
server._bufferMessage(msg.to, outMsg);
|
|
59
|
+
|
|
60
|
+
// Update channel activity timestamp (for idle detection)
|
|
61
|
+
server.channelLastActivity.set(msg.to, Date.now());
|
|
62
|
+
|
|
63
|
+
} else if (isAgent(msg.to)) {
|
|
64
|
+
// Direct message
|
|
65
|
+
const targetId = msg.to.slice(1);
|
|
66
|
+
const targetWs = server.agentById.get(targetId);
|
|
67
|
+
|
|
68
|
+
if (!targetWs) {
|
|
69
|
+
server._send(ws, createError(ErrorCode.AGENT_NOT_FOUND, `Agent ${msg.to} not found`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Send to target
|
|
74
|
+
server._send(targetWs, outMsg);
|
|
75
|
+
// Echo back to sender
|
|
76
|
+
server._send(ws, outMsg);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle JOIN command - add agent to channel
|
|
82
|
+
*/
|
|
83
|
+
export function handleJoin(server, ws, msg) {
|
|
84
|
+
const agent = server.agents.get(ws);
|
|
85
|
+
if (!agent) {
|
|
86
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const channel = server.channels.get(msg.channel);
|
|
91
|
+
if (!channel) {
|
|
92
|
+
server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check invite-only
|
|
97
|
+
if (channel.inviteOnly && !channel.invited.has(agent.id)) {
|
|
98
|
+
server._send(ws, createError(ErrorCode.NOT_INVITED, `Channel ${msg.channel} is invite-only`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add to channel
|
|
103
|
+
channel.agents.add(ws);
|
|
104
|
+
agent.channels.add(msg.channel);
|
|
105
|
+
|
|
106
|
+
server._log('join', { agent: agent.id, channel: msg.channel });
|
|
107
|
+
|
|
108
|
+
// Notify others
|
|
109
|
+
server._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_JOINED, {
|
|
110
|
+
channel: msg.channel,
|
|
111
|
+
agent: `@${agent.id}`
|
|
112
|
+
}), ws);
|
|
113
|
+
|
|
114
|
+
// Send confirmation with agent list
|
|
115
|
+
const agentList = [];
|
|
116
|
+
for (const memberWs of channel.agents) {
|
|
117
|
+
const member = server.agents.get(memberWs);
|
|
118
|
+
if (member) agentList.push(`@${member.id}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
server._send(ws, createMessage(ServerMessageType.JOINED, {
|
|
122
|
+
channel: msg.channel,
|
|
123
|
+
agents: agentList
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// Replay recent messages to the joining agent
|
|
127
|
+
server._replayMessages(ws, msg.channel);
|
|
128
|
+
|
|
129
|
+
// Send welcome prompt to the new joiner
|
|
130
|
+
server._send(ws, createMessage(ServerMessageType.MSG, {
|
|
131
|
+
from: '@server',
|
|
132
|
+
to: msg.channel,
|
|
133
|
+
content: `Welcome to ${msg.channel}, @${agent.id}! Say hello to introduce yourself and start collaborating with other agents.`
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
// Prompt existing agents to engage with the new joiner (if there are others)
|
|
137
|
+
const otherAgents = [];
|
|
138
|
+
for (const memberWs of channel.agents) {
|
|
139
|
+
if (memberWs !== ws) {
|
|
140
|
+
const member = server.agents.get(memberWs);
|
|
141
|
+
if (member) otherAgents.push({ ws: memberWs, id: member.id });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (otherAgents.length > 0) {
|
|
146
|
+
const welcomePrompt = createMessage(ServerMessageType.MSG, {
|
|
147
|
+
from: '@server',
|
|
148
|
+
to: msg.channel,
|
|
149
|
+
content: `Hey ${otherAgents.map(a => `@${a.id}`).join(', ')} - new agent @${agent.id} just joined! Say hi and share what you're working on.`
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
for (const other of otherAgents) {
|
|
153
|
+
server._send(other.ws, welcomePrompt);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update channel activity
|
|
158
|
+
server.channelLastActivity.set(msg.channel, Date.now());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle LEAVE command - remove agent from channel
|
|
163
|
+
*/
|
|
164
|
+
export function handleLeave(server, ws, msg) {
|
|
165
|
+
const agent = server.agents.get(ws);
|
|
166
|
+
if (!agent) {
|
|
167
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const channel = server.channels.get(msg.channel);
|
|
172
|
+
if (!channel) return;
|
|
173
|
+
|
|
174
|
+
channel.agents.delete(ws);
|
|
175
|
+
agent.channels.delete(msg.channel);
|
|
176
|
+
|
|
177
|
+
server._log('leave', { agent: agent.id, channel: msg.channel });
|
|
178
|
+
|
|
179
|
+
// Notify others
|
|
180
|
+
server._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_LEFT, {
|
|
181
|
+
channel: msg.channel,
|
|
182
|
+
agent: `@${agent.id}`
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
server._send(ws, createMessage(ServerMessageType.LEFT, {
|
|
186
|
+
channel: msg.channel
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle LIST_CHANNELS command
|
|
192
|
+
*/
|
|
193
|
+
export function handleListChannels(server, ws) {
|
|
194
|
+
const list = [];
|
|
195
|
+
for (const [name, channel] of server.channels) {
|
|
196
|
+
if (!channel.inviteOnly) {
|
|
197
|
+
list.push({
|
|
198
|
+
name,
|
|
199
|
+
agents: channel.agents.size
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
server._send(ws, createMessage(ServerMessageType.CHANNELS, { list }));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle LIST_AGENTS command
|
|
209
|
+
*/
|
|
210
|
+
export function handleListAgents(server, ws, msg) {
|
|
211
|
+
const channel = server.channels.get(msg.channel);
|
|
212
|
+
if (!channel) {
|
|
213
|
+
server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const list = [];
|
|
218
|
+
for (const memberWs of channel.agents) {
|
|
219
|
+
const member = server.agents.get(memberWs);
|
|
220
|
+
if (member) {
|
|
221
|
+
list.push({
|
|
222
|
+
id: `@${member.id}`,
|
|
223
|
+
name: member.name,
|
|
224
|
+
presence: member.presence || 'online',
|
|
225
|
+
status_text: member.statusText || null
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
server._send(ws, createMessage(ServerMessageType.AGENTS, {
|
|
231
|
+
channel: msg.channel,
|
|
232
|
+
list
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Handle CREATE_CHANNEL command
|
|
238
|
+
*/
|
|
239
|
+
export function handleCreateChannel(server, ws, msg) {
|
|
240
|
+
const agent = server.agents.get(ws);
|
|
241
|
+
if (!agent) {
|
|
242
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (server.channels.has(msg.channel)) {
|
|
247
|
+
server._send(ws, createError(ErrorCode.CHANNEL_EXISTS, `Channel ${msg.channel} already exists`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const channel = server._createChannel(msg.channel, msg.invite_only || false);
|
|
252
|
+
|
|
253
|
+
// Creator is automatically invited and joined
|
|
254
|
+
if (channel.inviteOnly) {
|
|
255
|
+
channel.invited.add(agent.id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
server._log('create_channel', { agent: agent.id, channel: msg.channel, inviteOnly: channel.inviteOnly });
|
|
259
|
+
|
|
260
|
+
// Auto-join creator
|
|
261
|
+
channel.agents.add(ws);
|
|
262
|
+
agent.channels.add(msg.channel);
|
|
263
|
+
|
|
264
|
+
server._send(ws, createMessage(ServerMessageType.JOINED, {
|
|
265
|
+
channel: msg.channel,
|
|
266
|
+
agents: [`@${agent.id}`]
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handle INVITE command
|
|
272
|
+
*/
|
|
273
|
+
export function handleInvite(server, ws, msg) {
|
|
274
|
+
const agent = server.agents.get(ws);
|
|
275
|
+
if (!agent) {
|
|
276
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const channel = server.channels.get(msg.channel);
|
|
281
|
+
if (!channel) {
|
|
282
|
+
server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Must be a member to invite
|
|
287
|
+
if (!agent.channels.has(msg.channel)) {
|
|
288
|
+
server._send(ws, createError(ErrorCode.NOT_INVITED, `Not a member of ${msg.channel}`));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const targetId = msg.agent.slice(1);
|
|
293
|
+
channel.invited.add(targetId);
|
|
294
|
+
|
|
295
|
+
server._log('invite', { agent: agent.id, target: targetId, channel: msg.channel });
|
|
296
|
+
|
|
297
|
+
// Notify target if connected
|
|
298
|
+
const targetWs = server.agentById.get(targetId);
|
|
299
|
+
if (targetWs) {
|
|
300
|
+
server._send(targetWs, createMessage(ServerMessageType.MSG, {
|
|
301
|
+
from: `@${agent.id}`,
|
|
302
|
+
to: msg.agent,
|
|
303
|
+
content: `You have been invited to ${msg.channel}`
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence Handlers
|
|
3
|
+
* Handles presence status updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ServerMessageType,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
createMessage,
|
|
10
|
+
createError,
|
|
11
|
+
} from '../../protocol.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle SET_PRESENCE command
|
|
15
|
+
*/
|
|
16
|
+
export function handleSetPresence(server, ws, msg) {
|
|
17
|
+
const agent = server.agents.get(ws);
|
|
18
|
+
if (!agent) {
|
|
19
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const oldPresence = agent.presence;
|
|
24
|
+
agent.presence = msg.status;
|
|
25
|
+
agent.statusText = msg.status_text || null;
|
|
26
|
+
|
|
27
|
+
server._log('presence_changed', {
|
|
28
|
+
agent: agent.id,
|
|
29
|
+
from: oldPresence,
|
|
30
|
+
to: msg.status,
|
|
31
|
+
statusText: agent.statusText
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Broadcast presence change to all channels the agent is in
|
|
35
|
+
const presenceMsg = createMessage(ServerMessageType.PRESENCE_CHANGED, {
|
|
36
|
+
agent_id: `@${agent.id}`,
|
|
37
|
+
presence: agent.presence,
|
|
38
|
+
status_text: agent.statusText
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
for (const channelName of agent.channels) {
|
|
42
|
+
server._broadcast(channelName, presenceMsg);
|
|
43
|
+
}
|
|
44
|
+
}
|