(rawMessage: string, contexts: ChatContext[] | undefined)
| 216 | } |
| 217 | |
| 218 | function sanitizeMessageForDocs(rawMessage: string, contexts: ChatContext[] | undefined): string { |
| 219 | if (!rawMessage) return '' |
| 220 | if (!Array.isArray(contexts) || contexts.length === 0) { |
| 221 | // No context mapping; conservatively strip all @mentions-like tokens |
| 222 | const stripped = rawMessage |
| 223 | .replace(/(^|\s)@([^\s]+)/g, ' ') |
| 224 | .replace(/\s{2,}/g, ' ') |
| 225 | .trim() |
| 226 | return stripped |
| 227 | } |
| 228 | |
| 229 | // Gather labels by kind |
| 230 | const blockLabels = new Set( |
| 231 | contexts |
| 232 | .filter((c) => c.kind === 'blocks') |
| 233 | .map((c) => c.label) |
| 234 | .filter((l): l is string => typeof l === 'string' && l.length > 0) |
| 235 | ) |
| 236 | const nonBlockLabels = new Set( |
| 237 | contexts |
| 238 | .filter((c) => c.kind !== 'blocks') |
| 239 | .map((c) => c.label) |
| 240 | .filter((l): l is string => typeof l === 'string' && l.length > 0) |
| 241 | ) |
| 242 | |
| 243 | let result = rawMessage |
| 244 | |
| 245 | // 1) Remove all non-block mentions entirely |
| 246 | for (const label of nonBlockLabels) { |
| 247 | const pattern = new RegExp(`(^|\\s)@${escapeRegExp(label)}(?!\\S)`, 'g') |
| 248 | result = result.replace(pattern, ' ') |
| 249 | } |
| 250 | |
| 251 | // 2) For block mentions, strip the '@' but keep the block name |
| 252 | for (const label of blockLabels) { |
| 253 | const pattern = new RegExp(`@${escapeRegExp(label)}(?!\\S)`, 'g') |
| 254 | result = result.replace(pattern, label) |
| 255 | } |
| 256 | |
| 257 | // 3) Remove any remaining @mentions (unknown or not in contexts) |
| 258 | result = result.replace(/(^|\s)@([^\s]+)/g, ' ') |
| 259 | |
| 260 | // Normalize whitespace |
| 261 | result = result.replace(/\s{2,}/g, ' ').trim() |
| 262 | return result |
| 263 | } |
| 264 | |
| 265 | async function processSkillFromDb( |
| 266 | skillId: string, |
no test coverage detected