agent-ecosystem/src/main/utils/pathDecoder.ts

228 lines
6.9 KiB
TypeScript

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');
}