ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
220 lines (188 loc) • 7.01 kB
text/typescript
export interface TruncationOptions {
maxTokens?: number;
maxCharacters?: number;
preserveLatest?: boolean;
prioritizeErrors?: boolean;
}
export class LogTruncator {
// Rough estimate: 1 token ≈ 4 characters
private static readonly CHARS_PER_TOKEN = 4;
private static readonly DEFAULT_MAX_TOKENS = 8000; // Safe limit for most LLMs
/**
* Truncate logs intelligently to fit within token limits
*/
static truncateLogs(
logs: Array<{ type: string; text: string; timestamp: Date }>,
options: TruncationOptions = {}
): Array<{ type: string; text: string; timestamp: Date }> {
const {
maxTokens = this.DEFAULT_MAX_TOKENS,
maxCharacters = maxTokens * this.CHARS_PER_TOKEN,
preserveLatest = true,
prioritizeErrors = true
} = options;
if (logs.length === 0) return [];
// Calculate total size
const totalChars = logs.reduce((sum, log) => sum + log.text.length, 0);
// If within limits, return as-is
if (totalChars <= maxCharacters) {
return logs;
}
// Separate logs by type if prioritizing errors
if (prioritizeErrors) {
const errors = logs.filter(log => log.type === 'error' || log.type === 'warn');
const others = logs.filter(log => log.type !== 'error' && log.type !== 'warn');
// Calculate space for each category
const errorChars = errors.reduce((sum, log) => sum + log.text.length, 0);
const errorSpace = Math.min(errorChars, maxCharacters * 0.6); // Up to 60% for errors
const otherSpace = maxCharacters - errorSpace;
// Truncate each category
const truncatedErrors = this.truncateLogsSimple(errors, errorSpace, preserveLatest);
const truncatedOthers = this.truncateLogsSimple(others, otherSpace, preserveLatest);
// Combine and sort by timestamp
return [...truncatedErrors, ...truncatedOthers].sort(
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
);
}
// Simple truncation without prioritization
return this.truncateLogsSimple(logs, maxCharacters, preserveLatest);
}
/**
* Simple truncation that preserves either latest or earliest logs
*/
private static truncateLogsSimple(
logs: Array<{ type: string; text: string; timestamp: Date }>,
maxCharacters: number,
preserveLatest: boolean
): Array<{ type: string; text: string; timestamp: Date }> {
if (logs.length === 0) return [];
const result: Array<{ type: string; text: string; timestamp: Date }> = [];
let currentSize = 0;
// Add truncation notice size
const truncationNotice = {
type: 'info',
text: `... ${logs.length} logs truncated to fit context window ...`,
timestamp: new Date()
};
const noticeSize = truncationNotice.text.length;
// Process logs in order (latest first if preserveLatest)
const orderedLogs = preserveLatest ? [...logs].reverse() : logs;
for (const log of orderedLogs) {
const logSize = log.text.length;
// Check if we can fit this log plus the truncation notice
if (currentSize + logSize + noticeSize <= maxCharacters) {
result.push(log);
currentSize += logSize;
} else if (currentSize + noticeSize < maxCharacters) {
// Truncate this log to fit
const remainingSpace = maxCharacters - currentSize - noticeSize;
if (remainingSpace > 50) { // Only truncate if meaningful content remains
result.push({
...log,
text: log.text.substring(0, remainingSpace - 3) + '...'
});
}
break;
} else {
break;
}
}
// Restore original order
if (preserveLatest) {
result.reverse();
}
// Add truncation notice if we actually truncated
if (result.length < logs.length) {
const position = preserveLatest ? 0 : result.length;
result.splice(position, 0, truncationNotice);
}
return result;
}
/**
* Truncate a single message intelligently
*/
static truncateMessage(message: string, maxCharacters: number): string {
if (message.length <= maxCharacters) {
return message;
}
// For very small limits, just truncate
if (maxCharacters < 100) {
return message.substring(0, maxCharacters - 3) + '...';
}
// Try to truncate at a reasonable boundary
const halfSize = Math.floor(maxCharacters / 2) - 20;
// Show beginning and end
const start = message.substring(0, halfSize);
const end = message.substring(message.length - halfSize);
return `${start}\n... truncated ${message.length - maxCharacters} characters ...\n${end}`;
}
/**
* Estimate token count for a string
*/
static estimateTokens(text: string): number {
// Rough estimate: 1 token ≈ 4 characters
// Adjust for whitespace and punctuation
const wordCount = text.split(/\s+/).length;
const charCount = text.length;
// Average between character and word-based estimates
const charEstimate = charCount / this.CHARS_PER_TOKEN;
const wordEstimate = wordCount * 1.3; // Words average ~1.3 tokens
return Math.ceil((charEstimate + wordEstimate) / 2);
}
/**
* Group similar logs to reduce redundancy
*/
static groupSimilarLogs(
logs: Array<{ type: string; text: string; timestamp: Date }>
): Array<{ type: string; text: string; timestamp: Date; count?: number }> {
const grouped: Array<{
type: string;
text: string;
timestamp: Date;
count?: number;
pattern?: string;
}> = [];
const patterns = new Map<string, {
count: number;
firstTimestamp: Date;
lastTimestamp: Date;
type: string;
}>();
for (const log of logs) {
// Create a pattern by removing numbers and timestamps
const pattern = log.text
.replace(/\d+/g, 'N')
.replace(/\b[0-9a-f]{8,}\b/gi, 'HEX')
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, 'TIMESTAMP');
const existing = patterns.get(pattern);
if (existing && existing.type === log.type) {
existing.count++;
existing.lastTimestamp = log.timestamp;
} else {
patterns.set(pattern, {
count: 1,
firstTimestamp: log.timestamp,
lastTimestamp: log.timestamp,
type: log.type
});
grouped.push({ ...log, pattern });
}
}
// Replace grouped logs with summaries
return grouped.map(log => {
if (log.pattern) {
const patternInfo = patterns.get(log.pattern);
if (patternInfo && patternInfo.count > 1) {
return {
type: log.type,
text: `${log.text} (repeated ${patternInfo.count} times)`,
timestamp: patternInfo.firstTimestamp,
count: patternInfo.count
};
}
}
delete log.pattern;
return log;
});
}
}