source dump of claude code
at main 250 lines 8.1 kB view raw
1/** 2 * Session file access analytics hooks. 3 * Tracks access to session memory and transcript files via Read, Grep, Glob tools. 4 * Also tracks memdir file access via Read, Grep, Glob, Edit, and Write tools. 5 */ 6import { feature } from 'bun:bundle' 7import { registerHookCallbacks } from '../bootstrap/state.js' 8import type { HookInput, HookJSONOutput } from '../entrypoints/agentSdkTypes.js' 9import { 10 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 11 logEvent, 12} from '../services/analytics/index.js' 13import { FILE_EDIT_TOOL_NAME } from '../tools/FileEditTool/constants.js' 14import { inputSchema as editInputSchema } from '../tools/FileEditTool/types.js' 15import { FileReadTool } from '../tools/FileReadTool/FileReadTool.js' 16import { FILE_READ_TOOL_NAME } from '../tools/FileReadTool/prompt.js' 17import { FileWriteTool } from '../tools/FileWriteTool/FileWriteTool.js' 18import { FILE_WRITE_TOOL_NAME } from '../tools/FileWriteTool/prompt.js' 19import { GlobTool } from '../tools/GlobTool/GlobTool.js' 20import { GLOB_TOOL_NAME } from '../tools/GlobTool/prompt.js' 21import { GrepTool } from '../tools/GrepTool/GrepTool.js' 22import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js' 23import type { HookCallback } from '../types/hooks.js' 24import { 25 detectSessionFileType, 26 detectSessionPatternType, 27 isAutoMemFile, 28 memoryScopeForPath, 29} from './memoryFileDetection.js' 30 31/* eslint-disable @typescript-eslint/no-require-imports */ 32const teamMemPaths = feature('TEAMMEM') 33 ? (require('../memdir/teamMemPaths.js') as typeof import('../memdir/teamMemPaths.js')) 34 : null 35const teamMemWatcher = feature('TEAMMEM') 36 ? (require('../services/teamMemorySync/watcher.js') as typeof import('../services/teamMemorySync/watcher.js')) 37 : null 38const memoryShapeTelemetry = feature('MEMORY_SHAPE_TELEMETRY') 39 ? (require('../memdir/memoryShapeTelemetry.js') as typeof import('../memdir/memoryShapeTelemetry.js')) 40 : null 41 42/* eslint-enable @typescript-eslint/no-require-imports */ 43import { getSubagentLogName } from './agentContext.js' 44 45/** 46 * Extract the file path from a tool input for memdir detection. 47 * Covers Read (file_path), Edit (file_path), and Write (file_path). 48 */ 49function getFilePathFromInput( 50 toolName: string, 51 toolInput: unknown, 52): string | null { 53 switch (toolName) { 54 case FILE_READ_TOOL_NAME: { 55 const parsed = FileReadTool.inputSchema.safeParse(toolInput) 56 return parsed.success ? parsed.data.file_path : null 57 } 58 case FILE_EDIT_TOOL_NAME: { 59 const parsed = editInputSchema().safeParse(toolInput) 60 return parsed.success ? parsed.data.file_path : null 61 } 62 case FILE_WRITE_TOOL_NAME: { 63 const parsed = FileWriteTool.inputSchema.safeParse(toolInput) 64 return parsed.success ? parsed.data.file_path : null 65 } 66 default: 67 return null 68 } 69} 70 71/** 72 * Extract file type from tool input. 73 * Returns the detected session file type or null. 74 */ 75function getSessionFileTypeFromInput( 76 toolName: string, 77 toolInput: unknown, 78): 'session_memory' | 'session_transcript' | null { 79 switch (toolName) { 80 case FILE_READ_TOOL_NAME: { 81 const parsed = FileReadTool.inputSchema.safeParse(toolInput) 82 if (!parsed.success) return null 83 return detectSessionFileType(parsed.data.file_path) 84 } 85 case GREP_TOOL_NAME: { 86 const parsed = GrepTool.inputSchema.safeParse(toolInput) 87 if (!parsed.success) return null 88 // Check path if provided 89 if (parsed.data.path) { 90 const pathType = detectSessionFileType(parsed.data.path) 91 if (pathType) return pathType 92 } 93 // Check glob pattern 94 if (parsed.data.glob) { 95 const globType = detectSessionPatternType(parsed.data.glob) 96 if (globType) return globType 97 } 98 return null 99 } 100 case GLOB_TOOL_NAME: { 101 const parsed = GlobTool.inputSchema.safeParse(toolInput) 102 if (!parsed.success) return null 103 // Check path if provided 104 if (parsed.data.path) { 105 const pathType = detectSessionFileType(parsed.data.path) 106 if (pathType) return pathType 107 } 108 // Check pattern 109 const patternType = detectSessionPatternType(parsed.data.pattern) 110 if (patternType) return patternType 111 return null 112 } 113 default: 114 return null 115 } 116} 117 118/** 119 * Check if a tool use constitutes a memory file access. 120 * Detects session memory (via Read/Grep/Glob) and memdir access (via Read/Edit/Write). 121 * Uses the same conditions as the PostToolUse session file access hooks. 122 */ 123export function isMemoryFileAccess( 124 toolName: string, 125 toolInput: unknown, 126): boolean { 127 if (getSessionFileTypeFromInput(toolName, toolInput) === 'session_memory') { 128 return true 129 } 130 131 const filePath = getFilePathFromInput(toolName, toolInput) 132 if ( 133 filePath && 134 (isAutoMemFile(filePath) || 135 (feature('TEAMMEM') && teamMemPaths!.isTeamMemFile(filePath))) 136 ) { 137 return true 138 } 139 140 return false 141} 142 143/** 144 * PostToolUse callback to log session file access events. 145 */ 146async function handleSessionFileAccess( 147 input: HookInput, 148 _toolUseID: string | null, 149 _signal: AbortSignal | undefined, 150): Promise<HookJSONOutput> { 151 if (input.hook_event_name !== 'PostToolUse') return {} 152 153 const fileType = getSessionFileTypeFromInput( 154 input.tool_name, 155 input.tool_input, 156 ) 157 158 const subagentName = getSubagentLogName() 159 const subagentProps = subagentName ? { subagent_name: subagentName } : {} 160 161 if (fileType === 'session_memory') { 162 logEvent('tengu_session_memory_accessed', { ...subagentProps }) 163 } else if (fileType === 'session_transcript') { 164 logEvent('tengu_transcript_accessed', { ...subagentProps }) 165 } 166 167 // Memdir access tracking 168 const filePath = getFilePathFromInput(input.tool_name, input.tool_input) 169 if (filePath && isAutoMemFile(filePath)) { 170 logEvent('tengu_memdir_accessed', { 171 tool: input.tool_name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 172 ...subagentProps, 173 }) 174 175 switch (input.tool_name) { 176 case FILE_READ_TOOL_NAME: 177 logEvent('tengu_memdir_file_read', { ...subagentProps }) 178 break 179 case FILE_EDIT_TOOL_NAME: 180 logEvent('tengu_memdir_file_edit', { ...subagentProps }) 181 break 182 case FILE_WRITE_TOOL_NAME: 183 logEvent('tengu_memdir_file_write', { ...subagentProps }) 184 break 185 } 186 } 187 188 // Team memory access tracking 189 if (feature('TEAMMEM') && filePath && teamMemPaths!.isTeamMemFile(filePath)) { 190 logEvent('tengu_team_mem_accessed', { 191 tool: input.tool_name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 192 ...subagentProps, 193 }) 194 195 switch (input.tool_name) { 196 case FILE_READ_TOOL_NAME: 197 logEvent('tengu_team_mem_file_read', { ...subagentProps }) 198 break 199 case FILE_EDIT_TOOL_NAME: 200 logEvent('tengu_team_mem_file_edit', { ...subagentProps }) 201 teamMemWatcher?.notifyTeamMemoryWrite() 202 break 203 case FILE_WRITE_TOOL_NAME: 204 logEvent('tengu_team_mem_file_write', { ...subagentProps }) 205 teamMemWatcher?.notifyTeamMemoryWrite() 206 break 207 } 208 } 209 210 if (feature('MEMORY_SHAPE_TELEMETRY') && filePath) { 211 const scope = memoryScopeForPath(filePath) 212 if ( 213 scope !== null && 214 (input.tool_name === FILE_EDIT_TOOL_NAME || 215 input.tool_name === FILE_WRITE_TOOL_NAME) 216 ) { 217 memoryShapeTelemetry!.logMemoryWriteShape( 218 input.tool_name, 219 input.tool_input, 220 filePath, 221 scope, 222 ) 223 } 224 } 225 226 return {} 227} 228 229/** 230 * Register session file access tracking hooks. 231 * Called during CLI initialization. 232 */ 233export function registerSessionFileAccessHooks(): void { 234 const hook: HookCallback = { 235 type: 'callback', 236 callback: handleSessionFileAccess, 237 timeout: 1, // Very short timeout - just logging 238 internal: true, 239 } 240 241 registerHookCallbacks({ 242 PostToolUse: [ 243 { matcher: FILE_READ_TOOL_NAME, hooks: [hook] }, 244 { matcher: GREP_TOOL_NAME, hooks: [hook] }, 245 { matcher: GLOB_TOOL_NAME, hooks: [hook] }, 246 { matcher: FILE_EDIT_TOOL_NAME, hooks: [hook] }, 247 { matcher: FILE_WRITE_TOOL_NAME, hooks: [hook] }, 248 ], 249 }) 250}