f556546994
Improvements to SessionStart context hook file display: 1. **Remove redundant files**: Files in "Modified" list are now excluded from "Read" list - Prevents duplication when a file was both read and modified - Reduces token usage by eliminating redundant information 2. **Use relative paths**: Convert absolute paths to project-relative paths - Example: /Users/alexnewman/Scripts/claude-mem/src/hooks/context.ts → src/hooks/context.ts - Significantly reduces token consumption in context injection - Makes file references more readable and portable Implementation: - Added toRelativePath() helper function to convert paths - Added filesModifiedSet.forEach(file => filesReadSet.delete(file)) to remove duplicates - Applied to both files_read and files_modified when building Sets Impact: Reduces token usage in Tier 1 summaries (most recent session) where file lists are displayed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
264 lines
8.2 KiB
TypeScript
264 lines
8.2 KiB
TypeScript
import path from 'path';
|
|
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
|
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
|
|
|
export interface SessionStartInput {
|
|
session_id?: string;
|
|
transcript_path?: string;
|
|
cwd?: string;
|
|
hook_event_name?: string;
|
|
source?: "startup" | "resume" | "clear" | "compact";
|
|
[key: string]: any;
|
|
}
|
|
|
|
// ANSI color codes for terminal output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
bright: '\x1b[1m',
|
|
dim: '\x1b[2m',
|
|
cyan: '\x1b[36m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
magenta: '\x1b[35m',
|
|
gray: '\x1b[90m',
|
|
};
|
|
|
|
/**
|
|
* Context Hook - SessionStart
|
|
* Shows user what happened in recent sessions
|
|
*/
|
|
export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string {
|
|
ensureWorkerRunning();
|
|
const cwd = input?.cwd ?? process.cwd();
|
|
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
|
|
|
const db = new SessionStore();
|
|
|
|
try {
|
|
// Get the most recent summaries, then display them chronologically (oldest to newest, like a chat)
|
|
const summaries = db.db.prepare(`
|
|
SELECT * FROM (
|
|
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
|
FROM session_summaries
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT 10
|
|
)
|
|
ORDER BY created_at_epoch ASC
|
|
`).all(project) as Array<{
|
|
sdk_session_id: string;
|
|
request: string | null;
|
|
learned: string | null;
|
|
completed: string | null;
|
|
next_steps: string | null;
|
|
created_at: string;
|
|
}>;
|
|
|
|
if (summaries.length === 0) {
|
|
if (useColors) {
|
|
return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous summaries found for this project yet.${colors.reset}\n`;
|
|
}
|
|
return `# [${project}] recent context\n\nNo previous summaries found for this project yet.`;
|
|
}
|
|
|
|
const output: string[] = [];
|
|
|
|
if (useColors) {
|
|
output.push('');
|
|
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
|
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
|
} else {
|
|
output.push(`# [${project}] recent context`);
|
|
output.push('');
|
|
}
|
|
|
|
let isFirstSummary = true;
|
|
|
|
for (let i = 0; i < summaries.length; i++) {
|
|
const summary = summaries[i];
|
|
|
|
// Determine verbosity tier based on position
|
|
// Most recent summary is at the end (highest index) since we display chronologically
|
|
const positionFromEnd = summaries.length - 1 - i;
|
|
const isTier1 = positionFromEnd === 0; // Most recent (full verbosity)
|
|
const isTier2 = positionFromEnd >= 1 && positionFromEnd <= 3; // Middle 3 (request + what was done)
|
|
const isTier3 = positionFromEnd > 3; // Oldest 6 (request only)
|
|
|
|
// Add separator between summaries (but not before the first one)
|
|
if (!isFirstSummary) {
|
|
if (useColors) {
|
|
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
|
output.push('');
|
|
} else {
|
|
output.push('---');
|
|
output.push('');
|
|
}
|
|
} else {
|
|
if (useColors) {
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
isFirstSummary = false;
|
|
|
|
// TIER 3: Minimal (just Request + Date)
|
|
if (isTier3) {
|
|
if (summary.request) {
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`**Request:** ${summary.request}`);
|
|
output.push('');
|
|
}
|
|
}
|
|
const dateTime = new Date(summary.created_at).toLocaleString();
|
|
if (useColors) {
|
|
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
|
} else {
|
|
output.push(`**Date:** ${dateTime}`);
|
|
output.push('');
|
|
}
|
|
continue; // Skip the rest for Tier 3
|
|
}
|
|
|
|
// TIER 1 & 2: Show Request
|
|
if (summary.request) {
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`**Request:** ${summary.request}`);
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
// TIER 1 ONLY: Show Learned
|
|
if (isTier1 && summary.learned) {
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`**Learned:** ${summary.learned}`);
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
// TIER 1 & 2: Show Completed
|
|
if (summary.completed) {
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`**Completed:** ${summary.completed}`);
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
// TIER 1 ONLY: Show Next Steps
|
|
if (isTier1 && summary.next_steps) {
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`**Next Steps:** ${summary.next_steps}`);
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
// TIER 1 ONLY: Get and show files
|
|
if (isTier1) {
|
|
const observations = db.db.prepare(`
|
|
SELECT files_read, files_modified
|
|
FROM observations
|
|
WHERE sdk_session_id = ?
|
|
`).all(summary.sdk_session_id) as Array<{
|
|
files_read: string | null;
|
|
files_modified: string | null;
|
|
}>;
|
|
|
|
const filesReadSet = new Set<string>();
|
|
const filesModifiedSet = new Set<string>();
|
|
|
|
// Helper function to convert absolute paths to relative paths
|
|
const toRelativePath = (filePath: string): string => {
|
|
try {
|
|
// Only convert if it's an absolute path
|
|
if (path.isAbsolute(filePath)) {
|
|
return path.relative(cwd, filePath);
|
|
}
|
|
return filePath;
|
|
} catch {
|
|
return filePath;
|
|
}
|
|
};
|
|
|
|
for (const obs of observations) {
|
|
if (obs.files_read) {
|
|
try {
|
|
const files = JSON.parse(obs.files_read);
|
|
if (Array.isArray(files)) {
|
|
files.forEach(f => filesReadSet.add(toRelativePath(f)));
|
|
}
|
|
} catch {
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
|
|
if (obs.files_modified) {
|
|
try {
|
|
const files = JSON.parse(obs.files_modified);
|
|
if (Array.isArray(files)) {
|
|
files.forEach(f => filesModifiedSet.add(toRelativePath(f)));
|
|
}
|
|
} catch {
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove files from filesReadSet if they're already in filesModifiedSet (avoid redundancy)
|
|
filesModifiedSet.forEach(file => filesReadSet.delete(file));
|
|
|
|
if (filesReadSet.size > 0) {
|
|
if (useColors) {
|
|
output.push(`${colors.dim}Files Read: ${Array.from(filesReadSet).join(', ')}${colors.reset}`);
|
|
} else {
|
|
output.push(`**Files Read:** ${Array.from(filesReadSet).join(', ')}`);
|
|
}
|
|
}
|
|
|
|
if (filesModifiedSet.size > 0) {
|
|
if (useColors) {
|
|
output.push(`${colors.dim}Files Modified: ${Array.from(filesModifiedSet).join(', ')}${colors.reset}`);
|
|
} else {
|
|
output.push(`**Files Modified:** ${Array.from(filesModifiedSet).join(', ')}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TIER 1 & 2: Show Date
|
|
const dateTime = new Date(summary.created_at).toLocaleString();
|
|
if (useColors) {
|
|
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
|
} else {
|
|
output.push(`**Date:** ${dateTime}`);
|
|
}
|
|
|
|
if (!useColors) {
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
if (useColors) {
|
|
output.push('');
|
|
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
|
}
|
|
|
|
return output.join('\n');
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|