mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
Deletes files and symbols with zero importers or self-declared deprecation.
No behavior change — structural cleanup only.
Removed:
- hook-response.ts, utils/bun-path.ts (zero importers)
- cli/handlers/user-message.ts (not wired in hooks.json)
- services/Context.ts + context-generator.ts (deprecated stubs)
- sqlite/migrations.ts (645 lines, pre-SDK schema, unused)
- DatabaseManager singleton + getDatabase + initializeDatabase
- 6 sqlite re-export shells (Observations/Sessions/Summaries/Prompts/Timeline/Import)
- worker/search/{strategies,filters}/ dirs (dead via unused SearchOrchestrator)
- SearchOrchestrator, TimelineBuilder, ResultFormatter
- TimelineService.formatTimeline (137 lines, unused)
- ProcessManager.cleanupOrphanedProcesses + createSignalHandler
- Duplicate php: key in smart-file-read/parser.ts
Rewired:
- SearchRoutes dynamic imports (services/Context → services/context)
- CorpusBuilder: SearchOrchestrator → SearchManager.search({format:'json'})
- build-hooks.js entry: context-generator.ts → services/context/index.ts
- scripts/ imports for moved transcript-parser.ts
Moved:
- utils/transcript-parser.ts → scripts/transcript-parser.ts (only callers)
Skipped (plan was wrong):
- consecutiveRestarts: live backoff math at SessionRoutes.ts:348
- AgentFormatter stubs: wired to HeaderRenderer with passing tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
267 lines
7.0 KiB
TypeScript
267 lines
7.0 KiB
TypeScript
/**
|
|
* TranscriptParser - Properly parse Claude Code transcript JSONL files
|
|
* Handles all transcript entry types based on validated model
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { logger } from './logger.js';
|
|
import { SYSTEM_REMINDER_REGEX } from './tag-stripping.js';
|
|
import type {
|
|
TranscriptEntry,
|
|
UserTranscriptEntry,
|
|
AssistantTranscriptEntry,
|
|
SummaryTranscriptEntry,
|
|
SystemTranscriptEntry,
|
|
QueueOperationTranscriptEntry,
|
|
ContentItem,
|
|
TextContent,
|
|
} from '../types/transcript.js';
|
|
|
|
export interface ParseStats {
|
|
totalLines: number;
|
|
parsedEntries: number;
|
|
failedLines: number;
|
|
entriesByType: Record<string, number>;
|
|
failureRate: number;
|
|
}
|
|
|
|
export class TranscriptParser {
|
|
private entries: TranscriptEntry[] = [];
|
|
private parseErrors: Array<{ lineNumber: number; error: string }> = [];
|
|
|
|
constructor(transcriptPath: string) {
|
|
this.parseTranscript(transcriptPath);
|
|
}
|
|
|
|
private parseTranscript(transcriptPath: string): void {
|
|
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
|
if (!content) return;
|
|
|
|
const lines = content.split('\n');
|
|
|
|
lines.forEach((line, index) => {
|
|
try {
|
|
const entry = JSON.parse(line) as TranscriptEntry;
|
|
this.entries.push(entry);
|
|
} catch (error) {
|
|
logger.debug('PARSER', 'Failed to parse transcript line', { lineNumber: index + 1 }, error as Error);
|
|
this.parseErrors.push({
|
|
lineNumber: index + 1,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Log summary if there were parse errors
|
|
if (this.parseErrors.length > 0) {
|
|
logger.error('PARSER', `Failed to parse ${this.parseErrors.length} lines`, {
|
|
path: transcriptPath,
|
|
totalLines: lines.length,
|
|
errorCount: this.parseErrors.length
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all entries of a specific type
|
|
*/
|
|
getEntriesByType<T extends TranscriptEntry>(type: T['type']): T[] {
|
|
return this.entries.filter((e) => e.type === type) as T[];
|
|
}
|
|
|
|
/**
|
|
* Get all user entries
|
|
*/
|
|
getUserEntries(): UserTranscriptEntry[] {
|
|
return this.getEntriesByType<UserTranscriptEntry>('user');
|
|
}
|
|
|
|
/**
|
|
* Get all assistant entries
|
|
*/
|
|
getAssistantEntries(): AssistantTranscriptEntry[] {
|
|
return this.getEntriesByType<AssistantTranscriptEntry>('assistant');
|
|
}
|
|
|
|
/**
|
|
* Get all summary entries
|
|
*/
|
|
getSummaryEntries(): SummaryTranscriptEntry[] {
|
|
return this.getEntriesByType<SummaryTranscriptEntry>('summary');
|
|
}
|
|
|
|
/**
|
|
* Get all system entries
|
|
*/
|
|
getSystemEntries(): SystemTranscriptEntry[] {
|
|
return this.getEntriesByType<SystemTranscriptEntry>('system');
|
|
}
|
|
|
|
/**
|
|
* Get all queue operation entries
|
|
*/
|
|
getQueueOperationEntries(): QueueOperationTranscriptEntry[] {
|
|
return this.getEntriesByType<QueueOperationTranscriptEntry>('queue-operation');
|
|
}
|
|
|
|
/**
|
|
* Get last entry of a specific type
|
|
*/
|
|
getLastEntryByType<T extends TranscriptEntry>(type: T['type']): T | null {
|
|
const entries = this.getEntriesByType<T>(type);
|
|
return entries.length > 0 ? entries[entries.length - 1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extract text content from content items
|
|
*/
|
|
private extractTextFromContent(content: string | ContentItem[]): string {
|
|
if (typeof content === 'string') {
|
|
return content;
|
|
}
|
|
|
|
if (Array.isArray(content)) {
|
|
return content
|
|
.filter((item): item is TextContent => item.type === 'text')
|
|
.map((item) => item.text)
|
|
.join('\n');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get last user message text (finds last entry with actual text content)
|
|
*/
|
|
getLastUserMessage(): string {
|
|
const userEntries = this.getUserEntries();
|
|
|
|
// Iterate backward to find the last user message with text content
|
|
for (let i = userEntries.length - 1; i >= 0; i--) {
|
|
const entry = userEntries[i];
|
|
if (!entry?.message?.content) continue;
|
|
|
|
const text = this.extractTextFromContent(entry.message.content);
|
|
if (text) return text;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get last assistant message text (finds last entry with text content, with optional system-reminder filtering)
|
|
*/
|
|
getLastAssistantMessage(filterSystemReminders = true): string {
|
|
const assistantEntries = this.getAssistantEntries();
|
|
|
|
// Iterate backward to find the last assistant message with text content
|
|
for (let i = assistantEntries.length - 1; i >= 0; i--) {
|
|
const entry = assistantEntries[i];
|
|
if (!entry?.message?.content) continue;
|
|
|
|
let text = this.extractTextFromContent(entry.message.content);
|
|
if (!text) continue;
|
|
|
|
if (filterSystemReminders) {
|
|
// Filter out system-reminder tags and their content
|
|
text = text.replace(SYSTEM_REMINDER_REGEX, '');
|
|
// Clean up excessive whitespace
|
|
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
|
}
|
|
|
|
if (text) return text;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get all tool use operations from assistant entries
|
|
*/
|
|
getToolUseHistory(): Array<{ name: string; timestamp: string; input: any }> {
|
|
const toolUses: Array<{ name: string; timestamp: string; input: any }> = [];
|
|
|
|
for (const entry of this.getAssistantEntries()) {
|
|
if (Array.isArray(entry.message.content)) {
|
|
for (const item of entry.message.content) {
|
|
if (item.type === 'tool_use') {
|
|
toolUses.push({
|
|
name: item.name,
|
|
timestamp: entry.timestamp,
|
|
input: item.input,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return toolUses;
|
|
}
|
|
|
|
/**
|
|
* Get total token usage across all assistant messages
|
|
*/
|
|
getTotalTokenUsage(): {
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
cacheCreationTokens: number;
|
|
cacheReadTokens: number;
|
|
} {
|
|
const assistantEntries = this.getAssistantEntries();
|
|
|
|
return assistantEntries.reduce(
|
|
(acc, entry) => {
|
|
const usage = entry.message.usage;
|
|
if (usage) {
|
|
acc.inputTokens += usage.input_tokens || 0;
|
|
acc.outputTokens += usage.output_tokens || 0;
|
|
acc.cacheCreationTokens += usage.cache_creation_input_tokens || 0;
|
|
acc.cacheReadTokens += usage.cache_read_input_tokens || 0;
|
|
}
|
|
return acc;
|
|
},
|
|
{
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
cacheReadTokens: 0,
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get parse statistics
|
|
*/
|
|
getParseStats(): ParseStats {
|
|
const entriesByType: Record<string, number> = {};
|
|
|
|
for (const entry of this.entries) {
|
|
entriesByType[entry.type] = (entriesByType[entry.type] || 0) + 1;
|
|
}
|
|
|
|
const totalLines = this.entries.length + this.parseErrors.length;
|
|
|
|
return {
|
|
totalLines,
|
|
parsedEntries: this.entries.length,
|
|
failedLines: this.parseErrors.length,
|
|
entriesByType,
|
|
failureRate: totalLines > 0 ? this.parseErrors.length / totalLines : 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get parse errors
|
|
*/
|
|
getParseErrors(): Array<{ lineNumber: number; error: string }> {
|
|
return this.parseErrors;
|
|
}
|
|
|
|
/**
|
|
* Get all entries (raw)
|
|
*/
|
|
getAllEntries(): TranscriptEntry[] {
|
|
return this.entries;
|
|
}
|
|
}
|