godcrm/backend/services/chat/mention-parsers.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

137 lines
4.9 KiB
JavaScript

/**
* ADR-116 / ADR-117: Structured Invocation Token Parsers
*
* Centralized mention and command parsing for the chat system.
* Separates INVOCATION (triggers agent delegation) from REFERENCE (display only).
*
* Token formats:
* <<@slug>> — mention invocation (triggers agent)
* <</slug>> — slash invocation (triggers agent)
* @slug — mention reference (display only, no trigger)
* /slug — command reference (display only, no trigger)
*
* Used by:
* - backend/routes/v3/chat.js — agent delegation + user message handling
* - Frontend display layer — rendering pills/chips
*/
/**
* Parse @mentions from message content (legacy, catches ALL @-prefixed words).
* Used for backward-compatible user input processing.
* @param {string} content - Message content
* @returns {string[]} Array of mentioned usernames (without @)
*/
export function parseMentions(content) {
if (!content || typeof content !== 'string') return [];
const mentionPattern = /@([a-z0-9_-]+)/gi;
const matches = content.match(mentionPattern) || [];
return matches.map(m => m.substring(1).toLowerCase());
}
/**
* ADR-117: Parse explicit agent delegation syntax from text.
* Only <<@slug>> triggers delegation. Plain @mentions are references only.
* @param {string} content - Message text
* @returns {string[]} Array of agent slugs to delegate to
*/
export function parseDelegations(content) {
if (!content || typeof content !== 'string') return [];
// Strip code blocks and inline code to avoid false matches when text
// merely discusses the <<@slug>> syntax (e.g. in reasoning/docs).
const stripped = content
.replace(/```[\s\S]*?```/g, '') // fenced code blocks
.replace(/`[^`]+`/g, ''); // inline code spans
const delegationPattern = /<<@([a-z0-9_-]+)>>/gi;
const matches = [];
let match;
while ((match = delegationPattern.exec(stripped)) !== null) {
matches.push(match[1].toLowerCase());
}
return matches;
}
/**
* ADR-116: Parse INVOCATION mentions — only <<@slug>> triggers delegation.
* Alias for parseDelegations() with unified naming convention.
* @param {string} content
* @returns {string[]} Array of slugs (lowercase)
*/
export function parseInvocationMentions(content) {
return parseDelegations(content);
}
/**
* ADR-116: Parse INVOCATION slash commands — only <</slug>> triggers delegation.
* Supports optional command index: <</slug/N>> where N picks from main_instruction JSON array.
* @param {string} content
* @returns {Array<{slug: string, commandIndex: number|null}>} Array of parsed commands
*/
export function parseInvocationCommands(content) {
if (!content || typeof content !== 'string') return [];
// Strip code blocks and inline code to avoid false matches
const stripped = content
.replace(/```[\s\S]*?```/g, '')
.replace(/`[^`]+`/g, '');
const pattern = /<<\/([a-z][a-z0-9_-]*)(?:\/(\d+))?>>/gi;
const matches = [];
let match;
while ((match = pattern.exec(stripped)) !== null) {
matches.push({
slug: match[1].toLowerCase(),
commandIndex: match[2] != null ? parseInt(match[2], 10) : null,
});
}
return matches;
}
/**
* ADR-116: Parse REFERENCE mentions — @slug outside of <<@...>> tokens.
* Used for display highlighting only, NOT for delegation.
* @param {string} content
* @returns {string[]}
*/
export function parseReferenceMentions(content) {
if (!content || typeof content !== 'string') return [];
// Remove all invocation tokens first
const cleaned = content.replace(/<<@[a-z0-9_-]+>>/gi, '');
const pattern = /@([a-z0-9_-]+)/gi;
const matches = cleaned.match(pattern) || [];
return matches.map(m => m.substring(1).toLowerCase());
}
/**
* ADR-116: Parse REFERENCE slash commands — /slug outside of <</...>> tokens.
* Used for display highlighting only, NOT for delegation.
* @param {string} content
* @returns {string[]}
*/
export function parseReferenceCommands(content) {
if (!content || typeof content !== 'string') return [];
// Remove all invocation tokens first
const cleaned = content.replace(/<<\/[a-z][a-z0-9_-]*>>/gi, '');
const pattern = /(?:^|\s)\/([a-z][a-z0-9_-]*)(?=\s|$)/gim;
const matches = [];
let match;
while ((match = pattern.exec(cleaned)) !== null) {
matches.push(match[1].toLowerCase());
}
return matches;
}
/**
* ADR-069: Parse /commands from message content (legacy, for user input).
* Only matches /command when preceded by start-of-string or whitespace,
* and followed by whitespace or end-of-string.
* @param {string} content - Message content
* @returns {string[]} Array of agent names (without /)
*/
export function parseAgentCommands(content) {
if (!content || typeof content !== 'string') return [];
const commandPattern = /(?:^|\s)\/([a-z][a-z0-9_-]*)(?=\s|$)/gim;
const matches = [];
let match;
while ((match = commandPattern.exec(content)) !== null) {
matches.push(match[1].toLowerCase());
}
return matches;
}