/** * Metadata extraction utilities for parsing first messages and session context from JSONL files. */ import { isCommandOutputContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer';import { createLogger } from '@shared/utils/logger';import * as readline from 'readline'; import { LocalFileSystemProvider } from '../services/infrastructure/LocalFileSystemProvider';import { type ChatHistoryEntry, isTextContent, type UserEntry } from '../types'; import type { FileSystemProvider } from '../services/infrastructure/FileSystemProvider'; const logger = createLogger('Util:metadataExtraction'); const defaultProvider = new LocalFileSystemProvider(); interface MessagePreview { text: string; timestamp: string; isCommand: boolean;} /** * Extract CWD (current working directory) from the first entry. * Used to get the actual project path from encoded directory names. */export async function extractCwd( filePath: string, fsProvider: FileSystemProvider = defaultProvider): Promise<string | null> { if (!(await fsProvider.exists(filePath))) { return null; } const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' }); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity, }); try { for await (const line of rl) { if (!line.trim()) continue; const entry = JSON.parse(line) as ChatHistoryEntry; // Only conversational entries have cwd if ('cwd' in entry && entry.cwd) { rl.close(); fileStream.destroy(); return entry.cwd; } } } catch (error) { logger.error(`Error extracting cwd from ${filePath}:`, error); } finally { rl.close(); fileStream.destroy(); } return null;} /** * Extract a lightweight title preview from the first user message. * For command-style sessions, falls back to a slash-command label. */export async function extractFirstUserMessagePreview( filePath: string, fsProvider: FileSystemProvider = defaultProvider, maxLines: number = 200): Promise<{ text: string; timestamp: string } | null> { const safeMaxLines = Math.max(1, maxLines); const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' }); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity, }); let commandFallback: { text: string; timestamp: string } | null = null; let linesRead = 0; try { for await (const line of rl) { if (linesRead++ >= safeMaxLines) { break; } const trimmed = line.trim(); if (!trimmed) continue; let entry: ChatHistoryEntry; try { entry = JSON.parse(trimmed) as ChatHistoryEntry; } catch { continue; } if (entry.type !== 'user') { continue; } const preview = extractPreviewFromUserEntry(entry); if (!preview) { continue; } if (!preview.isCommand) { return { text: preview.text, timestamp: preview.timestamp }; } if (!commandFallback) { commandFallback = { text: preview.text, timestamp: preview.timestamp }; } } } catch (error) { logger.debug(`Error extracting first user preview from ${filePath}:`, error); throw error; } finally { rl.close(); fileStream.destroy(); } return commandFallback;} function extractPreviewFromUserEntry(entry: UserEntry): MessagePreview | null { const timestamp = entry.timestamp ?? new Date().toISOString(); const message = entry.message; if (!message) { return null; } const content = message.content; if (typeof content === 'string') { if (isCommandOutputContent(content) || content.startsWith('[Request interrupted by user')) { return null; } if (content.startsWith('<command-name>')) { return { text: extractCommandName(content), timestamp, isCommand: true, }; } const sanitized = sanitizeDisplayContent(content).trim(); if (!sanitized) { return null; } return { text: sanitized.substring(0, 500), timestamp, isCommand: false, }; } if (!Array.isArray(content)) { return null; } const textContent = content .filter(isTextContent) .map((block) => block.text) .join(' ') .trim(); if (!textContent || textContent.startsWith('[Request interrupted by user')) { return null; } if (textContent.startsWith('<command-name>')) { return { text: extractCommandName(textContent), timestamp, isCommand: true, }; } const sanitized = sanitizeDisplayContent(textContent).trim(); if (!sanitized) { return null; } return { text: sanitized.substring(0, 500), timestamp, isCommand: false, };} function extractCommandName(content: string): string { const commandMatch = /<command-name>\/([^<]+)<\/command-name>/.exec(content); return commandMatch ? `/${commandMatch[1]}` : '/command';}
/**
* Metadata extraction utilities for parsing first messages and session context from JSONL files.
*/
import { isCommandOutputContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer';
import { createLogger } from '@shared/utils/logger';
import * as readline from 'readline';
import { LocalFileSystemProvider } from '../services/infrastructure/LocalFileSystemProvider';
import { type ChatHistoryEntry, isTextContent, type UserEntry } from '../types';
import type { FileSystemProvider } from '../services/infrastructure/FileSystemProvider';
const logger = createLogger('Util:metadataExtraction');
const defaultProvider = new LocalFileSystemProvider();
interface MessagePreview {
text: string;
timestamp: string;
isCommand: boolean;
}
/**
* Extract CWD (current working directory) from the first entry.
* Used to get the actual project path from encoded directory names.
*/
export async function extractCwd(
filePath: string,
fsProvider: FileSystemProvider = defaultProvider
): Promise<string | null> {
if (!(await fsProvider.exists(filePath))) {
return null;
}
const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
try {
for await (const line of rl) {
if (!line.trim()) continue;
const entry = JSON.parse(line) as ChatHistoryEntry;
// Only conversational entries have cwd
if ('cwd' in entry && entry.cwd) {
rl.close();
fileStream.destroy();
return entry.cwd;
}
}
} catch (error) {
logger.error(`Error extracting cwd from ${filePath}:`, error);
} finally {
rl.close();
fileStream.destroy();
}
return null;
}
/**
* Extract a lightweight title preview from the first user message.
* For command-style sessions, falls back to a slash-command label.
*/
export async function extractFirstUserMessagePreview(
filePath: string,
fsProvider: FileSystemProvider = defaultProvider,
maxLines: number = 200
): Promise<{ text: string; timestamp: string } | null> {
const safeMaxLines = Math.max(1, maxLines);
const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let commandFallback: { text: string; timestamp: string } | null = null;
let linesRead = 0;
try {
for await (const line of rl) {
if (linesRead++ >= safeMaxLines) {
break;
}
const trimmed = line.trim();
if (!trimmed) continue;
let entry: ChatHistoryEntry;
try {
entry = JSON.parse(trimmed) as ChatHistoryEntry;
} catch {
continue;
}
if (entry.type !== 'user') {
continue;
}
const preview = extractPreviewFromUserEntry(entry);
if (!preview) {
continue;
}
if (!preview.isCommand) {
return { text: preview.text, timestamp: preview.timestamp };
}
if (!commandFallback) {
commandFallback = { text: preview.text, timestamp: preview.timestamp };
}
}
} catch (error) {
logger.debug(`Error extracting first user preview from ${filePath}:`, error);
throw error;
} finally {
rl.close();
fileStream.destroy();
}
return commandFallback;
}
function extractPreviewFromUserEntry(entry: UserEntry): MessagePreview | null {
const timestamp = entry.timestamp ?? new Date().toISOString();
const message = entry.message;
if (!message) {
return null;
}
const content = message.content;
if (typeof content === 'string') {
if (isCommandOutputContent(content) || content.startsWith('[Request interrupted by user')) {
return null;
}
if (content.startsWith('<command-name>')) {
return {
text: extractCommandName(content),
timestamp,
isCommand: true,
};
}
const sanitized = sanitizeDisplayContent(content).trim();
if (!sanitized) {
return null;
}
return {
text: sanitized.substring(0, 500),
timestamp,
isCommand: false,
};
}
if (!Array.isArray(content)) {
return null;
}
const textContent = content
.filter(isTextContent)
.map((block) => block.text)
.join(' ')
.trim();
if (!textContent || textContent.startsWith('[Request interrupted by user')) {
return null;
}
if (textContent.startsWith('<command-name>')) {
return {
text: extractCommandName(textContent),
timestamp,
isCommand: true,
};
}
const sanitized = sanitizeDisplayContent(textContent).trim();
if (!sanitized) {
return null;
}
return {
text: sanitized.substring(0, 500),
timestamp,
isCommand: false,
};
}
function extractCommandName(content: string): string {
const commandMatch = /<command-name>\/([^<]+)<\/command-name>/.exec(content);
return commandMatch ? `/${commandMatch[1]}` : '/command';
}