/ utils / sessionFileAccessHooks.ts
sessionFileAccessHooks.ts
  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   */
  6  import { feature } from 'bun:bundle'
  7  import { registerHookCallbacks } from '../bootstrap/state.js'
  8  import type { HookInput, HookJSONOutput } from '../entrypoints/agentSdkTypes.js'
  9  import {
 10    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 11    logEvent,
 12  } from '../services/analytics/index.js'
 13  import { FILE_EDIT_TOOL_NAME } from '../tools/FileEditTool/constants.js'
 14  import { inputSchema as editInputSchema } from '../tools/FileEditTool/types.js'
 15  import { FileReadTool } from '../tools/FileReadTool/FileReadTool.js'
 16  import { FILE_READ_TOOL_NAME } from '../tools/FileReadTool/prompt.js'
 17  import { FileWriteTool } from '../tools/FileWriteTool/FileWriteTool.js'
 18  import { FILE_WRITE_TOOL_NAME } from '../tools/FileWriteTool/prompt.js'
 19  import { GlobTool } from '../tools/GlobTool/GlobTool.js'
 20  import { GLOB_TOOL_NAME } from '../tools/GlobTool/prompt.js'
 21  import { GrepTool } from '../tools/GrepTool/GrepTool.js'
 22  import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js'
 23  import type { HookCallback } from '../types/hooks.js'
 24  import {
 25    detectSessionFileType,
 26    detectSessionPatternType,
 27    isAutoMemFile,
 28    memoryScopeForPath,
 29  } from './memoryFileDetection.js'
 30  
 31  /* eslint-disable @typescript-eslint/no-require-imports */
 32  const teamMemPaths = feature('TEAMMEM')
 33    ? (require('../memdir/teamMemPaths.js') as typeof import('../memdir/teamMemPaths.js'))
 34    : null
 35  const teamMemWatcher = feature('TEAMMEM')
 36    ? (require('../services/teamMemorySync/watcher.js') as typeof import('../services/teamMemorySync/watcher.js'))
 37    : null
 38  const 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 */
 43  import { 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   */
 49  function 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   */
 75  function 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   */
123  export 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   */
146  async 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   */
233  export 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  }