Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:59:29 +08:00
commit 681c2e46c0
31 changed files with 6218 additions and 0 deletions

111
hooks/session-end.js Executable file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env node
/**
* SessionEnd Hook
*
* Handles cleanup when Claude Code session terminates.
* Ensures .active-session file is removed and index is updated.
*
* Hook receives:
* - session_id: string
* - transcript_path: string
* - cwd: string
* - permission_mode: string
* - hook_event_name: "SessionEnd"
* - reason: "exit" | "clear" | "logout" | "prompt_input_exit" | "other"
*
* SAFETY: Includes graceful failure handling to avoid blocking Claude Code shutdown
* 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 IndexManager = require('../cli/lib/index-manager');
// Constants
const SESSIONS_DIR = '.claude/sessions';
const ACTIVE_SESSION_FILE = path.join(SESSIONS_DIR, '.active-session');
const indexManager = new IndexManager(SESSIONS_DIR);
try {
// Read input from stdin (provided by Claude Code)
const input = fs.readFileSync(0, 'utf8').trim();
// Parse the JSON input
let eventData;
try {
eventData = JSON.parse(input);
} catch (parseError) {
// If parsing fails, exit silently (no input provided)
process.exit(0);
}
// Clean up active session marker
let sessionName = null;
if (fs.existsSync(ACTIVE_SESSION_FILE)) {
try {
sessionName = fs.readFileSync(ACTIVE_SESSION_FILE, 'utf8').trim();
} catch (readError) {
// Continue even if read fails
}
// Delete the .active-session file
try {
fs.unlinkSync(ACTIVE_SESSION_FILE);
} catch (unlinkError) {
// File may already be deleted, continue
}
}
// Mark session as closed in .auto-capture-state (fast, atomic)
if (sessionName) {
const sessionDir = path.join(SESSIONS_DIR, sessionName);
const statePath = path.join(sessionDir, '.auto-capture-state');
if (fs.existsSync(statePath)) {
try {
let state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
state.session_status = 'closed';
state.session_closed = new Date().toISOString();
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
} catch (stateError) {
// Silent fail - hook must be safe
// Session status update failed but continue with other cleanup
}
}
}
// Update the index.json to clear activeSession using IndexManager
// This uses proper locking and atomic writes to prevent corruption
try {
const index = indexManager.read({ skipValidation: true });
index.activeSession = null;
indexManager.write(index);
} catch (indexError) {
// Continue even if index update fails
// This prevents blocking the hook if index is temporarily locked
}
// Exit successfully
process.exit(0);
} catch (error) {
// Exit silently on any errors to avoid blocking Claude Code shutdown
process.exit(0);
}
} catch (error) {
// Outer catch: Handle plugin missing/uninstalled
// Exit silently to avoid blocking Claude Code
process.exit(0);
}