agent-social-mcp 0.2.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 +29 -0
- package/USAGE.md +7 -0
- package/dist/client/cli.js +13 -0
- package/dist/client/relayAuth.js +20 -0
- package/dist/client/remoteMcpServer.js +142 -0
- package/dist/companion/chatServer.js +369 -0
- package/dist/companion/chatUi.js +866 -0
- package/dist/companion/cli.js +18 -0
- package/dist/companion/mcpUi.js +234 -0
- package/dist/companion/relayClient.js +26 -0
- package/dist/companion/replyAssistance.js +164 -0
- package/dist/companion/state.js +344 -0
- package/dist/companion/syncState.js +50 -0
- package/dist/contracts.js +111 -0
- package/dist/datingContracts.js +1 -0
- package/dist/publicViews.js +115 -0
- package/dist/socialContracts.js +1 -0
- package/package.json +27 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
+
import { publicIdForToken, publicSessionIdForInternal } from "../publicViews.js";
|
|
5
|
+
export function defaultCompanionStatePath(env = process.env) {
|
|
6
|
+
if (env.AGENT_SOCIAL_CHAT_STATE_PATH) {
|
|
7
|
+
return env.AGENT_SOCIAL_CHAT_STATE_PATH;
|
|
8
|
+
}
|
|
9
|
+
if (env.AGENT_SOCIAL_STORE_PATH) {
|
|
10
|
+
const storeName = basename(env.AGENT_SOCIAL_STORE_PATH, extname(env.AGENT_SOCIAL_STORE_PATH));
|
|
11
|
+
return join(dirname(env.AGENT_SOCIAL_STORE_PATH), `${storeName}-chat-state.json`);
|
|
12
|
+
}
|
|
13
|
+
return join(homedir(), ".agent-social-mcp", "chat-state.json");
|
|
14
|
+
}
|
|
15
|
+
export class CompanionStateStore {
|
|
16
|
+
filePath;
|
|
17
|
+
constructor(filePath = defaultCompanionStatePath()) {
|
|
18
|
+
this.filePath = filePath;
|
|
19
|
+
}
|
|
20
|
+
read(at = new Date()) {
|
|
21
|
+
if (!existsSync(this.filePath)) {
|
|
22
|
+
return emptyState();
|
|
23
|
+
}
|
|
24
|
+
const parsed = JSON.parse(readFileSync(this.filePath, "utf8"));
|
|
25
|
+
const sourceSessions = Array.isArray(parsed.sessions) ? parsed.sessions : [];
|
|
26
|
+
const sourceMessages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
27
|
+
const sourceAssistance = Array.isArray(parsed.replyAssistance) ? parsed.replyAssistance : [];
|
|
28
|
+
const sourceRequests = Array.isArray(parsed.replyAssistanceRequests)
|
|
29
|
+
? parsed.replyAssistanceRequests
|
|
30
|
+
: [];
|
|
31
|
+
const state = {
|
|
32
|
+
version: 3,
|
|
33
|
+
userToken: typeof parsed.userToken === "string" ? parsed.userToken : undefined,
|
|
34
|
+
preferences: {
|
|
35
|
+
autoAnalyze: parsed.preferences?.autoAnalyze !== false
|
|
36
|
+
},
|
|
37
|
+
...(normalizeMatchContext(parsed.matchContext) ? {
|
|
38
|
+
matchContext: normalizeMatchContext(parsed.matchContext)
|
|
39
|
+
} : {}),
|
|
40
|
+
sessions: sourceSessions.map((session) => normalizeSession(session, parsed.userToken)),
|
|
41
|
+
messages: sourceMessages.map((message) => normalizeMessage(message)),
|
|
42
|
+
replyAssistance: sourceAssistance
|
|
43
|
+
.map(normalizeReplyAssistance)
|
|
44
|
+
.filter((value) => Boolean(value)),
|
|
45
|
+
replyAssistanceRequests: sourceRequests
|
|
46
|
+
.map(normalizeReplyAssistanceRequest)
|
|
47
|
+
.filter((value) => Boolean(value))
|
|
48
|
+
};
|
|
49
|
+
const identifiersChanged = sourceSessions.some((session, index) => typeof session === "object" && session !== null && "id" in session
|
|
50
|
+
&& state.sessions[index]?.id !== session.id) || sourceMessages.some((message, index) => typeof message === "object" && message !== null && "sessionId" in message
|
|
51
|
+
&& state.messages[index]?.sessionId !== message.sessionId) || sourceAssistance.some((assistance, index) => typeof assistance === "object" && assistance !== null && "sessionId" in assistance
|
|
52
|
+
&& state.replyAssistance[index]?.sessionId !== assistance.sessionId) || sourceRequests.some((request, index) => typeof request === "object" && request !== null && "sessionId" in request
|
|
53
|
+
&& state.replyAssistanceRequests[index]?.sessionId !== request.sessionId);
|
|
54
|
+
const purged = purgeExpiredState(state, at);
|
|
55
|
+
if (parsed.version !== 3 || identifiersChanged || purged.changed) {
|
|
56
|
+
this.write(purged.state);
|
|
57
|
+
}
|
|
58
|
+
return purged.state;
|
|
59
|
+
}
|
|
60
|
+
write(state) {
|
|
61
|
+
mkdirSync(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
62
|
+
const temporaryPath = `${this.filePath}.tmp-${process.pid}`;
|
|
63
|
+
writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
64
|
+
renameSync(temporaryPath, this.filePath);
|
|
65
|
+
chmodSync(this.filePath, 0o600);
|
|
66
|
+
}
|
|
67
|
+
setUserToken(userToken) {
|
|
68
|
+
const state = this.read();
|
|
69
|
+
state.userToken = userToken;
|
|
70
|
+
this.write(state);
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
setAutoAnalyze(autoAnalyze) {
|
|
74
|
+
const state = this.read();
|
|
75
|
+
state.preferences.autoAnalyze = autoAnalyze;
|
|
76
|
+
this.write(state);
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
setMatchContext(matchContext) {
|
|
80
|
+
const state = this.read();
|
|
81
|
+
state.matchContext = normalizeMatchContext(matchContext);
|
|
82
|
+
this.write(state);
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
upsertSessions(sessions) {
|
|
86
|
+
const state = this.read();
|
|
87
|
+
for (const session of sessions) {
|
|
88
|
+
const normalized = normalizeSession(session, state.userToken);
|
|
89
|
+
const index = state.sessions.findIndex((candidate) => candidate.id === normalized.id);
|
|
90
|
+
if (index >= 0) {
|
|
91
|
+
state.sessions[index] = {
|
|
92
|
+
...state.sessions[index],
|
|
93
|
+
...normalized,
|
|
94
|
+
cursor: normalized.cursor ?? state.sessions[index]?.cursor,
|
|
95
|
+
lastOpenedAt: normalized.lastOpenedAt ?? state.sessions[index]?.lastOpenedAt
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
state.sessions.push(normalized);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
state.sessions.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
103
|
+
this.write(state);
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
upsertMessages(messages, fallbackDirection) {
|
|
107
|
+
const state = this.read();
|
|
108
|
+
for (const value of messages) {
|
|
109
|
+
const message = normalizeMessage(value, fallbackDirection);
|
|
110
|
+
const index = state.messages.findIndex((candidate) => candidate.id === message.id);
|
|
111
|
+
if (index >= 0) {
|
|
112
|
+
state.messages[index] = { ...state.messages[index], ...message };
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
state.messages.push(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
state.messages.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt) || a.id.localeCompare(b.id));
|
|
119
|
+
this.write(state);
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
setSessionCursor(sessionId, cursor) {
|
|
123
|
+
const state = this.read();
|
|
124
|
+
if (cursor) {
|
|
125
|
+
state.sessions = state.sessions.map((session) => session.id === sessionId ? { ...session, cursor } : session);
|
|
126
|
+
this.write(state);
|
|
127
|
+
}
|
|
128
|
+
return state;
|
|
129
|
+
}
|
|
130
|
+
markSessionOpened(sessionId) {
|
|
131
|
+
const state = this.read();
|
|
132
|
+
const openedAt = new Date().toISOString();
|
|
133
|
+
state.sessions = state.sessions.map((session) => session.id === sessionId ? { ...session, lastOpenedAt: openedAt } : session);
|
|
134
|
+
this.write(state);
|
|
135
|
+
return state;
|
|
136
|
+
}
|
|
137
|
+
markSessionRead(sessionId) {
|
|
138
|
+
const state = this.markSessionOpened(sessionId);
|
|
139
|
+
const readAt = new Date().toISOString();
|
|
140
|
+
state.messages = state.messages.map((message) => message.sessionId === sessionId && message.direction === "received" && !message.readAt
|
|
141
|
+
? { ...message, readAt, deliveredAt: message.deliveredAt ?? readAt, deliveryState: "read" }
|
|
142
|
+
: message);
|
|
143
|
+
this.write(state);
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
beginReplyAssistance(request) {
|
|
147
|
+
const state = this.read();
|
|
148
|
+
const normalized = normalizeReplyAssistanceRequest(request);
|
|
149
|
+
if (!normalized) {
|
|
150
|
+
throw new Error("invalid reply assistance request");
|
|
151
|
+
}
|
|
152
|
+
state.replyAssistanceRequests = state.replyAssistanceRequests.filter((candidate) => candidate.sessionId !== normalized.sessionId);
|
|
153
|
+
state.replyAssistanceRequests.push(normalized);
|
|
154
|
+
this.write(state);
|
|
155
|
+
return state;
|
|
156
|
+
}
|
|
157
|
+
completeReplyAssistance(assistance) {
|
|
158
|
+
const state = this.read();
|
|
159
|
+
const normalized = normalizeReplyAssistance(assistance);
|
|
160
|
+
if (!normalized) {
|
|
161
|
+
throw new Error("invalid reply assistance");
|
|
162
|
+
}
|
|
163
|
+
state.replyAssistance = state.replyAssistance.filter((candidate) => candidate.sessionId !== normalized.sessionId);
|
|
164
|
+
state.replyAssistance.push(normalized);
|
|
165
|
+
state.replyAssistanceRequests = state.replyAssistanceRequests.filter((candidate) => candidate.sessionId !== normalized.sessionId);
|
|
166
|
+
this.write(state);
|
|
167
|
+
return state;
|
|
168
|
+
}
|
|
169
|
+
clearReplyAssistanceRequest(sessionId, requestId) {
|
|
170
|
+
const state = this.read();
|
|
171
|
+
state.replyAssistanceRequests = state.replyAssistanceRequests.filter((candidate) => candidate.sessionId !== sessionId || (requestId && candidate.requestId !== requestId));
|
|
172
|
+
this.write(state);
|
|
173
|
+
return state;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function sentMessageFromRelay(message) {
|
|
177
|
+
return normalizeMessage(message, "sent");
|
|
178
|
+
}
|
|
179
|
+
export function receivedMessageFromRelay(message) {
|
|
180
|
+
return normalizeMessage(message, "received");
|
|
181
|
+
}
|
|
182
|
+
function emptyState() {
|
|
183
|
+
return {
|
|
184
|
+
version: 3,
|
|
185
|
+
preferences: { autoAnalyze: true },
|
|
186
|
+
sessions: [],
|
|
187
|
+
messages: [],
|
|
188
|
+
replyAssistance: [],
|
|
189
|
+
replyAssistanceRequests: []
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function normalizeSession(session, userToken) {
|
|
193
|
+
if ("peerId" in session) {
|
|
194
|
+
return {
|
|
195
|
+
id: publicSessionIdForInternal(session.id),
|
|
196
|
+
peerId: session.peerId,
|
|
197
|
+
...(session.peerIntro ? { peerIntro: session.peerIntro } : {}),
|
|
198
|
+
createdAt: toIso(session.createdAt),
|
|
199
|
+
status: session.status,
|
|
200
|
+
label: "label" in session && session.label ? session.label : sessionLabel(session.peerId),
|
|
201
|
+
...("cursor" in session && session.cursor ? { cursor: session.cursor } : {}),
|
|
202
|
+
...("lastOpenedAt" in session && session.lastOpenedAt ? { lastOpenedAt: session.lastOpenedAt } : {})
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const peerToken = userToken === session.userA ? session.userB : session.userA;
|
|
206
|
+
const peerId = publicIdForToken(peerToken);
|
|
207
|
+
return {
|
|
208
|
+
id: publicSessionIdForInternal(session.id),
|
|
209
|
+
peerId,
|
|
210
|
+
createdAt: toIso(session.createdAt),
|
|
211
|
+
status: session.status,
|
|
212
|
+
label: sessionLabel(peerId)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function normalizeMessage(message, fallbackDirection) {
|
|
216
|
+
const createdAt = toIso(message.createdAt);
|
|
217
|
+
const direction = "direction" in message
|
|
218
|
+
? message.direction === "self" || message.direction === "sent" ? "sent" : "received"
|
|
219
|
+
: fallbackDirection ?? "received";
|
|
220
|
+
const retentionUntil = "expiresAt" in message
|
|
221
|
+
? message.expiresAt
|
|
222
|
+
: "retentionUntil" in message
|
|
223
|
+
? message.retentionUntil
|
|
224
|
+
: new Date(Date.parse(createdAt) + 7 * 24 * 60 * 60 * 1000);
|
|
225
|
+
return {
|
|
226
|
+
id: message.id,
|
|
227
|
+
sessionId: publicSessionIdForInternal(message.sessionId),
|
|
228
|
+
body: message.body,
|
|
229
|
+
createdAt,
|
|
230
|
+
updatedAt: "updatedAt" in message ? toIso(message.updatedAt) : createdAt,
|
|
231
|
+
expiresAt: toIso(retentionUntil),
|
|
232
|
+
deliveredAt: "deliveredAt" in message ? nullableIso(message.deliveredAt) : null,
|
|
233
|
+
readAt: "readAt" in message ? nullableIso(message.readAt) : null,
|
|
234
|
+
deliveryState: "deliveryState" in message ? message.deliveryState : "queued",
|
|
235
|
+
direction,
|
|
236
|
+
...(message.replyContext ? { replyContext: message.replyContext } : {})
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function purgeExpiredState(state, at) {
|
|
240
|
+
let changed = false;
|
|
241
|
+
const messages = state.messages.map((message) => {
|
|
242
|
+
if (Date.parse(message.expiresAt) <= at.getTime()
|
|
243
|
+
&& (message.body !== null || message.replyContext !== undefined)) {
|
|
244
|
+
changed = true;
|
|
245
|
+
return { ...message, body: null, replyContext: undefined };
|
|
246
|
+
}
|
|
247
|
+
return message;
|
|
248
|
+
});
|
|
249
|
+
const retainedMessageIds = new Set(messages.filter((message) => message.body !== null).map((message) => message.id));
|
|
250
|
+
const replyAssistance = state.replyAssistance.filter((assistance) => retainedMessageIds.has(assistance.sourceMessageId));
|
|
251
|
+
const replyAssistanceRequests = state.replyAssistanceRequests.filter((request) => retainedMessageIds.has(request.sourceMessageId));
|
|
252
|
+
if (replyAssistance.length !== state.replyAssistance.length
|
|
253
|
+
|| replyAssistanceRequests.length !== state.replyAssistanceRequests.length) {
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
changed,
|
|
258
|
+
state: {
|
|
259
|
+
...state,
|
|
260
|
+
messages,
|
|
261
|
+
replyAssistance,
|
|
262
|
+
replyAssistanceRequests
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function normalizeMatchContext(value) {
|
|
267
|
+
if (!value || typeof value !== "object") {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
const candidate = value;
|
|
271
|
+
if (typeof candidate.desiredTone !== "string"
|
|
272
|
+
|| typeof candidate.shareableIntro !== "string"
|
|
273
|
+
|| !Array.isArray(candidate.topics)
|
|
274
|
+
|| !candidate.topics.every((item) => typeof item === "string")
|
|
275
|
+
|| !Array.isArray(candidate.boundaries)
|
|
276
|
+
|| !candidate.boundaries.every((item) => typeof item === "string")) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
desiredTone: candidate.desiredTone,
|
|
281
|
+
topics: [...candidate.topics],
|
|
282
|
+
boundaries: [...candidate.boundaries],
|
|
283
|
+
shareableIntro: candidate.shareableIntro
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function normalizeReplyAssistance(value) {
|
|
287
|
+
if (!value || typeof value !== "object") {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const candidate = value;
|
|
291
|
+
if (typeof candidate.requestId !== "string"
|
|
292
|
+
|| typeof candidate.sessionId !== "string"
|
|
293
|
+
|| typeof candidate.sourceMessageId !== "string"
|
|
294
|
+
|| typeof candidate.contextHash !== "string"
|
|
295
|
+
|| typeof candidate.analysis !== "string"
|
|
296
|
+
|| !isRecommendedAction(candidate.recommendedAction)
|
|
297
|
+
|| !Array.isArray(candidate.suggestions)
|
|
298
|
+
|| !candidate.suggestions.every((item) => typeof item === "string")
|
|
299
|
+
|| typeof candidate.createdAt !== "string") {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
requestId: candidate.requestId,
|
|
304
|
+
sessionId: publicSessionIdForInternal(candidate.sessionId),
|
|
305
|
+
sourceMessageId: candidate.sourceMessageId,
|
|
306
|
+
contextHash: candidate.contextHash,
|
|
307
|
+
analysis: candidate.analysis,
|
|
308
|
+
recommendedAction: candidate.recommendedAction,
|
|
309
|
+
suggestions: [...candidate.suggestions],
|
|
310
|
+
createdAt: candidate.createdAt
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function normalizeReplyAssistanceRequest(value) {
|
|
314
|
+
if (!value || typeof value !== "object") {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
const candidate = value;
|
|
318
|
+
if (typeof candidate.requestId !== "string"
|
|
319
|
+
|| typeof candidate.sessionId !== "string"
|
|
320
|
+
|| typeof candidate.sourceMessageId !== "string"
|
|
321
|
+
|| typeof candidate.contextHash !== "string"
|
|
322
|
+
|| typeof candidate.createdAt !== "string") {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
requestId: candidate.requestId,
|
|
327
|
+
sessionId: publicSessionIdForInternal(candidate.sessionId),
|
|
328
|
+
sourceMessageId: candidate.sourceMessageId,
|
|
329
|
+
contextHash: candidate.contextHash,
|
|
330
|
+
createdAt: candidate.createdAt
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function isRecommendedAction(value) {
|
|
334
|
+
return value === "reply" || value === "wait" || value === "close" || value === "block" || value === "report";
|
|
335
|
+
}
|
|
336
|
+
function sessionLabel(peerId) {
|
|
337
|
+
return `Match ${peerId.replace(/^anon_/, "").slice(0, 6)}`;
|
|
338
|
+
}
|
|
339
|
+
function toIso(value) {
|
|
340
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
341
|
+
}
|
|
342
|
+
function nullableIso(value) {
|
|
343
|
+
return value ? toIso(value) : null;
|
|
344
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { MCP_TOOLS } from "../contracts.js";
|
|
2
|
+
import { CompanionStateStore, defaultCompanionStatePath, receivedMessageFromRelay, sentMessageFromRelay } from "./state.js";
|
|
3
|
+
export function syncCompanionState(storePath, tool, payload, input) {
|
|
4
|
+
const state = new CompanionStateStore(defaultCompanionStatePath({
|
|
5
|
+
...process.env,
|
|
6
|
+
AGENT_SOCIAL_STORE_PATH: storePath
|
|
7
|
+
}));
|
|
8
|
+
if (tool === MCP_TOOLS.onboard) {
|
|
9
|
+
const user = payload.user;
|
|
10
|
+
if (user?.token) {
|
|
11
|
+
state.setUserToken(user.token);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (tool === MCP_TOOLS.requestMatch) {
|
|
15
|
+
if (typeof input.userToken === "string") {
|
|
16
|
+
state.setUserToken(input.userToken);
|
|
17
|
+
}
|
|
18
|
+
if (typeof input.desiredTone === "string"
|
|
19
|
+
&& Array.isArray(input.topics)
|
|
20
|
+
&& input.topics.every((item) => typeof item === "string")
|
|
21
|
+
&& Array.isArray(input.boundaries)
|
|
22
|
+
&& input.boundaries.every((item) => typeof item === "string")
|
|
23
|
+
&& typeof input.shareableIntro === "string") {
|
|
24
|
+
state.setMatchContext({
|
|
25
|
+
desiredTone: input.desiredTone,
|
|
26
|
+
topics: input.topics,
|
|
27
|
+
boundaries: input.boundaries,
|
|
28
|
+
shareableIntro: input.shareableIntro
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (payload.session) {
|
|
32
|
+
state.upsertSessions([payload.session]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (tool === MCP_TOOLS.getMatchStatus && Array.isArray(payload.sessions)) {
|
|
36
|
+
state.upsertSessions(payload.sessions);
|
|
37
|
+
}
|
|
38
|
+
if (tool === MCP_TOOLS.sendMessage && payload.message) {
|
|
39
|
+
state.upsertMessages([sentMessageFromRelay(payload.message)]);
|
|
40
|
+
}
|
|
41
|
+
if (tool === MCP_TOOLS.receiveMessages && Array.isArray(payload.messages)) {
|
|
42
|
+
state.upsertMessages(payload.messages.map(receivedMessageFromRelay));
|
|
43
|
+
if (typeof input.sessionId === "string" && typeof payload.cursor === "string") {
|
|
44
|
+
state.setSessionCursor(input.sessionId, payload.cursor);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (tool === MCP_TOOLS.markRead && typeof input.sessionId === "string") {
|
|
48
|
+
state.markSessionRead(input.sessionId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const MCP_TOOLS = {
|
|
2
|
+
onboard: "dating_onboard",
|
|
3
|
+
requestMatch: "dating_request_match",
|
|
4
|
+
getMatchStatus: "dating_get_match_status",
|
|
5
|
+
sendMessage: "dating_send_message",
|
|
6
|
+
receiveMessages: "dating_receive_messages",
|
|
7
|
+
markRead: "dating_mark_read",
|
|
8
|
+
reportSession: "dating_report_session",
|
|
9
|
+
blockUser: "dating_block_user",
|
|
10
|
+
closeSession: "dating_close_session"
|
|
11
|
+
};
|
|
12
|
+
const dayInMs = 24 * 60 * 60 * 1000;
|
|
13
|
+
export function createAnonymousUser(input) {
|
|
14
|
+
return {
|
|
15
|
+
...input,
|
|
16
|
+
status: "active",
|
|
17
|
+
reportCount: 0,
|
|
18
|
+
blockCount: 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function validateMatchCapsuleInput(input) {
|
|
22
|
+
if (!input.userToken.trim()) {
|
|
23
|
+
return { ok: false, error: "userToken is required" };
|
|
24
|
+
}
|
|
25
|
+
if (!input.desiredTone.trim()) {
|
|
26
|
+
return { ok: false, error: "desiredTone is required" };
|
|
27
|
+
}
|
|
28
|
+
if (input.topics.length === 0) {
|
|
29
|
+
return { ok: false, error: "topics must include at least one topic" };
|
|
30
|
+
}
|
|
31
|
+
if (!input.shareableIntro.trim()) {
|
|
32
|
+
return { ok: false, error: "shareableIntro is required" };
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isInteger(input.ttlMinutes) || input.ttlMinutes < 1 || input.ttlMinutes > 1440) {
|
|
35
|
+
return { ok: false, error: "ttlMinutes must be between 1 and 1440" };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
export function createMatchCapsule(input) {
|
|
40
|
+
const validation = validateMatchCapsuleInput(input);
|
|
41
|
+
if (!validation.ok) {
|
|
42
|
+
throw new Error(validation.error);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...input,
|
|
46
|
+
id: `capsule_${input.userToken}_${input.createdAt.getTime()}`,
|
|
47
|
+
expiresAt: new Date(input.createdAt.getTime() + input.ttlMinutes * 60 * 1000)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function isCapsuleExpired(capsule, at) {
|
|
51
|
+
return at.getTime() >= capsule.expiresAt.getTime();
|
|
52
|
+
}
|
|
53
|
+
export function createMatchSession(input) {
|
|
54
|
+
return {
|
|
55
|
+
...input,
|
|
56
|
+
status: "active"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function validateMessageInput(input) {
|
|
60
|
+
if (!input.sessionId.trim()) {
|
|
61
|
+
return { ok: false, error: "sessionId is required" };
|
|
62
|
+
}
|
|
63
|
+
if (!input.senderToken.trim()) {
|
|
64
|
+
return { ok: false, error: "senderToken is required" };
|
|
65
|
+
}
|
|
66
|
+
if (!input.body.trim()) {
|
|
67
|
+
return { ok: false, error: "body is required" };
|
|
68
|
+
}
|
|
69
|
+
if (input.userConfirmed !== true) {
|
|
70
|
+
return { ok: false, error: "userConfirmed must be true before sending" };
|
|
71
|
+
}
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
74
|
+
export function createMessage(input) {
|
|
75
|
+
const validation = validateMessageInput(input);
|
|
76
|
+
if (!validation.ok) {
|
|
77
|
+
throw new Error(validation.error);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...input,
|
|
81
|
+
retentionUntil: new Date(input.createdAt.getTime() + 7 * dayInMs),
|
|
82
|
+
updatedAt: input.createdAt,
|
|
83
|
+
deliveredAt: null,
|
|
84
|
+
readAt: null,
|
|
85
|
+
deliveryState: "queued"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function createReport(input) {
|
|
89
|
+
return {
|
|
90
|
+
...input,
|
|
91
|
+
status: "open",
|
|
92
|
+
contextMessageIds: [],
|
|
93
|
+
retainedMessageBodies: []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function createBlock(input) {
|
|
97
|
+
return {
|
|
98
|
+
...input
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function createRateLimitState(input) {
|
|
102
|
+
return {
|
|
103
|
+
...input,
|
|
104
|
+
onboardingAttempts: 0,
|
|
105
|
+
matchRequests: 0,
|
|
106
|
+
messagesSent: 0,
|
|
107
|
+
activeSessions: 0,
|
|
108
|
+
rejectedMessages: 0,
|
|
109
|
+
cooldownUntil: null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function publicIdForToken(token) {
|
|
3
|
+
return `anon_${createHash("sha256").update(token).digest("hex").slice(0, 12)}`;
|
|
4
|
+
}
|
|
5
|
+
export function publicSessionIdForInternal(sessionId) {
|
|
6
|
+
if (/^social_session_[a-f0-9]{20}$/.test(sessionId)) {
|
|
7
|
+
return sessionId;
|
|
8
|
+
}
|
|
9
|
+
const digest = createHash("sha256")
|
|
10
|
+
.update(`agent-social-session:${sessionId}`)
|
|
11
|
+
.digest("hex")
|
|
12
|
+
.slice(0, 20);
|
|
13
|
+
return `social_session_${digest}`;
|
|
14
|
+
}
|
|
15
|
+
export function sessionMatchesPublicOrInternalId(session, sessionId) {
|
|
16
|
+
return session.id === sessionId || publicSessionIdForInternal(session.id) === sessionId;
|
|
17
|
+
}
|
|
18
|
+
export function toPublicSession(session, viewerToken, peerCapsule) {
|
|
19
|
+
const peerToken = peerTokenForSession(session, viewerToken);
|
|
20
|
+
return {
|
|
21
|
+
id: publicSessionIdForInternal(session.id),
|
|
22
|
+
peerId: publicIdForToken(peerToken),
|
|
23
|
+
createdAt: toIso(session.createdAt),
|
|
24
|
+
status: session.status,
|
|
25
|
+
...(peerCapsule?.userToken === peerToken
|
|
26
|
+
? { peerIntro: redactCredentials(peerCapsule.shareableIntro, [peerCapsule.userToken]) }
|
|
27
|
+
: {})
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function toPublicCandidate(capsule) {
|
|
31
|
+
return {
|
|
32
|
+
datingIntent: capsule.datingIntent,
|
|
33
|
+
genderPreference: capsule.genderPreference,
|
|
34
|
+
desiredTone: redactCredentials(capsule.desiredTone, [capsule.userToken]),
|
|
35
|
+
topics: capsule.topics.map((value) => redactCredentials(value, [capsule.userToken])),
|
|
36
|
+
boundaries: capsule.boundaries.map((value) => redactCredentials(value, [capsule.userToken])),
|
|
37
|
+
shareableIntro: redactCredentials(capsule.shareableIntro, [capsule.userToken])
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function toPublicMessage(message, viewerToken) {
|
|
41
|
+
return {
|
|
42
|
+
id: message.id,
|
|
43
|
+
sessionId: publicSessionIdForInternal(message.sessionId),
|
|
44
|
+
direction: message.senderToken === viewerToken ? "self" : "peer",
|
|
45
|
+
body: message.body === null ? null : redactCredentials(message.body),
|
|
46
|
+
createdAt: toIso(message.createdAt),
|
|
47
|
+
updatedAt: toIso(message.updatedAt),
|
|
48
|
+
deliveredAt: nullableIso(message.deliveredAt),
|
|
49
|
+
readAt: nullableIso(message.readAt),
|
|
50
|
+
expiresAt: toIso(message.retentionUntil),
|
|
51
|
+
deliveryState: message.deliveryState,
|
|
52
|
+
...(message.replyContext ? { replyContext: copyReplyContext(message.replyContext) } : {})
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function redactCredentials(value, knownTokens = []) {
|
|
56
|
+
let redacted = value;
|
|
57
|
+
for (const token of knownTokens) {
|
|
58
|
+
if (token) {
|
|
59
|
+
redacted = redacted.replaceAll(token, "[redacted credential]");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return redacted.replace(/\buser_[a-zA-Z0-9_-]+\b/g, "[redacted credential]");
|
|
63
|
+
}
|
|
64
|
+
export function encodeEventCursor(updatedAt, id) {
|
|
65
|
+
return Buffer.from(JSON.stringify([updatedAt.toISOString(), id]), "utf8").toString("base64url");
|
|
66
|
+
}
|
|
67
|
+
export function decodeEventCursor(cursor) {
|
|
68
|
+
if (!cursor) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
73
|
+
if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "string" || typeof value[1] !== "string") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const updatedAt = new Date(value[0]);
|
|
77
|
+
if (Number.isNaN(updatedAt.getTime()) || !value[1]) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return { updatedAt, id: value[1] };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function isAfterCursor(message, cursor) {
|
|
87
|
+
if (!cursor) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const timeDelta = message.updatedAt.getTime() - cursor.updatedAt.getTime();
|
|
91
|
+
return timeDelta > 0 || (timeDelta === 0 && message.id > cursor.id);
|
|
92
|
+
}
|
|
93
|
+
function peerTokenForSession(session, viewerToken) {
|
|
94
|
+
if (session.userA === viewerToken) {
|
|
95
|
+
return session.userB;
|
|
96
|
+
}
|
|
97
|
+
if (session.userB === viewerToken) {
|
|
98
|
+
return session.userA;
|
|
99
|
+
}
|
|
100
|
+
throw new Error("viewer is not in session");
|
|
101
|
+
}
|
|
102
|
+
function copyReplyContext(context) {
|
|
103
|
+
return {
|
|
104
|
+
tone: context.tone,
|
|
105
|
+
inferredSignal: context.inferredSignal,
|
|
106
|
+
suggestedAngles: [...context.suggestedAngles],
|
|
107
|
+
safetyReminders: [...context.safetyReminders]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function toIso(value) {
|
|
111
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
112
|
+
}
|
|
113
|
+
function nullableIso(value) {
|
|
114
|
+
return value ? toIso(value) : null;
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-social-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Public client for Agent Social discovery, realtime conversation, and Agent-assisted replies.",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"USAGE.md"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"agent-social-mcp": "dist/client/cli.js",
|
|
21
|
+
"agent-social-chat": "dist/companion/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
|
+
"zod": "^4.4.3"
|
|
26
|
+
}
|
|
27
|
+
}
|