Initial commit
This commit is contained in:
188
hooks/post-tool-use.js
Executable file
188
hooks/post-tool-use.js
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
// Intelligent Post-Tool-Use Hook - Tracks file modifications for smart analysis
|
||||
// Works with async analysis system for intelligent snapshots
|
||||
//
|
||||
// SAFETY: Includes graceful failure handling to avoid blocking Claude Code
|
||||
// if plugin is uninstalled or dependencies are missing.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Graceful failure wrapper - protect against plugin uninstallation
|
||||
try {
|
||||
// Check if critical dependencies exist (indicates plugin is installed)
|
||||
const cliLibPath = path.join(__dirname, '../cli/lib');
|
||||
if (!fs.existsSync(cliLibPath)) {
|
||||
// Plugin likely uninstalled, exit silently
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const LockManager = require('../cli/lib/lock-manager');
|
||||
|
||||
// Read tool operation data from stdin (Claude Code provides this as JSON)
|
||||
// NOTE: Do NOT use process.env.CLAUDE_TOOL_NAME - it's always "unknown" due to Claude Code bug
|
||||
let toolName = 'unknown';
|
||||
let filePath = null;
|
||||
|
||||
try {
|
||||
const input = fs.readFileSync(0, 'utf8').trim();
|
||||
if (input) {
|
||||
const eventData = JSON.parse(input);
|
||||
|
||||
// Get tool name from stdin data (NOT environment variable)
|
||||
toolName = eventData.tool_name || 'unknown';
|
||||
|
||||
// Extract file path from tool input
|
||||
// Different tools use different field names for file paths
|
||||
if (eventData.tool_input) {
|
||||
if (eventData.tool_input.file_path) {
|
||||
filePath = eventData.tool_input.file_path;
|
||||
} else if (eventData.tool_input.notebook_path) {
|
||||
// NotebookEdit uses notebook_path instead of file_path
|
||||
filePath = eventData.tool_input.notebook_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// If we can't read stdin or parse it, exit early
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Only track file modifications (Write, Edit, and NotebookEdit tools)
|
||||
if (toolName !== 'Write' && toolName !== 'Edit' && toolName !== 'NotebookEdit') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Configuration
|
||||
const SESSIONS_DIR = '.claude/sessions';
|
||||
const ACTIVE_SESSION_FILE = path.join(SESSIONS_DIR, '.active-session');
|
||||
const lockManager = new LockManager(SESSIONS_DIR);
|
||||
|
||||
// Exit early if no active session
|
||||
if (!fs.existsSync(ACTIVE_SESSION_FILE)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read active session name
|
||||
let activeSession;
|
||||
try {
|
||||
activeSession = fs.readFileSync(ACTIVE_SESSION_FILE, 'utf8').trim();
|
||||
} catch (err) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!activeSession) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionDir = path.join(SESSIONS_DIR, activeSession);
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if auto-capture is enabled
|
||||
const sessionMd = path.join(sessionDir, 'session.md');
|
||||
if (fs.existsSync(sessionMd)) {
|
||||
try {
|
||||
const content = fs.readFileSync(sessionMd, 'utf8');
|
||||
if (content.includes('Auto-capture: disabled')) {
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue if we can't read the file
|
||||
}
|
||||
}
|
||||
|
||||
// State file
|
||||
const stateFile = path.join(sessionDir, '.auto-capture-state');
|
||||
|
||||
// Use lock to prevent race conditions with user-prompt-submit.js hook
|
||||
const lock = lockManager.acquireLock(`auto-capture-${activeSession}`, {
|
||||
timeout: 1000,
|
||||
wait: true
|
||||
});
|
||||
|
||||
if (!lock.acquired) {
|
||||
// Could not acquire lock - skip this update to avoid blocking
|
||||
// The file will be captured on next tool use
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize state if doesn't exist
|
||||
let state = {
|
||||
file_count: 0,
|
||||
interaction_count: 0,
|
||||
interactions_since_last_analysis: 0,
|
||||
last_snapshot: '',
|
||||
last_reason: '',
|
||||
last_analysis_timestamp: '',
|
||||
modified_files: [] // Track actual file paths
|
||||
};
|
||||
|
||||
if (fs.existsSync(stateFile)) {
|
||||
try {
|
||||
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
} catch (err) {
|
||||
// Use default state if parse fails
|
||||
}
|
||||
}
|
||||
|
||||
// Increment file count
|
||||
state.file_count++;
|
||||
|
||||
// Track the modified file path if we captured it
|
||||
if (filePath) {
|
||||
// Initialize modified_files array if it doesn't exist (backward compatibility)
|
||||
if (!state.modified_files) {
|
||||
state.modified_files = [];
|
||||
}
|
||||
|
||||
// Add file to list if not already tracked
|
||||
const fileEntry = {
|
||||
path: filePath,
|
||||
operation: toolName.toLowerCase(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Avoid duplicates - check if file already in list
|
||||
const existingIndex = state.modified_files.findIndex(f => f.path === filePath);
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing entry with latest operation
|
||||
state.modified_files[existingIndex] = fileEntry;
|
||||
} else {
|
||||
// Add new file
|
||||
state.modified_files.push(fileEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// Update state atomically
|
||||
// Note: We no longer immediately create snapshot markers here
|
||||
// Instead, the file_count contributes to analysis queue logic in user-prompt-submit.js
|
||||
const tempPath = `${stateFile}.tmp.${Date.now()}`;
|
||||
try {
|
||||
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
|
||||
fs.renameSync(tempPath, stateFile);
|
||||
} catch (writeError) {
|
||||
// Clean up temp file
|
||||
if (fs.existsSync(tempPath)) {
|
||||
try {
|
||||
fs.unlinkSync(tempPath);
|
||||
} catch (cleanupError) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
throw writeError;
|
||||
}
|
||||
} finally {
|
||||
// Always release lock
|
||||
lock.release();
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
// Outer catch: Handle plugin missing/uninstalled
|
||||
// Exit silently to avoid blocking Claude Code
|
||||
process.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user