@tritard/waterbrother 0.16.138 → 0.16.140
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/package.json +1 -1
- package/src/discord.js +371 -7
- package/src/gateway-state.js +16 -1
package/package.json
CHANGED
package/src/discord.js
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
releaseSharedOperator,
|
|
19
19
|
removeSharedMember,
|
|
20
20
|
setSharedRoom,
|
|
21
|
-
setSharedRoomMode
|
|
21
|
+
setSharedRoomMode,
|
|
22
|
+
upsertSharedAgent
|
|
22
23
|
} from "./shared-project.js";
|
|
23
24
|
|
|
24
25
|
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
@@ -158,6 +159,12 @@ function buildDiscordHelp() {
|
|
|
158
159
|
"/project show the current project and sharing state",
|
|
159
160
|
"/project share enable Roundtable on the current project and bind this Discord channel",
|
|
160
161
|
"/room show shared room status for the current project",
|
|
162
|
+
"/people list recently seen Discord users in this channel",
|
|
163
|
+
"/terminals list live Waterbrother terminals for this project",
|
|
164
|
+
"/executor show the selected executor",
|
|
165
|
+
"/reviewer show the assigned reviewer",
|
|
166
|
+
"/verifier show the assigned verifier",
|
|
167
|
+
"/assign-role <terminal|this terminal> <executor|reviewer|verifier|standby> assign a room role",
|
|
161
168
|
"/members list shared room members",
|
|
162
169
|
"/tasks list shared room tasks",
|
|
163
170
|
"/events list recent shared room events",
|
|
@@ -282,6 +289,74 @@ function formatDiscordEvents(events = []) {
|
|
|
282
289
|
].join("\n");
|
|
283
290
|
}
|
|
284
291
|
|
|
292
|
+
function formatDiscordPeople({ people = [], project = null } = {}) {
|
|
293
|
+
if (!people.length) {
|
|
294
|
+
return [
|
|
295
|
+
"Recent Discord people",
|
|
296
|
+
"- none",
|
|
297
|
+
"Talk in this channel first, then run /people again."
|
|
298
|
+
].join("\n");
|
|
299
|
+
}
|
|
300
|
+
const members = Array.isArray(project?.members) ? project.members : [];
|
|
301
|
+
const pendingInvites = Array.isArray(project?.pendingInvites) ? project.pendingInvites : [];
|
|
302
|
+
return [
|
|
303
|
+
"Recent Discord people",
|
|
304
|
+
...people.map((person) => {
|
|
305
|
+
const member = members.find((entry) => String(entry?.id || "").trim() === person.userId);
|
|
306
|
+
const pendingInvite = pendingInvites.find((entry) => String(entry?.memberId || "").trim() === person.userId);
|
|
307
|
+
const bits = [`[${person.userId}]`];
|
|
308
|
+
if (person.usernameHandle) bits.push(person.usernameHandle);
|
|
309
|
+
bits.push(person.displayName || person.userId);
|
|
310
|
+
if (member?.role) bits.push(`member:${member.role}`);
|
|
311
|
+
else if (pendingInvite?.id) bits.push(`pending:${pendingInvite.id}`);
|
|
312
|
+
else if (person.paired) bits.push("paired");
|
|
313
|
+
return `- ${bits.join(" ")}`;
|
|
314
|
+
}),
|
|
315
|
+
"Invite by id, @username, full name, or mention with /invite."
|
|
316
|
+
].join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getAgentOwnerDisplay(agent = {}, fallback = "") {
|
|
320
|
+
return String(agent?.ownerName || fallback || agent?.label || agent?.ownerId || agent?.id || "unknown").trim() || "unknown";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getAgentTerminalDisplay(agent = {}, fallback = "") {
|
|
324
|
+
return String(agent?.label || fallback || agent?.ownerName || agent?.ownerId || agent?.id || "terminal").trim() || "terminal";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatBridgeHostLabel(host = {}) {
|
|
328
|
+
const owner = String(host?.ownerName || host?.ownerId || "").trim();
|
|
329
|
+
const label = String(host?.label || "").trim();
|
|
330
|
+
const sessionSuffix = String(host?.sessionId || "").trim().slice(-6);
|
|
331
|
+
const runtime = host?.provider && host?.model ? `${host.provider}/${host.model}` : "";
|
|
332
|
+
const primary = label || owner || (sessionSuffix ? `terminal ${sessionSuffix}` : "live terminal");
|
|
333
|
+
return [primary, owner && label && owner !== label ? `(${owner})` : "", runtime ? `[${runtime}]` : ""].filter(Boolean).join(" ").trim();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function chooseAgentByRole(project = {}, role = "") {
|
|
337
|
+
return (Array.isArray(project?.agents) ? project.agents : []).find((agent) => String(agent?.role || "").trim() === String(role || "").trim()) || null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function formatDiscordAgentStatus(title, agent = null, options = {}) {
|
|
341
|
+
if (!agent) return `${title}\nNo ${String(title || "").toLowerCase()} is assigned yet.`;
|
|
342
|
+
return [
|
|
343
|
+
title,
|
|
344
|
+
`owner: ${getAgentOwnerDisplay(agent)}`,
|
|
345
|
+
`terminal: ${getAgentTerminalDisplay(agent)}`,
|
|
346
|
+
`role: ${agent.role || "standby"}`,
|
|
347
|
+
`runtime: ${agent.provider && agent.model ? `${agent.provider}/${agent.model}` : "unknown"}`,
|
|
348
|
+
`live: ${options.live ? "yes" : "no"}`
|
|
349
|
+
].join("\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function formatDiscordLiveHosts(hosts = []) {
|
|
353
|
+
if (!hosts.length) return "Live terminals\n- none";
|
|
354
|
+
return [
|
|
355
|
+
"Live terminals",
|
|
356
|
+
...hosts.map((host) => `- ${formatBridgeHostLabel(host)}${host?.surface ? ` (${host.surface})` : ""}`)
|
|
357
|
+
].join("\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
285
360
|
function parseDiscordInviteCommand(message = {}, text = "") {
|
|
286
361
|
const value = String(text || "").trim();
|
|
287
362
|
const match = value.match(/^\/?(?:room\s+)?invite\s+(.+)$/i);
|
|
@@ -302,6 +377,197 @@ function parseDiscordInviteCommand(message = {}, text = "") {
|
|
|
302
377
|
return { memberId, memberName, role };
|
|
303
378
|
}
|
|
304
379
|
|
|
380
|
+
function knownPersonKey(chatId, userId) {
|
|
381
|
+
return `${String(chatId || "").trim()}:${String(userId || "").trim()}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function listKnownDiscordPeople(state, message = {}) {
|
|
385
|
+
const chatId = String(message?.channel_id || "").trim();
|
|
386
|
+
const byId = new Map();
|
|
387
|
+
const upsert = (person = {}) => {
|
|
388
|
+
const userId = String(person.userId || person.id || "").trim();
|
|
389
|
+
if (!userId) return;
|
|
390
|
+
const existing = byId.get(userId) || { userId, usernameHandle: "", displayName: userId, paired: false, lastSeenAt: "" };
|
|
391
|
+
byId.set(userId, {
|
|
392
|
+
...existing,
|
|
393
|
+
...person,
|
|
394
|
+
userId,
|
|
395
|
+
username: String(person.username || existing.username || "").trim(),
|
|
396
|
+
usernameHandle: String(person.usernameHandle || existing.usernameHandle || "").trim(),
|
|
397
|
+
displayName: String(person.displayName || existing.displayName || userId).trim() || userId,
|
|
398
|
+
paired: person.paired === true || existing.paired === true,
|
|
399
|
+
lastSeenAt: String(person.lastSeenAt || existing.lastSeenAt || "").trim()
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
for (const person of Object.values(state?.knownPeople || {})) {
|
|
404
|
+
if (String(person?.chatId || "").trim() !== chatId) continue;
|
|
405
|
+
upsert(person);
|
|
406
|
+
}
|
|
407
|
+
for (const [userId, peer] of Object.entries(state?.peers || {})) {
|
|
408
|
+
if (String(peer?.chatId || "").trim() !== chatId) continue;
|
|
409
|
+
upsert({
|
|
410
|
+
userId,
|
|
411
|
+
username: String(peer?.username || "").trim(),
|
|
412
|
+
usernameHandle: String(peer?.usernameHandle || "").trim(),
|
|
413
|
+
displayName: String(peer?.displayName || peer?.username || userId).trim(),
|
|
414
|
+
paired: true,
|
|
415
|
+
lastSeenAt: String(peer?.lastSeenAt || "").trim()
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const current = describeDiscordUser(message);
|
|
420
|
+
if (current.userId) upsert({ ...current, paired: Boolean(state?.peers?.[current.userId]) });
|
|
421
|
+
for (const mention of Array.isArray(message?.mentions) ? message.mentions : []) {
|
|
422
|
+
const mentioned = {
|
|
423
|
+
userId: String(mention?.id || "").trim(),
|
|
424
|
+
username: String(mention?.username || "").trim(),
|
|
425
|
+
usernameHandle: mention?.username ? `@${String(mention.username).trim()}` : "",
|
|
426
|
+
displayName: String(mention?.global_name || mention?.username || mention?.id || "").trim()
|
|
427
|
+
};
|
|
428
|
+
if (mentioned.userId) upsert(mentioned);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return [...byId.values()].sort((a, b) => {
|
|
432
|
+
const aSeen = Date.parse(String(a.lastSeenAt || "").trim()) || 0;
|
|
433
|
+
const bSeen = Date.parse(String(b.lastSeenAt || "").trim()) || 0;
|
|
434
|
+
return bSeen - aSeen || String(a.displayName || "").localeCompare(String(b.displayName || ""));
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function rememberKnownDiscordPeople(state, message = {}) {
|
|
439
|
+
const chatId = String(message?.channel_id || "").trim();
|
|
440
|
+
if (!chatId) return false;
|
|
441
|
+
const entries = [];
|
|
442
|
+
const author = describeDiscordUser(message);
|
|
443
|
+
if (author.userId) {
|
|
444
|
+
entries.push({
|
|
445
|
+
chatId,
|
|
446
|
+
userId: author.userId,
|
|
447
|
+
username: author.username,
|
|
448
|
+
usernameHandle: author.usernameHandle,
|
|
449
|
+
displayName: author.displayName,
|
|
450
|
+
paired: Boolean(state?.peers?.[author.userId]),
|
|
451
|
+
lastSeenAt: new Date().toISOString()
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
for (const mention of Array.isArray(message?.mentions) ? message.mentions : []) {
|
|
455
|
+
const userId = String(mention?.id || "").trim();
|
|
456
|
+
if (!userId) continue;
|
|
457
|
+
entries.push({
|
|
458
|
+
chatId,
|
|
459
|
+
userId,
|
|
460
|
+
username: String(mention?.username || "").trim(),
|
|
461
|
+
usernameHandle: mention?.username ? `@${String(mention.username).trim()}` : "",
|
|
462
|
+
displayName: String(mention?.global_name || mention?.username || mention?.id || "").trim(),
|
|
463
|
+
paired: Boolean(state?.peers?.[userId]),
|
|
464
|
+
lastSeenAt: new Date().toISOString()
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (!entries.length) return false;
|
|
468
|
+
const knownPeople = state.knownPeople && typeof state.knownPeople === "object" ? { ...state.knownPeople } : {};
|
|
469
|
+
for (const person of entries) {
|
|
470
|
+
knownPeople[knownPersonKey(person.chatId, person.userId)] = {
|
|
471
|
+
...(knownPeople[knownPersonKey(person.chatId, person.userId)] || {}),
|
|
472
|
+
...person
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const trimmed = Object.entries(knownPeople)
|
|
476
|
+
.sort((a, b) => (Date.parse(String(b[1]?.lastSeenAt || "").trim()) || 0) - (Date.parse(String(a[1]?.lastSeenAt || "").trim()) || 0))
|
|
477
|
+
.slice(0, 250);
|
|
478
|
+
state.knownPeople = Object.fromEntries(trimmed);
|
|
479
|
+
await persistDiscordGatewayState(state);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function resolveDiscordInviteTarget(state, message = {}, rawTarget = "") {
|
|
484
|
+
const direct = parseDiscordInviteCommand(message, `/invite ${String(rawTarget || "").trim()}`);
|
|
485
|
+
if (direct?.memberId && (direct.memberId === direct.memberName || direct.memberId.match(/^\d+$/) || String(rawTarget || "").includes("<@"))) {
|
|
486
|
+
return direct;
|
|
487
|
+
}
|
|
488
|
+
const value = String(rawTarget || "").trim();
|
|
489
|
+
if (!value) return direct;
|
|
490
|
+
const known = listKnownDiscordPeople(state, message);
|
|
491
|
+
if (/^\d+$/.test(value)) {
|
|
492
|
+
const match = known.find((person) => person.userId === value);
|
|
493
|
+
return { memberId: value, memberName: match?.displayName || value, role: direct?.role || "editor" };
|
|
494
|
+
}
|
|
495
|
+
const normalized = value.replace(/^@/, "").trim().toLowerCase();
|
|
496
|
+
const byUsername = known.filter((person) => String(person.usernameHandle || "").trim().toLowerCase() === `@${normalized}` || String(person.username || "").trim().toLowerCase() === normalized);
|
|
497
|
+
if (byUsername.length === 1) {
|
|
498
|
+
return { memberId: byUsername[0].userId, memberName: byUsername[0].displayName || byUsername[0].userId, role: direct?.role || "editor" };
|
|
499
|
+
}
|
|
500
|
+
if (byUsername.length > 1) {
|
|
501
|
+
throw new Error(`Multiple Discord users matched @${normalized}. Use /people and invite by id.`);
|
|
502
|
+
}
|
|
503
|
+
const exact = known.find((person) => String(person.displayName || "").trim().toLowerCase() === normalized);
|
|
504
|
+
if (exact) {
|
|
505
|
+
return { memberId: exact.userId, memberName: exact.displayName || exact.userId, role: direct?.role || "editor" };
|
|
506
|
+
}
|
|
507
|
+
const partial = known.filter((person) => String(person.displayName || "").trim().toLowerCase().includes(normalized));
|
|
508
|
+
if (partial.length === 1) {
|
|
509
|
+
return { memberId: partial[0].userId, memberName: partial[0].displayName || partial[0].userId, role: direct?.role || "editor" };
|
|
510
|
+
}
|
|
511
|
+
if (partial.length > 1) {
|
|
512
|
+
throw new Error(`Multiple Discord users matched ${value}. Use /people and invite by id or @username.`);
|
|
513
|
+
}
|
|
514
|
+
return direct;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function resolveDiscordAgentTarget(project = {}, liveHosts = [], rawTarget = "") {
|
|
518
|
+
const value = String(rawTarget || "").trim().toLowerCase();
|
|
519
|
+
if (!value) return null;
|
|
520
|
+
if (/^(?:this|my)(?:\s+terminal)?$/.test(value)) {
|
|
521
|
+
const host = liveHosts[0] || null;
|
|
522
|
+
if (!host) return null;
|
|
523
|
+
return {
|
|
524
|
+
id: `agent:discord-bridge:${String(host.sessionId || host.pid || "current").trim()}`,
|
|
525
|
+
ownerId: String(host.ownerId || "").trim(),
|
|
526
|
+
ownerName: String(host.ownerName || "").trim(),
|
|
527
|
+
label: String(host.label || "").trim(),
|
|
528
|
+
surface: String(host.surface || "live-tui").trim(),
|
|
529
|
+
provider: String(host.provider || "").trim(),
|
|
530
|
+
model: String(host.model || "").trim(),
|
|
531
|
+
runtimeProfile: String(host.runtimeProfile || "").trim(),
|
|
532
|
+
sessionId: String(host.sessionId || "").trim(),
|
|
533
|
+
cwd: String(host.cwd || "").trim(),
|
|
534
|
+
role: "standby"
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (["executor", "reviewer", "verifier"].includes(value)) {
|
|
538
|
+
return chooseAgentByRole(project, value);
|
|
539
|
+
}
|
|
540
|
+
const candidates = [
|
|
541
|
+
...(Array.isArray(project?.agents) ? project.agents : []),
|
|
542
|
+
...liveHosts.map((host) => ({
|
|
543
|
+
id: `agent:discord-bridge:${String(host.sessionId || host.pid || "current").trim()}`,
|
|
544
|
+
ownerId: String(host.ownerId || "").trim(),
|
|
545
|
+
ownerName: String(host.ownerName || "").trim(),
|
|
546
|
+
label: String(host.label || "").trim(),
|
|
547
|
+
surface: String(host.surface || "live-tui").trim(),
|
|
548
|
+
provider: String(host.provider || "").trim(),
|
|
549
|
+
model: String(host.model || "").trim(),
|
|
550
|
+
runtimeProfile: String(host.runtimeProfile || "").trim(),
|
|
551
|
+
sessionId: String(host.sessionId || "").trim(),
|
|
552
|
+
cwd: String(host.cwd || "").trim(),
|
|
553
|
+
role: "standby"
|
|
554
|
+
}))
|
|
555
|
+
];
|
|
556
|
+
const exact = candidates.find((agent) => {
|
|
557
|
+
const label = String(agent?.label || "").trim().toLowerCase();
|
|
558
|
+
const owner = String(agent?.ownerName || "").trim().toLowerCase();
|
|
559
|
+
const sessionId = String(agent?.sessionId || "").trim().toLowerCase();
|
|
560
|
+
return value === label || value === owner || value === sessionId;
|
|
561
|
+
});
|
|
562
|
+
if (exact) return exact;
|
|
563
|
+
const partial = candidates.filter((agent) => {
|
|
564
|
+
const label = String(agent?.label || "").trim().toLowerCase();
|
|
565
|
+
const owner = String(agent?.ownerName || "").trim().toLowerCase();
|
|
566
|
+
return (label && label.includes(value)) || (owner && owner.includes(value));
|
|
567
|
+
});
|
|
568
|
+
return partial.length === 1 ? partial[0] : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
305
571
|
async function loadDiscordGatewayState() {
|
|
306
572
|
return loadGatewayState("discord");
|
|
307
573
|
}
|
|
@@ -710,6 +976,74 @@ async function handleDiscordControlCommand(runtime, state, message, rawText) {
|
|
|
710
976
|
"Use /room to inspect room state."
|
|
711
977
|
].join("\n");
|
|
712
978
|
}
|
|
979
|
+
if (normalized === "/terminals" || normalized === "terminals" || normalized === "/live" || normalized === "live" || normalizedRoomAlias === "terminals") {
|
|
980
|
+
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
981
|
+
const session = await loadSession(sessionId).catch(() => null);
|
|
982
|
+
const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
|
|
983
|
+
return formatDiscordLiveHosts(liveHosts);
|
|
984
|
+
}
|
|
985
|
+
if (normalized === "/executor" || normalized === "executor" || normalizedRoomAlias === "executor") {
|
|
986
|
+
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
987
|
+
const { project, session } = await bindSharedRoomForMessage(message, sessionId);
|
|
988
|
+
if (!project?.enabled) return "This project is not shared.";
|
|
989
|
+
const agent = chooseAgentByRole(project, "executor");
|
|
990
|
+
const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
|
|
991
|
+
const live = agent ? liveHosts.some((host) => String(host?.ownerId || "") === String(agent?.ownerId || "") || String(host?.sessionId || "") === String(agent?.sessionId || "")) : false;
|
|
992
|
+
return formatDiscordAgentStatus("Executor", agent, { live });
|
|
993
|
+
}
|
|
994
|
+
if (normalized === "/reviewer" || normalized === "reviewer" || normalizedRoomAlias === "reviewer") {
|
|
995
|
+
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
996
|
+
const { project, session } = await bindSharedRoomForMessage(message, sessionId);
|
|
997
|
+
if (!project?.enabled) return "This project is not shared.";
|
|
998
|
+
const agent = chooseAgentByRole(project, "reviewer");
|
|
999
|
+
const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
|
|
1000
|
+
const live = agent ? liveHosts.some((host) => String(host?.ownerId || "") === String(agent?.ownerId || "") || String(host?.sessionId || "") === String(agent?.sessionId || "")) : false;
|
|
1001
|
+
return formatDiscordAgentStatus("Reviewer", agent, { live });
|
|
1002
|
+
}
|
|
1003
|
+
if (normalized === "/verifier" || normalized === "verifier" || normalizedRoomAlias === "verifier") {
|
|
1004
|
+
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
1005
|
+
const { project, session } = await bindSharedRoomForMessage(message, sessionId);
|
|
1006
|
+
if (!project?.enabled) return "This project is not shared.";
|
|
1007
|
+
const agent = chooseAgentByRole(project, "verifier");
|
|
1008
|
+
const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
|
|
1009
|
+
const live = agent ? liveHosts.some((host) => String(host?.ownerId || "") === String(agent?.ownerId || "") || String(host?.sessionId || "") === String(agent?.sessionId || "")) : false;
|
|
1010
|
+
return formatDiscordAgentStatus("Verifier", agent, { live });
|
|
1011
|
+
}
|
|
1012
|
+
if (normalized.startsWith("/assign-role ") || normalizedRoomAlias.startsWith("assign-role ")) {
|
|
1013
|
+
const body = normalized.startsWith("/assign-role ") ? value.slice("/assign-role ".length).trim() : roomAlias.slice("assign-role ".length).trim();
|
|
1014
|
+
const match = body.match(/^(.+?)\s+(executor|reviewer|verifier|standby)\s*$/i);
|
|
1015
|
+
if (!match) return "Usage: /assign-role <terminal|this terminal> <executor|reviewer|verifier|standby>";
|
|
1016
|
+
const targetText = String(match[1] || "").trim();
|
|
1017
|
+
const nextRole = String(match[2] || "").trim().toLowerCase();
|
|
1018
|
+
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
1019
|
+
const { project, session } = await bindSharedRoomForMessage(message, sessionId);
|
|
1020
|
+
if (!project?.enabled) return "This project is not shared.";
|
|
1021
|
+
const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
|
|
1022
|
+
const targetAgent = resolveDiscordAgentTarget(project, liveHosts, targetText);
|
|
1023
|
+
if (!targetAgent) {
|
|
1024
|
+
return `No terminal found for ${targetText}. Use /terminals first.`;
|
|
1025
|
+
}
|
|
1026
|
+
const actor = describeDiscordUser(message);
|
|
1027
|
+
const nextProject = await upsertSharedAgent(session.cwd || process.cwd(), {
|
|
1028
|
+
...targetAgent,
|
|
1029
|
+
role: nextRole
|
|
1030
|
+
}, {
|
|
1031
|
+
actorId: actor.userId,
|
|
1032
|
+
actorName: actor.displayName
|
|
1033
|
+
});
|
|
1034
|
+
return [
|
|
1035
|
+
"Room role updated",
|
|
1036
|
+
`owner: ${getAgentOwnerDisplay(targetAgent, actor.displayName || actor.userId)}`,
|
|
1037
|
+
`terminal: ${getAgentTerminalDisplay(targetAgent, "terminal")}`,
|
|
1038
|
+
`role: ${nextRole}`,
|
|
1039
|
+
`project: ${nextProject.projectName || "project"}`
|
|
1040
|
+
].join("\n");
|
|
1041
|
+
}
|
|
1042
|
+
if (normalized === "/people" || normalized === "people" || normalizedRoomAlias === "people") {
|
|
1043
|
+
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
1044
|
+
const { project } = await bindSharedRoomForMessage(message, sessionId);
|
|
1045
|
+
return formatDiscordPeople({ people: listKnownDiscordPeople(state, message), project });
|
|
1046
|
+
}
|
|
713
1047
|
if (normalized === "/events" || normalized === "events" || normalizedRoomAlias === "events") {
|
|
714
1048
|
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
715
1049
|
const { session, project } = await bindSharedRoomForMessage(message, sessionId);
|
|
@@ -730,10 +1064,10 @@ async function handleDiscordControlCommand(runtime, state, message, rawText) {
|
|
|
730
1064
|
|| normalizedRoomAlias.startsWith("invite ")
|
|
731
1065
|
) {
|
|
732
1066
|
const sessionId = await ensurePeerSession(runtime, state, message);
|
|
733
|
-
const sourceText = normalizedRoomAlias.startsWith("invite ") ?
|
|
734
|
-
const inviteRequest =
|
|
1067
|
+
const sourceText = normalizedRoomAlias.startsWith("invite ") ? roomAlias.slice("invite ".length).trim() : value.replace(/^\/?invite\s+/i, "").trim();
|
|
1068
|
+
const inviteRequest = resolveDiscordInviteTarget(state, message, sourceText);
|
|
735
1069
|
if (!inviteRequest?.memberId) {
|
|
736
|
-
return "Usage: /invite <@user|user-id> [owner|editor|observer]";
|
|
1070
|
+
return "Usage: /invite <@user|user-id|name> [owner|editor|observer]";
|
|
737
1071
|
}
|
|
738
1072
|
const { session, project } = await bindSharedRoomForMessage(message, sessionId);
|
|
739
1073
|
if (!project?.enabled) return "This project is not shared.";
|
|
@@ -926,6 +1260,34 @@ async function getLiveBridgeHost({ cwd = "" } = {}) {
|
|
|
926
1260
|
return matchingHosts[0] || null;
|
|
927
1261
|
}
|
|
928
1262
|
|
|
1263
|
+
async function getLiveBridgeHosts({ cwd = "" } = {}) {
|
|
1264
|
+
const bridge = await loadGatewayBridge("discord");
|
|
1265
|
+
const hosts = Array.isArray(bridge.hosts) ? bridge.hosts : [];
|
|
1266
|
+
const nextHosts = [];
|
|
1267
|
+
let changed = false;
|
|
1268
|
+
for (const host of hosts) {
|
|
1269
|
+
const pid = Number(host?.pid || 0);
|
|
1270
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
1271
|
+
changed = true;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
process.kill(pid, 0);
|
|
1276
|
+
} catch {
|
|
1277
|
+
changed = true;
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
nextHosts.push(host);
|
|
1281
|
+
}
|
|
1282
|
+
if (changed || nextHosts.length !== hosts.length) {
|
|
1283
|
+
bridge.hosts = nextHosts;
|
|
1284
|
+
await saveGatewayBridge("discord", bridge);
|
|
1285
|
+
}
|
|
1286
|
+
return cwd
|
|
1287
|
+
? nextHosts.filter((host) => !host?.cwd || String(host.cwd) === String(cwd || ""))
|
|
1288
|
+
: nextHosts;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
929
1291
|
async function runPromptViaBridge(runtime, message, promptText, options = {}) {
|
|
930
1292
|
const host = await getLiveBridgeHost();
|
|
931
1293
|
if (!host) {
|
|
@@ -1124,10 +1486,12 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
1124
1486
|
if (!msg || msg.author?.bot) return;
|
|
1125
1487
|
const scope = msg.guild_id ? `guild:${msg.guild_id}` : "dm";
|
|
1126
1488
|
log(`discord: ${scope} #${msg.channel_id} ${msg.author?.username || "unknown"} -> ${String(msg.content || "").trim()}`);
|
|
1127
|
-
const shouldReply = shouldReplyToMessage(msg, botUser?.id);
|
|
1128
|
-
const rawText = extractMentionContent(String(msg.content || "").trim(), botUser?.id);
|
|
1129
|
-
const reply = shouldReply ? buildReply(msg, botUser?.id) : null;
|
|
1130
1489
|
try {
|
|
1490
|
+
const trackingState = await loadDiscordGatewayState();
|
|
1491
|
+
await rememberKnownDiscordPeople(trackingState, msg);
|
|
1492
|
+
const shouldReply = shouldReplyToMessage(msg, botUser?.id);
|
|
1493
|
+
const rawText = extractMentionContent(String(msg.content || "").trim(), botUser?.id);
|
|
1494
|
+
const reply = shouldReply ? buildReply(msg, botUser?.id) : null;
|
|
1131
1495
|
if (reply) {
|
|
1132
1496
|
await sendChannelMessage(discord, msg.channel_id, reply);
|
|
1133
1497
|
log(`discord: replied in ${msg.channel_id}`);
|
package/src/gateway-state.js
CHANGED
|
@@ -19,6 +19,7 @@ function gatewayBridgePath(serviceId) {
|
|
|
19
19
|
|
|
20
20
|
function normalizeGatewayState(parsed = {}) {
|
|
21
21
|
const continuations = parsed?.continuations && typeof parsed.continuations === "object" ? parsed.continuations : {};
|
|
22
|
+
const knownPeople = parsed?.knownPeople && typeof parsed.knownPeople === "object" ? parsed.knownPeople : {};
|
|
22
23
|
return {
|
|
23
24
|
offset: Number.isFinite(Number(parsed?.offset)) ? Math.max(0, Math.floor(Number(parsed.offset))) : 0,
|
|
24
25
|
peers: parsed?.peers && typeof parsed.peers === "object" ? parsed.peers : {},
|
|
@@ -26,6 +27,20 @@ function normalizeGatewayState(parsed = {}) {
|
|
|
26
27
|
announcedEvents: Array.isArray(parsed?.announcedEvents)
|
|
27
28
|
? parsed.announcedEvents.map((value) => String(value || "").trim()).filter(Boolean).slice(-100)
|
|
28
29
|
: [],
|
|
30
|
+
knownPeople: Object.fromEntries(
|
|
31
|
+
Object.entries(knownPeople).map(([key, item]) => [
|
|
32
|
+
key,
|
|
33
|
+
{
|
|
34
|
+
chatId: String(item?.chatId || "").trim(),
|
|
35
|
+
userId: String(item?.userId || "").trim(),
|
|
36
|
+
username: String(item?.username || "").trim(),
|
|
37
|
+
usernameHandle: String(item?.usernameHandle || "").trim(),
|
|
38
|
+
displayName: String(item?.displayName || item?.username || item?.userId || "").trim(),
|
|
39
|
+
paired: item?.paired === true,
|
|
40
|
+
lastSeenAt: String(item?.lastSeenAt || "").trim()
|
|
41
|
+
}
|
|
42
|
+
]).filter(([, item]) => item.chatId && item.userId)
|
|
43
|
+
),
|
|
29
44
|
continuations: Object.fromEntries(
|
|
30
45
|
Object.entries(continuations).map(([key, item]) => [
|
|
31
46
|
key,
|
|
@@ -154,7 +169,7 @@ export async function loadGatewayState(serviceId) {
|
|
|
154
169
|
return normalizeGatewayState(JSON.parse(raw));
|
|
155
170
|
} catch (error) {
|
|
156
171
|
if (error?.code === "ENOENT") {
|
|
157
|
-
return { offset: 0, peers: {}, pendingPairings: {}, announcedEvents: [], continuations: {} };
|
|
172
|
+
return { offset: 0, peers: {}, pendingPairings: {}, announcedEvents: [], knownPeople: {}, continuations: {} };
|
|
158
173
|
}
|
|
159
174
|
throw error;
|
|
160
175
|
}
|