memoryFileDetection.ts
utils/memoryFileDetection.ts
290
Lines
10212
Bytes
9
Exports
6
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with file-tools, memory-layers. It contains 290 lines, 6 detected imports, and 9 detected exports.
Important relationships
Detected exports
detectSessionFileTypedetectSessionPatternTypeisAutoMemFileMemoryScopememoryScopeForPathisAutoManagedMemoryFileisMemoryDirectoryisShellCommandTargetingMemoryisAutoManagedMemoryPattern
Keywords
includesmemorypathfilepathnormalizedchecktocomparablefilepathsnormalizedcmp
Detected imports
bun:bundlepath../memdir/paths.js../tools/AgentTool/agentMemory.js./envUtils.js./windowsPaths.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { feature } from 'bun:bundle'
import { normalize, posix, win32 } from 'path'
import {
getAutoMemPath,
getMemoryBaseDir,
isAutoMemoryEnabled,
isAutoMemPath,
} from '../memdir/paths.js'
import { isAgentMemoryPath } from '../tools/AgentTool/agentMemory.js'
import { getClaudeConfigHomeDir } from './envUtils.js'
import {
posixPathToWindowsPath,
windowsPathToPosixPath,
} from './windowsPaths.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const teamMemPaths = feature('TEAMMEM')
? (require('../memdir/teamMemPaths.js') as typeof import('../memdir/teamMemPaths.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
const IS_WINDOWS = process.platform === 'win32'
// Normalize path separators to posix (/). Does NOT translate drive encoding.
function toPosix(p: string): string {
return p.split(win32.sep).join(posix.sep)
}
// Convert a path to a stable string-comparable form: forward-slash separated,
// and on Windows, lowercased (Windows filesystems are case-insensitive).
function toComparable(p: string): string {
const posixForm = toPosix(p)
return IS_WINDOWS ? posixForm.toLowerCase() : posixForm
}
/**
* Detects if a file path is a session-related file under ~/.claude.
* Returns the type of session file or null if not a session file.
*/
export function detectSessionFileType(
filePath: string,
): 'session_memory' | 'session_transcript' | null {
const configDir = getClaudeConfigHomeDir()
// Compare in forward-slash form; on Windows also case-fold. The caller
// (isShellCommandTargetingMemory) converts MinGW /c/... → native before
// reaching here, so we only need separator + case normalization.
const normalized = toComparable(filePath)
const configDirCmp = toComparable(configDir)
if (!normalized.startsWith(configDirCmp)) {
return null
}
if (normalized.includes('/session-memory/') && normalized.endsWith('.md')) {
return 'session_memory'
}
if (normalized.includes('/projects/') && normalized.endsWith('.jsonl')) {
return 'session_transcript'
}
return null
}
/**
* Checks if a glob/pattern string indicates session file access intent.
* Used for Grep/Glob tools where we check patterns, not actual file paths.
*/
export function detectSessionPatternType(
pattern: string,
): 'session_memory' | 'session_transcript' | null {
const normalized = pattern.split(win32.sep).join(posix.sep)
if (
normalized.includes('session-memory') &&
(normalized.includes('.md') || normalized.endsWith('*'))
) {
return 'session_memory'
}
if (
normalized.includes('.jsonl') ||
(normalized.includes('projects') && normalized.includes('*.jsonl'))
) {
return 'session_transcript'
}
return null
}
/**
* Check if a file path is within the memdir directory.
*/
export function isAutoMemFile(filePath: string): boolean {
if (isAutoMemoryEnabled()) {
return isAutoMemPath(filePath)
}
return false
}
export type MemoryScope = 'personal' | 'team'
/**
* Determine which memory store (if any) a path belongs to.
*
* Team dir is a subdirectory of memdir (getTeamMemPath = join(getAutoMemPath, 'team')),
* so a team path matches both isTeamMemFile and isAutoMemFile. Check team first.
*
* Use this for scope-keyed telemetry where a single event name distinguishes
* by scope field — the existing tengu_memdir_* / tengu_team_mem_* event-name
* hierarchy handles the overlap differently (team writes intentionally fire both).
*/
export function memoryScopeForPath(filePath: string): MemoryScope | null {
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemFile(filePath)) {
return 'team'
}
if (isAutoMemFile(filePath)) {
return 'personal'
}
return null
}
/**
* Check if a file path is within an agent memory directory.
*/
function isAgentMemFile(filePath: string): boolean {
if (isAutoMemoryEnabled()) {
return isAgentMemoryPath(filePath)
}
return false
}
/**
* Check if a file is a Claude-managed memory file (NOT user-managed instruction files).
* Includes: auto-memory (memdir), agent memory, session memory/transcripts.
* Excludes: CLAUDE.md, CLAUDE.local.md, .claude/rules/*.md (user-managed).
*
* Use this for collapse/badge logic where user-managed files should show full diffs.
*/
export function isAutoManagedMemoryFile(filePath: string): boolean {
if (isAutoMemFile(filePath)) {
return true
}
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemFile(filePath)) {
return true
}
if (detectSessionFileType(filePath) !== null) {
return true
}
if (isAgentMemFile(filePath)) {
return true
}
return false
}
// Check if a directory path is a memory-related directory.
// Used by Grep/Glob which take a directory `path` rather than a specific file.
// Checks both configDir and memoryBaseDir to handle custom memory dir paths.
export function isMemoryDirectory(dirPath: string): boolean {
// SECURITY: Normalize to prevent path traversal bypasses via .. segments.
// On Windows this produces backslashes; toComparable flips them back for
// string matching. MinGW /c/... paths are converted to native before
// reaching here (extraction-time in isShellCommandTargetingMemory), so
// normalize() never sees them.
const normalizedPath = normalize(dirPath)
const normalizedCmp = toComparable(normalizedPath)
// Agent memory directories can be under cwd (project scope), configDir, or memoryBaseDir
if (
isAutoMemoryEnabled() &&
(normalizedCmp.includes('/agent-memory/') ||
normalizedCmp.includes('/agent-memory-local/'))
) {
return true
}
// Team memory directories live under <autoMemPath>/team/
if (
feature('TEAMMEM') &&
teamMemPaths!.isTeamMemoryEnabled() &&
teamMemPaths!.isTeamMemPath(normalizedPath)
) {
return true
}
// Check the auto-memory path override (CLAUDE_COWORK_MEMORY_PATH_OVERRIDE)
if (isAutoMemoryEnabled()) {
const autoMemPath = getAutoMemPath()
const autoMemDirCmp = toComparable(autoMemPath.replace(/[/\\]+$/, ''))
const autoMemPathCmp = toComparable(autoMemPath)
if (
normalizedCmp === autoMemDirCmp ||
normalizedCmp.startsWith(autoMemPathCmp)
) {
return true
}
}
const configDirCmp = toComparable(getClaudeConfigHomeDir())
const memoryBaseCmp = toComparable(getMemoryBaseDir())
const underConfig = normalizedCmp.startsWith(configDirCmp)
const underMemoryBase = normalizedCmp.startsWith(memoryBaseCmp)
if (!underConfig && !underMemoryBase) {
return false
}
if (normalizedCmp.includes('/session-memory/')) {
return true
}
if (underConfig && normalizedCmp.includes('/projects/')) {
return true
}
if (isAutoMemoryEnabled() && normalizedCmp.includes('/memory/')) {
return true
}
return false
}
/**
* Check if a shell command string (Bash or PowerShell) targets memory files
* by extracting absolute path tokens and checking them against memory
* detection functions. Used for Bash/PowerShell grep/search commands in the
* collapse logic.
*/
export function isShellCommandTargetingMemory(command: string): boolean {
const configDir = getClaudeConfigHomeDir()
const memoryBase = getMemoryBaseDir()
const autoMemDir = isAutoMemoryEnabled()
? getAutoMemPath().replace(/[/\\]+$/, '')
: ''
// Quick check: does the command mention the config, memory base, or
// auto-mem directory? Compare in forward-slash form (PowerShell on Windows
// may use either separator while configDir uses the platform-native one).
// On Windows also check the MinGW form (/c/...) since BashTool runs under
// Git Bash which emits that encoding. On Linux/Mac, configDir is already
// posix so only one form to check — and crucially, windowsPathToPosixPath
// is NOT called, so Linux paths like /m/foo aren't misinterpreted as MinGW.
const commandCmp = toComparable(command)
const dirs = [configDir, memoryBase, autoMemDir].filter(Boolean)
const matchesAnyDir = dirs.some(d => {
if (commandCmp.includes(toComparable(d))) return true
if (IS_WINDOWS) {
// BashTool on Windows (Git Bash) emits /c/Users/... — check MinGW form too
return commandCmp.includes(windowsPathToPosixPath(d).toLowerCase())
}
return false
})
if (!matchesAnyDir) {
return false
}
// Extract absolute path-like tokens. Matches Unix absolute paths (/foo/bar),
// Windows drive-letter paths (C:\foo, C:/foo), and MinGW paths (/c/foo —
// they're /-prefixed so the regex already captures them). Bare backslash
// tokens (\foo) are intentionally excluded — they appear in regex/grep
// patterns and would cause false-positive memory classification after
// normalization flips backslashes to forward slashes.
const matches = command.match(/(?:[A-Za-z]:[/\\]|\/)[^\s'"]+/g)
if (!matches) {
return false
}
for (const match of matches) {
// Strip trailing shell metacharacters that could be adjacent to a path
const cleanPath = match.replace(/[,;|&>]+$/, '')
// On Windows, convert MinGW /c/... → native C:\... at this single
// point. Downstream predicates (isAutoManagedMemoryFile, isMemoryDirectory,
// isAutoMemPath, isAgentMemoryPath) then receive native paths and only
// need toComparable() for matching. On other platforms, paths are already
// native — no conversion, so /m/foo etc. pass through unmodified.
const nativePath = IS_WINDOWS
? posixPathToWindowsPath(cleanPath)
: cleanPath
if (isAutoManagedMemoryFile(nativePath) || isMemoryDirectory(nativePath)) {
return true
}
}
return false
}
// Check if a glob/pattern targets auto-managed memory files only.
// Excludes CLAUDE.md, CLAUDE.local.md, .claude/rules/ (user-managed).
// Used for collapse badge logic where user-managed files should not be
// counted as "memory" operations.
export function isAutoManagedMemoryPattern(pattern: string): boolean {
if (detectSessionPatternType(pattern) !== null) {
return true
}
if (
isAutoMemoryEnabled() &&
(pattern.replace(/\\/g, '/').includes('agent-memory/') ||
pattern.replace(/\\/g, '/').includes('agent-memory-local/'))
) {
return true
}
return false
}