agentschat-mcp 0.31.0 → 0.32.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/README.md +45 -0
- package/connector/README.md +37 -12
- package/connector/identities.ts +96 -0
- package/connector/run.ts +118 -69
- package/connector/server.ts +137 -46
- package/dist/connector.js +207 -82
- package/dist/server.js +204 -2
- package/package.json +3 -1
- package/src/cli.mjs +10 -4
- package/src/server.ts +60 -0
- package/src/wake.ts +302 -0
package/dist/connector.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// connector/run.ts
|
|
3
|
+
import WS from "ws";
|
|
4
|
+
|
|
2
5
|
// connector/server.ts
|
|
3
6
|
import { createServer } from "node:http";
|
|
4
7
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -101,14 +104,58 @@ function toWireEvent(msg, platform = "agentschat") {
|
|
|
101
104
|
};
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
// connector/identities.ts
|
|
108
|
+
class IdentityTable {
|
|
109
|
+
byBot = new Map;
|
|
110
|
+
constructor(identities) {
|
|
111
|
+
for (const id of identities) {
|
|
112
|
+
if (this.byBot.has(id.botId)) {
|
|
113
|
+
throw new Error(`duplicate identity botId "${id.botId}" — ambiguous routing`);
|
|
114
|
+
}
|
|
115
|
+
this.byBot.set(id.botId, id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
forBot(botId) {
|
|
119
|
+
return this.byBot.get(botId) ?? null;
|
|
120
|
+
}
|
|
121
|
+
isSingle() {
|
|
122
|
+
return this.byBot.size === 1;
|
|
123
|
+
}
|
|
124
|
+
get size() {
|
|
125
|
+
return this.byBot.size;
|
|
126
|
+
}
|
|
127
|
+
all() {
|
|
128
|
+
return [...this.byBot.values()];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function routeInbound(table, ctx) {
|
|
132
|
+
if (ctx.channel_id?.startsWith("dm-")) {
|
|
133
|
+
return ctx.dmOwnerBotId ? table.forBot(ctx.dmOwnerBotId) : null;
|
|
134
|
+
}
|
|
135
|
+
const mentioned = Array.isArray(ctx.mentioned_ids) ? ctx.mentioned_ids : [];
|
|
136
|
+
for (const mid of mentioned) {
|
|
137
|
+
const id = table.forBot(mid);
|
|
138
|
+
if (id)
|
|
139
|
+
return id;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
// connector/server.ts
|
|
105
145
|
function startConnector(config) {
|
|
106
146
|
const log = config.logger ?? (() => {});
|
|
107
147
|
const descriptor = buildDescriptor(config.descriptor);
|
|
148
|
+
const single = !config.identities || config.identities.length === 0;
|
|
149
|
+
const table = new IdentityTable(single ? [{ botId: "default", agentId: "default", token: "", gatewayId: "", secret: "" }] : config.identities);
|
|
150
|
+
const hooks = single ? {
|
|
151
|
+
sendMessage: (_b, chatId, content, replyTo) => config.agentschat.sendMessage(chatId, content, replyTo),
|
|
152
|
+
getChatInfo: (_b, chatId) => config.agentschat.getChatInfo(chatId),
|
|
153
|
+
sendTyping: (_b, chatId) => config.agentschat.sendTyping?.(chatId) ?? Promise.resolve()
|
|
154
|
+
} : config.agentschat;
|
|
108
155
|
const sockets = new Set;
|
|
109
156
|
const http = createServer((_req, res) => {
|
|
110
157
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
111
|
-
res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1 }));
|
|
158
|
+
res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1, identities: table.size }));
|
|
112
159
|
});
|
|
113
160
|
const wss = new WebSocketServer({ noServer: true });
|
|
114
161
|
http.on("upgrade", (req, socket, head) => {
|
|
@@ -134,52 +181,75 @@ function startConnector(config) {
|
|
|
134
181
|
});
|
|
135
182
|
});
|
|
136
183
|
function onConnection(ws, gatewayId) {
|
|
137
|
-
|
|
184
|
+
const conn = { ws, gatewayId, fronted: new Set };
|
|
185
|
+
sockets.add(conn);
|
|
138
186
|
log(`[connector] gateway connected: ${gatewayId} (${sockets.size} total)`);
|
|
139
187
|
ws.on("message", async (data) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
const text = data.toString();
|
|
189
|
+
for (const line of text.split(`
|
|
190
|
+
`)) {
|
|
191
|
+
if (!line.trim())
|
|
192
|
+
continue;
|
|
193
|
+
let frame;
|
|
194
|
+
try {
|
|
195
|
+
frame = JSON.parse(line);
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
await handleFrame(conn, frame).catch((e) => log(`[connector] frame error: ${e}`));
|
|
145
200
|
}
|
|
146
|
-
await handleFrame(ws, frame).catch((e) => log(`[connector] frame error: ${e}`));
|
|
147
201
|
});
|
|
148
202
|
ws.on("close", () => {
|
|
149
|
-
sockets.delete(
|
|
203
|
+
sockets.delete(conn);
|
|
150
204
|
log(`[connector] gateway disconnected: ${gatewayId} (${sockets.size} left)`);
|
|
151
205
|
});
|
|
152
|
-
ws.on("error", () => sockets.delete(
|
|
206
|
+
ws.on("error", () => sockets.delete(conn));
|
|
153
207
|
}
|
|
154
|
-
async function handleFrame(
|
|
208
|
+
async function handleFrame(conn, frame) {
|
|
155
209
|
const t = frame?.type;
|
|
156
210
|
if (t === "hello") {
|
|
157
|
-
|
|
211
|
+
const botId = String(frame.botId ?? "");
|
|
212
|
+
const identity = single ? table.all()[0] : botId ? table.forBot(botId) : null;
|
|
213
|
+
if (!identity) {
|
|
214
|
+
send(conn.ws, { type: "error", error: `unknown identity botId: ${botId || "(none)"}` });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
conn.fronted.add(identity.botId);
|
|
218
|
+
send(conn.ws, { type: "descriptor", descriptor: { ...descriptor, platform: "agentschat" } });
|
|
158
219
|
return;
|
|
159
220
|
}
|
|
160
221
|
if (t === "outbound") {
|
|
161
|
-
const result = await handleOutbound(frame
|
|
162
|
-
send(ws, { type: "outbound_result", requestId: frame.requestId, result });
|
|
222
|
+
const result = await handleOutbound(conn, frame);
|
|
223
|
+
send(conn.ws, { type: "outbound_result", requestId: frame.requestId, result });
|
|
163
224
|
return;
|
|
164
225
|
}
|
|
165
226
|
}
|
|
166
|
-
async function handleOutbound(
|
|
227
|
+
async function handleOutbound(conn, frame) {
|
|
228
|
+
const action = frame?.action ?? {};
|
|
167
229
|
const op = action?.op;
|
|
168
230
|
const chatId = action?.chat_id ?? "";
|
|
231
|
+
const firstFronted = [...conn.fronted][0];
|
|
232
|
+
const requested = typeof frame?.botId === "string" && frame.botId ? frame.botId : firstFronted ?? null;
|
|
233
|
+
const identity = single ? table.all()[0] : requested && conn.fronted.has(requested) ? table.forBot(requested) : null;
|
|
234
|
+
if (!identity) {
|
|
235
|
+
log(`[connector] outbound failed: no fronted identity for botId=${requested ?? "?"}`);
|
|
236
|
+
return { success: false, error: `no fronted identity for outbound (botId=${requested ?? "?"})` };
|
|
237
|
+
}
|
|
169
238
|
switch (op) {
|
|
170
239
|
case "send": {
|
|
171
|
-
const r = await
|
|
240
|
+
const r = await hooks.sendMessage(identity.botId, chatId, action.content ?? "", action.reply_to);
|
|
172
241
|
return { success: true, message_id: r?.id };
|
|
173
242
|
}
|
|
174
243
|
case "typing": {
|
|
175
|
-
await
|
|
244
|
+
await hooks.sendTyping?.(identity.botId, chatId);
|
|
176
245
|
return { success: true };
|
|
177
246
|
}
|
|
178
247
|
case "get_chat_info": {
|
|
179
|
-
const info = await
|
|
248
|
+
const info = await hooks.getChatInfo(identity.botId, chatId);
|
|
180
249
|
return info ?? {};
|
|
181
250
|
}
|
|
182
251
|
default:
|
|
252
|
+
log(`[connector] outbound failed: unsupported op ${String(op)}`);
|
|
183
253
|
return { success: false, error: `unsupported op: ${String(op)}` };
|
|
184
254
|
}
|
|
185
255
|
}
|
|
@@ -194,20 +264,33 @@ function startConnector(config) {
|
|
|
194
264
|
return {
|
|
195
265
|
port,
|
|
196
266
|
stop() {
|
|
197
|
-
for (const
|
|
267
|
+
for (const c of sockets) {
|
|
198
268
|
try {
|
|
199
|
-
ws.close(1001, "connector shutdown");
|
|
269
|
+
c.ws.close(1001, "connector shutdown");
|
|
200
270
|
} catch {}
|
|
201
271
|
}
|
|
202
272
|
wss.close();
|
|
203
273
|
http.close();
|
|
204
274
|
},
|
|
205
275
|
injectAgentsChatMessage(msg) {
|
|
206
|
-
const
|
|
276
|
+
const bySocket = msg.__botId ? table.forBot(String(msg.__botId)) : null;
|
|
277
|
+
const target = bySocket ?? routeInbound(table, {
|
|
278
|
+
channel_id: msg.channel_id,
|
|
279
|
+
mentioned_ids: msg.mentioned_ids,
|
|
280
|
+
dmOwnerBotId: msg.dm_owner
|
|
281
|
+
}) ?? (table.isSingle() ? table.all()[0] : null);
|
|
282
|
+
if (!target) {
|
|
283
|
+
log(`[connector] inbound unaddressed (channel=${msg.channel_id ?? "?"} mentions=${JSON.stringify(msg.mentioned_ids ?? [])}) — dropped`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const event = toWireEvent(msg, "agentschat");
|
|
207
287
|
if (!event)
|
|
208
288
|
return;
|
|
209
|
-
|
|
210
|
-
|
|
289
|
+
event.source.profile = target.botId;
|
|
290
|
+
for (const conn of sockets) {
|
|
291
|
+
if (conn.fronted.has(target.botId))
|
|
292
|
+
send(conn.ws, { type: "inbound", event });
|
|
293
|
+
}
|
|
211
294
|
},
|
|
212
295
|
connections() {
|
|
213
296
|
return sockets.size;
|
|
@@ -227,7 +310,6 @@ function peekPayload(token) {
|
|
|
227
310
|
}
|
|
228
311
|
|
|
229
312
|
// connector/run.ts
|
|
230
|
-
import WS from "ws";
|
|
231
313
|
var log = (m) => process.stderr.write(`[agentschat-connector] ${m}
|
|
232
314
|
`);
|
|
233
315
|
function need(name) {
|
|
@@ -238,96 +320,139 @@ function need(name) {
|
|
|
238
320
|
}
|
|
239
321
|
return v;
|
|
240
322
|
}
|
|
241
|
-
var AGENT_ID = need("AGENTCHAT_AGENT_ID");
|
|
242
|
-
var TOKEN = need("AGENTCHAT_TOKEN");
|
|
243
|
-
var GATEWAY_ID = need("RELAY_GATEWAY_ID");
|
|
244
|
-
var GATEWAY_SECRET = need("RELAY_GATEWAY_SECRET");
|
|
245
323
|
var API = (process.env.AGENTCHAT_API_URL || "https://agents-chat.com").replace(/\/$/, "");
|
|
246
324
|
var WS_URL = process.env.AGENTCHAT_WS_URL || API.replace(/^http/, "ws") + "/ws";
|
|
247
325
|
var PORT = Number(process.env.RELAY_PORT || 8765);
|
|
248
326
|
var HOST = process.env.RELAY_HOST || "127.0.0.1";
|
|
327
|
+
function resolveIdentities() {
|
|
328
|
+
const raw = (process.env.RELAY_IDENTITIES || "").trim();
|
|
329
|
+
if (raw) {
|
|
330
|
+
let parsed;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(raw);
|
|
333
|
+
} catch (e) {
|
|
334
|
+
log(`ERROR: RELAY_IDENTITIES is not valid JSON: ${e}`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
338
|
+
log(`ERROR: RELAY_IDENTITIES must be a non-empty JSON array`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
for (const it of parsed) {
|
|
342
|
+
if (!it?.botId || !it?.token || !it?.gatewayId || !it?.secret) {
|
|
343
|
+
log(`ERROR: each RELAY_IDENTITIES entry needs botId, token, gatewayId, secret — got: ${JSON.stringify(it).slice(0, 80)}`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return parsed.map((it) => ({
|
|
348
|
+
botId: String(it.botId),
|
|
349
|
+
agentId: String(it.agentId ?? it.botId),
|
|
350
|
+
token: String(it.token),
|
|
351
|
+
gatewayId: String(it.gatewayId),
|
|
352
|
+
secret: String(it.secret)
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
const agentId = need("AGENTCHAT_AGENT_ID");
|
|
356
|
+
const token = need("AGENTCHAT_TOKEN");
|
|
357
|
+
const gatewayId = need("RELAY_GATEWAY_ID");
|
|
358
|
+
const secret = need("RELAY_GATEWAY_SECRET");
|
|
359
|
+
return [{ botId: agentId, agentId, token, gatewayId, secret }];
|
|
360
|
+
}
|
|
361
|
+
var identities = resolveIdentities();
|
|
362
|
+
var single = identities.length === 1;
|
|
363
|
+
var secrets = {};
|
|
364
|
+
for (const id of identities) {
|
|
365
|
+
(secrets[id.gatewayId] ??= []).push(id.secret);
|
|
366
|
+
}
|
|
249
367
|
var broadcast = null;
|
|
368
|
+
var socketsByBot = new Map;
|
|
369
|
+
var backoffByBot = new Map;
|
|
370
|
+
function connectIdentity(id) {
|
|
371
|
+
const ws = new WS(WS_URL);
|
|
372
|
+
socketsByBot.set(id.botId, ws);
|
|
373
|
+
ws.on("open", () => {
|
|
374
|
+
backoffByBot.set(id.botId, 1000);
|
|
375
|
+
try {
|
|
376
|
+
ws.send(JSON.stringify({ type: "auth", agent_id: id.agentId, token: id.token, capabilities: ["chat"] }));
|
|
377
|
+
} catch {}
|
|
378
|
+
});
|
|
379
|
+
ws.on("message", (raw) => {
|
|
380
|
+
let data;
|
|
381
|
+
try {
|
|
382
|
+
data = JSON.parse(String(raw));
|
|
383
|
+
} catch {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (data.type === "auth_ok") {
|
|
387
|
+
log(`connected to agentschat as ${id.agentId}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (data.type === "message" && data.sender_id !== id.agentId) {
|
|
391
|
+
broadcast?.({ ...data, __botId: id.botId });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
ws.on("close", () => {
|
|
395
|
+
if (process.__shutdown)
|
|
396
|
+
return;
|
|
397
|
+
const delay = backoffByBot.get(id.botId) ?? 1000;
|
|
398
|
+
log(`agentschat WS closed for ${id.botId}; reconnecting in ${delay}ms`);
|
|
399
|
+
setTimeout(() => connectIdentity(id), delay);
|
|
400
|
+
backoffByBot.set(id.botId, Math.min(delay * 2, 30000));
|
|
401
|
+
});
|
|
402
|
+
ws.on("error", (e) => log(`agentschat WS error (${id.botId}): ${e?.message ?? e}`));
|
|
403
|
+
}
|
|
250
404
|
var connector = startConnector({
|
|
251
405
|
port: PORT,
|
|
252
406
|
host: HOST,
|
|
253
|
-
secrets
|
|
407
|
+
secrets,
|
|
408
|
+
identities: single ? undefined : identities,
|
|
254
409
|
agentschat: {
|
|
255
|
-
async sendMessage(chatId, content, replyTo) {
|
|
410
|
+
async sendMessage(botId, chatId, content, replyTo) {
|
|
411
|
+
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
256
412
|
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages`, {
|
|
257
413
|
method: "POST",
|
|
258
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${
|
|
259
|
-
body: JSON.stringify({ sender_id:
|
|
414
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${id.token}` },
|
|
415
|
+
body: JSON.stringify({ sender_id: id.agentId, content_type: "text", content, ...replyTo ? { parent_id: replyTo } : {} })
|
|
260
416
|
});
|
|
261
417
|
if (!res.ok)
|
|
262
418
|
throw new Error(`agentschat send failed: ${res.status}`);
|
|
263
419
|
const data = await res.json();
|
|
264
420
|
return { id: data?.id };
|
|
265
421
|
},
|
|
266
|
-
async getChatInfo(chatId) {
|
|
422
|
+
async getChatInfo(botId, chatId) {
|
|
423
|
+
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
267
424
|
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
|
|
268
|
-
headers: { Authorization: `Bearer ${
|
|
425
|
+
headers: { Authorization: `Bearer ${id.token}` }
|
|
269
426
|
});
|
|
270
427
|
if (!res.ok)
|
|
271
428
|
return { name: chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
|
|
272
429
|
const data = await res.json();
|
|
273
430
|
return { name: data?.name ?? chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
|
|
274
431
|
},
|
|
275
|
-
async sendTyping(chatId) {
|
|
276
|
-
|
|
432
|
+
async sendTyping(botId, chatId) {
|
|
433
|
+
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
434
|
+
const ws = socketsByBot.get(id.botId);
|
|
435
|
+
if (ws && ws.readyState === WS.OPEN) {
|
|
436
|
+
try {
|
|
437
|
+
ws.send(JSON.stringify({ type: "typing", channel_id: chatId, sender_id: id.agentId }));
|
|
438
|
+
} catch {}
|
|
439
|
+
}
|
|
277
440
|
}
|
|
278
441
|
},
|
|
279
442
|
logger: log
|
|
280
443
|
});
|
|
281
444
|
broadcast = (msg) => connector.injectAgentsChatMessage(msg);
|
|
282
|
-
log(`listening on ${HOST}:${connector.port} (contract v1,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
function sendAgentsChatFrame(frame) {
|
|
286
|
-
if (ws && ws.readyState === WS.OPEN) {
|
|
287
|
-
try {
|
|
288
|
-
ws.send(JSON.stringify(frame));
|
|
289
|
-
} catch {}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
function connectAgentsChat() {
|
|
293
|
-
ws = new WS(WS_URL);
|
|
294
|
-
ws.on("open", () => {
|
|
295
|
-
reconnectDelay = 1000;
|
|
296
|
-
sendAgentsChatFrame({ type: "auth", agent_id: AGENT_ID, token: TOKEN, capabilities: ["chat"] });
|
|
297
|
-
});
|
|
298
|
-
ws.on("message", (raw) => {
|
|
299
|
-
let data;
|
|
300
|
-
try {
|
|
301
|
-
data = JSON.parse(String(raw));
|
|
302
|
-
} catch {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
if (data.type === "auth_ok") {
|
|
306
|
-
log(`connected to agentschat as ${AGENT_ID}`);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (data.type === "message" && data.sender_id !== AGENT_ID) {
|
|
310
|
-
broadcast?.(data);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
ws.on("close", () => {
|
|
314
|
-
if (process.__shutdown)
|
|
315
|
-
return;
|
|
316
|
-
log(`agentschat WS closed; reconnecting in ${reconnectDelay}ms`);
|
|
317
|
-
setTimeout(connectAgentsChat, reconnectDelay);
|
|
318
|
-
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
319
|
-
});
|
|
320
|
-
ws.on("error", (e) => {
|
|
321
|
-
log(`agentschat WS error: ${e?.message ?? e}`);
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
connectAgentsChat();
|
|
445
|
+
log(`listening on ${HOST}:${connector.port} (contract v1, ${identities.length} identit${identities.length === 1 ? "y" : "ies"})`);
|
|
446
|
+
for (const id of identities)
|
|
447
|
+
connectIdentity(id);
|
|
325
448
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
326
449
|
process.on(sig, () => {
|
|
327
450
|
process.__shutdown = true;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
451
|
+
for (const ws of socketsByBot.values()) {
|
|
452
|
+
try {
|
|
453
|
+
ws.close();
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
331
456
|
connector.stop();
|
|
332
457
|
process.exit(0);
|
|
333
458
|
});
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
5
|
|
|
4
6
|
// src/server.ts
|
|
5
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -193,11 +195,160 @@ function decideTermsConsent(i) {
|
|
|
193
195
|
` + ` Refusing to send acceptance you did not give — no account was created.`
|
|
194
196
|
};
|
|
195
197
|
}
|
|
198
|
+
|
|
199
|
+
// src/wake.ts
|
|
200
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
201
|
+
|
|
202
|
+
// src/redact.ts
|
|
203
|
+
function redactSecrets2(text) {
|
|
204
|
+
return text.replace(/ac_[A-Za-z0-9_-]{16,}/g, "ac_***REDACTED***").replace(/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "***JWT_REDACTED***");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/wake.ts
|
|
208
|
+
var WAKE_SIG_HEADER = "x-agentschat-signature";
|
|
209
|
+
var WAKE_CONTENT_MAX = 500;
|
|
210
|
+
function buildWakePayload(msg) {
|
|
211
|
+
const content = typeof msg.content === "string" ? redactSecrets2(msg.content.slice(0, WAKE_CONTENT_MAX)) : undefined;
|
|
212
|
+
return {
|
|
213
|
+
type: typeof msg.type === "string" ? msg.type : "message",
|
|
214
|
+
channel_id: msg.channel_id,
|
|
215
|
+
message_id: msg.id,
|
|
216
|
+
sender_id: msg.sender_id,
|
|
217
|
+
content,
|
|
218
|
+
mentioned_ids: Array.isArray(msg.mentioned_ids) ? msg.mentioned_ids.filter((x) => typeof x === "string") : undefined,
|
|
219
|
+
timestamp: typeof msg.timestamp === "string" ? msg.timestamp : undefined
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function signWakeBody(body, secret) {
|
|
223
|
+
return createHmac("sha256", secret).update(body, "utf8").digest("hex");
|
|
224
|
+
}
|
|
225
|
+
async function fireWake(msg, cfg) {
|
|
226
|
+
const log = cfg.logger ?? (() => {});
|
|
227
|
+
const url = (cfg.url ?? "").trim();
|
|
228
|
+
if (!url)
|
|
229
|
+
return;
|
|
230
|
+
const payload = buildWakePayload(msg);
|
|
231
|
+
const body = JSON.stringify(payload);
|
|
232
|
+
const headers = { "Content-Type": "application/json" };
|
|
233
|
+
if (cfg.secret)
|
|
234
|
+
headers[WAKE_SIG_HEADER] = signWakeBody(body, cfg.secret);
|
|
235
|
+
const doPost = async () => {
|
|
236
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
237
|
+
const ctrl = new AbortController;
|
|
238
|
+
const t = setTimeout(() => ctrl.abort(), cfg.timeoutMs ?? 5000);
|
|
239
|
+
try {
|
|
240
|
+
const res = await f(url, { method: "POST", headers, body, signal: ctrl.signal });
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
log(`[agentchat] wake POST ${url} → HTTP ${res.status}`);
|
|
243
|
+
} finally {
|
|
244
|
+
clearTimeout(t);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
try {
|
|
248
|
+
await doPost();
|
|
249
|
+
} catch (e) {
|
|
250
|
+
try {
|
|
251
|
+
await doPost();
|
|
252
|
+
} catch (e2) {
|
|
253
|
+
log(`[agentchat] wake POST ${url} failed: ${e2}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function buildGrokPrompt(msg, agentId) {
|
|
258
|
+
const content = typeof msg.content === "string" ? redactSecrets2(msg.content.slice(0, WAKE_CONTENT_MAX)) : "";
|
|
259
|
+
const channel = msg.channel_id ?? "?";
|
|
260
|
+
const sender = msg.sender_id ?? "someone";
|
|
261
|
+
const isDm = channel.startsWith("dm-");
|
|
262
|
+
const where = isDm ? "私聊 (DM)" : `频道 ${channel}`;
|
|
263
|
+
const prompt = `[AgentsChat] ${sender} 在${where}提到了你` + (content ? `:"${content}"` : "。") + (msg.message_id ? ` (channel_id=${channel}, message_id=${msg.message_id}——用 get_history 拉上下文、reply 回复)` : "");
|
|
264
|
+
return { agentId, prompt };
|
|
265
|
+
}
|
|
266
|
+
function grokBearerFromGatewayConfig(cfg) {
|
|
267
|
+
if (!cfg || typeof cfg !== "object")
|
|
268
|
+
return null;
|
|
269
|
+
if (typeof cfg.token === "string" && cfg.token)
|
|
270
|
+
return cfg.token;
|
|
271
|
+
if (cfg.auth && typeof cfg.auth.bearer === "string" && cfg.auth.bearer)
|
|
272
|
+
return cfg.auth.bearer;
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
function grokPortFromGatewayConfig(cfg, fallback = 1340) {
|
|
276
|
+
const p = cfg?.port;
|
|
277
|
+
return typeof p === "number" && p > 0 ? p : fallback;
|
|
278
|
+
}
|
|
279
|
+
async function resolveGrokAgentId(opts) {
|
|
280
|
+
const log = opts.logger ?? (() => {});
|
|
281
|
+
if (opts.explicitId && opts.explicitId.trim())
|
|
282
|
+
return opts.explicitId.trim();
|
|
283
|
+
if (opts.listAgents && opts.agentschatName) {
|
|
284
|
+
try {
|
|
285
|
+
const agents = await opts.listAgents();
|
|
286
|
+
const hit = agents.find((a) => a.name && a.name === opts.agentschatName);
|
|
287
|
+
if (hit?.id) {
|
|
288
|
+
log(`[agentchat] grok wake: no AGENTCHAT_GROK_AGENT_ID; matched "${opts.agentschatName}" → ${hit.id} via listAgents (set the env to bind explicitly)`);
|
|
289
|
+
return hit.id;
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
log(`[agentchat] grok wake: listAgents fallback failed: ${e}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
log(`[agentchat] grok wake: no Grok agentId bound. Set AGENTCHAT_GROK_AGENT_ID=<gateway agent uuid> (1:1 mapping).`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
async function fireGrokWake(msg, cfg) {
|
|
299
|
+
const log = cfg.logger ?? (() => {});
|
|
300
|
+
if (!cfg.agentId) {
|
|
301
|
+
log(`[agentchat] grok wake: no agentId configured, skipping`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
let gwcfg;
|
|
305
|
+
try {
|
|
306
|
+
const { readFileSync } = await import("node:fs");
|
|
307
|
+
gwcfg = JSON.parse(readFileSync(cfg.gatewayConfigPath, "utf8"));
|
|
308
|
+
} catch (e) {
|
|
309
|
+
log(`[agentchat] grok wake: cannot read ${cfg.gatewayConfigPath}: ${e}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const token = grokBearerFromGatewayConfig(gwcfg);
|
|
313
|
+
if (!token) {
|
|
314
|
+
log(`[agentchat] grok wake: no bearer token in ${cfg.gatewayConfigPath}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const port = grokPortFromGatewayConfig(gwcfg);
|
|
318
|
+
const url = `http://127.0.0.1:${port}/api/sendPrompt`;
|
|
319
|
+
const body = JSON.stringify(buildGrokPrompt(msg, cfg.agentId));
|
|
320
|
+
const doPost = async () => {
|
|
321
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
322
|
+
const ctrl = new AbortController;
|
|
323
|
+
const t = setTimeout(() => ctrl.abort(), cfg.timeoutMs ?? 5000);
|
|
324
|
+
try {
|
|
325
|
+
const res = await f(url, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
328
|
+
body,
|
|
329
|
+
signal: ctrl.signal
|
|
330
|
+
});
|
|
331
|
+
if (!res.ok)
|
|
332
|
+
log(`[agentchat] grok wake POST ${url} → HTTP ${res.status}`);
|
|
333
|
+
} finally {
|
|
334
|
+
clearTimeout(t);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
try {
|
|
338
|
+
await doPost();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
try {
|
|
341
|
+
await doPost();
|
|
342
|
+
} catch (e2) {
|
|
343
|
+
log(`[agentchat] grok wake POST ${url} failed: ${e2}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
196
347
|
// package.json
|
|
197
348
|
var package_default = {
|
|
198
349
|
name: "agentschat-mcp",
|
|
199
350
|
mcpName: "io.github.swswordholy-tech/agentschat-mcp",
|
|
200
|
-
version: "0.
|
|
351
|
+
version: "0.32.0",
|
|
201
352
|
description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
202
353
|
type: "module",
|
|
203
354
|
bin: {
|
|
@@ -264,11 +415,13 @@ var package_default = {
|
|
|
264
415
|
"src/argcheck.ts",
|
|
265
416
|
"src/identity.ts",
|
|
266
417
|
"src/terms.ts",
|
|
418
|
+
"src/wake.ts",
|
|
267
419
|
"src/profile-store.ts",
|
|
268
420
|
"src/read-cursor.ts",
|
|
269
421
|
"connector/auth.ts",
|
|
270
422
|
"connector/descriptor.ts",
|
|
271
423
|
"connector/normalize.ts",
|
|
424
|
+
"connector/identities.ts",
|
|
272
425
|
"connector/server.ts",
|
|
273
426
|
"connector/run.ts",
|
|
274
427
|
"connector/README.md",
|
|
@@ -476,6 +629,17 @@ Options:
|
|
|
476
629
|
--caps <a,b,c> Capabilities (comma-separated)
|
|
477
630
|
-h, --help Show this help
|
|
478
631
|
|
|
632
|
+
Wake a host that has no channel-notification surface (Grok Bot, generic MCP clients):
|
|
633
|
+
AGENTCHAT_WAKE_URL + AGENTCHAT_WAKE_SECRET
|
|
634
|
+
POST @mentions/DMs to that URL, HMAC-signed (x-agentschat-signature)
|
|
635
|
+
AGENTCHAT_WAKE_MODE=grok
|
|
636
|
+
Same-machine Grok gateway: loopback POST to its /api/sendPrompt
|
|
637
|
+
with the Bearer token read from a local gateway.json.
|
|
638
|
+
AGENTCHAT_GROK_GATEWAY path to gateway.json (default ~/.grok/gateway.json)
|
|
639
|
+
AGENTCHAT_GROK_AGENT_ID the Grok gateway agent uuid to wake (1:1 binding)
|
|
640
|
+
|
|
641
|
+
Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
|
|
642
|
+
|
|
479
643
|
Identity is never created implicitly: with no --name/--profile/AGENTSCHAT_PROFILE and
|
|
480
644
|
no token, the server runs ANONYMOUS (lists tools, but never registers an account).
|
|
481
645
|
|
|
@@ -4061,6 +4225,44 @@ ${context}
|
|
|
4061
4225
|
process.stderr.write(`[agentchat] Notification FAILED: ${notifErr}
|
|
4062
4226
|
`);
|
|
4063
4227
|
}
|
|
4228
|
+
if (process.env.AGENTCHAT_WAKE_MODE === "grok") {
|
|
4229
|
+
(async () => {
|
|
4230
|
+
try {
|
|
4231
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
4232
|
+
const gwPath = process.env.AGENTCHAT_GROK_GATEWAY || `${process.env.HOME}/.grok/gateway.json`;
|
|
4233
|
+
let agentId = process.env.AGENTCHAT_GROK_AGENT_ID || "";
|
|
4234
|
+
if (!agentId) {
|
|
4235
|
+
agentId = await resolveGrokAgentId({
|
|
4236
|
+
explicitId: "",
|
|
4237
|
+
agentschatName: profile.display_name || AGENT_ID,
|
|
4238
|
+
listAgents: async () => {
|
|
4239
|
+
const gwcfg = JSON.parse(readFileSync3(gwPath, "utf8"));
|
|
4240
|
+
const token = grokBearerFromGatewayConfig(gwcfg);
|
|
4241
|
+
const port = grokPortFromGatewayConfig(gwcfg);
|
|
4242
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/listAgents`, {
|
|
4243
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
4244
|
+
});
|
|
4245
|
+
if (!res.ok)
|
|
4246
|
+
throw new Error(`listAgents HTTP ${res.status}`);
|
|
4247
|
+
const d = await res.json();
|
|
4248
|
+
return Array.isArray(d) ? d : d?.agents ?? [];
|
|
4249
|
+
}
|
|
4250
|
+
}) ?? "";
|
|
4251
|
+
}
|
|
4252
|
+
if (agentId) {
|
|
4253
|
+
fireGrokWake(data, { gatewayConfigPath: gwPath, agentId });
|
|
4254
|
+
}
|
|
4255
|
+
} catch (e) {
|
|
4256
|
+
process.stderr.write(`[agentchat] grok wake resolve failed: ${e}
|
|
4257
|
+
`);
|
|
4258
|
+
}
|
|
4259
|
+
})();
|
|
4260
|
+
} else {
|
|
4261
|
+
fireWake(data, {
|
|
4262
|
+
url: process.env.AGENTCHAT_WAKE_URL,
|
|
4263
|
+
secret: process.env.AGENTCHAT_WAKE_SECRET
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4064
4266
|
if (activeHi)
|
|
4065
4267
|
clearFinishedHiddenIdentityGamesFromMessage(data);
|
|
4066
4268
|
} else {
|
|
@@ -4213,7 +4415,7 @@ async function checkVersionStaleness() {
|
|
|
4213
4415
|
return;
|
|
4214
4416
|
const latest = (await r.json())?.version;
|
|
4215
4417
|
if (typeof latest === "string" && latest !== package_default.version) {
|
|
4216
|
-
process.stderr.write(`[agentchat] Update available: running agentschat-mcp ${package_default.version}, latest published is ${latest}.
|
|
4418
|
+
process.stderr.write(`[agentchat] Update available: running agentschat-mcp ${package_default.version}, latest published is ${latest}. New tools/capabilities load only on a SESSION RESTART (hot-reload isn't possible); update (bunx agentschat-mcp@latest / reinstall) then restart this session to pick them up.
|
|
4217
4419
|
`);
|
|
4218
4420
|
}
|
|
4219
4421
|
} catch {} finally {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.32.0",
|
|
5
5
|
"description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -68,11 +68,13 @@
|
|
|
68
68
|
"src/argcheck.ts",
|
|
69
69
|
"src/identity.ts",
|
|
70
70
|
"src/terms.ts",
|
|
71
|
+
"src/wake.ts",
|
|
71
72
|
"src/profile-store.ts",
|
|
72
73
|
"src/read-cursor.ts",
|
|
73
74
|
"connector/auth.ts",
|
|
74
75
|
"connector/descriptor.ts",
|
|
75
76
|
"connector/normalize.ts",
|
|
77
|
+
"connector/identities.ts",
|
|
76
78
|
"connector/server.ts",
|
|
77
79
|
"connector/run.ts",
|
|
78
80
|
"connector/README.md",
|