Spaces:
Sleeping
Sleeping
| ; | |
| const { readdir, readFile, stat } = require('fs/promises'); | |
| const { join, basename } = require('path'); | |
| const safe = require('./safe-exec'); | |
| const db = require('./db'); | |
| const config = require('./config'); | |
| const logger = require('./logger'); | |
| const CLAUDE_HOME = safe.CLAUDE_HOME; | |
| const WORKSPACE = safe.WORKSPACE; | |
| function sessionsDir(projectPath) { | |
| return safe.findSessionsDir(projectPath); | |
| } | |
| async function parseSessionFile(filepath) { | |
| try { | |
| const sessionId = basename(filepath, '.jsonl'); | |
| const fileStat = await stat(filepath); | |
| const mtime = fileStat.mtimeMs; | |
| const size = fileStat.size; | |
| const cached = db.getSessionMeta(sessionId); | |
| if (cached && cached.file_mtime === mtime && cached.file_size === size) { | |
| return { | |
| name: cached.name || 'Untitled Session', | |
| timestamp: cached.timestamp || new Date().toISOString(), | |
| messageCount: cached.message_count || 0, | |
| }; | |
| } | |
| const content = await readFile(filepath, 'utf-8'); | |
| const lines = content.trim().split('\n'); | |
| let name = null; | |
| let timestamp = null; | |
| let messageCount = 0; | |
| for (const line of lines) { | |
| try { | |
| const entry = JSON.parse(line); | |
| if (!name && entry.type === 'user' && entry.message?.content) { | |
| const text = | |
| typeof entry.message.content === 'string' | |
| ? entry.message.content | |
| : entry.message.content[0]?.text || ''; | |
| name = text.substring(0, 80); | |
| if (text.length > 80) name += '...'; | |
| } | |
| if (entry.type === 'summary' && entry.summary) { | |
| name = entry.summary.substring(0, 80); | |
| } | |
| if (entry.type === 'user' || entry.type === 'assistant') { | |
| messageCount++; | |
| } | |
| if (entry.timestamp) { | |
| timestamp = entry.timestamp; | |
| } | |
| } catch (parseErr) { | |
| if (!(parseErr instanceof SyntaxError)) { | |
| logger.debug('Unexpected error parsing JSONL line in parseSessionFile', { | |
| module: 'session-utils', | |
| err: parseErr.message, | |
| }); | |
| } | |
| /* expected: malformed JSONL lines during active session writes */ | |
| } | |
| } | |
| const result = { | |
| name: name || 'Untitled Session', | |
| timestamp: timestamp || new Date().toISOString(), | |
| messageCount, | |
| }; | |
| db.upsertSessionMeta( | |
| sessionId, | |
| filepath, | |
| mtime, | |
| size, | |
| result.name, | |
| result.timestamp, | |
| result.messageCount, | |
| ); | |
| return result; | |
| } catch (err) { | |
| if (err.code === 'ENOENT') { | |
| /* expected: session file may not exist yet */ | |
| return null; | |
| } | |
| logger.error('Unexpected error in parseSessionFile', { | |
| module: 'session-utils', | |
| err: err.message, | |
| }); | |
| return null; | |
| } | |
| } | |
| function extractMessageText(entry) { | |
| if (entry.type !== 'user' && entry.type !== 'assistant') return ''; | |
| const content = entry.message?.content; | |
| if (typeof content === 'string') return content; | |
| return content?.[0]?.text || ''; | |
| } | |
| async function searchSessions(query, projectFilter, maxResults = 15) { | |
| const q = query.toLowerCase(); | |
| const results = []; | |
| const dbProjects = db.getProjects(); | |
| for (const dbProj of dbProjects) { | |
| if (projectFilter && dbProj.name !== projectFilter) continue; | |
| const sDir = sessionsDir(dbProj.path); | |
| try { | |
| const files = await readdir(sDir); | |
| for (const file of files) { | |
| if (!file.endsWith('.jsonl')) continue; | |
| const sessionId = basename(file, '.jsonl'); | |
| const content = await readFile(join(sDir, file), 'utf-8'); | |
| const matches = []; | |
| let firstName = null; | |
| for (const line of content.split('\n')) { | |
| try { | |
| const e = JSON.parse(line); | |
| if (!firstName && e.type === 'user' && e.message?.content) { | |
| const t = | |
| typeof e.message.content === 'string' | |
| ? e.message.content | |
| : e.message.content[0]?.text || ''; | |
| firstName = t.substring(0, 80); | |
| } | |
| const text = extractMessageText(e); | |
| if (text && text.toLowerCase().includes(q)) { | |
| matches.push({ type: e.type, text: text.substring(0, 200), timestamp: e.timestamp }); | |
| } | |
| } catch (parseErr) { | |
| if (!(parseErr instanceof SyntaxError)) { | |
| logger.debug('Unexpected error parsing JSONL line in searchSessions', { | |
| module: 'session-utils', | |
| err: parseErr.message, | |
| }); | |
| } | |
| /* expected: malformed JSONL line */ | |
| } | |
| } | |
| if (matches.length > 0) { | |
| const cached = db.getSessionMeta(sessionId); | |
| const sessionName = cached?.name || firstName || 'Untitled'; | |
| results.push({ | |
| session_id: sessionId, | |
| sessionId, | |
| project: dbProj.name, | |
| name: sessionName, | |
| match_count: matches.length, | |
| matchCount: matches.length, | |
| snippets: matches.slice(0, 3).map((m) => m.text), | |
| matches: matches.slice(0, 3), | |
| }); | |
| } | |
| } | |
| } catch (err) { | |
| if (err.code === 'ENOENT') { | |
| /* expected: no sessions dir for this project */ | |
| } else { | |
| logger.error('Error reading sessions dir in searchSessions', { | |
| module: 'session-utils', | |
| project: dbProj.name, | |
| err: err.message, | |
| }); | |
| } | |
| } | |
| } | |
| return results.sort((a, b) => b.match_count - a.match_count).slice(0, maxResults); | |
| } | |
| async function summarizeSession(sessionId, project) { | |
| const dbProj = db.getProject(project); | |
| const projectPath = dbProj ? dbProj.path : join(WORKSPACE, project); | |
| const sDir = sessionsDir(projectPath); | |
| const jsonlFile = join(sDir, `${sessionId}.jsonl`); | |
| const content = await readFile(jsonlFile, 'utf-8'); | |
| const lines = content.trim().split('\n'); | |
| const messages = []; | |
| const maxTranscriptChars = config.get('session.summaryMaxTranscriptChars', 1500); | |
| const maxMessageChars = config.get('session.summaryMaxMessageChars', 500); | |
| let charCount = 0; | |
| for (let i = lines.length - 1; i >= 0 && charCount < maxTranscriptChars; i--) { | |
| try { | |
| const entry = JSON.parse(lines[i]); | |
| const text = extractMessageText(entry); | |
| if (text) { | |
| messages.unshift({ role: entry.type, text: text.substring(0, maxMessageChars) }); | |
| charCount += text.length; | |
| } | |
| } catch (parseErr) { | |
| if (!(parseErr instanceof SyntaxError)) { | |
| logger.debug('Unexpected error parsing JSONL line in summarizeSession', { | |
| module: 'session-utils', | |
| err: parseErr.message, | |
| }); | |
| } | |
| /* expected: malformed JSONL line */ | |
| } | |
| } | |
| if (messages.length === 0) | |
| return { summary: 'Empty session.', recent_messages: [], recentMessages: [] }; | |
| const transcript = messages | |
| .map((m) => `${m.role === 'user' ? 'Human' : 'Claude'}: ${m.text}`) | |
| .join('\n\n'); | |
| const prompt = config.getPrompt('summarize-session', { TRANSCRIPT: transcript }); | |
| const summaryModel = config.get('session.summaryModel', 'claude-sonnet-4-6'); | |
| const claudeTimeout = config.get('claude.defaultTimeoutMs', 120000); | |
| try { | |
| const summary = ( | |
| await safe.claudeExecAsync( | |
| ['--print', '--no-session-persistence', '--model', summaryModel, prompt], | |
| { cwd: projectPath, timeout: claudeTimeout }, | |
| ) | |
| ).trim(); | |
| const recent = messages.slice(-3); | |
| return { summary, recent_messages: recent, recentMessages: recent }; | |
| } catch (err) { | |
| logger.error('Failed to generate session summary', { | |
| module: 'session-utils', | |
| sessionId: sessionId.substring(0, 8), | |
| err: err.message, | |
| }); | |
| const recent = messages.slice(-3); | |
| return { | |
| summary: 'Failed to generate summary: ' + (err.message?.substring(0, 100) || 'unknown error'), | |
| recent_messages: recent, | |
| recentMessages: recent, | |
| }; | |
| } | |
| } | |
| async function getTokenUsage(sessionId, project) { | |
| if (sessionId.startsWith('new_')) return { input_tokens: 0, model: null, max_tokens: 200000 }; | |
| const dbProj = db.getProject(project); | |
| const projectPath = dbProj ? dbProj.path : join(WORKSPACE, project); | |
| const sDir = sessionsDir(projectPath); | |
| const jsonlFile = join(sDir, `${sessionId}.jsonl`); | |
| try { | |
| const content = await readFile(jsonlFile, 'utf-8'); | |
| const lines = content.trim().split('\n'); | |
| let inputTokens = 0; | |
| let model = null; | |
| for (let i = lines.length - 1; i >= 0; i--) { | |
| try { | |
| const entry = JSON.parse(lines[i]); | |
| if (entry.type === 'assistant' && entry.message?.usage) { | |
| const m = entry.message.model || ''; | |
| if (m.includes('synthetic') || m.includes('system')) continue; | |
| const usage = entry.message.usage; | |
| const total = | |
| (usage.input_tokens || 0) + | |
| (usage.cache_read_input_tokens || 0) + | |
| (usage.cache_creation_input_tokens || 0); | |
| if (total === 0) continue; | |
| inputTokens = total; | |
| model = m || null; | |
| break; | |
| } | |
| } catch (parseErr) { | |
| if (!(parseErr instanceof SyntaxError)) { | |
| logger.debug('Unexpected error parsing JSONL line in getTokenUsage', { | |
| module: 'session-utils', | |
| err: parseErr.message, | |
| }); | |
| } | |
| /* expected: malformed JSONL line */ | |
| } | |
| } | |
| return { | |
| input_tokens: inputTokens, | |
| model, | |
| max_tokens: model?.includes('opus') || model?.includes('1m') ? 1000000 : 200000, | |
| }; | |
| } catch (err) { | |
| if (err.code === 'ENOENT') { | |
| /* expected: session file may not exist yet */ | |
| return { input_tokens: 0, model: null, max_tokens: 200000 }; | |
| } | |
| logger.error('Unexpected error in getTokenUsage', { | |
| module: 'session-utils', | |
| sessionId: sessionId.substring(0, 8), | |
| err: err.message, | |
| }); | |
| return { input_tokens: 0, model: null, max_tokens: 200000 }; | |
| } | |
| } | |
| async function getSessionSlug(sessionId, projectPath) { | |
| const jsonlFile = join(sessionsDir(projectPath), `${sessionId}.jsonl`); | |
| try { | |
| const content = await readFile(jsonlFile, 'utf-8'); | |
| for (const line of content.split('\n')) { | |
| try { | |
| const entry = JSON.parse(line); | |
| if (entry.slug) return entry.slug; | |
| } catch (parseErr) { | |
| if (!(parseErr instanceof SyntaxError)) { | |
| logger.debug('Unexpected error parsing JSONL line in getSessionSlug', { | |
| module: 'session-utils', | |
| err: parseErr.message, | |
| }); | |
| } | |
| /* expected: malformed JSONL line */ | |
| } | |
| } | |
| } catch (err) { | |
| if (err.code !== 'ENOENT') { | |
| logger.error('Unexpected error in getSessionSlug', { | |
| module: 'session-utils', | |
| sessionId: sessionId.substring(0, 8), | |
| err: err.message, | |
| }); | |
| } | |
| /* expected for ENOENT: session file may not exist */ | |
| } | |
| return null; | |
| } | |
| module.exports = { | |
| sessionsDir, | |
| parseSessionFile, | |
| extractMessageText, | |
| searchSessions, | |
| summarizeSession, | |
| getTokenUsage, | |
| getSessionSlug, | |
| CLAUDE_HOME, | |
| }; | |