mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
feat: security observation types + Telegram notifier
Adds two severity-axis security observation types (security_alert, security_note) to the code mode and a fire-and-forget Telegram notifier that posts when a saved observation matches configured type or concept triggers. Default trigger fires on security_alert only; notifier is disabled until BOT_TOKEN and CHAT_ID are set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,20 @@
|
||||
"description": "Architectural/design choice with rationale",
|
||||
"emoji": "⚖️",
|
||||
"work_emoji": "⚖️"
|
||||
},
|
||||
{
|
||||
"id": "security_alert",
|
||||
"label": "Security Alert",
|
||||
"description": "A security issue that needs attention before continuing.",
|
||||
"emoji": "🚨",
|
||||
"work_emoji": "🚨"
|
||||
},
|
||||
{
|
||||
"id": "security_note",
|
||||
"label": "Security Note",
|
||||
"description": "A security-relevant observation worth recording, but not urgent.",
|
||||
"emoji": "🔐",
|
||||
"work_emoji": "🔐"
|
||||
}
|
||||
],
|
||||
"observation_concepts": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
100
src/services/integrations/TelegramNotifier.ts
Normal file
100
src/services/integrations/TelegramNotifier.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* TelegramNotifier
|
||||
*
|
||||
* Fire-and-forget Telegram notification module. Fires one message per observation
|
||||
* whose type or concepts match user-configured triggers. Never throws; all errors
|
||||
* are caught per-observation and logged as warnings. Bot token is never logged.
|
||||
*/
|
||||
|
||||
import { ParsedObservation } from '../../sdk/parser.js';
|
||||
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
export interface TelegramNotifyInput {
|
||||
observations: ParsedObservation[];
|
||||
observationIds: number[];
|
||||
project: string;
|
||||
memorySessionId: string;
|
||||
}
|
||||
|
||||
const MARKDOWN_V2_RESERVED = /[_*\[\]()~`>#+\-=|{}.!\\]/g;
|
||||
|
||||
function escapeMarkdownV2(value: string): string {
|
||||
return value.replace(MARKDOWN_V2_RESERVED, '\\$&');
|
||||
}
|
||||
|
||||
function splitCsv(value: string): string[] {
|
||||
return value
|
||||
.split(',')
|
||||
.map(entry => entry.trim())
|
||||
.filter(entry => entry.length > 0);
|
||||
}
|
||||
|
||||
function formatMessage(
|
||||
obs: ParsedObservation,
|
||||
project: string,
|
||||
memorySessionId: string,
|
||||
observationId: number,
|
||||
): string {
|
||||
const type = escapeMarkdownV2(obs.type);
|
||||
const title = escapeMarkdownV2(obs.title ?? '');
|
||||
const subtitle = escapeMarkdownV2(obs.subtitle ?? '');
|
||||
const projectEscaped = escapeMarkdownV2(project);
|
||||
const idEscaped = escapeMarkdownV2(String(observationId));
|
||||
return `🚨 *${type}* — ${title}\n${subtitle}\nProject: \`${projectEscaped}\` · obs \\#${idEscaped}`;
|
||||
}
|
||||
|
||||
async function postOne(botToken: string, chatId: string, text: string): Promise<void> {
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: 'MarkdownV2',
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const status = response.status;
|
||||
const statusText = response.statusText;
|
||||
throw new Error(`Telegram API responded ${status} ${statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function notifyTelegram(input: TelegramNotifyInput): Promise<void> {
|
||||
const botToken = SettingsDefaultsManager.get('CLAUDE_MEM_TELEGRAM_BOT_TOKEN');
|
||||
const chatId = SettingsDefaultsManager.get('CLAUDE_MEM_TELEGRAM_CHAT_ID');
|
||||
if (!botToken || !chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerTypes = splitCsv(SettingsDefaultsManager.get('CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES'));
|
||||
const triggerConcepts = splitCsv(SettingsDefaultsManager.get('CLAUDE_MEM_TELEGRAM_TRIGGER_CONCEPTS'));
|
||||
if (triggerTypes.length === 0 && triggerConcepts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { observations, observationIds, project, memorySessionId } = input;
|
||||
for (let i = 0; i < observations.length; i++) {
|
||||
const obs = observations[i];
|
||||
const matchesType = triggerTypes.includes(obs.type);
|
||||
const matchesConcept = obs.concepts.some(c => triggerConcepts.includes(c));
|
||||
if (!matchesType && !matchesConcept) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const observationId = observationIds[i];
|
||||
try {
|
||||
const text = formatMessage(obs, project, memorySessionId, observationId);
|
||||
await postOne(botToken, chatId, text);
|
||||
} catch (error) {
|
||||
logger.warn('TELEGRAM', 'Failed to send Telegram notification', {
|
||||
observationId,
|
||||
project,
|
||||
memorySessionId,
|
||||
type: obs.type,
|
||||
}, error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { logger } from '../../../utils/logger.js';
|
||||
import { parseObservations, parseSummary, type ParsedObservation, type ParsedSummary } from '../../../sdk/parser.js';
|
||||
import { SUMMARY_MODE_MARKER, MAX_CONSECUTIVE_SUMMARY_FAILURES } from '../../../sdk/prompts.js';
|
||||
import { updateCursorContextForProject } from '../../integrations/CursorHooksInstaller.js';
|
||||
import { notifyTelegram } from '../../integrations/TelegramNotifier.js';
|
||||
import { updateFolderClaudeMdFiles } from '../../../utils/claude-md-utils.js';
|
||||
import { getWorkerPort } from '../../../shared/worker-utils.js';
|
||||
import { SettingsDefaultsManager } from '../../../shared/SettingsDefaultsManager.js';
|
||||
@@ -213,6 +214,13 @@ export async function processAgentResponse(
|
||||
// Clear the tracking array after confirmation
|
||||
session.processingMessageIds = [];
|
||||
|
||||
void notifyTelegram({
|
||||
observations: labeledObservations,
|
||||
observationIds: result.observationIds,
|
||||
project: session.project,
|
||||
memorySessionId: session.memorySessionId,
|
||||
});
|
||||
|
||||
// AFTER transaction commits - async operations (can fail safely without data loss)
|
||||
await syncAndBroadcastObservations(
|
||||
observations,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CorpusBuilder } from '../../knowledge/CorpusBuilder.js';
|
||||
import { KnowledgeAgent } from '../../knowledge/KnowledgeAgent.js';
|
||||
import type { CorpusFilter } from '../../knowledge/types.js';
|
||||
|
||||
const ALLOWED_CORPUS_TYPES = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
||||
const ALLOWED_CORPUS_TYPES = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change', 'security_alert', 'security_note']);
|
||||
|
||||
export class CorpusRoutes extends BaseRouteHandler {
|
||||
constructor(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { logger } from '../../../../utils/logger.js';
|
||||
|
||||
type ObservationType = 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
|
||||
type ObservationType = 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change' | 'security_alert' | 'security_note';
|
||||
|
||||
/**
|
||||
* Valid observation types
|
||||
@@ -16,7 +16,9 @@ export const OBSERVATION_TYPES: ObservationType[] = [
|
||||
'feature',
|
||||
'refactor',
|
||||
'discovery',
|
||||
'change'
|
||||
'change',
|
||||
'security_alert',
|
||||
'security_note'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,6 +76,11 @@ export interface SettingsDefaults {
|
||||
CLAUDE_MEM_CHROMA_API_KEY: string;
|
||||
CLAUDE_MEM_CHROMA_TENANT: string;
|
||||
CLAUDE_MEM_CHROMA_DATABASE: string;
|
||||
// Telegram Notifier
|
||||
CLAUDE_MEM_TELEGRAM_BOT_TOKEN: string;
|
||||
CLAUDE_MEM_TELEGRAM_CHAT_ID: string;
|
||||
CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES: string;
|
||||
CLAUDE_MEM_TELEGRAM_TRIGGER_CONCEPTS: string;
|
||||
}
|
||||
|
||||
export class SettingsDefaultsManager {
|
||||
@@ -147,6 +152,11 @@ export class SettingsDefaultsManager {
|
||||
CLAUDE_MEM_CHROMA_API_KEY: '',
|
||||
CLAUDE_MEM_CHROMA_TENANT: 'default_tenant',
|
||||
CLAUDE_MEM_CHROMA_DATABASE: 'default_database',
|
||||
// Telegram Notifier
|
||||
CLAUDE_MEM_TELEGRAM_BOT_TOKEN: '',
|
||||
CLAUDE_MEM_TELEGRAM_CHAT_ID: '',
|
||||
CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES: 'security_alert',
|
||||
CLAUDE_MEM_TELEGRAM_TRIGGER_CONCEPTS: '',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ export enum LogLevel {
|
||||
SILENT = 4
|
||||
}
|
||||
|
||||
export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' | 'HTTP' | 'SESSION' | 'CHROMA' | 'CHROMA_MCP' | 'CHROMA_SYNC' | 'FOLDER_INDEX' | 'CLAUDE_MD' | 'QUEUE';
|
||||
export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' | 'HTTP' | 'SESSION' | 'CHROMA' | 'CHROMA_MCP' | 'CHROMA_SYNC' | 'FOLDER_INDEX' | 'CLAUDE_MD' | 'QUEUE' | 'TELEGRAM';
|
||||
|
||||
interface LogContext {
|
||||
sessionId?: number;
|
||||
|
||||
Reference in New Issue
Block a user