Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:56:10 +08:00
commit 400ca062d1
48 changed files with 18674 additions and 0 deletions

View 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();
}