c6f932988a
* MAESTRO: fix ChromaDB core issues — Python pinning, Windows paths, disable toggle, metadata sanitization, transport errors - Add --python version pinning to uvx args in both local and remote mode (fixes #1196, #1206, #1208) - Convert backslash paths to forward slashes for --data-dir on Windows (fixes #1199) - Add CLAUDE_MEM_CHROMA_ENABLED setting for SQLite-only fallback mode (fixes #707) - Sanitize metadata in addDocuments() to filter null/undefined/empty values (fixes #1183, #1188) - Wrap callTool() in try/catch for transport errors with auto-reconnect (fixes #1162) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix data integrity — content-hash deduplication, project name collision, empty project guard, stuck isProcessing - Add SHA-256 content-hash deduplication to observations INSERT (store.ts, transactions.ts, SessionStore.ts) - Add content_hash column via migration 22 with backfill and index - Fix project name collision: getCurrentProjectName() now returns parent/basename - Guard against empty project string with cwd-derived fallback - Fix stuck isProcessing: hasAnyPendingWork() resets processing messages older than 5 minutes - Add 12 new tests covering all four fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix hook lifecycle — stderr suppression, output isolation, conversation pollution prevention - Suppress process.stderr.write in hookCommand() to prevent Claude Code showing diagnostic output as error UI (#1181). Restores stderr in finally block for worker-continues case. - Convert console.error() to logger.warn()/error() in hook-command.ts and handlers/index.ts so all diagnostics route to log file instead of stderr. - Verified all 7 handlers return suppressOutput: true (prevents conversation pollution #598, #784). - Verified session-complete is a recognized event type (fixes #984). - Verified unknown event types return no-op handler with exit 0 (graceful degradation). - Added 10 new tests in tests/hook-lifecycle.test.ts covering event dispatch, adapter defaults, stderr suppression, and standard response constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix worker lifecycle — restart loop coordination, stale transport retry, ENOENT shutdown race - Add PID file mtime guard to prevent concurrent restart storms (#1145): isPidFileRecent() + touchPidFile() coordinate across sessions - Add transparent retry in ChromaMcpManager.callTool() on transport error — reconnects and retries once instead of failing (#1131) - Wrap getInstalledPluginVersion() with ENOENT/EBUSY handling (#1042) - Verified ChromaMcpManager.stop() already called on all shutdown paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix Windows platform support — uvx.cmd spawn, PowerShell $_ elimination, windowsHide, FTS5 fallback - Route uvx spawn through cmd.exe /c on Windows since MCP SDK lacks shell:true (#1190, #1192, #1199) - Replace all PowerShell Where-Object {$_} pipelines with WQL -Filter server-side filtering (#1024, #1062) - Add windowsHide: true to all exec/spawn calls missing it to prevent console popups (#1048) - Add FTS5 runtime probe with graceful fallback when unavailable on Windows (#791) - Guard FTS5 table creation in migrations, SessionSearch, and SessionStore with try/catch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix skills/ distribution — build-time verification and regression tests (#1187) Add post-build verification in build-hooks.js that fails if critical distribution files (skills, hooks, plugin manifest) are missing. Add 10 regression tests covering skill file presence, YAML frontmatter, hooks.json integrity, and package.json files field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix MigrationRunner schema initialization (#979) — version conflict between parallel migration systems Root cause: old DatabaseManager migrations 1-7 shared schema_versions table with MigrationRunner's 4-22, causing version number collisions (5=drop tables vs add column, 6=FTS5 vs prompt tracking, 7=discovery_tokens vs remove UNIQUE). initializeSchema() was gated behind maxApplied===0, so core tables were never created when old versions were present. Fixes: - initializeSchema() always creates core tables via CREATE TABLE IF NOT EXISTS - Migrations 5-7 check actual DB state (columns/constraints) not just version tracking - Crash-safe temp table rebuilds (DROP IF EXISTS _new before CREATE) - Added missing migration 21 (ON UPDATE CASCADE) to MigrationRunner - Added ON UPDATE CASCADE to FK definitions in initializeSchema() - All changes applied to both runner.ts and SessionStore.ts Tests: 13 new tests in migration-runner.test.ts covering fresh DB, idempotency, version conflicts, crash recovery, FK constraints, and data integrity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix 21 test failures — stale mocks, outdated assertions, missing OpenClaw guards Server tests (12): Added missing workerPath and getAiStatus to ServerOptions mocks after interface expansion. ChromaSync tests (3): Updated to verify transport cleanup in ChromaMcpManager after architecture refactor. OpenClaw (2): Added memory_ tool skipping and response truncation to prevent recursive loops and oversized payloads. MarkdownFormatter (2): Updated assertions to match current output. SettingsDefaultsManager (1): Used correct default key for getBool test. Logger standards (1): Excluded CLI transcript command from background service check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix Codex CLI compatibility (#744) — session_id fallbacks, unknown platform tolerance, undefined guard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix Cursor IDE integration (#838, #1049) — adapter field fallbacks, tolerant session-init validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix /api/logs OOM (#1203) — tail-read replaces full-file readFileSync Replace readFileSync (loads entire file into memory) with readLastLines() that reads only from the end of the file in expanding chunks (64KB → 10MB cap). Prevents OOM on large log files while preserving the same API response shape. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix Settings CORS error (#1029) — explicit methods and allowedHeaders in CORS config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: add session custom_title for agent attribution (#1213) — migration 23, endpoint + store support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: prevent CLAUDE.md/AGENTS.md writes inside .git/ directories (#1165) Add .git path guard to all 4 write sites to prevent ref corruption when paths resolve inside .git internals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix plugin disabled state not respected (#781) — early exit check in all hook entry points Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix UserPromptSubmit context re-injection on every turn (#1079) — contextInjected session flag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MAESTRO: fix stale AbortController queue stall (#1099) — lastGeneratorActivity tracking + 30s timeout Three-layer fix: 1. Added lastGeneratorActivity timestamp to ActiveSession, updated by processAgentResponse (all agents), getMessageIterator (queue yields), and startGeneratorWithProvider (generator launch) 2. Added stale generator detection in ensureGeneratorRunning — if no activity for >30s, aborts stale controller, resets state, restarts 3. Added AbortSignal.timeout(30000) in deleteSession to prevent indefinite hang when awaiting a stuck generator promise Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
464 lines
16 KiB
TypeScript
464 lines
16 KiB
TypeScript
/**
|
|
* CLAUDE.md File Utilities
|
|
*
|
|
* Shared utilities for writing folder-level CLAUDE.md files with
|
|
* auto-generated context sections. Preserves user content outside
|
|
* <claude-mem-context> tags.
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
import { logger } from './logger.js';
|
|
import { formatDate, groupByDate } from '../shared/timeline-formatting.js';
|
|
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
|
import { getWorkerHost } from '../shared/worker-utils.js';
|
|
|
|
const SETTINGS_PATH = path.join(os.homedir(), '.claude-mem', 'settings.json');
|
|
|
|
/**
|
|
* Check for consecutive duplicate path segments like frontend/frontend/ or src/src/.
|
|
* This catches paths created when cwd already includes the directory name (Issue #814).
|
|
*
|
|
* @param resolvedPath - The resolved absolute path to check
|
|
* @returns true if consecutive duplicate segments are found
|
|
*/
|
|
function hasConsecutiveDuplicateSegments(resolvedPath: string): boolean {
|
|
const segments = resolvedPath.split(path.sep).filter(s => s && s !== '.' && s !== '..');
|
|
for (let i = 1; i < segments.length; i++) {
|
|
if (segments[i] === segments[i - 1]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validate that a file path is safe for CLAUDE.md generation.
|
|
* Rejects tilde paths, URLs, command-like strings, and paths with invalid chars.
|
|
*
|
|
* @param filePath - The file path to validate
|
|
* @param projectRoot - Optional project root for boundary checking
|
|
* @returns true if path is valid for CLAUDE.md processing
|
|
*/
|
|
function isValidPathForClaudeMd(filePath: string, projectRoot?: string): boolean {
|
|
// Reject empty or whitespace-only
|
|
if (!filePath || !filePath.trim()) return false;
|
|
|
|
// Reject tilde paths (Node.js doesn't expand ~)
|
|
if (filePath.startsWith('~')) return false;
|
|
|
|
// Reject URLs
|
|
if (filePath.startsWith('http://') || filePath.startsWith('https://')) return false;
|
|
|
|
// Reject paths with spaces (likely command text or PR references)
|
|
if (filePath.includes(' ')) return false;
|
|
|
|
// Reject paths with # (GitHub issue/PR references)
|
|
if (filePath.includes('#')) return false;
|
|
|
|
// If projectRoot provided, ensure path stays within project boundaries
|
|
if (projectRoot) {
|
|
// For relative paths, resolve against projectRoot; for absolute paths, use directly
|
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath);
|
|
const normalizedRoot = path.resolve(projectRoot);
|
|
if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
|
|
return false;
|
|
}
|
|
|
|
// Reject paths with consecutive duplicate segments (Issue #814)
|
|
// e.g., frontend/frontend/, backend/backend/, src/src/
|
|
if (hasConsecutiveDuplicateSegments(resolved)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Replace tagged content in existing file, preserving content outside tags.
|
|
*
|
|
* Handles three cases:
|
|
* 1. No existing content → wraps new content in tags
|
|
* 2. Has existing tags → replaces only tagged section
|
|
* 3. No tags in existing content → appends tagged content at end
|
|
*/
|
|
export function replaceTaggedContent(existingContent: string, newContent: string): string {
|
|
const startTag = '<claude-mem-context>';
|
|
const endTag = '</claude-mem-context>';
|
|
|
|
// If no existing content, wrap new content in tags
|
|
if (!existingContent) {
|
|
return `${startTag}\n${newContent}\n${endTag}`;
|
|
}
|
|
|
|
// If existing has tags, replace only tagged section
|
|
const startIdx = existingContent.indexOf(startTag);
|
|
const endIdx = existingContent.indexOf(endTag);
|
|
|
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
return existingContent.substring(0, startIdx) +
|
|
`${startTag}\n${newContent}\n${endTag}` +
|
|
existingContent.substring(endIdx + endTag.length);
|
|
}
|
|
|
|
// If no tags exist, append tagged content at end
|
|
return existingContent + `\n\n${startTag}\n${newContent}\n${endTag}`;
|
|
}
|
|
|
|
/**
|
|
* Write CLAUDE.md file to folder with atomic writes.
|
|
* Only writes to existing folders; skips non-existent paths to prevent
|
|
* creating spurious directory structures from malformed paths.
|
|
*
|
|
* @param folderPath - Absolute path to the folder (must already exist)
|
|
* @param newContent - Content to write inside tags
|
|
*/
|
|
export function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
|
|
const resolvedPath = path.resolve(folderPath);
|
|
|
|
// Never write inside .git directories — corrupts refs (#1165)
|
|
if (resolvedPath.includes('/.git/') || resolvedPath.includes('\\.git\\') || resolvedPath.endsWith('/.git') || resolvedPath.endsWith('\\.git')) return;
|
|
|
|
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
|
const tempFile = `${claudeMdPath}.tmp`;
|
|
|
|
// Only write to folders that already exist - never create new directories
|
|
// This prevents creating spurious folder structures from malformed paths
|
|
if (!existsSync(folderPath)) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping non-existent folder', { folderPath });
|
|
return;
|
|
}
|
|
|
|
// Read existing content if file exists
|
|
let existingContent = '';
|
|
if (existsSync(claudeMdPath)) {
|
|
existingContent = readFileSync(claudeMdPath, 'utf-8');
|
|
}
|
|
|
|
// Replace only tagged content, preserve user content
|
|
const finalContent = replaceTaggedContent(existingContent, newContent);
|
|
|
|
// Atomic write: temp file + rename
|
|
writeFileSync(tempFile, finalContent);
|
|
renameSync(tempFile, claudeMdPath);
|
|
}
|
|
|
|
/**
|
|
* Parsed observation from API response text
|
|
*/
|
|
interface ParsedObservation {
|
|
id: string;
|
|
time: string;
|
|
typeEmoji: string;
|
|
title: string;
|
|
tokens: string;
|
|
epoch: number; // For date grouping
|
|
}
|
|
|
|
/**
|
|
* Format timeline text from API response to timeline format.
|
|
*
|
|
* Uses the same format as search results:
|
|
* - Grouped by date (### Jan 4, 2026)
|
|
* - Grouped by file within each date (**filename**)
|
|
* - Table with columns: ID, Time, T (type emoji), Title, Read (tokens)
|
|
* - Ditto marks for repeated times
|
|
*
|
|
* @param timelineText - Raw API response text
|
|
* @returns Formatted markdown with date/file grouping
|
|
*/
|
|
export function formatTimelineForClaudeMd(timelineText: string): string {
|
|
const lines: string[] = [];
|
|
lines.push('# Recent Activity');
|
|
lines.push('');
|
|
|
|
// Parse the API response to extract observation rows
|
|
const apiLines = timelineText.split('\n');
|
|
|
|
// Note: We skip file grouping since we're querying by folder - all results are from the same folder
|
|
|
|
// Parse observations: | #123 | 4:30 PM | 🔧 | Title | ~250 | ... |
|
|
const observations: ParsedObservation[] = [];
|
|
let lastTimeStr = '';
|
|
let currentDate: Date | null = null;
|
|
|
|
for (const line of apiLines) {
|
|
// Check for date headers: ### Jan 4, 2026
|
|
const dateMatch = line.match(/^###\s+(.+)$/);
|
|
if (dateMatch) {
|
|
const dateStr = dateMatch[1].trim();
|
|
const parsedDate = new Date(dateStr);
|
|
// Validate the parsed date
|
|
if (!isNaN(parsedDate.getTime())) {
|
|
currentDate = parsedDate;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Match table rows: | #123 | 4:30 PM | 🔧 | Title | ~250 | ... |
|
|
// Also handles ditto marks and session IDs (#S123)
|
|
const match = line.match(/^\|\s*(#[S]?\d+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
|
|
if (match) {
|
|
const [, id, timeStr, typeEmoji, title, tokens] = match;
|
|
|
|
// Handle ditto mark (″) - use last time
|
|
let time: string;
|
|
if (timeStr.trim() === '″' || timeStr.trim() === '"') {
|
|
time = lastTimeStr;
|
|
} else {
|
|
time = timeStr.trim();
|
|
lastTimeStr = time;
|
|
}
|
|
|
|
// Parse time and combine with current date header (or fallback to today)
|
|
const baseDate = currentDate ? new Date(currentDate) : new Date();
|
|
const timeParts = time.match(/(\d+):(\d+)\s*(AM|PM)/i);
|
|
let epoch = baseDate.getTime();
|
|
if (timeParts) {
|
|
let hours = parseInt(timeParts[1], 10);
|
|
const minutes = parseInt(timeParts[2], 10);
|
|
const isPM = timeParts[3].toUpperCase() === 'PM';
|
|
if (isPM && hours !== 12) hours += 12;
|
|
if (!isPM && hours === 12) hours = 0;
|
|
baseDate.setHours(hours, minutes, 0, 0);
|
|
epoch = baseDate.getTime();
|
|
}
|
|
|
|
observations.push({
|
|
id: id.trim(),
|
|
time,
|
|
typeEmoji: typeEmoji.trim(),
|
|
title: title.trim(),
|
|
tokens: tokens.trim(),
|
|
epoch
|
|
});
|
|
}
|
|
}
|
|
|
|
if (observations.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
// Group by date
|
|
const byDate = groupByDate(observations, obs => new Date(obs.epoch).toISOString());
|
|
|
|
// Render each date group
|
|
for (const [day, dayObs] of byDate) {
|
|
lines.push(`### ${day}`);
|
|
lines.push('');
|
|
lines.push('| ID | Time | T | Title | Read |');
|
|
lines.push('|----|------|---|-------|------|');
|
|
|
|
let lastTime = '';
|
|
for (const obs of dayObs) {
|
|
const timeDisplay = obs.time === lastTime ? '"' : obs.time;
|
|
lastTime = obs.time;
|
|
lines.push(`| ${obs.id} | ${timeDisplay} | ${obs.typeEmoji} | ${obs.title} | ${obs.tokens} |`);
|
|
}
|
|
|
|
lines.push('');
|
|
}
|
|
|
|
return lines.join('\n').trim();
|
|
}
|
|
|
|
/**
|
|
* Built-in directory names where CLAUDE.md generation is unsafe or undesirable.
|
|
* e.g. Android res/ is compiler-strict (non-XML breaks build); .git, build, node_modules are tooling-owned.
|
|
*/
|
|
const EXCLUDED_UNSAFE_DIRECTORIES = new Set([
|
|
'res',
|
|
'.git',
|
|
'build',
|
|
'node_modules',
|
|
'__pycache__'
|
|
]);
|
|
|
|
/**
|
|
* Returns true if folder path contains any excluded segment (e.g. .../res/..., .../node_modules/...).
|
|
*/
|
|
function isExcludedUnsafeDirectory(folderPath: string): boolean {
|
|
const normalized = path.normalize(folderPath);
|
|
const segments = normalized.split(path.sep);
|
|
return segments.some(segment => EXCLUDED_UNSAFE_DIRECTORIES.has(segment));
|
|
}
|
|
|
|
/**
|
|
* Check if a folder is a project root (contains .git directory).
|
|
* Project root CLAUDE.md files should remain user-managed, not auto-updated.
|
|
*/
|
|
function isProjectRoot(folderPath: string): boolean {
|
|
const gitPath = path.join(folderPath, '.git');
|
|
return existsSync(gitPath);
|
|
}
|
|
|
|
/**
|
|
* Check if a folder path is excluded from CLAUDE.md generation.
|
|
* A folder is excluded if it matches or is within any path in the exclude list.
|
|
*
|
|
* @param folderPath - Absolute path to check
|
|
* @param excludePaths - Array of paths to exclude
|
|
* @returns true if folder should be excluded
|
|
*/
|
|
function isExcludedFolder(folderPath: string, excludePaths: string[]): boolean {
|
|
const normalizedFolder = path.resolve(folderPath);
|
|
for (const excludePath of excludePaths) {
|
|
const normalizedExclude = path.resolve(excludePath);
|
|
if (normalizedFolder === normalizedExclude ||
|
|
normalizedFolder.startsWith(normalizedExclude + path.sep)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update CLAUDE.md files for folders containing the given files.
|
|
* Fetches timeline from worker API and writes formatted content.
|
|
*
|
|
* NOTE: Project root folders (containing .git) are excluded to preserve
|
|
* user-managed root CLAUDE.md files. Only subfolder CLAUDE.md files are auto-updated.
|
|
*
|
|
* @param filePaths - Array of absolute file paths (modified or read)
|
|
* @param project - Project identifier for API query
|
|
* @param port - Worker API port
|
|
*/
|
|
export async function updateFolderClaudeMdFiles(
|
|
filePaths: string[],
|
|
project: string,
|
|
port: number,
|
|
projectRoot?: string
|
|
): Promise<void> {
|
|
// Load settings to get configurable observation limit and exclude list
|
|
const settings = SettingsDefaultsManager.loadFromFile(SETTINGS_PATH);
|
|
const limit = parseInt(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10) || 50;
|
|
|
|
// Parse exclude paths from settings
|
|
let folderMdExcludePaths: string[] = [];
|
|
try {
|
|
const parsed = JSON.parse(settings.CLAUDE_MEM_FOLDER_MD_EXCLUDE || '[]');
|
|
if (Array.isArray(parsed)) {
|
|
folderMdExcludePaths = parsed.filter((p): p is string => typeof p === 'string');
|
|
}
|
|
} catch {
|
|
logger.warn('FOLDER_INDEX', 'Failed to parse CLAUDE_MEM_FOLDER_MD_EXCLUDE setting');
|
|
}
|
|
|
|
// Track folders containing CLAUDE.md files that were read/modified in this observation.
|
|
// We must NOT update these - it would cause "file modified since read" errors in Claude Code.
|
|
// See: https://github.com/thedotmack/claude-mem/issues/859
|
|
const foldersWithActiveClaudeMd = new Set<string>();
|
|
|
|
// First pass: identify folders with actively-used CLAUDE.md files
|
|
for (const filePath of filePaths) {
|
|
if (!filePath) continue;
|
|
const basename = path.basename(filePath);
|
|
if (basename === 'CLAUDE.md') {
|
|
let absoluteFilePath = filePath;
|
|
if (projectRoot && !path.isAbsolute(filePath)) {
|
|
absoluteFilePath = path.join(projectRoot, filePath);
|
|
}
|
|
const folderPath = path.dirname(absoluteFilePath);
|
|
foldersWithActiveClaudeMd.add(folderPath);
|
|
logger.debug('FOLDER_INDEX', 'Detected active CLAUDE.md, will skip folder', { folderPath });
|
|
}
|
|
}
|
|
|
|
// Extract unique folder paths from file paths
|
|
const folderPaths = new Set<string>();
|
|
for (const filePath of filePaths) {
|
|
if (!filePath || filePath === '') continue;
|
|
// VALIDATE PATH BEFORE PROCESSING
|
|
if (!isValidPathForClaudeMd(filePath, projectRoot)) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping invalid file path', {
|
|
filePath,
|
|
reason: 'Failed path validation'
|
|
});
|
|
continue;
|
|
}
|
|
// Resolve relative paths to absolute using projectRoot
|
|
let absoluteFilePath = filePath;
|
|
if (projectRoot && !path.isAbsolute(filePath)) {
|
|
absoluteFilePath = path.join(projectRoot, filePath);
|
|
}
|
|
const folderPath = path.dirname(absoluteFilePath);
|
|
if (folderPath && folderPath !== '.' && folderPath !== '/') {
|
|
// Skip project root - root CLAUDE.md should remain user-managed
|
|
if (isProjectRoot(folderPath)) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping project root CLAUDE.md', { folderPath });
|
|
continue;
|
|
}
|
|
// Skip known-unsafe directories (e.g. Android res/, .git, build, node_modules)
|
|
if (isExcludedUnsafeDirectory(folderPath)) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping unsafe directory for CLAUDE.md', { folderPath });
|
|
continue;
|
|
}
|
|
// Skip folders where CLAUDE.md was read/modified in this observation (issue #859)
|
|
if (foldersWithActiveClaudeMd.has(folderPath)) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping folder with active CLAUDE.md to avoid race condition', { folderPath });
|
|
continue;
|
|
}
|
|
// Skip folders in user-configured exclude list
|
|
if (folderMdExcludePaths.length > 0 && isExcludedFolder(folderPath, folderMdExcludePaths)) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping excluded folder', { folderPath });
|
|
continue;
|
|
}
|
|
folderPaths.add(folderPath);
|
|
}
|
|
}
|
|
|
|
if (folderPaths.size === 0) return;
|
|
|
|
logger.debug('FOLDER_INDEX', 'Updating CLAUDE.md files', {
|
|
project,
|
|
folderCount: folderPaths.size
|
|
});
|
|
|
|
// Process each folder
|
|
for (const folderPath of folderPaths) {
|
|
try {
|
|
// Fetch timeline via existing API
|
|
const host = getWorkerHost();
|
|
const response = await fetch(
|
|
`http://${host}:${port}/api/search/by-file?filePath=${encodeURIComponent(folderPath)}&limit=${limit}&project=${encodeURIComponent(project)}&isFolder=true`
|
|
);
|
|
|
|
if (!response.ok) {
|
|
logger.error('FOLDER_INDEX', 'Failed to fetch timeline', { folderPath, status: response.status });
|
|
continue;
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!result.content?.[0]?.text) {
|
|
logger.debug('FOLDER_INDEX', 'No content for folder', { folderPath });
|
|
continue;
|
|
}
|
|
|
|
const formatted = formatTimelineForClaudeMd(result.content[0].text);
|
|
|
|
// Fix for #794: Don't create new CLAUDE.md files if there's no activity
|
|
// But update existing ones to show "No recent activity" if they already exist
|
|
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
|
const hasNoActivity = formatted.includes('*No recent activity*');
|
|
const fileExists = existsSync(claudeMdPath);
|
|
|
|
if (hasNoActivity && !fileExists) {
|
|
logger.debug('FOLDER_INDEX', 'Skipping empty CLAUDE.md creation', { folderPath });
|
|
continue;
|
|
}
|
|
|
|
writeClaudeMdToFolder(folderPath, formatted);
|
|
|
|
logger.debug('FOLDER_INDEX', 'Updated CLAUDE.md', { folderPath });
|
|
} catch (error) {
|
|
// Fire-and-forget: log warning but don't fail
|
|
const err = error as Error;
|
|
logger.error('FOLDER_INDEX', 'Failed to update CLAUDE.md', {
|
|
folderPath,
|
|
errorMessage: err.message,
|
|
errorStack: err.stack
|
|
});
|
|
}
|
|
}
|
|
}
|