The isDirectChild() function failed to match files when the API used absolute paths (/Users/x/project/app/api) but the database stored relative paths (app/api/router.py). This caused all folder CLAUDE.md files to incorrectly show "No recent activity". Changes: - Create shared path-utils module with proper path normalization - Implement suffix matching strategy for mixed path formats - Update SessionSearch.ts to use shared utilities - Update regenerate-claude-md.ts to use shared utilities (was using outdated broken logic) - Prevent spurious directory creation from malformed paths - Add comprehensive test coverage for path matching edge cases This is the proper fix for #794, replacing PR #809 which only masked the bug by skipping file creation when "no activity" was shown. Co-authored-by: bigphoot <bigphoot@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,8 +43,10 @@ interface ObservationRow {
|
||||
discovery_tokens: number | null;
|
||||
}
|
||||
|
||||
// Import shared formatting utilities
|
||||
// Import shared utilities
|
||||
import { formatTime, groupByDate } from '../src/shared/timeline-formatting.js';
|
||||
import { isDirectChild } from '../src/shared/path-utils.js';
|
||||
import { replaceTaggedContent } from '../src/utils/claude-md-utils.js';
|
||||
|
||||
// Type icon map (matches ModeManager)
|
||||
const TYPE_ICONS: Record<string, string> = {
|
||||
@@ -135,19 +137,6 @@ function walkDirectoriesWithIgnore(dir: string, folders: Set<string>, depth: num
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is a direct child of a folder (not in a subfolder)
|
||||
* @param filePath - File path like "src/services/foo.ts"
|
||||
* @param folderPath - Folder path like "src/services"
|
||||
* @returns true if file is directly in folder, false if in a subfolder
|
||||
*/
|
||||
function isDirectChild(filePath: string, folderPath: string): boolean {
|
||||
if (!filePath.startsWith(folderPath + '/')) return false;
|
||||
const remainder = filePath.slice(folderPath.length + 1);
|
||||
// If remainder contains a slash, it's in a subfolder
|
||||
return !remainder.includes('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an observation has any files that are direct children of the folder
|
||||
*/
|
||||
@@ -288,37 +277,27 @@ function formatObservationsForClaudeMd(observations: ObservationRow[], folderPat
|
||||
|
||||
/**
|
||||
* Write CLAUDE.md file with tagged content preservation
|
||||
* Note: For the CLI regenerate tool, we DO create directories since the user
|
||||
* explicitly requested regeneration. This differs from the runtime behavior
|
||||
* which only writes to existing folders.
|
||||
*/
|
||||
function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
|
||||
function writeClaudeMdToFolderForRegenerate(folderPath: string, newContent: string): void {
|
||||
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
||||
const tempFile = `${claudeMdPath}.tmp`;
|
||||
|
||||
// For regenerate CLI, we create the folder if needed
|
||||
mkdirSync(folderPath, { recursive: true });
|
||||
|
||||
// Read existing content if file exists
|
||||
let existingContent = '';
|
||||
if (existsSync(claudeMdPath)) {
|
||||
existingContent = readFileSync(claudeMdPath, 'utf-8');
|
||||
}
|
||||
|
||||
const startTag = '<claude-mem-context>';
|
||||
const endTag = '</claude-mem-context>';
|
||||
|
||||
let finalContent: string;
|
||||
if (!existingContent) {
|
||||
finalContent = `${startTag}\n${newContent}\n${endTag}`;
|
||||
} else {
|
||||
const startIdx = existingContent.indexOf(startTag);
|
||||
const endIdx = existingContent.indexOf(endTag);
|
||||
|
||||
if (startIdx !== -1 && endIdx !== -1) {
|
||||
finalContent = existingContent.substring(0, startIdx) +
|
||||
`${startTag}\n${newContent}\n${endTag}` +
|
||||
existingContent.substring(endIdx + endTag.length);
|
||||
} else {
|
||||
finalContent = existingContent + `\n\n${startTag}\n${newContent}\n${endTag}`;
|
||||
}
|
||||
}
|
||||
// Use shared utility to preserve user content outside tags
|
||||
const finalContent = replaceTaggedContent(existingContent, newContent);
|
||||
|
||||
// Atomic write: temp file + rename
|
||||
writeFileSync(tempFile, finalContent);
|
||||
renameSync(tempFile, claudeMdPath);
|
||||
}
|
||||
@@ -450,7 +429,7 @@ function regenerateFolder(
|
||||
|
||||
// Format using relative path for display, write to absolute path
|
||||
const formatted = formatObservationsForClaudeMd(observations, relativeFolder);
|
||||
writeClaudeMdToFolder(absoluteFolder, formatted);
|
||||
writeClaudeMdToFolderForRegenerate(absoluteFolder, formatted);
|
||||
|
||||
return { success: true, observationCount: observations.length };
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user