316 lines
9.8 KiB
TypeScript
316 lines
9.8 KiB
TypeScript
/**
|
|
* ClaudeMdReader service - Reads CLAUDE.md files and calculates token counts.
|
|
*
|
|
* Responsibilities:
|
|
* - Read CLAUDE.md files from various locations
|
|
* - Calculate character counts and estimate token counts
|
|
* - Handle file not found gracefully
|
|
* - Support tilde (~) expansion to home directory
|
|
*/
|
|
|
|
import { encodePath, getClaudeBasePath, getHomeDir } from '@main/utils/pathDecoder';
|
|
import { countTokens } from '@main/utils/tokenizer';
|
|
import { createLogger } from '@shared/utils/logger';
|
|
import * as path from 'path';
|
|
|
|
import { LocalFileSystemProvider } from '../infrastructure/LocalFileSystemProvider';
|
|
|
|
import type { FileSystemProvider } from '../infrastructure/FileSystemProvider';
|
|
|
|
const logger = createLogger('Service:ClaudeMdReader');
|
|
|
|
const defaultProvider = new LocalFileSystemProvider();
|
|
|
|
// ===========================================================================
|
|
// Types
|
|
// ===========================================================================
|
|
|
|
export interface ClaudeMdFileInfo {
|
|
path: string;
|
|
exists: boolean;
|
|
charCount: number;
|
|
estimatedTokens: number; // charCount / 4
|
|
}
|
|
|
|
export interface ClaudeMdReadResult {
|
|
files: Map<string, ClaudeMdFileInfo>;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Helper Functions
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Expands tilde (~) in a path to the actual home directory.
|
|
* @param filePath - Path that may contain ~
|
|
* @returns Expanded path with ~ replaced by home directory
|
|
*/
|
|
function expandTilde(filePath: string): string {
|
|
if (filePath.startsWith('~')) {
|
|
const homeDir = getHomeDir();
|
|
return path.join(homeDir, filePath.slice(1));
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Main Functions
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Reads a single CLAUDE.md file and returns its info.
|
|
* @param filePath - Path to the CLAUDE.md file (supports ~ expansion)
|
|
* @param fsProvider - Optional filesystem provider (defaults to local)
|
|
* @returns ClaudeMdFileInfo with file details
|
|
*/
|
|
async function readClaudeMdFile(
|
|
filePath: string,
|
|
fsProvider: FileSystemProvider = defaultProvider
|
|
): Promise<ClaudeMdFileInfo> {
|
|
const expandedPath = expandTilde(filePath);
|
|
|
|
try {
|
|
if (!(await fsProvider.exists(expandedPath))) {
|
|
return {
|
|
path: expandedPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
|
|
const content = await fsProvider.readFile(expandedPath);
|
|
const charCount = content.length;
|
|
const estimatedTokens = countTokens(content);
|
|
|
|
return {
|
|
path: expandedPath,
|
|
exists: true,
|
|
charCount,
|
|
estimatedTokens,
|
|
};
|
|
} catch (error) {
|
|
// Handle permission denied, file not readable, etc.
|
|
logger.error(`Error reading CLAUDE.md file at ${expandedPath}:`, error);
|
|
return {
|
|
path: expandedPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads all .md files in a directory and returns combined info.
|
|
* Used for project rules directory.
|
|
* @param dirPath - Path to the directory (supports ~ expansion)
|
|
* @param fsProvider - Optional filesystem provider (defaults to local)
|
|
* @returns ClaudeMdFileInfo with combined stats from all .md files
|
|
*/
|
|
async function readDirectoryMdFiles(
|
|
dirPath: string,
|
|
fsProvider: FileSystemProvider = defaultProvider
|
|
): Promise<ClaudeMdFileInfo> {
|
|
const expandedPath = expandTilde(dirPath);
|
|
|
|
try {
|
|
if (!(await fsProvider.exists(expandedPath))) {
|
|
return {
|
|
path: expandedPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
|
|
const stats = await fsProvider.stat(expandedPath);
|
|
if (!stats.isDirectory()) {
|
|
return {
|
|
path: expandedPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
|
|
const mdFiles = await collectMdFiles(expandedPath, fsProvider);
|
|
|
|
if (mdFiles.length === 0) {
|
|
return {
|
|
path: expandedPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
|
|
let totalCharCount = 0;
|
|
const allContent: string[] = [];
|
|
|
|
for (const filePath of mdFiles) {
|
|
try {
|
|
const content = await fsProvider.readFile(filePath);
|
|
totalCharCount += content.length;
|
|
allContent.push(content);
|
|
} catch {
|
|
// Skip files we can't read
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Count tokens on combined content for accuracy
|
|
const estimatedTokens = countTokens(allContent.join('\n'));
|
|
|
|
return {
|
|
path: expandedPath,
|
|
exists: true,
|
|
charCount: totalCharCount,
|
|
estimatedTokens,
|
|
};
|
|
} catch (error) {
|
|
logger.error(`Error reading directory ${expandedPath}:`, error);
|
|
return {
|
|
path: expandedPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively collect all .md files in a directory tree.
|
|
*/
|
|
async function collectMdFiles(
|
|
dir: string,
|
|
fsProvider: FileSystemProvider = defaultProvider
|
|
): Promise<string[]> {
|
|
const mdFiles: string[] = [];
|
|
try {
|
|
const entries = await fsProvider.readdir(dir);
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
try {
|
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
mdFiles.push(fullPath);
|
|
} else if (entry.isDirectory()) {
|
|
mdFiles.push(...(await collectMdFiles(fullPath, fsProvider)));
|
|
}
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
} catch {
|
|
// Directory not readable
|
|
}
|
|
return mdFiles;
|
|
}
|
|
|
|
/**
|
|
* Returns the platform-specific enterprise CLAUDE.md path.
|
|
*/
|
|
function getEnterprisePath(): string {
|
|
switch (process.platform) {
|
|
case 'win32':
|
|
return 'C:\\Program Files\\ClaudeCode\\CLAUDE.md';
|
|
case 'darwin':
|
|
return '/Library/Application Support/ClaudeCode/CLAUDE.md';
|
|
default:
|
|
return '/etc/claude-code/CLAUDE.md';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads auto memory MEMORY.md file for a project.
|
|
* Only reads the first 200 lines, matching Claude Code behavior.
|
|
*/
|
|
async function readAutoMemoryFile(
|
|
projectRoot: string,
|
|
fsProvider: FileSystemProvider = defaultProvider
|
|
): Promise<ClaudeMdFileInfo> {
|
|
const expandedRoot = expandTilde(projectRoot);
|
|
const encoded = encodePath(expandedRoot);
|
|
const memoryPath = path.join(getClaudeBasePath(), 'projects', encoded, 'memory', 'MEMORY.md');
|
|
|
|
try {
|
|
if (!(await fsProvider.exists(memoryPath))) {
|
|
return { path: memoryPath, exists: false, charCount: 0, estimatedTokens: 0 };
|
|
}
|
|
|
|
const content = await fsProvider.readFile(memoryPath);
|
|
// Only first 200 lines, matching Claude Code behavior
|
|
const lines = content.split('\n');
|
|
const truncated = lines.slice(0, 200).join('\n');
|
|
const charCount = truncated.length;
|
|
const estimatedTokens = countTokens(truncated);
|
|
|
|
return { path: memoryPath, exists: true, charCount, estimatedTokens };
|
|
} catch (error) {
|
|
logger.error(`Error reading auto memory at ${memoryPath}:`, error);
|
|
return { path: memoryPath, exists: false, charCount: 0, estimatedTokens: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads all potential CLAUDE.md locations for a project.
|
|
* @param projectRoot - The root directory of the project
|
|
* @param fsProvider - Optional filesystem provider (defaults to local)
|
|
* @returns ClaudeMdReadResult with Map of path -> ClaudeMdFileInfo
|
|
*/
|
|
export async function readAllClaudeMdFiles(
|
|
projectRoot: string,
|
|
fsProvider: FileSystemProvider = defaultProvider
|
|
): Promise<ClaudeMdReadResult> {
|
|
const files = new Map<string, ClaudeMdFileInfo>();
|
|
const expandedProjectRoot = expandTilde(projectRoot);
|
|
|
|
// 1. Enterprise CLAUDE.md (platform-specific path)
|
|
const enterprisePath = getEnterprisePath();
|
|
files.set('enterprise', await readClaudeMdFile(enterprisePath, fsProvider));
|
|
|
|
// 2. User memory: <Claude root>/CLAUDE.md
|
|
const userMemoryPath = path.join(getClaudeBasePath(), 'CLAUDE.md');
|
|
files.set('user', await readClaudeMdFile(userMemoryPath, fsProvider));
|
|
|
|
// 3. Project memory: ${projectRoot}/CLAUDE.md
|
|
const projectMemoryPath = path.join(expandedProjectRoot, 'CLAUDE.md');
|
|
files.set('project', await readClaudeMdFile(projectMemoryPath, fsProvider));
|
|
|
|
// 4. Project memory alt: ${projectRoot}/.claude/CLAUDE.md
|
|
const projectMemoryAltPath = path.join(expandedProjectRoot, '.claude', 'CLAUDE.md');
|
|
files.set('project-alt', await readClaudeMdFile(projectMemoryAltPath, fsProvider));
|
|
|
|
// 5. Project rules: ${projectRoot}/.claude/rules/*.md
|
|
const projectRulesPath = path.join(expandedProjectRoot, '.claude', 'rules');
|
|
files.set('project-rules', await readDirectoryMdFiles(projectRulesPath, fsProvider));
|
|
|
|
// 6. Project local: ${projectRoot}/CLAUDE.local.md
|
|
const projectLocalPath = path.join(expandedProjectRoot, 'CLAUDE.local.md');
|
|
files.set('project-local', await readClaudeMdFile(projectLocalPath, fsProvider));
|
|
|
|
// 7. User rules: <Claude root>/rules/**/*.md
|
|
const userRulesPath = path.join(getClaudeBasePath(), 'rules');
|
|
files.set('user-rules', await readDirectoryMdFiles(userRulesPath, fsProvider));
|
|
|
|
// 8. Auto memory: ~/.claude/projects/<encoded>/memory/MEMORY.md
|
|
files.set('auto-memory', await readAutoMemoryFile(projectRoot, fsProvider));
|
|
|
|
return { files };
|
|
}
|
|
|
|
/**
|
|
* Reads a specific directory's CLAUDE.md file.
|
|
* Used for directory-specific CLAUDE.md detected from file reads.
|
|
* @param dirPath - Path to the directory (supports ~ expansion)
|
|
* @param fsProvider - Optional filesystem provider (defaults to local)
|
|
* @returns ClaudeMdFileInfo for the CLAUDE.md file in that directory
|
|
*/
|
|
export async function readDirectoryClaudeMd(
|
|
dirPath: string,
|
|
fsProvider: FileSystemProvider = defaultProvider
|
|
): Promise<ClaudeMdFileInfo> {
|
|
const expandedDirPath = expandTilde(dirPath);
|
|
const claudeMdPath = path.join(expandedDirPath, 'CLAUDE.md');
|
|
return readClaudeMdFile(claudeMdPath, fsProvider);
|
|
}
|