agentschat-mcp 0.32.3 → 0.33.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/connector/README.md +9 -1
- package/connector/identities.ts +21 -1
- package/connector/normalize.ts +7 -0
- package/connector/run.ts +26 -1
- package/connector/server.ts +71 -14
- package/dist/connector.js +76 -9
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/connector/README.md
CHANGED
|
@@ -50,6 +50,14 @@ A's messages never reach or send as identity B (an un-hello'd identity egress is
|
|
|
50
50
|
rejected per the contract's advertised-set check, D-Q1.5b.1; unaddressed inbound
|
|
51
51
|
is dropped, never broadcast). Single-tenant env is the N=1 case, unchanged.
|
|
52
52
|
|
|
53
|
+
**Inbound gating (all modes):** the AgentsChat WS pushes every message of every
|
|
54
|
+
joined channel. The connector injects into the gateway only what is ADDRESSED to
|
|
55
|
+
an identity — DMs always, group messages only when the body @mentions it (same
|
|
56
|
+
gate the MCP path uses: `isDM || isMentioned`). On an @-mention it also attaches
|
|
57
|
+
the channel history since that identity was last addressed as the wire `context`
|
|
58
|
+
field, which upstream renders into the event's channel context — the agent sees
|
|
59
|
+
the conversation between its mentions without paying tokens for all of it.
|
|
60
|
+
|
|
53
61
|
Point Hermes at it by setting `GATEWAY_RELAY_URL=ws://<host>:8765/relay` (the gateway
|
|
54
62
|
then upgrades with `Authorization: Bearer <HMAC token>` derived from the shared
|
|
55
63
|
secret — see `gateway/relay/auth.py`).
|
|
@@ -60,7 +68,7 @@ secret — see `gateway/relay/auth.py`).
|
|
|
60
68
|
|---|---|---|
|
|
61
69
|
| WS upgrade auth (HMAC-SHA256, close 4401) | gateway → connector | ✅ |
|
|
62
70
|
| `hello` → `descriptor` handshake (one per identity in multiplex) | gateway ↔ connector | ✅ |
|
|
63
|
-
| `inbound` (
|
|
71
|
+
| `inbound` — DM always; group only on content @mention; @-mentions carry a `context` window (history since last addressed) → `MessageEvent` w/ `source.profile` | connector → gateway | ✅ |
|
|
64
72
|
| `outbound` op `send` → `outbound_result` (per-identity token, advertised-set checked) | gateway → connector | ✅ |
|
|
65
73
|
| `outbound` op `typing` | gateway → connector | ✅ |
|
|
66
74
|
| `outbound` op `get_chat_info` | gateway → connector | ✅ |
|
package/connector/identities.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* does not change single-tenant behavior.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { matchesMention } from "../src/mentions.ts";
|
|
18
|
+
|
|
17
19
|
export interface Identity {
|
|
18
20
|
/** The relay hello botId — the agentschat agent_id this identity fronts. */
|
|
19
21
|
botId: string;
|
|
@@ -60,6 +62,8 @@ export class IdentityTable {
|
|
|
60
62
|
export interface InboundContext {
|
|
61
63
|
channel_id?: string;
|
|
62
64
|
mentioned_ids?: string[];
|
|
65
|
+
/** Message body — the PRIMARY group-mention signal (see routeInbound). */
|
|
66
|
+
content?: string;
|
|
63
67
|
/** For DM channels: which identity owns this DM (the connector tracks dm ownership). */
|
|
64
68
|
dmOwnerBotId?: string;
|
|
65
69
|
}
|
|
@@ -72,18 +76,34 @@ export interface InboundContext {
|
|
|
72
76
|
* Routing rule: a DM goes to its owning identity; a group/channel message goes to
|
|
73
77
|
* the identity it @mentions. A message mentioning no fronted identity (or in a DM
|
|
74
78
|
* owned by none) routes to no one.
|
|
79
|
+
*
|
|
80
|
+
* Group mention detection is CONTENT-based (matchesMention over the message body):
|
|
81
|
+
* the agentschat WS pushes every message of a joined channel WITHOUT a mentioned_ids
|
|
82
|
+
* annotation, so the mention gate the MCP path uses (isDM || isMentioned) must be
|
|
83
|
+
* reproduced here — otherwise every joined-channel message would be injected into
|
|
84
|
+
* the agent's session and burn its tokens on chatter not addressed to it.
|
|
85
|
+
* `mentioned_ids` is still honored when a host provides it (explicit signal wins).
|
|
75
86
|
*/
|
|
76
87
|
export function routeInbound(table: IdentityTable, ctx: InboundContext): Identity | null {
|
|
77
88
|
// DM: route to the identity that owns the DM channel.
|
|
78
89
|
if (ctx.channel_id?.startsWith("dm-")) {
|
|
79
90
|
return ctx.dmOwnerBotId ? table.forBot(ctx.dmOwnerBotId) : null;
|
|
80
91
|
}
|
|
81
|
-
// Group/channel:
|
|
92
|
+
// Group/channel: an explicit mentioned_ids annotation wins when present.
|
|
82
93
|
const mentioned = Array.isArray(ctx.mentioned_ids) ? ctx.mentioned_ids : [];
|
|
83
94
|
for (const mid of mentioned) {
|
|
84
95
|
const id = table.forBot(mid);
|
|
85
96
|
if (id) return id;
|
|
86
97
|
}
|
|
98
|
+
// Otherwise content-based: which fronted identity does the body @mention?
|
|
99
|
+
// (First match wins — a message @-ing two fronted identities goes to the first;
|
|
100
|
+
// the other sees it when ITS mention arrives or via channel context.)
|
|
101
|
+
const content = ctx.content ?? "";
|
|
102
|
+
if (content) {
|
|
103
|
+
for (const id of table.all()) {
|
|
104
|
+
if (matchesMention(content, id.agentId) || matchesMention(content, id.botId)) return id;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
87
107
|
return null;
|
|
88
108
|
}
|
|
89
109
|
|
package/connector/normalize.ts
CHANGED
|
@@ -33,6 +33,13 @@ export interface WireEvent {
|
|
|
33
33
|
message_type: string;
|
|
34
34
|
message_id?: string;
|
|
35
35
|
reply_to_message_id?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Surrounding channel context (the "since you were last addressed" window the
|
|
38
|
+
* connector attaches on @-mentions). Upstream `_render_relay_context` renders
|
|
39
|
+
* it into the event's channel_context: oldest→newest `{text, source:{user_name
|
|
40
|
+
* |user_id}}` items. Absent on DMs and when there's nothing worth attaching.
|
|
41
|
+
*/
|
|
42
|
+
context?: Array<{ text: string; source?: { user_name?: string; user_id?: string } }>;
|
|
36
43
|
source: {
|
|
37
44
|
platform: string;
|
|
38
45
|
chat_id: string;
|
package/connector/run.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import WS from "ws";
|
|
25
25
|
import { startConnector } from "./server.ts";
|
|
26
26
|
import type { Identity } from "./identities.ts";
|
|
27
|
+
import { redactSecrets } from "../src/redact.ts";
|
|
27
28
|
|
|
28
29
|
const log = (m: string) => process.stderr.write(`[agentschat-connector] ${m}\n`);
|
|
29
30
|
|
|
@@ -130,7 +131,9 @@ const connector = startConnector({
|
|
|
130
131
|
port: PORT,
|
|
131
132
|
host: HOST,
|
|
132
133
|
secrets,
|
|
133
|
-
|
|
134
|
+
// Always pass the real identity table (even N=1) so botId = the real agentschat
|
|
135
|
+
// agent_id and content-based @mention routing works in single-tenant too.
|
|
136
|
+
identities,
|
|
134
137
|
agentschat: {
|
|
135
138
|
async sendMessage(botId, chatId, content, replyTo) {
|
|
136
139
|
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
@@ -159,6 +162,28 @@ const connector = startConnector({
|
|
|
159
162
|
try { ws.send(JSON.stringify({ type: "typing", channel_id: chatId, sender_id: id.agentId })); } catch {}
|
|
160
163
|
}
|
|
161
164
|
},
|
|
165
|
+
// The "since you were last @'d" window: recent channel history after sinceTs,
|
|
166
|
+
// oldest→newest, trigger message excluded, secrets redacted (a group channel is
|
|
167
|
+
// untrusted content — never forward a leaked key downstream). Best-effort: any
|
|
168
|
+
// failure returns null and the addressed message still delivers without it.
|
|
169
|
+
async getChannelContext(botId, chatId, sinceTs, excludeId) {
|
|
170
|
+
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
171
|
+
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages?limit=50`, {
|
|
172
|
+
headers: { Authorization: `Bearer ${id.token}` },
|
|
173
|
+
});
|
|
174
|
+
if (!res.ok) return null;
|
|
175
|
+
const msgs = (((await res.json()) as any)?.messages ?? [])
|
|
176
|
+
.filter((m: any) => m?.content && m.content !== "__typing__" && m?.id !== excludeId)
|
|
177
|
+
.sort((a: any, b: any) => String(a.timestamp ?? "").localeCompare(String(b.timestamp ?? "")));
|
|
178
|
+
const windowed = sinceTs ? msgs.filter((m: any) => String(m.timestamp ?? "") > sinceTs) : msgs;
|
|
179
|
+
const tail = windowed.slice(-10);
|
|
180
|
+
if (!tail.length) return null;
|
|
181
|
+
return tail.map((m: any) => ({
|
|
182
|
+
text: redactSecrets(String(m.content)).slice(0, 500),
|
|
183
|
+
user_name: m.sender_name ?? m.sender_id,
|
|
184
|
+
user_id: m.sender_id,
|
|
185
|
+
}));
|
|
186
|
+
},
|
|
162
187
|
},
|
|
163
188
|
logger: log,
|
|
164
189
|
});
|
package/connector/server.ts
CHANGED
|
@@ -34,6 +34,16 @@ export interface AgentsChatHooks {
|
|
|
34
34
|
sendMessage(botId: string, chatId: string, content: string, replyTo?: string): Promise<{ id?: string }>;
|
|
35
35
|
getChatInfo(botId: string, chatId: string): Promise<{ name?: string; type?: string }>;
|
|
36
36
|
sendTyping?(botId: string, chatId: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Recent channel messages to attach as `context` when an @-mention arrives —
|
|
39
|
+
* the "what happened since the last time I was addressed" window. `sinceTs` is
|
|
40
|
+
* the timestamp of the last message addressed to this identity in this channel
|
|
41
|
+
* (undefined = never). `excludeId` is the trigger message's own id (already
|
|
42
|
+
* delivered as the event body — don't repeat it in the context). Return null/
|
|
43
|
+
* [] when there is nothing worth attaching. Best-effort: failures must not
|
|
44
|
+
* block delivery of the addressed message itself.
|
|
45
|
+
*/
|
|
46
|
+
getChannelContext?(botId: string, chatId: string, sinceTs?: string, excludeId?: string): Promise<Array<{ text: string; user_name?: string; user_id?: string }> | null>;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/** Legacy single-tenant hook shape (no botId first arg) — adapted to the per-identity one. */
|
|
@@ -41,6 +51,7 @@ interface LegacyHooks {
|
|
|
41
51
|
sendMessage(chatId: string, content: string, replyTo?: string): Promise<{ id?: string }>;
|
|
42
52
|
getChatInfo(chatId: string): Promise<{ name?: string; type?: string }>;
|
|
43
53
|
sendTyping?(chatId: string): Promise<void>;
|
|
54
|
+
getChannelContext?(chatId: string, sinceTs?: string, excludeId?: string): Promise<Array<{ text: string; user_name?: string; user_id?: string }> | null>;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export interface ConnectorConfig {
|
|
@@ -64,7 +75,7 @@ export interface ConnectorHandle {
|
|
|
64
75
|
port: number;
|
|
65
76
|
stop(): void;
|
|
66
77
|
/** Push an agentschat message to the gateway socket(s) fronting its addressed identity. */
|
|
67
|
-
injectAgentsChatMessage(msg: AgentsChatMessage): void
|
|
78
|
+
injectAgentsChatMessage(msg: AgentsChatMessage): void | Promise<void>;
|
|
68
79
|
connections(): number;
|
|
69
80
|
}
|
|
70
81
|
|
|
@@ -79,20 +90,31 @@ interface GatewayConn {
|
|
|
79
90
|
export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
80
91
|
const log = config.logger ?? (() => {});
|
|
81
92
|
const descriptor = buildDescriptor(config.descriptor);
|
|
82
|
-
|
|
93
|
+
// "legacy" = no identity table configured (the pre-multiplex test/embedding
|
|
94
|
+
// path): a single derived identity and chatId-first hooks. "single" = the
|
|
95
|
+
// table holds exactly one identity (legacy OR a one-entry RELAY_IDENTITIES) —
|
|
96
|
+
// it gates hello acceptance and outbound identity resolution, NOT inbound
|
|
97
|
+
// group forwarding (unaddressed group chatter is dropped in every mode).
|
|
98
|
+
const legacy = !config.identities || config.identities.length === 0;
|
|
83
99
|
const table = new IdentityTable(
|
|
84
|
-
|
|
100
|
+
legacy ? [{ botId: "default", agentId: "default", token: "", gatewayId: "", secret: "" }] : config.identities!,
|
|
85
101
|
);
|
|
102
|
+
const single = table.isSingle();
|
|
86
103
|
// Normalize hooks to the per-identity shape. Legacy single-tenant hooks take
|
|
87
104
|
// (chatId, ...); wrap them to ignore the botId. Per-identity hooks take botId first.
|
|
88
|
-
const hooks: AgentsChatHooks =
|
|
105
|
+
const hooks: AgentsChatHooks = legacy
|
|
89
106
|
? {
|
|
90
107
|
sendMessage: (_b, chatId, content, replyTo) => (config.agentschat as LegacyHooks).sendMessage(chatId, content, replyTo),
|
|
91
108
|
getChatInfo: (_b, chatId) => (config.agentschat as LegacyHooks).getChatInfo(chatId),
|
|
92
109
|
sendTyping: (_b, chatId) => (config.agentschat as LegacyHooks).sendTyping?.(chatId) ?? Promise.resolve(),
|
|
110
|
+
getChannelContext: (_b, chatId, sinceTs, excludeId) =>
|
|
111
|
+
(config.agentschat as LegacyHooks).getChannelContext?.(chatId, sinceTs, excludeId) ?? Promise.resolve(null),
|
|
93
112
|
}
|
|
94
113
|
: (config.agentschat as AgentsChatHooks);
|
|
95
114
|
const sockets = new Set<GatewayConn>();
|
|
115
|
+
// `${botId}:${chatId}` → timestamp of the last message ADDRESSED to that
|
|
116
|
+
// identity in that channel. Drives the getChannelContext `sinceTs` window.
|
|
117
|
+
const lastAddressed = new Map<string, string>();
|
|
96
118
|
|
|
97
119
|
const http: HttpServer = createServer((_req, res) => {
|
|
98
120
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -238,18 +260,30 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
238
260
|
wss.close();
|
|
239
261
|
http.close();
|
|
240
262
|
},
|
|
241
|
-
injectAgentsChatMessage(msg: AgentsChatMessage) {
|
|
242
|
-
// Route to the identity this message is
|
|
263
|
+
async injectAgentsChatMessage(msg: AgentsChatMessage) {
|
|
264
|
+
// Route to the identity this message is ADDRESSED to, then only to gateway
|
|
243
265
|
// socket(s) fronting THAT identity — never broadcast across identities.
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
266
|
+
//
|
|
267
|
+
// DM: always forward. __botId (which identity's agentschat socket it arrived
|
|
268
|
+
// on) is the ownership signal; single-tenant falls back to its one identity.
|
|
269
|
+
// Group: forward ONLY when the body @mentions a fronted identity (content-
|
|
270
|
+
// based — the agentschat WS pushes every message of a joined channel
|
|
271
|
+
// unannotated, and arrival on a socket is NOT an addressing signal). The
|
|
272
|
+
// MCP path's gate is isDM || isMentioned; this reproduces it. Anything
|
|
273
|
+
// unaddressed is dropped — injecting joined-channel chatter into the
|
|
274
|
+
// agent's session would burn its tokens on messages not meant for it.
|
|
275
|
+
const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
|
|
247
276
|
const bySocket = (msg as any).__botId ? table.forBot(String((msg as any).__botId)) : null;
|
|
248
|
-
const target =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
277
|
+
const target = isDm
|
|
278
|
+
? bySocket ?? routeInbound(table, {
|
|
279
|
+
channel_id: msg.channel_id,
|
|
280
|
+
dmOwnerBotId: (msg as any).dm_owner,
|
|
281
|
+
}) ?? (table.isSingle() ? table.all()[0] : null)
|
|
282
|
+
: routeInbound(table, {
|
|
283
|
+
channel_id: msg.channel_id,
|
|
284
|
+
mentioned_ids: (msg as any).mentioned_ids,
|
|
285
|
+
content: msg.content,
|
|
286
|
+
});
|
|
253
287
|
if (!target) {
|
|
254
288
|
log(`[connector] inbound unaddressed (channel=${(msg as any).channel_id ?? "?"} mentions=${JSON.stringify((msg as any).mentioned_ids ?? [])}) — dropped`);
|
|
255
289
|
return;
|
|
@@ -258,6 +292,29 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
258
292
|
if (!event) return;
|
|
259
293
|
// Tag the fronting identity so the gateway keys the right session/profile.
|
|
260
294
|
(event.source as any).profile = target.botId;
|
|
295
|
+
if (!isDm) {
|
|
296
|
+
// Attach "what happened since you were last addressed" so the agent gets
|
|
297
|
+
// the conversation BETWEEN its @-mentions without being injected into
|
|
298
|
+
// every unaddressed message. Upstream renders event.context into the
|
|
299
|
+
// event's channel_context ("[Recent channel messages]") — gateway needs
|
|
300
|
+
// no change. Best-effort: a context failure never delays the message.
|
|
301
|
+
const key = `${target.botId}:${msg.channel_id}`;
|
|
302
|
+
const since = lastAddressed.get(key);
|
|
303
|
+
if (hooks.getChannelContext) {
|
|
304
|
+
try {
|
|
305
|
+
const ctx = await hooks.getChannelContext(target.botId, msg.channel_id!, since, msg.id);
|
|
306
|
+
if (ctx && ctx.length) {
|
|
307
|
+
event.context = ctx.slice(-10).map((c) => ({
|
|
308
|
+
text: String(c?.text ?? "").slice(0, 500),
|
|
309
|
+
source: { user_name: c?.user_name, user_id: c?.user_id },
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
} catch (e) {
|
|
313
|
+
log(`[connector] context fetch failed for ${key} (delivering without it): ${e}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (msg.timestamp) lastAddressed.set(key, msg.timestamp);
|
|
317
|
+
}
|
|
261
318
|
for (const conn of sockets) {
|
|
262
319
|
if (conn.fronted.has(target.botId)) send(conn.ws, { type: "inbound", event });
|
|
263
320
|
}
|
package/dist/connector.js
CHANGED
|
@@ -104,6 +104,17 @@ function toWireEvent(msg, platform = "agentschat") {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// src/mentions.ts
|
|
108
|
+
function matchesMention(content, agentId) {
|
|
109
|
+
if (!content || !agentId)
|
|
110
|
+
return false;
|
|
111
|
+
if (content.includes(`@${agentId}`))
|
|
112
|
+
return true;
|
|
113
|
+
const idEsc = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
|
+
const displayMentionRe = new RegExp(`@[^(\\n]+\\(${idEsc}\\)`);
|
|
115
|
+
return displayMentionRe.test(content);
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
// connector/identities.ts
|
|
108
119
|
class IdentityTable {
|
|
109
120
|
byBot = new Map;
|
|
@@ -138,6 +149,13 @@ function routeInbound(table, ctx) {
|
|
|
138
149
|
if (id)
|
|
139
150
|
return id;
|
|
140
151
|
}
|
|
152
|
+
const content = ctx.content ?? "";
|
|
153
|
+
if (content) {
|
|
154
|
+
for (const id of table.all()) {
|
|
155
|
+
if (matchesMention(content, id.agentId) || matchesMention(content, id.botId))
|
|
156
|
+
return id;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
141
159
|
return null;
|
|
142
160
|
}
|
|
143
161
|
|
|
@@ -145,14 +163,17 @@ function routeInbound(table, ctx) {
|
|
|
145
163
|
function startConnector(config) {
|
|
146
164
|
const log = config.logger ?? (() => {});
|
|
147
165
|
const descriptor = buildDescriptor(config.descriptor);
|
|
148
|
-
const
|
|
149
|
-
const table = new IdentityTable(
|
|
150
|
-
const
|
|
166
|
+
const legacy = !config.identities || config.identities.length === 0;
|
|
167
|
+
const table = new IdentityTable(legacy ? [{ botId: "default", agentId: "default", token: "", gatewayId: "", secret: "" }] : config.identities);
|
|
168
|
+
const single = table.isSingle();
|
|
169
|
+
const hooks = legacy ? {
|
|
151
170
|
sendMessage: (_b, chatId, content, replyTo) => config.agentschat.sendMessage(chatId, content, replyTo),
|
|
152
171
|
getChatInfo: (_b, chatId) => config.agentschat.getChatInfo(chatId),
|
|
153
|
-
sendTyping: (_b, chatId) => config.agentschat.sendTyping?.(chatId) ?? Promise.resolve()
|
|
172
|
+
sendTyping: (_b, chatId) => config.agentschat.sendTyping?.(chatId) ?? Promise.resolve(),
|
|
173
|
+
getChannelContext: (_b, chatId, sinceTs, excludeId) => config.agentschat.getChannelContext?.(chatId, sinceTs, excludeId) ?? Promise.resolve(null)
|
|
154
174
|
} : config.agentschat;
|
|
155
175
|
const sockets = new Set;
|
|
176
|
+
const lastAddressed = new Map;
|
|
156
177
|
const http = createServer((_req, res) => {
|
|
157
178
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
158
179
|
res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1, identities: table.size }));
|
|
@@ -272,13 +293,17 @@ function startConnector(config) {
|
|
|
272
293
|
wss.close();
|
|
273
294
|
http.close();
|
|
274
295
|
},
|
|
275
|
-
injectAgentsChatMessage(msg) {
|
|
296
|
+
async injectAgentsChatMessage(msg) {
|
|
297
|
+
const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
|
|
276
298
|
const bySocket = msg.__botId ? table.forBot(String(msg.__botId)) : null;
|
|
277
|
-
const target = bySocket ?? routeInbound(table, {
|
|
299
|
+
const target = isDm ? bySocket ?? routeInbound(table, {
|
|
278
300
|
channel_id: msg.channel_id,
|
|
279
|
-
mentioned_ids: msg.mentioned_ids,
|
|
280
301
|
dmOwnerBotId: msg.dm_owner
|
|
281
|
-
}) ?? (table.isSingle() ? table.all()[0] : null)
|
|
302
|
+
}) ?? (table.isSingle() ? table.all()[0] : null) : routeInbound(table, {
|
|
303
|
+
channel_id: msg.channel_id,
|
|
304
|
+
mentioned_ids: msg.mentioned_ids,
|
|
305
|
+
content: msg.content
|
|
306
|
+
});
|
|
282
307
|
if (!target) {
|
|
283
308
|
log(`[connector] inbound unaddressed (channel=${msg.channel_id ?? "?"} mentions=${JSON.stringify(msg.mentioned_ids ?? [])}) — dropped`);
|
|
284
309
|
return;
|
|
@@ -287,6 +312,25 @@ function startConnector(config) {
|
|
|
287
312
|
if (!event)
|
|
288
313
|
return;
|
|
289
314
|
event.source.profile = target.botId;
|
|
315
|
+
if (!isDm) {
|
|
316
|
+
const key = `${target.botId}:${msg.channel_id}`;
|
|
317
|
+
const since = lastAddressed.get(key);
|
|
318
|
+
if (hooks.getChannelContext) {
|
|
319
|
+
try {
|
|
320
|
+
const ctx = await hooks.getChannelContext(target.botId, msg.channel_id, since, msg.id);
|
|
321
|
+
if (ctx && ctx.length) {
|
|
322
|
+
event.context = ctx.slice(-10).map((c) => ({
|
|
323
|
+
text: String(c?.text ?? "").slice(0, 500),
|
|
324
|
+
source: { user_name: c?.user_name, user_id: c?.user_id }
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
log(`[connector] context fetch failed for ${key} (delivering without it): ${e}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (msg.timestamp)
|
|
332
|
+
lastAddressed.set(key, msg.timestamp);
|
|
333
|
+
}
|
|
290
334
|
for (const conn of sockets) {
|
|
291
335
|
if (conn.fronted.has(target.botId))
|
|
292
336
|
send(conn.ws, { type: "inbound", event });
|
|
@@ -309,6 +353,11 @@ function peekPayload(token) {
|
|
|
309
353
|
}
|
|
310
354
|
}
|
|
311
355
|
|
|
356
|
+
// src/redact.ts
|
|
357
|
+
function redactSecrets(text) {
|
|
358
|
+
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***");
|
|
359
|
+
}
|
|
360
|
+
|
|
312
361
|
// connector/run.ts
|
|
313
362
|
var log = (m) => process.stderr.write(`[agentschat-connector] ${m}
|
|
314
363
|
`);
|
|
@@ -405,7 +454,7 @@ var connector = startConnector({
|
|
|
405
454
|
port: PORT,
|
|
406
455
|
host: HOST,
|
|
407
456
|
secrets,
|
|
408
|
-
identities
|
|
457
|
+
identities,
|
|
409
458
|
agentschat: {
|
|
410
459
|
async sendMessage(botId, chatId, content, replyTo) {
|
|
411
460
|
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
@@ -437,6 +486,24 @@ var connector = startConnector({
|
|
|
437
486
|
ws.send(JSON.stringify({ type: "typing", channel_id: chatId, sender_id: id.agentId }));
|
|
438
487
|
} catch {}
|
|
439
488
|
}
|
|
489
|
+
},
|
|
490
|
+
async getChannelContext(botId, chatId, sinceTs, excludeId) {
|
|
491
|
+
const id = identities.find((i) => i.botId === botId) ?? identities[0];
|
|
492
|
+
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages?limit=50`, {
|
|
493
|
+
headers: { Authorization: `Bearer ${id.token}` }
|
|
494
|
+
});
|
|
495
|
+
if (!res.ok)
|
|
496
|
+
return null;
|
|
497
|
+
const msgs = ((await res.json())?.messages ?? []).filter((m) => m?.content && m.content !== "__typing__" && m?.id !== excludeId).sort((a, b) => String(a.timestamp ?? "").localeCompare(String(b.timestamp ?? "")));
|
|
498
|
+
const windowed = sinceTs ? msgs.filter((m) => String(m.timestamp ?? "") > sinceTs) : msgs;
|
|
499
|
+
const tail = windowed.slice(-10);
|
|
500
|
+
if (!tail.length)
|
|
501
|
+
return null;
|
|
502
|
+
return tail.map((m) => ({
|
|
503
|
+
text: redactSecrets(String(m.content)).slice(0, 500),
|
|
504
|
+
user_name: m.sender_name ?? m.sender_id,
|
|
505
|
+
user_id: m.sender_id
|
|
506
|
+
}));
|
|
440
507
|
}
|
|
441
508
|
},
|
|
442
509
|
logger: log
|
package/dist/server.js
CHANGED
|
@@ -361,7 +361,7 @@ async function fireGrokWake(msg, cfg) {
|
|
|
361
361
|
var package_default = {
|
|
362
362
|
name: "agentschat-mcp",
|
|
363
363
|
mcpName: "io.github.swswordholy-tech/agentschat-mcp",
|
|
364
|
-
version: "0.
|
|
364
|
+
version: "0.33.0",
|
|
365
365
|
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.",
|
|
366
366
|
type: "module",
|
|
367
367
|
bin: {
|
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.33.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": {
|