this repo has no description
0
fork

Configure Feed

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

Fix monorepo subdirectory detection with smart path resolution

Claude's path encoding is lossy (/ becomes -), so we can't tell if
"taper-calculator-apps-web" means a project with dashes or nested
directories.

New approach:
- Try interpretations from right to left (literal dashes first)
- Probe filesystem to find which path actually exists
- Use git root as canonical project name for monorepo subdirs

This correctly handles both:
- drink-reminder-native → keeps dashes (project name has dashes)
- taper-calculator/apps/web → merges to taper-calculator (monorepo)

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

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

alice dc84f121 60ee056a

+99 -28
+99 -28
src/core/session-detector.ts
··· 1 - import { join } from 'path'; 1 + import { join, dirname } from 'path'; 2 2 import { homedir } from 'os'; 3 3 import { existsSync, readdirSync, statSync } from 'fs'; 4 4 import { createHash } from 'crypto'; 5 5 import { isFileProcessed } from './db'; 6 6 7 + /** 8 + * Find the git root for a given path. 9 + * Returns the path if it's a git root, or walks up to find one. 10 + * Returns null if no git root is found. 11 + */ 12 + function findGitRoot(path: string): string | null { 13 + let current = path; 14 + const root = '/'; 15 + 16 + while (current !== root) { 17 + if (existsSync(join(current, '.git'))) { 18 + return current; 19 + } 20 + const parent = dirname(current); 21 + if (parent === current) break; // Reached filesystem root 22 + current = parent; 23 + } 24 + 25 + return null; 26 + } 27 + 7 28 export interface SessionFile { 8 29 path: string; 9 30 projectPath: string; ··· 29 50 } 30 51 31 52 /** 53 + * Try different interpretations of a dash-separated string by progressively 54 + * replacing dashes with slashes from right to left. 55 + * 56 + * For "taper-calculator-apps-web", tries: 57 + * 1. taper-calculator-apps-web (all dashes literal) 58 + * 2. taper-calculator-apps/web 59 + * 3. taper-calculator/apps/web 60 + * 4. taper/calculator/apps/web (all dashes as slashes) 61 + * 62 + * Returns the first path that exists on the filesystem. 63 + */ 64 + function resolveProjectPath(basePath: string, projectPart: string): string { 65 + // Split on dashes 66 + const parts = projectPart.split('-'); 67 + 68 + // Try interpretations from "all dashes literal" to "all dashes as slashes" 69 + // We iterate by how many trailing parts are split off as directories 70 + for (let splitCount = 0; splitCount <= parts.length - 1; splitCount++) { 71 + let path: string; 72 + 73 + if (splitCount === 0) { 74 + // Keep all dashes - treat entire projectPart as folder name 75 + path = `${basePath}/${projectPart}`; 76 + } else { 77 + // Split the last N parts as subdirectories 78 + const projectNameParts = parts.slice(0, parts.length - splitCount); 79 + const subdirParts = parts.slice(parts.length - splitCount); 80 + const projectName = projectNameParts.join('-'); 81 + const subdirs = subdirParts.join('/'); 82 + path = `${basePath}/${projectName}/${subdirs}`; 83 + } 84 + 85 + if (existsSync(path)) { 86 + return path; 87 + } 88 + } 89 + 90 + // Nothing exists - return the literal interpretation (all dashes preserved) 91 + return `${basePath}/${projectPart}`; 92 + } 93 + 94 + /** 32 95 * Decode project folder name back to path and extract project name. 33 96 * 34 97 * Claude encodes paths by replacing / with - but project folders under 35 - * src/a/ or src/tries/ keep their dashes as the project name. 98 + * src/a/ or src/tries/ may have dashes in their actual names. 99 + * 100 + * Since the encoding is lossy, we probe the filesystem to find the correct 101 + * interpretation, then use git root as the canonical project identity. 36 102 * 37 103 * Examples: 38 104 * -Users-USERNAME-src-a-drink-reminder-native 105 + * -> tries: drink-reminder-native (exists!) ✓ 39 106 * -> path: /Users/USERNAME/src/a/drink-reminder-native 40 107 * -> name: drink-reminder-native 41 108 * 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 109 + * -Users-USERNAME-src-a-taper-calculator-apps-web 110 + * -> tries: taper-calculator-apps-web (doesn't exist) 111 + * -> tries: taper-calculator-apps/web (doesn't exist) 112 + * -> tries: taper-calculator/apps/web (exists!) ✓ 113 + * -> git root: /Users/USERNAME/src/a/taper-calculator 114 + * -> name: taper-calculator 45 115 */ 46 116 export function decodeProjectFolder(folderName: string): { path: string; name: string } { 47 117 // Remove leading dash ··· 51 121 const srcAMatch = withoutLeading.match(/^(Users-[^-]+-src-a)-(.+)$/); 52 122 const srcTriesMatch = withoutLeading.match(/^(Users-[^-]+-src-tries)-(.+)$/); 53 123 124 + let decodedPath: string; 125 + 54 126 if (srcAMatch) { 55 127 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) { 128 + const projectPart = srcAMatch[2]; 129 + decodedPath = resolveProjectPath(basePath, projectPart); 130 + } else if (srcTriesMatch) { 64 131 const basePath = '/' + srcTriesMatch[1].replace(/-/g, '/'); 65 - const projectName = srcTriesMatch[2]; // Keep dashes 66 - return { 67 - path: `${basePath}/${projectName}`, 68 - name: projectName, 69 - }; 132 + const projectPart = srcTriesMatch[2]; 133 + decodedPath = resolveProjectPath(basePath, projectPart); 134 + } else { 135 + // Fallback: just replace all dashes with slashes 136 + decodedPath = '/' + withoutLeading.replace(/-/g, '/'); 70 137 } 71 138 72 - // Fallback: just the folder name with leading dash removed, last segment as name 73 - const parts = withoutLeading.split('-'); 74 - const path = '/' + withoutLeading.replace(/-/g, '/'); 75 - 76 139 // Special case: home directory should show as "~" 77 140 const homeDir = homedir(); 78 - if (path === homeDir) { 79 - return { path, name: '~' }; 141 + if (decodedPath === homeDir) { 142 + return { path: decodedPath, name: '~' }; 143 + } 144 + 145 + // Try to find git root to normalize monorepo subdirectories 146 + if (existsSync(decodedPath)) { 147 + const gitRoot = findGitRoot(decodedPath); 148 + if (gitRoot) { 149 + const projectName = gitRoot.split('/').pop() || 'unknown'; 150 + return { path: gitRoot, name: projectName }; 151 + } 80 152 } 81 153 82 - return { 83 - path, 84 - name: parts[parts.length - 1] || 'unknown', 85 - }; 154 + // No git root found - use the decoded path as-is 155 + const projectName = decodedPath.split('/').pop() || 'unknown'; 156 + return { path: decodedPath, name: projectName }; 86 157 } 87 158 88 159 /**