evOo-local/packages/core/src/group-mentions.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

82 lines
2.6 KiB
TypeScript

export type GroupMemberRef = {
id: string;
name: string;
};
const MENTION_PATTERN = /@([A-Za-z0-9][A-Za-z0-9_-]{0,39})/g;
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function hasMentionToken(text: string, name: string): boolean {
const normalized = name.trim();
if (!normalized) return false;
return new RegExp(
`(?:^|[^\\p{L}\\p{N}_-])@${escapeRegExp(normalized)}(?![\\p{L}\\p{N}_-])`,
"iu",
).test(text);
}
export function parseMentionNames(text: string): string[] {
const names = new Set<string>();
for (const match of text.matchAll(MENTION_PATTERN)) {
const name = match[1];
if (name) names.add(name.toLowerCase());
}
return [...names];
}
export function resolveGroupTargetBotIds(input: {
text: string;
members: GroupMemberRef[];
/** Bot ids from typed mention chips (non-members are ignored for wake). */
explicitMentions?: string[];
}): string[] {
const membersById = new Map(input.members.map((member) => [member.id, member]));
const targetIds = new Set<string>();
for (const mentionId of input.explicitMentions ?? []) {
// Out-of-chat bots stay as @Name in the prompt; only members are woken here.
if (membersById.has(mentionId)) targetIds.add(mentionId);
}
if (hasMentionToken(input.text, "everyone")) {
for (const member of input.members) targetIds.add(member.id);
} else {
for (const member of input.members) {
if (hasMentionToken(input.text, member.name)) targetIds.add(member.id);
}
}
if (targetIds.size === 0 && input.members[0]) {
targetIds.add(input.members[0].id);
}
return [...targetIds];
}
export function inferHandoffTargetName(prompt: string): string | undefined {
const handoffMatch =
/hand(?:\s+this|\s+off|\s+it)?\s+to\s+@?([A-Za-z0-9][A-Za-z0-9_-]{0,39})/i.exec(prompt) ??
/@([A-Za-z0-9][A-Za-z0-9_-]{0,39})\s+take/i.exec(prompt);
return handoffMatch?.[1];
}
export function inferHandoffTargetBotId(
prompt: string,
members: GroupMemberRef[],
): string | undefined {
const handedTo = members.find((member) => {
const escaped = escapeRegExp(member.name.trim());
return escaped
? new RegExp(`\\bto\\s+@?${escaped}(?![\\p{L}\\p{N}_-])`, "iu").test(prompt)
: false;
});
if (handedTo) return handedTo.id;
const mentioned = members.find((member) => hasMentionToken(prompt, member.name));
if (mentioned) return mentioned.id;
const name = inferHandoffTargetName(prompt)?.toLowerCase();
return name ? members.find((member) => member.name.toLowerCase() === name)?.id : undefined;
}