69b17e15a2
Implements three-layer defense against native module version mismatches: Layer 1: Node.js Version Tracking - Track Node.js version alongside package version in .install-version marker - Auto-trigger npm install when Node.js version changes - Backward compatible with old plain-text version marker format Layer 2: Native Module Verification - Add verifyNativeModules() function to test better-sqlite3 loads correctly - Verify after install completes to catch corrupted builds - Retry with force flag if initial install verification fails Layer 3: Graceful Failure - Catch ERR_DLOPEN_FAILED in context-hook and delete version marker - Exit cleanly to avoid error spam in Claude Code UI - Auto-fix on next session start Changes: - scripts/smart-install.js: Add Node.js version tracking and verification - src/hooks/context-hook.ts: Add graceful failure handling for native module errors - tests/smart-install.test.js: Add tests for version marker format compatibility - plugin/scripts/context-hook.js: Built output from TypeScript source Fixes the issue where users see ERR_DLOPEN_FAILED errors after Node.js upgrades, requiring manual npm install. Now automatically detects and fixes the issue. Related design doc: docs/context/native-module-auto-fix-design.md Implementation plan: docs/context/native-module-auto-fix-implementation.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alex Newman <thedotmack@gmail.com>
583 lines
22 KiB
TypeScript
583 lines
22 KiB
TypeScript
/**
|
|
* Context Hook - SessionStart
|
|
* Consolidated entry point + logic
|
|
*/
|
|
|
|
import path from 'path';
|
|
import { homedir } from 'os';
|
|
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
import { stdin } from 'process';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname } from 'path';
|
|
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
|
|
|
// Get __dirname equivalent in ESM
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Version marker path (same as smart-install.js)
|
|
// From src/hooks/ we need to go up to plugin root: ../../
|
|
const VERSION_MARKER_PATH = path.join(__dirname, '../../.install-version');
|
|
|
|
/**
|
|
* Get context depth from settings
|
|
* Priority: ~/.claude/settings.json > env var > default
|
|
*/
|
|
function getContextDepth(): number {
|
|
try {
|
|
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
|
|
if (existsSync(settingsPath)) {
|
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
if (settings.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
|
const count = parseInt(settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
|
if (!isNaN(count) && count > 0) {
|
|
return count;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Fall through to env var or default
|
|
}
|
|
return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10);
|
|
}
|
|
|
|
// Configuration: Read from settings.json or environment
|
|
const DISPLAY_OBSERVATION_COUNT = getContextDepth();
|
|
const DISPLAY_SESSION_COUNT = 10; // Recent sessions for timeline context
|
|
const CHARS_PER_TOKEN_ESTIMATE = 4; // Rough estimate for token counting
|
|
const SUMMARY_LOOKAHEAD = 1; // Fetch one extra summary for offset calculation
|
|
|
|
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',
|
|
red: '\x1b[31m',
|
|
};
|
|
|
|
interface Observation {
|
|
id: number;
|
|
sdk_session_id: string;
|
|
type: string;
|
|
title: string | null;
|
|
subtitle: string | null;
|
|
narrative: string | null;
|
|
facts: string | null;
|
|
concepts: string | null;
|
|
files_read: string | null;
|
|
files_modified: string | null;
|
|
discovery_tokens: number | null;
|
|
created_at: string;
|
|
created_at_epoch: number;
|
|
}
|
|
|
|
interface SessionSummary {
|
|
id: number;
|
|
sdk_session_id: string;
|
|
request: string | null;
|
|
investigated: string | null;
|
|
learned: string | null;
|
|
completed: string | null;
|
|
next_steps: string | null;
|
|
created_at: string;
|
|
created_at_epoch: number;
|
|
}
|
|
|
|
// Helper: Parse JSON array safely
|
|
function parseJsonArray(json: string | null): string[] {
|
|
if (!json) return [];
|
|
try {
|
|
const parsed = JSON.parse(json);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch (err) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Helper: Format date with time
|
|
function formatDateTime(dateStr: string): string {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
}
|
|
|
|
// Helper: Format just time (no date)
|
|
function formatTime(dateStr: string): string {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
}
|
|
|
|
// Helper: Format just date
|
|
function formatDate(dateStr: string): string {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
});
|
|
}
|
|
|
|
// Helper: Convert absolute paths to relative paths
|
|
function toRelativePath(filePath: string, cwd: string): string {
|
|
if (path.isAbsolute(filePath)) {
|
|
return path.relative(cwd, filePath);
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
// Helper: Render a summary field (investigated, learned, etc.)
|
|
function renderSummaryField(label: string, value: string | null, color: string, useColors: boolean): string[] {
|
|
if (!value) return [];
|
|
|
|
if (useColors) {
|
|
return [`${color}${label}:${colors.reset} ${value}`, ''];
|
|
}
|
|
return [`**${label}**: ${value}`, ''];
|
|
}
|
|
|
|
/**
|
|
* Context Hook Main Logic
|
|
*/
|
|
async function contextHook(input?: SessionStartInput, useColors: boolean = false): Promise<string> {
|
|
const cwd = input?.cwd ?? process.cwd();
|
|
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
|
|
|
let db: SessionStore;
|
|
try {
|
|
db = new SessionStore();
|
|
} catch (error: any) {
|
|
if (error.code === 'ERR_DLOPEN_FAILED') {
|
|
// Native module ABI mismatch - delete version marker to trigger reinstall
|
|
try {
|
|
unlinkSync(VERSION_MARKER_PATH);
|
|
} catch (unlinkError) {
|
|
// Marker might not exist, that's okay
|
|
}
|
|
|
|
// Log once (not error spam) and exit cleanly
|
|
console.error('⚠️ Native module rebuild needed - restart Claude Code to auto-fix');
|
|
console.error(' (This happens after Node.js version upgrades)');
|
|
process.exit(0); // Exit cleanly to avoid error spam
|
|
}
|
|
|
|
// Other errors should still throw
|
|
throw error;
|
|
}
|
|
|
|
// Get ALL recent observations for this project (not filtered by summaries)
|
|
// This ensures we show observations even when summaries haven't been generated
|
|
// Configurable via CLAUDE_MEM_CONTEXT_OBSERVATIONS env var (default: 50)
|
|
const allObservations = db.db.prepare(`
|
|
SELECT
|
|
id, sdk_session_id, type, title, subtitle, narrative,
|
|
facts, concepts, files_read, files_modified, discovery_tokens,
|
|
created_at, created_at_epoch
|
|
FROM observations
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`).all(project, DISPLAY_OBSERVATION_COUNT) as Observation[];
|
|
|
|
// Get recent summaries (optional - may not exist for recent sessions)
|
|
// Fetch one extra for offset calculation
|
|
const recentSummaries = db.db.prepare(`
|
|
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
|
FROM session_summaries
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`).all(project, DISPLAY_SESSION_COUNT + SUMMARY_LOOKAHEAD) as SessionSummary[];
|
|
|
|
// If we have neither observations nor summaries, show empty state
|
|
if (allObservations.length === 0 && recentSummaries.length === 0) {
|
|
db.close();
|
|
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 sessions found for this project yet.${colors.reset}\n`;
|
|
}
|
|
return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`;
|
|
}
|
|
|
|
// Use observations for display (summaries are supplementary)
|
|
const observations = allObservations;
|
|
const displaySummaries = recentSummaries.slice(0, DISPLAY_SESSION_COUNT);
|
|
|
|
// All observations are shown in timeline (filtered by type, not concepts)
|
|
const timelineObs = observations;
|
|
|
|
// Build output
|
|
const output: string[] = [];
|
|
|
|
// Header
|
|
if (useColors) {
|
|
output.push('');
|
|
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
|
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`# [${project}] recent context`);
|
|
output.push('');
|
|
}
|
|
|
|
// Chronological Timeline
|
|
if (timelineObs.length > 0) {
|
|
// Legend/Key
|
|
if (useColors) {
|
|
output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | ⚖️ decision${colors.reset}`);
|
|
} else {
|
|
output.push(`**Legend:** 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | ⚖️ decision`);
|
|
}
|
|
output.push('');
|
|
|
|
// Column Key
|
|
if (useColors) {
|
|
output.push(`${colors.bright}💡 Column Key${colors.reset}`);
|
|
output.push(`${colors.dim} Read: Tokens to read this observation (cost to learn it now)${colors.reset}`);
|
|
output.push(`${colors.dim} Work: Tokens spent on work that produced this record (🔍 research, 🛠️ building, ⚖️ deciding)${colors.reset}`);
|
|
} else {
|
|
output.push(`💡 **Column Key**:`);
|
|
output.push(`- **Read**: Tokens to read this observation (cost to learn it now)`);
|
|
output.push(`- **Work**: Tokens spent on work that produced this record (🔍 research, 🛠️ building, ⚖️ deciding)`);
|
|
}
|
|
output.push('');
|
|
|
|
// Context Index Usage Instructions
|
|
if (useColors) {
|
|
output.push(`${colors.dim}💡 Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${colors.reset}`);
|
|
output.push('');
|
|
output.push(`${colors.dim}When you need implementation details, rationale, or debugging context:${colors.reset}`);
|
|
output.push(`${colors.dim} - Use the mem-search skill to fetch full observations on-demand${colors.reset}`);
|
|
output.push(`${colors.dim} - Critical types (🔴 bugfix, ⚖️ decision) often need detailed fetching${colors.reset}`);
|
|
output.push(`${colors.dim} - Trust this index over re-reading code for past decisions and learnings${colors.reset}`);
|
|
} else {
|
|
output.push(`💡 **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.`);
|
|
output.push('');
|
|
output.push(`When you need implementation details, rationale, or debugging context:`);
|
|
output.push(`- Use the mem-search skill to fetch full observations on-demand`);
|
|
output.push(`- Critical types (🔴 bugfix, ⚖️ decision) often need detailed fetching`);
|
|
output.push(`- Trust this index over re-reading code for past decisions and learnings`);
|
|
}
|
|
output.push('');
|
|
|
|
// Section 1: Aggregate ROI Metrics
|
|
const totalObservations = observations.length;
|
|
const totalReadTokens = observations.reduce((sum, obs) => {
|
|
// Estimate read tokens from observation size
|
|
const obsSize = (obs.title?.length || 0) +
|
|
(obs.subtitle?.length || 0) +
|
|
(obs.narrative?.length || 0) +
|
|
JSON.stringify(obs.facts || []).length;
|
|
return sum + Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
|
|
}, 0);
|
|
const totalDiscoveryTokens = observations.reduce((sum, obs) => sum + (obs.discovery_tokens || 0), 0);
|
|
const savings = totalDiscoveryTokens - totalReadTokens;
|
|
const savingsPercent = totalDiscoveryTokens > 0
|
|
? Math.round((savings / totalDiscoveryTokens) * 100)
|
|
: 0;
|
|
|
|
// Display Context Economics section
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.cyan}📊 Context Economics${colors.reset}`);
|
|
output.push(`${colors.dim} Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)${colors.reset}`);
|
|
output.push(`${colors.dim} Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${colors.reset}`);
|
|
if (totalDiscoveryTokens > 0) {
|
|
output.push(`${colors.green} Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)${colors.reset}`);
|
|
}
|
|
output.push('');
|
|
} else {
|
|
output.push(`📊 **Context Economics**:`);
|
|
output.push(`- Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)`);
|
|
output.push(`- Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`);
|
|
if (totalDiscoveryTokens > 0) {
|
|
output.push(`- Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)`);
|
|
}
|
|
output.push('');
|
|
}
|
|
|
|
// Prepare summaries for timeline display
|
|
// The most recent summary shows full details (investigated, learned, etc.)
|
|
// Older summaries only show as timeline markers (no link needed)
|
|
const mostRecentSummaryId = recentSummaries[0]?.id;
|
|
|
|
interface SummaryTimelineItem extends SessionSummary {
|
|
displayEpoch: number;
|
|
displayTime: string;
|
|
shouldShowLink: boolean;
|
|
}
|
|
|
|
const summariesForTimeline: SummaryTimelineItem[] = displaySummaries.map((summary, i) => {
|
|
// For visual grouping, display each summary at the time range it covers
|
|
// Most recent: shows at its own time (current session)
|
|
// Older: shows at the previous (older) summary's time to mark the session range
|
|
const olderSummary = i === 0 ? null : recentSummaries[i + 1];
|
|
return {
|
|
...summary,
|
|
displayEpoch: olderSummary ? olderSummary.created_at_epoch : summary.created_at_epoch,
|
|
displayTime: olderSummary ? olderSummary.created_at : summary.created_at,
|
|
shouldShowLink: summary.id !== mostRecentSummaryId
|
|
};
|
|
});
|
|
|
|
type TimelineItem =
|
|
| { type: 'observation'; data: Observation }
|
|
| { type: 'summary'; data: SummaryTimelineItem };
|
|
|
|
const timeline: TimelineItem[] = [
|
|
...timelineObs.map(obs => ({ type: 'observation' as const, data: obs })),
|
|
...summariesForTimeline.map(summary => ({ type: 'summary' as const, data: summary }))
|
|
];
|
|
|
|
// Sort chronologically
|
|
timeline.sort((a, b) => {
|
|
const aEpoch = a.type === 'observation' ? a.data.created_at_epoch : a.data.displayEpoch;
|
|
const bEpoch = b.type === 'observation' ? b.data.created_at_epoch : b.data.displayEpoch;
|
|
return aEpoch - bEpoch;
|
|
});
|
|
|
|
// Group by day for rendering
|
|
const itemsByDay = new Map<string, TimelineItem[]>();
|
|
for (const item of timeline) {
|
|
const itemDate = item.type === 'observation' ? item.data.created_at : item.data.displayTime;
|
|
const day = formatDate(itemDate);
|
|
if (!itemsByDay.has(day)) {
|
|
itemsByDay.set(day, []);
|
|
}
|
|
itemsByDay.get(day)!.push(item);
|
|
}
|
|
|
|
// Sort days chronologically
|
|
const sortedDays = Array.from(itemsByDay.entries()).sort((a, b) => {
|
|
const aDate = new Date(a[0]).getTime();
|
|
const bDate = new Date(b[0]).getTime();
|
|
return aDate - bDate;
|
|
});
|
|
|
|
// Render each day's timeline
|
|
for (const [day, dayItems] of sortedDays) {
|
|
// Day header
|
|
if (useColors) {
|
|
output.push(`${colors.bright}${colors.cyan}${day}${colors.reset}`);
|
|
output.push('');
|
|
} else {
|
|
output.push(`### ${day}`);
|
|
output.push('');
|
|
}
|
|
|
|
// Render items chronologically with visual file grouping
|
|
let currentFile: string | null = null;
|
|
let lastTime = '';
|
|
let tableOpen = false;
|
|
|
|
for (const item of dayItems) {
|
|
if (item.type === 'summary') {
|
|
// Close any open table
|
|
if (tableOpen) {
|
|
output.push('');
|
|
tableOpen = false;
|
|
currentFile = null;
|
|
lastTime = '';
|
|
}
|
|
|
|
// Render summary
|
|
const summary = item.data;
|
|
const summaryTitle = `${summary.request || 'Session started'} (${formatDateTime(summary.displayTime)})`;
|
|
const link = summary.shouldShowLink ? `claude-mem://session-summary/${summary.id}` : '';
|
|
|
|
if (useColors) {
|
|
const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : '';
|
|
output.push(`🎯 ${colors.yellow}#S${summary.id}${colors.reset} ${summaryTitle} ${linkPart}`);
|
|
} else {
|
|
const linkPart = link ? ` [→](${link})` : '';
|
|
output.push(`**🎯 #S${summary.id}** ${summaryTitle}${linkPart}`);
|
|
}
|
|
output.push('');
|
|
} else {
|
|
// Render observation
|
|
const obs = item.data;
|
|
const files = parseJsonArray(obs.files_modified);
|
|
const file = files.length > 0 ? toRelativePath(files[0], cwd) : 'General';
|
|
|
|
// Check if we need a new file section
|
|
if (file !== currentFile) {
|
|
// Close previous table
|
|
if (tableOpen) {
|
|
output.push('');
|
|
}
|
|
|
|
// File header
|
|
if (useColors) {
|
|
output.push(`${colors.dim}${file}${colors.reset}`);
|
|
} else {
|
|
output.push(`**${file}**`);
|
|
}
|
|
|
|
// Table header (markdown only)
|
|
if (!useColors) {
|
|
output.push(`| ID | Time | T | Title | Read | Work |`);
|
|
output.push(`|----|------|---|-------|------|------|`);
|
|
}
|
|
|
|
currentFile = file;
|
|
tableOpen = true;
|
|
lastTime = '';
|
|
}
|
|
|
|
const time = formatTime(obs.created_at);
|
|
const title = obs.title || 'Untitled';
|
|
|
|
// Map observation type to emoji icon
|
|
let icon = '•';
|
|
switch (obs.type) {
|
|
case 'bugfix':
|
|
icon = '🔴';
|
|
break;
|
|
case 'feature':
|
|
icon = '🟣';
|
|
break;
|
|
case 'refactor':
|
|
icon = '🔄';
|
|
break;
|
|
case 'change':
|
|
icon = '✅';
|
|
break;
|
|
case 'discovery':
|
|
icon = '🔵';
|
|
break;
|
|
case 'decision':
|
|
icon = '⚖️';
|
|
break;
|
|
default:
|
|
icon = '•';
|
|
}
|
|
|
|
// Section 2: Calculate read tokens (estimate from observation size)
|
|
const obsSize = (obs.title?.length || 0) +
|
|
(obs.subtitle?.length || 0) +
|
|
(obs.narrative?.length || 0) +
|
|
JSON.stringify(obs.facts || []).length;
|
|
const readTokens = Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
|
|
|
|
// Get discovery tokens (handle old observations without this field)
|
|
const discoveryTokens = obs.discovery_tokens || 0;
|
|
|
|
// Map observation type to work emoji
|
|
let workEmoji = '🔍'; // default to research/discovery
|
|
switch (obs.type) {
|
|
case 'discovery':
|
|
workEmoji = '🔍'; // research/exploration
|
|
break;
|
|
case 'change':
|
|
case 'feature':
|
|
case 'bugfix':
|
|
case 'refactor':
|
|
workEmoji = '🛠️'; // building/modifying
|
|
break;
|
|
case 'decision':
|
|
workEmoji = '⚖️'; // decision-making
|
|
break;
|
|
}
|
|
|
|
const discoveryDisplay = discoveryTokens > 0 ? `${workEmoji} ${discoveryTokens.toLocaleString()}` : '-';
|
|
|
|
const showTime = time !== lastTime;
|
|
const timeDisplay = showTime ? time : '';
|
|
lastTime = time;
|
|
|
|
if (useColors) {
|
|
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
|
|
const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
|
|
const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
|
|
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`);
|
|
} else {
|
|
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${readTokens} | ${discoveryDisplay} |`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close final table if open
|
|
if (tableOpen) {
|
|
output.push('');
|
|
}
|
|
}
|
|
|
|
// Add full summary details for most recent session
|
|
// Only show if summary was generated AFTER the last observation
|
|
const mostRecentSummary = recentSummaries[0];
|
|
const mostRecentObservation = observations[0]; // observations are DESC by created_at_epoch
|
|
|
|
const shouldShowSummary = mostRecentSummary &&
|
|
(mostRecentSummary.investigated || mostRecentSummary.learned || mostRecentSummary.completed || mostRecentSummary.next_steps) &&
|
|
(!mostRecentObservation || mostRecentSummary.created_at_epoch > mostRecentObservation.created_at_epoch);
|
|
|
|
if (shouldShowSummary) {
|
|
output.push(...renderSummaryField('Investigated', mostRecentSummary.investigated, colors.blue, useColors));
|
|
output.push(...renderSummaryField('Learned', mostRecentSummary.learned, colors.yellow, useColors));
|
|
output.push(...renderSummaryField('Completed', mostRecentSummary.completed, colors.green, useColors));
|
|
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
|
|
}
|
|
|
|
// Footer with token savings message
|
|
if (totalDiscoveryTokens > 0 && savings > 0) {
|
|
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
|
|
output.push('');
|
|
if (useColors) {
|
|
output.push(`${colors.dim}💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.${colors.reset}`);
|
|
} else {
|
|
output.push(`💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
db.close();
|
|
return output.join('\n').trimEnd();
|
|
}
|
|
|
|
// Entry Point - handle stdin/stdout
|
|
const forceColors = process.argv.includes('--colors');
|
|
|
|
if (stdin.isTTY || forceColors) {
|
|
// Running manually from terminal - print formatted output with colors
|
|
contextHook(undefined, true).then(contextOutput => {
|
|
console.log(contextOutput);
|
|
process.exit(0);
|
|
});
|
|
} else {
|
|
// Running from hook - wrap in hookSpecificOutput JSON format
|
|
let input = '';
|
|
stdin.on('data', (chunk) => input += chunk);
|
|
stdin.on('end', async () => {
|
|
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
|
const contextOutput = await contextHook(parsed, false);
|
|
const result = {
|
|
hookSpecificOutput: {
|
|
hookEventName: "SessionStart",
|
|
additionalContext: contextOutput
|
|
}
|
|
};
|
|
console.log(JSON.stringify(result));
|
|
process.exit(0);
|
|
});
|
|
} |