this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add session detector for finding unprocessed sessions

- Scan ~/.claude/projects for session files
- Smart path decoding for src/a/ and src/tries/ projects
- Track processed files via hash to detect changes
- Support for CLAUDE_CONFIG_DIR environment variable

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice 61234942 5fbc550a

+214
+214
src/core/session-detector.ts
··· 1 + import { join } from 'path'; 2 + import { homedir } from 'os'; 3 + import { existsSync, readdirSync, statSync } from 'fs'; 4 + import { createHash } from 'crypto'; 5 + import { isFileProcessed } from './db'; 6 + 7 + export interface SessionFile { 8 + path: string; 9 + projectPath: string; 10 + projectName: string; 11 + sessionId: string; 12 + modifiedAt: Date; 13 + fileHash: string; 14 + } 15 + 16 + /** 17 + * Get possible Claude config directories 18 + */ 19 + export function getClaudePaths(): string[] { 20 + const envPaths = process.env.CLAUDE_CONFIG_DIR?.split(',') ?? []; 21 + const defaults = [ 22 + join(homedir(), '.config', 'claude'), 23 + join(homedir(), '.claude'), 24 + ]; 25 + 26 + return [...envPaths, ...defaults].filter((p) => 27 + existsSync(join(p, 'projects')) 28 + ); 29 + } 30 + 31 + /** 32 + * Decode project folder name back to path and extract project name. 33 + * 34 + * Claude encodes paths by replacing / with - but project folders under 35 + * src/a/ or src/tries/ keep their dashes as the project name. 36 + * 37 + * Examples: 38 + * -Users-USERNAME-src-a-drink-reminder-native 39 + * -> path: /Users/USERNAME/src/a/drink-reminder-native 40 + * -> name: drink-reminder-native 41 + * 42 + * -Users-USERNAME-src-tries-2025-12-01-myproject 43 + * -> path: /Users/USERNAME/src/tries/2025-12-01-myproject 44 + * -> name: 2025-12-01-myproject 45 + */ 46 + export function decodeProjectFolder(folderName: string): { path: string; name: string } { 47 + // Remove leading dash 48 + const withoutLeading = folderName.slice(1); 49 + 50 + // Find the src/a/ or src/tries/ marker 51 + const srcAMatch = withoutLeading.match(/^(Users-[^-]+-src-a)-(.+)$/); 52 + const srcTriesMatch = withoutLeading.match(/^(Users-[^-]+-src-tries)-(.+)$/); 53 + 54 + if (srcAMatch) { 55 + const basePath = '/' + srcAMatch[1].replace(/-/g, '/'); 56 + const projectName = srcAMatch[2]; // Keep dashes 57 + return { 58 + path: `${basePath}/${projectName}`, 59 + name: projectName, 60 + }; 61 + } 62 + 63 + if (srcTriesMatch) { 64 + const basePath = '/' + srcTriesMatch[1].replace(/-/g, '/'); 65 + const projectName = srcTriesMatch[2]; // Keep dashes 66 + return { 67 + path: `${basePath}/${projectName}`, 68 + name: projectName, 69 + }; 70 + } 71 + 72 + // Fallback: just the folder name with leading dash removed, last segment as name 73 + const parts = withoutLeading.split('-'); 74 + return { 75 + path: '/' + withoutLeading.replace(/-/g, '/'), 76 + name: parts[parts.length - 1] || 'unknown', 77 + }; 78 + } 79 + 80 + /** 81 + * @deprecated Use decodeProjectFolder instead 82 + */ 83 + export function decodeProjectPath(folderName: string): string { 84 + return decodeProjectFolder(folderName).path; 85 + } 86 + 87 + /** 88 + * @deprecated Use decodeProjectFolder instead 89 + */ 90 + export function getProjectName(projectPath: string): string { 91 + return projectPath.split('/').filter(Boolean).pop() || 'unknown'; 92 + } 93 + 94 + /** 95 + * Calculate MD5 hash of a file 96 + */ 97 + export function getFileHash(filePath: string): string { 98 + const content = Bun.file(filePath).toString(); 99 + return createHash('md5').update(content).digest('hex'); 100 + } 101 + 102 + /** 103 + * Scan for all session files 104 + */ 105 + export function findAllSessionFiles(): SessionFile[] { 106 + const sessions: SessionFile[] = []; 107 + 108 + for (const claudePath of getClaudePaths()) { 109 + const projectsDir = join(claudePath, 'projects'); 110 + if (!existsSync(projectsDir)) continue; 111 + 112 + const projectFolders = readdirSync(projectsDir); 113 + 114 + for (const folder of projectFolders) { 115 + const folderPath = join(projectsDir, folder); 116 + const stat = statSync(folderPath); 117 + if (!stat.isDirectory()) continue; 118 + 119 + const { path: projectPath, name: projectName } = decodeProjectFolder(folder); 120 + 121 + // Find all .jsonl files in this project folder 122 + const files = readdirSync(folderPath).filter((f) => f.endsWith('.jsonl')); 123 + 124 + for (const file of files) { 125 + const filePath = join(folderPath, file); 126 + const fileStat = statSync(filePath); 127 + const sessionId = file.replace('.jsonl', ''); 128 + 129 + sessions.push({ 130 + path: filePath, 131 + projectPath, 132 + projectName, 133 + sessionId, 134 + modifiedAt: fileStat.mtime, 135 + fileHash: '', // Computed lazily 136 + }); 137 + } 138 + } 139 + } 140 + 141 + // Sort by modification time (newest first) 142 + sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime()); 143 + 144 + return sessions; 145 + } 146 + 147 + /** 148 + * Find unprocessed or modified session files 149 + */ 150 + export async function findUnprocessedSessions( 151 + force = false 152 + ): Promise<SessionFile[]> { 153 + const allSessions = findAllSessionFiles(); 154 + 155 + if (force) { 156 + // Compute hashes for all files 157 + for (const session of allSessions) { 158 + session.fileHash = await computeFileHash(session.path); 159 + } 160 + return allSessions; 161 + } 162 + 163 + const unprocessed: SessionFile[] = []; 164 + 165 + for (const session of allSessions) { 166 + const hash = await computeFileHash(session.path); 167 + session.fileHash = hash; 168 + 169 + if (!isFileProcessed(session.path, hash)) { 170 + unprocessed.push(session); 171 + } 172 + } 173 + 174 + return unprocessed; 175 + } 176 + 177 + /** 178 + * Compute file hash asynchronously 179 + */ 180 + async function computeFileHash(filePath: string): Promise<string> { 181 + const file = Bun.file(filePath); 182 + const content = await file.text(); 183 + return createHash('md5').update(content).digest('hex'); 184 + } 185 + 186 + /** 187 + * Filter sessions by date 188 + */ 189 + export function filterSessionsByDate( 190 + sessions: SessionFile[], 191 + targetDate: string 192 + ): SessionFile[] { 193 + // We need to peek into files to check dates, but that's expensive 194 + // For now, return all and let the processor filter 195 + return sessions; 196 + } 197 + 198 + /** 199 + * Get stats about session files 200 + */ 201 + export function getSessionStats(): { 202 + totalFiles: number; 203 + totalProjects: number; 204 + claudePaths: string[]; 205 + } { 206 + const allSessions = findAllSessionFiles(); 207 + const projects = new Set(allSessions.map((s) => s.projectPath)); 208 + 209 + return { 210 + totalFiles: allSessions.length, 211 + totalProjects: projects.size, 212 + claudePaths: getClaudePaths(), 213 + }; 214 + }