π File detail
utils/memoryFileDetection.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes detectSessionFileType, detectSessionPatternType, isAutoMemFile, MemoryScope, and memoryScopeForPath (and more) β mainly functions, hooks, or classes. Dependencies touch bun:bundle and Node path helpers. It composes internal code from memdir, tools, envUtils, and windowsPaths (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { feature } from 'bun:bundle' import { normalize, posix, win32 } from 'path' import { getAutoMemPath, getMemoryBaseDir,
π€ Exports (heuristic)
detectSessionFileTypedetectSessionPatternTypeisAutoMemFileMemoryScopememoryScopeForPathisAutoManagedMemoryFileisMemoryDirectoryisShellCommandTargetingMemoryisAutoManagedMemoryPattern
π External import roots
Package roots from from "β¦" (relative paths omitted).
bun:bundlepath
π₯οΈ Source preview
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
}