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.
@@ -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
+ }