import * as os from 'os'; import * as path from 'path'; /** * Utility functions for encoding/decoding Claude Code project directory names. * * Directory naming pattern: * - Path: /Users/username/projectname * - Encoded: -Users-username-projectname * * IMPORTANT: This encoding is LOSSY for paths containing dashes. * For accurate path resolution, use extractCwd() from jsonl.ts to read * the actual cwd from session files. */ // ============================================================================= // Core Encoding/Decoding // ============================================================================= /** * Encodes an absolute path into Claude Code's directory naming format. * Replaces all path separators (/ and \) with dashes. * * @param absolutePath - The absolute path to encode (e.g., "/Users/username/projectname") * @returns The encoded directory name (e.g., "-Users-username-projectname") */ export function encodePath(absolutePath: string): string { if (!absolutePath) { return ''; } const encoded = absolutePath.replace(/[/\\]/g, '-'); // Ensure leading dash for absolute paths return encoded.startsWith('-') ? encoded : `-${encoded}`; } /** * Decodes a project directory name to its original path. * Note: This is a best-effort decode. Paths with dashes cannot be decoded accurately. * * @param encodedName - The encoded directory name (e.g., "-Users-username-projectname") * @returns The decoded path (e.g., "/Users/username/projectname") */ export function decodePath(encodedName: string): string { if (!encodedName) { return ''; } // Remove leading dash if present (indicates absolute path) const withoutLeadingDash = encodedName.startsWith('-') ? encodedName.slice(1) : encodedName; // Replace dashes with slashes const decodedPath = withoutLeadingDash.replace(/-/g, '/'); // Windows paths may decode to "C:/..." if (/^[a-zA-Z]:\//.test(decodedPath)) { return decodedPath; } // Ensure leading slash for POSIX-style absolute paths return decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`; } /** * Extract the project name (last path segment) from an encoded directory name. * * @param encodedName - The encoded directory name * @returns The project name */ export function extractProjectName(encodedName: string): string { const decoded = decodePath(encodedName); const segments = decoded.split('/').filter(Boolean); return segments[segments.length - 1] || encodedName; } // ============================================================================= // Validation // ============================================================================= /** * Validates if a directory name follows the Claude Code encoding pattern. * * @param encodedName - The directory name to validate * @returns true if valid, false otherwise */ export function isValidEncodedPath(encodedName: string): boolean { if (!encodedName) { return false; } // Must start with a dash (indicates absolute path) if (!encodedName.startsWith('-')) { return false; } // Allow only expected encoded characters: // - alphanumeric, underscores, dots, spaces, dashes // - optional ":" for Windows drive notation (e.g., -C:-Users-name-project) const validPattern = /^-[a-zA-Z0-9_.\s:-]+$/; if (!validPattern.test(encodedName)) { return false; } // Windows-style drive syntax is allowed only at the beginning after "-" // e.g. "-C:-Users-name-project". Reject stray ":" elsewhere. const firstColon = encodedName.indexOf(':'); if (firstColon === -1) { return true; } if (!/^-[a-zA-Z]:/.test(encodedName)) { return false; } return !encodedName.includes(':', firstColon + 1); } /** * Validates a project ID that may be either a plain encoded path or * a composite subproject ID (`{encodedPath}::{8-char-hex}`). * * @param projectId - The project ID to validate * @returns true if valid */ export function isValidProjectId(projectId: string): boolean { if (!projectId) { return false; } const sep = projectId.indexOf('::'); if (sep === -1) { // Plain encoded path return isValidEncodedPath(projectId); } // Composite ID: validate base part and hash suffix const basePart = projectId.slice(0, sep); const hashPart = projectId.slice(sep + 2); return isValidEncodedPath(basePart) && /^[a-f0-9]{8}$/.test(hashPart); } /** * Extract the base directory (encoded path) from a project ID. * For composite IDs (`{encoded}::{hash}`), returns the encoded part. * For plain IDs, returns the ID as-is. */ export function extractBaseDir(projectId: string): string { const sep = projectId.indexOf('::'); if (sep !== -1) { return projectId.slice(0, sep); } return projectId; } // ============================================================================= // Session ID Extraction // ============================================================================= /** * Extract session ID from a JSONL filename. * * @param filename - The filename (e.g., "abc123.jsonl") * @returns The session ID (e.g., "abc123") */ export function extractSessionId(filename: string): string { return filename.replace(/\.jsonl$/, ''); } // ============================================================================= // Path Construction // ============================================================================= /** * Construct the path to a session JSONL file. * Handles composite project IDs by extracting the base directory. */ export function buildSessionPath(basePath: string, projectId: string, sessionId: string): string { return path.join(basePath, extractBaseDir(projectId), `${sessionId}.jsonl`); } /** * Construct the path to a session's subagents directory. * Handles composite project IDs by extracting the base directory. */ export function buildSubagentsPath(basePath: string, projectId: string, sessionId: string): string { return path.join(basePath, extractBaseDir(projectId), sessionId, 'subagents'); } /** * Construct the path to a task list file (stored in todos directory). */ export function buildTodoPath(claudeBasePath: string, sessionId: string): string { return path.join(claudeBasePath, 'todos', `${sessionId}.json`); } // ============================================================================= // Home Directory // ============================================================================= /** * Get the user's home directory. */ function getHomeDir(): string { return process.env.HOME || process.env.USERPROFILE || os.homedir() || '/'; } /** * Get the Claude config base path (~/.claude). */ function getClaudeBasePath(): string { return path.join(getHomeDir(), '.claude'); } /** * Get the projects directory path (~/.claude/projects). */ export function getProjectsBasePath(): string { return path.join(getClaudeBasePath(), 'projects'); } /** * Get the todos directory path (~/.claude/todos). */ export function getTodosBasePath(): string { return path.join(getClaudeBasePath(), 'todos'); }