Refactor path management: Migrate path discovery logic to shared paths module

- Removed `path-discovery.ts` service and replaced its usage with a new `paths.ts` module.
- Updated all commands and services to utilize the new path constants and helper functions.
- Ensured all necessary directories are created using the new utility functions.
- Improved code readability and maintainability by centralizing path configurations.
This commit is contained in:
Alex Newman
2025-10-16 14:46:47 -04:00
parent 2ba840aaac
commit f2551ac366
18 changed files with 327 additions and 233 deletions
+9 -12
View File
@@ -1,7 +1,7 @@
import { OptionValues } from 'commander';
import fs from 'fs';
import path from 'path';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
import { HooksDatabase } from '../services/sqlite/index.js';
type CheckStatus = 'pass' | 'fail' | 'warn';
@@ -20,24 +20,22 @@ function printCheck(result: CheckResult): void {
}
export async function doctor(options: OptionValues = {}): Promise<void> {
const discovery = PathDiscovery.getInstance();
const checks: CheckResult[] = [];
// Data directory
try {
const dataDir = discovery.getDataDirectory();
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
checks.push({ name: `Data directory created at ${dataDir}`, status: 'warn' });
if (!fs.existsSync(paths.DATA_DIR)) {
fs.mkdirSync(paths.DATA_DIR, { recursive: true });
checks.push({ name: `Data directory created at ${paths.DATA_DIR}`, status: 'warn' });
} else {
const stats = fs.statSync(dataDir);
const stats = fs.statSync(paths.DATA_DIR);
let writable = false;
try {
fs.accessSync(dataDir, fs.constants.W_OK);
fs.accessSync(paths.DATA_DIR, fs.constants.W_OK);
writable = true;
} catch {}
checks.push({
name: `Data directory ${dataDir}`,
name: `Data directory ${paths.DATA_DIR}`,
status: stats.isDirectory() && writable ? 'pass' : 'fail',
details: stats.isDirectory() && writable ? 'accessible' : 'not writable'
});
@@ -68,12 +66,11 @@ export async function doctor(options: OptionValues = {}): Promise<void> {
// Chroma connectivity
try {
const chromaDir = discovery.getChromaDirectory();
const chromaExists = fs.existsSync(chromaDir);
const chromaExists = fs.existsSync(paths.CHROMA_DIR);
checks.push({
name: 'Chroma vector store',
status: chromaExists ? 'pass' : 'warn',
details: chromaExists ? `data dir ${path.resolve(chromaDir)}` : 'Not yet initialized'
details: chromaExists ? `data dir ${path.resolve(paths.CHROMA_DIR)}` : 'Not yet initialized'
});
} catch (error: any) {
checks.push({
+17 -29
View File
@@ -10,8 +10,8 @@ import chalk from 'chalk';
import boxen from 'boxen';
import { PACKAGE_NAME } from '../shared/config.js';
import type { Settings } from '../shared/types.js';
import { PathDiscovery } from '../services/path-discovery.js';
import { Platform } from '../utils/platform.js';
import * as paths from '../shared/paths.js';
// Enhanced animation utilities
@@ -60,8 +60,7 @@ function installUv(): void {
function hasExistingInstallation(): boolean {
const pathDiscovery = PathDiscovery.getInstance();
const settingsPath = pathDiscovery.getClaudeSettingsPath();
const settingsPath = paths.CLAUDE_SETTINGS_PATH;
if (!existsSync(settingsPath)) return false;
@@ -181,16 +180,14 @@ async function runInstallationWizard(existingInstall: boolean): Promise<InstallC
// <Block> Directory structure creation - natural setup flow
function ensureDirectoryStructure(): void {
const pathDiscovery = PathDiscovery.getInstance();
// Create all data directories
pathDiscovery.ensureAllDataDirectories();
paths.ensureAllDataDirs();
// Create all Claude integration directories
pathDiscovery.ensureAllClaudeDirectories();
paths.ensureAllClaudeDirs();
// Create package.json in .claude-mem to fix ESM module issues
const packageJsonPath = join(pathDiscovery.getDataDirectory(), 'package.json');
const packageJsonPath = join(paths.DATA_DIR, 'package.json');
if (!existsSync(packageJsonPath)) {
const packageJson = {
name: "claude-mem-data",
@@ -204,9 +201,7 @@ function ensureDirectoryStructure(): void {
function copyFileRecursively(src: string, dest: string): void {
const stat = statSync(src);
if (stat.isDirectory()) {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
mkdirSync(dest, { recursive: true });
const files = readdirSync(src);
files.forEach((file: string) => {
copyFileRecursively(join(src, file), join(dest, file));
@@ -219,14 +214,11 @@ function copyFileRecursively(src: string, dest: string): void {
function ensureClaudeMdInstructions(): void {
const pathDiscovery = PathDiscovery.getInstance();
const claudeMdPath = pathDiscovery.getClaudeMdPath();
const claudeMdPath = paths.CLAUDE_MD_PATH;
const claudeMdDir = dirname(claudeMdPath);
// Ensure .claude directory exists
if (!existsSync(claudeMdDir)) {
mkdirSync(claudeMdDir, { recursive: true });
}
mkdirSync(claudeMdDir, { recursive: true });
const instructions = `
<!-- CLAUDE-MEM QUICK REFERENCE -->
@@ -308,7 +300,7 @@ function installChromaMcp(forceReinstall: boolean = false): void {
}
}
const chromaMcpCommand = `claude mcp add claude-mem -- uvx chroma-mcp --client-type persistent --data-dir ${PathDiscovery.getInstance().getChromaDirectory()}`;
const chromaMcpCommand = `claude mcp add claude-mem -- uvx chroma-mcp --client-type persistent --data-dir ${paths.CHROMA_DIR}`;
execSync(chromaMcpCommand, { stdio: 'inherit' });
}
@@ -355,13 +347,12 @@ function getSettingsPath(config: InstallConfig): string {
} else if (config.scope === 'project') {
return join(process.cwd(), '.claude', 'settings.json');
} else {
return PathDiscovery.getInstance().getClaudeSettingsPath();
return paths.CLAUDE_SETTINGS_PATH;
}
}
function configureUserSettings(config: InstallConfig): void {
const pathDiscovery = PathDiscovery.getInstance();
const userSettingsPath = pathDiscovery.getUserSettingsPath();
const userSettingsPath = paths.USER_SETTINGS_PATH;
let userSettings: Settings = existsSync(userSettingsPath)
? JSON.parse(readFileSync(userSettingsPath, 'utf8'))
@@ -384,9 +375,7 @@ function configureSmartTrashAlias(): void {
if (!existsSync(configPath)) {
// Create the file if it doesn't exist (especially for PowerShell profiles)
const dir = dirname(configPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
mkdirSync(dir, { recursive: true });
writeFileSync(configPath, '');
}
@@ -401,9 +390,8 @@ function configureSmartTrashAlias(): void {
function installClaudeCommands(): void {
const pathDiscovery = PathDiscovery.getInstance();
const claudeCommandsDir = pathDiscovery.getClaudeCommandsDirectory();
const packageCommandsDir = pathDiscovery.findPackageCommandsDirectory();
const claudeCommandsDir = paths.CLAUDE_COMMANDS_DIR;
const packageCommandsDir = paths.getPackageCommandsDir();
mkdirSync(claudeCommandsDir, { recursive: true });
+2 -2
View File
@@ -1,7 +1,7 @@
import { OptionValues } from 'commander';
import { readFileSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
// <Block> 1.1 ====================================
async function showLog(logPath: string, logType: string, tail: number): Promise<void> {
@@ -39,7 +39,7 @@ async function showLog(logPath: string, logType: string, tail: number): Promise<
// <Block> 2.1 ====================================
export async function logs(options: OptionValues = {}): Promise<void> {
// <Block> 2.2 ====================================
const logsDir = PathDiscovery.getLogsDirectory();
const logsDir = paths.LOGS_DIR;
const tail = parseInt(options.tail) || 20;
// </Block> =======================================
+2 -2
View File
@@ -1,10 +1,10 @@
import { readdirSync, renameSync } from 'fs';
import { join } from 'path';
import * as p from '@clack/prompts';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
export async function restore(): Promise<void> {
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
const trashDir = paths.TRASH_DIR;
const files = readdirSync(trashDir);
if (files.length === 0) {
+6 -6
View File
@@ -2,7 +2,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
import { HooksDatabase } from '../services/sqlite/index.js';
import chalk from 'chalk';
@@ -12,7 +12,7 @@ export async function status(): Promise<void> {
console.log('🔍 Claude Memory System Status Check');
console.log('=====================================\n');
const pathDiscovery = PathDiscovery.getInstance();
// paths imported
console.log('⚙️ Settings Configuration:');
@@ -53,13 +53,13 @@ export async function status(): Promise<void> {
}
};
checkSettings('Global', pathDiscovery.getClaudeSettingsPath());
checkSettings('Global', paths.ClaudeSettingsPath());
checkSettings('Project', join(process.cwd(), '.claude', 'settings.json'));
console.log('');
console.log('📦 Compressed Transcripts:');
const claudeProjectsDir = join(pathDiscovery.getClaudeConfigDirectory(), 'projects');
const claudeProjectsDir = join(paths.ClaudeConfigDirectory(), 'projects');
if (existsSync(claudeProjectsDir)) {
try {
@@ -116,13 +116,13 @@ export async function status(): Promise<void> {
console.log('🧠 Chroma Storage Status:');
console.log(' ✅ Storage backend: Chroma MCP');
console.log(` 📍 Data location: ${pathDiscovery.getChromaDirectory()}`);
console.log(` 📍 Data location: ${paths.ChromaDirectory()}`);
console.log(' 🔍 Features: Vector search, semantic similarity, document storage');
console.log('');
console.log('📊 Summary:');
const globalPath = pathDiscovery.getClaudeSettingsPath();
const globalPath = paths.ClaudeSettingsPath();
const projectPath = join(process.cwd(), '.claude', 'settings.json');
let isInstalled = false;
+2 -2
View File
@@ -1,10 +1,10 @@
import { rmSync, readdirSync, existsSync, statSync } from 'fs';
import { join } from 'path';
import * as p from '@clack/prompts';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
export async function emptyTrash(options: { force?: boolean } = {}): Promise<void> {
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
const trashDir = paths.TRASH_DIR;
// Check if trash directory exists
if (!existsSync(trashDir)) {
+2 -2
View File
@@ -1,7 +1,7 @@
import { readdirSync, statSync } from 'fs';
import { join, basename } from 'path';
import * as p from '@clack/prompts';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
interface TrashItem {
originalName: string;
@@ -51,7 +51,7 @@ function getDirectorySize(dirPath: string): number {
}
export async function viewTrash(): Promise<void> {
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
const trashDir = paths.TRASH_DIR;
try {
const files = readdirSync(trashDir);
+2 -2
View File
@@ -1,7 +1,7 @@
import { renameSync, existsSync, mkdirSync, statSync } from 'fs';
import { join, basename } from 'path';
import { glob } from 'glob';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
interface TrashOptions {
force?: boolean;
@@ -9,7 +9,7 @@ interface TrashOptions {
}
export async function trash(filePaths: string | string[], options: TrashOptions = {}): Promise<void> {
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
const trashDir = paths.TRASH_DIR;
if (!existsSync(trashDir)) mkdirSync(trashDir, { recursive: true });
// Handle single string or array of paths
+3 -4
View File
@@ -2,7 +2,7 @@ import { OptionValues } from 'commander';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { PathDiscovery } from '../services/path-discovery.js';
import * as paths from '../shared/paths.js';
async function removeSmartTrashAlias(): Promise<boolean> {
const homeDir = homedir();
@@ -71,7 +71,7 @@ export async function uninstall(options: OptionValues = {}): Promise<void> {
if (options.all) {
locations.push({
name: 'User',
path: PathDiscovery.getInstance().getClaudeSettingsPath()
path: paths.CLAUDE_SETTINGS_PATH
});
locations.push({
name: 'Project',
@@ -79,10 +79,9 @@ export async function uninstall(options: OptionValues = {}): Promise<void> {
});
} else {
const isProject = options.project;
const pathDiscovery = PathDiscovery.getInstance();
locations.push({
name: isProject ? 'Project' : 'User',
path: isProject ? join(process.cwd(), '.claude', 'settings.json') : pathDiscovery.getClaudeSettingsPath()
path: isProject ? join(process.cwd(), '.claude', 'settings.json') : paths.CLAUDE_SETTINGS_PATH
});
}
+1 -2
View File
@@ -1,5 +1,4 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { PathDiscovery } from '../services/path-discovery.js';
import path from 'path';
export interface SessionStartInput {
@@ -69,7 +68,7 @@ export function contextHook(input: SessionStartInput): void {
output.push(`**Files Edited:** ${files.join(', ')}`);
}
} catch {
// If not valid JSON, show as text
// Backwards compatibility: if not valid JSON, show as text
if (summary.files_edited.trim()) {
output.push(`**Files Edited:** ${summary.files_edited}`);
}
+2 -4
View File
@@ -1,7 +1,6 @@
import net from 'net';
import { join } from 'path';
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { PathDiscovery } from '../services/path-discovery.js';
import { getWorkerSocketPath } from '../shared/paths.js';
export interface PostToolUseInput {
session_id: string;
@@ -45,8 +44,7 @@ export function saveHook(input: PostToolUseInput): void {
}
// Get socket path
const dataDir = PathDiscovery.getInstance().getDataDirectory();
const socketPath = join(dataDir, `worker-${session.id}.sock`);
const socketPath = getWorkerSocketPath(session.id);
// Send observation via Unix socket
const message = {
+2 -4
View File
@@ -1,7 +1,6 @@
import net from 'net';
import { join } from 'path';
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { PathDiscovery } from '../services/path-discovery.js';
import { getWorkerSocketPath } from '../shared/paths.js';
export interface StopInput {
session_id: string;
@@ -29,8 +28,7 @@ export function summaryHook(input: StopInput): void {
}
// Get socket path
const dataDir = PathDiscovery.getInstance().getDataDirectory();
const socketPath = join(dataDir, `worker-${session.id}.sock`);
const socketPath = getWorkerSocketPath(session.id);
// Send FINALIZE message via Unix socket
const message = {
+2 -6
View File
@@ -6,10 +6,9 @@
import net from 'net';
import { unlinkSync, existsSync } from 'fs';
import { join } from 'path';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { PathDiscovery } from '../services/path-discovery.js';
import { getWorkerSocketPath } from '../shared/paths.js';
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from './prompts.js';
import { parseObservations, parseSummary } from './parser.js';
import type { SDKSession } from './prompts.js';
@@ -64,10 +63,7 @@ class SDKWorker {
this.sessionDbId = sessionDbId;
this.db = new HooksDatabase();
this.abortController = new AbortController();
// Socket path: ~/.claude-mem/worker-{sessionId}.sock
const dataDir = PathDiscovery.getInstance().getDataDirectory();
this.socketPath = join(dataDir, `worker-${sessionDbId}.sock`);
this.socketPath = getWorkerSocketPath(sessionDbId);
}
/**
+3 -7
View File
@@ -1,7 +1,5 @@
import { Database as BunDatabase } from 'bun:sqlite';
import path from 'path';
import fs from 'fs';
import { PathDiscovery } from '../path-discovery.js';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
// Type alias for better-sqlite3 compatibility
type Database = BunDatabase;
@@ -47,11 +45,9 @@ export class DatabaseManager {
}
// Ensure the data directory exists
const dataDir = PathDiscovery.getInstance().getDataDirectory();
fs.mkdirSync(dataDir, { recursive: true });
ensureDir(DATA_DIR);
const dbPath = path.join(dataDir, 'claude-mem.db');
this.db = new BunDatabase(dbPath, { create: true, readwrite: true });
this.db = new BunDatabase(DB_PATH, { create: true, readwrite: true });
// Apply optimized SQLite settings
this.db.run('PRAGMA journal_mode = WAL');
+3 -8
View File
@@ -1,7 +1,5 @@
import { Database } from 'bun:sqlite';
import path from 'path';
import fs from 'fs';
import { PathDiscovery } from '../path-discovery.js';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
/**
* Lightweight database interface for hooks
@@ -12,11 +10,8 @@ export class HooksDatabase {
private db: Database;
constructor() {
const dataDir = PathDiscovery.getInstance().getDataDirectory();
fs.mkdirSync(dataDir, { recursive: true });
const dbPath = path.join(dataDir, 'claude-mem.db');
this.db = new Database(dbPath, { create: true, readwrite: true });
ensureDir(DATA_DIR);
this.db = new Database(DB_PATH, { create: true, readwrite: true });
// Ensure optimized settings
this.db.run('PRAGMA journal_mode = WAL');
+145
View File
@@ -0,0 +1,145 @@
import { join, dirname, basename, sep } from 'path';
import { homedir } from 'os';
import { existsSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
/**
* Simple path configuration for claude-mem
* Standard paths based on Claude Code conventions
*/
// Base directories
export const DATA_DIR = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
// Data subdirectories
export const ARCHIVES_DIR = join(DATA_DIR, 'archives');
export const LOGS_DIR = join(DATA_DIR, 'logs');
export const TRASH_DIR = join(DATA_DIR, 'trash');
export const BACKUPS_DIR = join(DATA_DIR, 'backups');
export const CHROMA_DIR = join(DATA_DIR, 'chroma');
export const USER_SETTINGS_PATH = join(DATA_DIR, 'settings.json');
export const DB_PATH = join(DATA_DIR, 'claude-mem.db');
// Claude integration paths
export const CLAUDE_SETTINGS_PATH = join(CLAUDE_CONFIG_DIR, 'settings.json');
export const CLAUDE_COMMANDS_DIR = join(CLAUDE_CONFIG_DIR, 'commands');
export const CLAUDE_MD_PATH = join(CLAUDE_CONFIG_DIR, 'CLAUDE.md');
/**
* Get project-specific archive directory
*/
export function getProjectArchiveDir(projectName: string): string {
return join(ARCHIVES_DIR, projectName);
}
/**
* Get worker socket path for a session
*/
export function getWorkerSocketPath(sessionId: number): string {
return join(DATA_DIR, `worker-${sessionId}.sock`);
}
/**
* Ensure a directory exists
*/
export function ensureDir(dirPath: string): void {
mkdirSync(dirPath, { recursive: true });
}
/**
* Ensure all data directories exist
*/
export function ensureAllDataDirs(): void {
ensureDir(DATA_DIR);
ensureDir(ARCHIVES_DIR);
ensureDir(LOGS_DIR);
ensureDir(TRASH_DIR);
ensureDir(BACKUPS_DIR);
ensureDir(CHROMA_DIR);
}
/**
* Ensure all Claude integration directories exist
*/
export function ensureAllClaudeDirs(): void {
ensureDir(CLAUDE_CONFIG_DIR);
ensureDir(CLAUDE_COMMANDS_DIR);
}
/**
* Get current project name from git root or cwd
*/
export function getCurrentProjectName(): string {
try {
const gitRoot = execSync('git rev-parse --show-toplevel', {
cwd: process.cwd(),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return basename(gitRoot);
} catch {
return basename(process.cwd());
}
}
/**
* Find package root directory (for install command)
*/
export function getPackageRoot(): string {
// Method 1: Try require.resolve for package.json
try {
const packageJsonPath = require.resolve('claude-mem/package.json');
return dirname(packageJsonPath);
} 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') {
return currentDir;
}
}
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
*/
export function getPackageCommandsDir(): string {
const packageRoot = getPackageRoot();
const commandsDir = join(packageRoot, 'commands');
if (!existsSync(join(commandsDir, 'save.md'))) {
throw new Error('Package commands directory missing required files');
}
return commandsDir;
}
/**
* Create a timestamped backup filename
*/
export function createBackupFilename(originalPath: string): string {
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.slice(0, 19);
return `${originalPath}.backup.${timestamp}`;
}