eddb321489
- Updated CODEMAP to reflect changes in fallback methods for package root discovery. - Removed the `detectClaudePath` function and replaced its usage with direct references to `PACKAGE_NAME`. - Eliminated the `claudePath` property from Settings interface and user settings configuration. - Cleaned up path discovery logic by removing the npm list command method. - Removed the `findExecutable` method from the Platform utility as it was no longer needed.
326 lines
8.8 KiB
TypeScript
326 lines
8.8 KiB
TypeScript
import { join, dirname, sep } from 'path';
|
|
import { homedir } from 'os';
|
|
import { existsSync, statSync } from 'fs';
|
|
import { execSync } from 'child_process';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
/**
|
|
* PathDiscovery Service - Central path resolution for claude-mem
|
|
*
|
|
* Handles dynamic discovery of all required paths across different installation scenarios:
|
|
* - npm global installs, local installs, and development environments
|
|
* - Cross-platform path resolution (Windows, macOS, Linux)
|
|
* - Environment variable overrides for customization
|
|
* - Package resource discovery (hooks, commands)
|
|
*/
|
|
export class PathDiscovery {
|
|
private static instance: PathDiscovery | null = null;
|
|
|
|
// Cached paths for performance
|
|
private _dataDirectory: string | null = null;
|
|
private _packageRoot: string | null = null;
|
|
private _claudeConfigDirectory: string | null = null;
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
static getInstance(): PathDiscovery {
|
|
if (!PathDiscovery.instance) {
|
|
PathDiscovery.instance = new PathDiscovery();
|
|
}
|
|
return PathDiscovery.instance;
|
|
}
|
|
|
|
// =============================================================================
|
|
// DATA DIRECTORIES - Where claude-mem stores its data
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Base data directory for claude-mem
|
|
* Environment override: CLAUDE_MEM_DATA_DIR
|
|
*/
|
|
getDataDirectory(): string {
|
|
if (this._dataDirectory) return this._dataDirectory;
|
|
|
|
this._dataDirectory = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
|
return this._dataDirectory;
|
|
}
|
|
|
|
/**
|
|
* Archives directory for compressed sessions
|
|
*/
|
|
getArchivesDirectory(): string {
|
|
return join(this.getDataDirectory(), 'archives');
|
|
}
|
|
|
|
|
|
/**
|
|
* Logs directory for claude-mem operation logs
|
|
*/
|
|
getLogsDirectory(): string {
|
|
return join(this.getDataDirectory(), 'logs');
|
|
}
|
|
|
|
/**
|
|
* Index directory for memory indexing
|
|
*/
|
|
getIndexDirectory(): string {
|
|
return this.getDataDirectory();
|
|
}
|
|
|
|
/**
|
|
* Index file path for memory indexing
|
|
*/
|
|
getIndexPath(): string {
|
|
return join(this.getIndexDirectory(), 'claude-mem-index.jsonl');
|
|
}
|
|
|
|
/**
|
|
* Trash directory for smart trash feature
|
|
*/
|
|
getTrashDirectory(): string {
|
|
return join(this.getDataDirectory(), 'trash');
|
|
}
|
|
|
|
/**
|
|
* Backups directory for configuration backups
|
|
*/
|
|
getBackupsDirectory(): string {
|
|
return join(this.getDataDirectory(), 'backups');
|
|
}
|
|
|
|
/**
|
|
* Chroma database directory
|
|
*/
|
|
getChromaDirectory(): string {
|
|
return join(this.getDataDirectory(), 'chroma');
|
|
}
|
|
|
|
/**
|
|
* Project-specific archive directory
|
|
*/
|
|
getProjectArchiveDirectory(projectName: string): string {
|
|
return join(this.getArchivesDirectory(), projectName);
|
|
}
|
|
|
|
/**
|
|
* User settings file path
|
|
*/
|
|
getUserSettingsPath(): string {
|
|
return join(this.getDataDirectory(), 'settings.json');
|
|
}
|
|
|
|
// =============================================================================
|
|
// CLAUDE INTEGRATION PATHS - Where Claude Code expects configuration
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Claude configuration directory
|
|
* Environment override: CLAUDE_CONFIG_DIR
|
|
*/
|
|
getClaudeConfigDirectory(): string {
|
|
if (this._claudeConfigDirectory) return this._claudeConfigDirectory;
|
|
|
|
this._claudeConfigDirectory = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
return this._claudeConfigDirectory;
|
|
}
|
|
|
|
/**
|
|
* Claude settings file path
|
|
*/
|
|
getClaudeSettingsPath(): string {
|
|
return join(this.getClaudeConfigDirectory(), 'settings.json');
|
|
}
|
|
|
|
/**
|
|
* Claude commands directory where custom commands are installed
|
|
*/
|
|
getClaudeCommandsDirectory(): string {
|
|
return join(this.getClaudeConfigDirectory(), 'commands');
|
|
}
|
|
|
|
/**
|
|
* CLAUDE.md instructions file path
|
|
*/
|
|
getClaudeMdPath(): string {
|
|
return join(this.getClaudeConfigDirectory(), 'CLAUDE.md');
|
|
}
|
|
|
|
/**
|
|
* MCP configuration file path (user-level)
|
|
*/
|
|
getMcpConfigPath(): string {
|
|
return join(homedir(), '.claude.json');
|
|
}
|
|
|
|
/**
|
|
* MCP configuration file path (project-level)
|
|
*/
|
|
getProjectMcpConfigPath(): string {
|
|
return join(process.cwd(), '.mcp.json');
|
|
}
|
|
|
|
// =============================================================================
|
|
// PACKAGE DISCOVERY - Find claude-mem package resources
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Discover the claude-mem package root directory
|
|
*/
|
|
getPackageRoot(): string {
|
|
if (this._packageRoot) return this._packageRoot;
|
|
|
|
// Method 1: Try require.resolve for package.json
|
|
try {
|
|
const packageJsonPath = require.resolve('claude-mem/package.json');
|
|
this._packageRoot = dirname(packageJsonPath);
|
|
return this._packageRoot;
|
|
} catch {
|
|
// Continue to next method
|
|
}
|
|
|
|
// Method 2: Walk up from current module location
|
|
const currentFile = fileURLToPath(import.meta.url);
|
|
let currentDir = dirname(currentFile);
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
const packageJsonPath = join(currentDir, 'package.json');
|
|
if (existsSync(packageJsonPath)) {
|
|
const packageJson = require(packageJsonPath);
|
|
if (packageJson.name === 'claude-mem') {
|
|
this._packageRoot = currentDir;
|
|
return this._packageRoot;
|
|
}
|
|
}
|
|
|
|
const parentDir = dirname(currentDir);
|
|
if (parentDir === currentDir) break;
|
|
currentDir = parentDir;
|
|
}
|
|
|
|
throw new Error('Cannot locate claude-mem package root. Ensure claude-mem is properly installed.');
|
|
}
|
|
|
|
|
|
/**
|
|
* Find commands directory in the installed package
|
|
*/
|
|
findPackageCommandsDirectory(): string {
|
|
const packageRoot = this.getPackageRoot();
|
|
const commandsDir = join(packageRoot, 'commands');
|
|
|
|
// Verify it contains expected command files
|
|
const requiredCommands = ['save.md'];
|
|
for (const commandFile of requiredCommands) {
|
|
if (!existsSync(join(commandsDir, commandFile))) {
|
|
throw new Error(`Package commands directory missing required file: ${commandFile}`);
|
|
}
|
|
}
|
|
|
|
return commandsDir;
|
|
}
|
|
|
|
// =============================================================================
|
|
// UTILITY METHODS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Ensure a directory exists, creating it if necessary
|
|
*/
|
|
ensureDirectory(dirPath: string): void {
|
|
if (!existsSync(dirPath)) {
|
|
require('fs').mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure multiple directories exist
|
|
*/
|
|
ensureDirectories(dirPaths: string[]): void {
|
|
dirPaths.forEach(dirPath => this.ensureDirectory(dirPath));
|
|
}
|
|
|
|
/**
|
|
* Create all claude-mem data directories
|
|
*/
|
|
ensureAllDataDirectories(): void {
|
|
this.ensureDirectories([
|
|
this.getDataDirectory(),
|
|
this.getArchivesDirectory(),
|
|
this.getLogsDirectory(),
|
|
this.getTrashDirectory(),
|
|
this.getBackupsDirectory(),
|
|
this.getChromaDirectory()
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create all Claude integration directories
|
|
*/
|
|
ensureAllClaudeDirectories(): void {
|
|
this.ensureDirectories([
|
|
this.getClaudeConfigDirectory(),
|
|
this.getClaudeCommandsDirectory()
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Extract project name from a file path (improved from PathResolver)
|
|
*/
|
|
static extractProjectName(filePath: string): string {
|
|
const pathParts = filePath.split(sep);
|
|
|
|
// Look for common project indicators
|
|
const projectIndicators = ['src', 'lib', 'app', 'project', 'workspace'];
|
|
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
if (projectIndicators.includes(pathParts[i]) && i > 0) {
|
|
return pathParts[i - 1];
|
|
}
|
|
}
|
|
|
|
// Fallback to directory containing the file
|
|
if (pathParts.length > 1) {
|
|
return pathParts[pathParts.length - 2];
|
|
}
|
|
|
|
return 'unknown-project';
|
|
}
|
|
|
|
/**
|
|
* Get current project directory name
|
|
* Uses git repository root's basename if in a git repo, otherwise falls back to cwd basename
|
|
*/
|
|
static getCurrentProjectName(): string {
|
|
try {
|
|
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'ignore']
|
|
}).trim();
|
|
return require('path').basename(gitRoot);
|
|
} catch {
|
|
return require('path').basename(process.cwd());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a timestamped backup filename
|
|
*/
|
|
static createBackupFilename(originalPath: string): string {
|
|
const timestamp = new Date()
|
|
.toISOString()
|
|
.replace(/[:.]/g, '-')
|
|
.replace('T', '_')
|
|
.slice(0, 19);
|
|
|
|
return `${originalPath}.backup.${timestamp}`;
|
|
}
|
|
|
|
/**
|
|
* Check if a path exists and is accessible
|
|
*/
|
|
static isPathAccessible(path: string): boolean {
|
|
return existsSync(path) && statSync(path).isDirectory();
|
|
}
|
|
|
|
} |