Initial commit
This commit is contained in:
317
hooks/session_start_git_context.js
Executable file
317
hooks/session_start_git_context.js
Executable file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* SessionStart Git Context Injector
|
||||
*
|
||||
* Injects differential git context at session start:
|
||||
* - Commits since last session
|
||||
* - Files changed since last session
|
||||
* - Current git status
|
||||
* - Branch information
|
||||
*
|
||||
* Token Overhead: ~1-2K tokens (differential only, not full history)
|
||||
* Blocking: No
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('yaml');
|
||||
|
||||
/**
|
||||
* Load last session metadata
|
||||
*/
|
||||
function loadLastSession() {
|
||||
try {
|
||||
const cacheDir = path.join(
|
||||
process.env.HOME,
|
||||
'.claude',
|
||||
'plugins',
|
||||
'contextune',
|
||||
'.cache'
|
||||
);
|
||||
const lastSessionFile = path.join(cacheDir, 'last_session.yaml');
|
||||
|
||||
if (!fs.existsSync(lastSessionFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(lastSessionFile, 'utf8');
|
||||
return yaml.parse(content);
|
||||
} catch (error) {
|
||||
console.error('DEBUG: Failed to load last session:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commits since last session
|
||||
*/
|
||||
function getCommitsSinceLastSession(lastCommit, limit = 10) {
|
||||
try {
|
||||
const cmd = `git log --oneline ${lastCommit}..HEAD -n ${limit}`;
|
||||
const output = execSync(cmd, { encoding: 'utf8', timeout: 2000 });
|
||||
|
||||
const commits = output.trim().split('\n').filter(line => line);
|
||||
return commits;
|
||||
} catch (error) {
|
||||
console.error('DEBUG: Failed to get commits:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files changed since last session
|
||||
*/
|
||||
function getFilesChanged(lastCommit) {
|
||||
try {
|
||||
const cmd = `git diff --name-status ${lastCommit}..HEAD`;
|
||||
const output = execSync(cmd, { encoding: 'utf8', timeout: 2000 });
|
||||
|
||||
const changes = [];
|
||||
for (const line of output.trim().split('\n')) {
|
||||
if (!line) continue;
|
||||
|
||||
const parts = line.split('\t');
|
||||
if (parts.length >= 2) {
|
||||
const status = parts[0];
|
||||
const file = parts[1];
|
||||
|
||||
// Decode status
|
||||
let changeType = 'modified';
|
||||
if (status === 'A') changeType = 'added';
|
||||
else if (status === 'D') changeType = 'deleted';
|
||||
else if (status === 'M') changeType = 'modified';
|
||||
else if (status.startsWith('R')) changeType = 'renamed';
|
||||
|
||||
changes.push({ file, type: changeType, status });
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
} catch (error) {
|
||||
console.error('DEBUG: Failed to get file changes:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diff statistics
|
||||
*/
|
||||
function getDiffStats(lastCommit) {
|
||||
try {
|
||||
const cmd = `git diff --shortstat ${lastCommit}..HEAD`;
|
||||
const output = execSync(cmd, { encoding: 'utf8', timeout: 2000 });
|
||||
return output.trim();
|
||||
} catch (error) {
|
||||
return 'Unable to calculate diff stats';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current git status
|
||||
*/
|
||||
function getCurrentStatus() {
|
||||
try {
|
||||
const cmd = 'git status --short';
|
||||
const output = execSync(cmd, { encoding: 'utf8', timeout: 1000 });
|
||||
|
||||
const lines = output.trim().split('\n').filter(line => line);
|
||||
|
||||
if (lines.length === 0) {
|
||||
return { clean: true, uncommitted: 0 };
|
||||
}
|
||||
|
||||
return { clean: false, uncommitted: lines.length, files: lines.slice(0, 5) };
|
||||
} catch (error) {
|
||||
return { clean: true, uncommitted: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time since last session
|
||||
*/
|
||||
function getTimeSince(timestamp) {
|
||||
try {
|
||||
const lastTime = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now - lastTime;
|
||||
|
||||
const minutes = Math.floor(diffMs / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
return 'just now';
|
||||
} catch (error) {
|
||||
return 'recently';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate context summary
|
||||
*/
|
||||
function generateContextSummary(lastSession) {
|
||||
const commits = getCommitsSinceLastSession(lastSession.last_commit);
|
||||
const filesChanged = getFilesChanged(lastSession.last_commit);
|
||||
const diffStats = getDiffStats(lastSession.last_commit);
|
||||
const currentStatus = getCurrentStatus();
|
||||
const timeSince = getTimeSince(lastSession.ended_at);
|
||||
|
||||
// Build summary
|
||||
let summary = `📋 Git Context Since Last Session (${timeSince})\n\n`;
|
||||
|
||||
// Commit activity
|
||||
if (commits.length > 0) {
|
||||
summary += `**Git Activity:**\n`;
|
||||
summary += `- ${commits.length} new commit${commits.length > 1 ? 's' : ''}\n`;
|
||||
summary += `- ${diffStats}\n`;
|
||||
summary += `- Branch: ${lastSession.branch}\n\n`;
|
||||
|
||||
summary += `**Recent Commits:**\n`;
|
||||
commits.slice(0, 5).forEach(commit => {
|
||||
summary += ` ${commit}\n`;
|
||||
});
|
||||
|
||||
if (commits.length > 5) {
|
||||
summary += ` ... and ${commits.length - 5} more\n`;
|
||||
}
|
||||
summary += '\n';
|
||||
} else {
|
||||
summary += `**Git Activity:** No commits since last session\n\n`;
|
||||
}
|
||||
|
||||
// File changes
|
||||
if (filesChanged.length > 0) {
|
||||
summary += `**Files Changed (${filesChanged.length} total):**\n`;
|
||||
|
||||
const byType = { added: [], modified: [], deleted: [], renamed: [] };
|
||||
filesChanged.forEach(change => {
|
||||
const list = byType[change.type] || byType.modified;
|
||||
list.push(change.file);
|
||||
});
|
||||
|
||||
if (byType.added.length > 0) {
|
||||
summary += ` Added (${byType.added.length}):\n`;
|
||||
byType.added.slice(0, 3).forEach(f => summary += ` - ${f}\n`);
|
||||
if (byType.added.length > 3) summary += ` ... and ${byType.added.length - 3} more\n`;
|
||||
}
|
||||
|
||||
if (byType.modified.length > 0) {
|
||||
summary += ` Modified (${byType.modified.length}):\n`;
|
||||
byType.modified.slice(0, 3).forEach(f => summary += ` - ${f}\n`);
|
||||
if (byType.modified.length > 3) summary += ` ... and ${byType.modified.length - 3} more\n`;
|
||||
}
|
||||
|
||||
if (byType.deleted.length > 0) {
|
||||
summary += ` Deleted (${byType.deleted.length}):\n`;
|
||||
byType.deleted.slice(0, 3).forEach(f => summary += ` - ${f}\n`);
|
||||
}
|
||||
|
||||
summary += '\n';
|
||||
}
|
||||
|
||||
// Current working directory status
|
||||
if (!currentStatus.clean) {
|
||||
summary += `**Current Status:**\n`;
|
||||
summary += `- ${currentStatus.uncommitted} uncommitted change${currentStatus.uncommitted > 1 ? 's' : ''}\n`;
|
||||
|
||||
if (currentStatus.files && currentStatus.files.length > 0) {
|
||||
summary += `\n**Uncommitted:**\n`;
|
||||
currentStatus.files.forEach(file => {
|
||||
summary += ` ${file}\n`;
|
||||
});
|
||||
}
|
||||
summary += '\n';
|
||||
} else {
|
||||
summary += `**Current Status:** Working directory clean ✅\n\n`;
|
||||
}
|
||||
|
||||
// Last session context
|
||||
if (lastSession.files_worked_on && lastSession.files_worked_on.length > 0) {
|
||||
summary += `**Last Session Work:**\n`;
|
||||
summary += `- Worked on ${lastSession.file_count} file${lastSession.file_count > 1 ? 's' : ''}\n`;
|
||||
|
||||
if (lastSession.files_worked_on.length <= 5) {
|
||||
lastSession.files_worked_on.forEach(f => {
|
||||
summary += ` - ${f}\n`;
|
||||
});
|
||||
} else {
|
||||
lastSession.files_worked_on.slice(0, 3).forEach(f => {
|
||||
summary += ` - ${f}\n`;
|
||||
});
|
||||
summary += ` ... and ${lastSession.files_worked_on.length - 3} more\n`;
|
||||
}
|
||||
summary += '\n';
|
||||
}
|
||||
|
||||
summary += `---\n\n`;
|
||||
summary += `**Ready to continue!** Git is synced and tracking all changes.\n`;
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main hook entry point
|
||||
*/
|
||||
function main() {
|
||||
try {
|
||||
// Read stdin
|
||||
const chunks = [];
|
||||
process.stdin.on('data', chunk => chunks.push(chunk));
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
const hookData = JSON.parse(Buffer.concat(chunks).toString());
|
||||
|
||||
console.error('DEBUG: SessionStart git context injector triggered');
|
||||
|
||||
// Load last session
|
||||
const lastSession = loadLastSession();
|
||||
|
||||
if (!lastSession) {
|
||||
console.error('DEBUG: No previous session found, skipping git context');
|
||||
// First session or cache cleared
|
||||
const response = { continue: true };
|
||||
console.log(JSON.stringify(response));
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`DEBUG: Last session: ${lastSession.session_id}`);
|
||||
console.error(`DEBUG: Last commit: ${lastSession.last_commit}`);
|
||||
|
||||
// Generate context summary
|
||||
const summary = generateContextSummary(lastSession);
|
||||
|
||||
console.error(`DEBUG: Generated context summary (${summary.length} chars)`);
|
||||
|
||||
// Inject context
|
||||
const response = {
|
||||
continue: true,
|
||||
additionalContext: summary,
|
||||
suppressOutput: false
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
|
||||
} catch (error) {
|
||||
console.error('DEBUG: SessionStart error:', error.message);
|
||||
// Never block - always continue
|
||||
const response = { continue: true };
|
||||
console.log(JSON.stringify(response));
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('DEBUG: SessionStart fatal error:', error.message);
|
||||
// Never block
|
||||
const response = { continue: true };
|
||||
console.log(JSON.stringify(response));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stdin
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user