Files
EverOS/use-cases/claude-code-plugin/hooks/scripts/session-context.js
Elliot Chen 518b8eca85 chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents.

Markdown is the single source of truth; SQLite holds state and LanceDB
provides the rebuildable vector + BM25 + scalar index. The codebase follows
a single-direction DDD layering (entrypoints -> service -> memory -> infra,
with component / core / config cross-cutting) enforced by import-linter.

Engineering surface:
- Coding conventions in .claude/rules/ (path-scoped) and workflows in
  .claude/skills/ (/commit, /new-branch, /pr).
- GitHub Actions CI runs make lint + test + integration; pre-commit mirrors
  the gates locally (ruff, hygiene hooks, gitlint commit-msg).
- Commit messages follow Conventional Commits, enforced by gitlint.
- make lint also enforces datetime two-zone discipline and OpenAPI drift.
2026-06-06 07:33:17 +08:00

258 lines
8.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* EverMem SessionStart Hook
* Retrieves recent memories and displays last session summary
* No AI summarization - uses local data only
*/
// Check Node.js version early
const nodeVersion = process.versions?.node;
if (!nodeVersion) {
console.error(JSON.stringify({
continue: true,
systemMessage: '⚠️ EverMem: Node.js environment not detected. Please install Node.js 18+ to use EverMem.'
}));
process.exit(0);
}
const [major] = nodeVersion.split('.').map(Number);
if (major < 18) {
console.error(JSON.stringify({
continue: true,
systemMessage: `⚠️ EverMem: Node.js ${nodeVersion} is too old. Please upgrade to Node.js 18+.`
}));
process.exit(0);
}
import { readFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { getMemories, transformGetMemoriesResults } from './utils/evermem-api.js';
import { getConfig, getGroupId } from './utils/config.js';
import { saveGroup } from './utils/groups-store.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SESSIONS_FILE = resolve(__dirname, '../../data/sessions.jsonl');
const RECENT_MEMORY_COUNT = 5; // Number of recent memories to load
const PAGE_SIZE = 100; // Fetch more to get the latest (API returns old to new)
/**
* Get the most recent session summary for current group
* @param {string} groupId - The group ID to filter by
* @returns {Object|null} Most recent session summary or null
*/
function getLastSessionSummary(groupId) {
try {
if (!existsSync(SESSIONS_FILE)) {
return null;
}
const content = readFileSync(SESSIONS_FILE, 'utf8');
const lines = content.trim().split('\n').filter(Boolean);
// Search from end (most recent first)
for (let i = lines.length - 1; i >= 0; i--) {
try {
const entry = JSON.parse(lines[i]);
if (entry.groupId === groupId) {
return entry;
}
} catch {}
}
return null;
} catch {
return null;
}
}
/**
* Format relative time (e.g., "2h ago", "1d ago")
*/
function formatRelativeTime(isoTime) {
const now = Date.now();
const then = new Date(isoTime).getTime();
const diffMs = now - then;
const minutes = Math.floor(diffMs / 60000);
const hours = Math.floor(diffMs / 3600000);
const days = Math.floor(diffMs / 86400000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 30) return `${days}d ago`;
return `${Math.floor(days / 30)}mo ago`;
}
async function main() {
// Read hook input to get cwd
let hookInput = {};
try {
let input = '';
for await (const chunk of process.stdin) {
input += chunk;
}
if (input) {
hookInput = JSON.parse(input);
}
} catch (parseError) {
console.log(JSON.stringify({
continue: true,
systemMessage: `⚠️ EverMem: Failed to parse hook input - ${parseError.message}`
}));
return;
}
// Set cwd from hook input for config.getGroupId()
if (hookInput.cwd) {
process.env.EVERMEM_CWD = hookInput.cwd;
}
const config = getConfig();
// Save group to local storage (track which projects use EverMem)
if (hookInput.cwd) {
try {
saveGroup(getGroupId(), hookInput.cwd);
} catch (groupError) {
// Non-blocking, but log for debugging
console.error(`EverMem groups-store error: ${groupError.message}`);
}
}
if (!config.isConfigured) {
// Silently skip if not configured
console.log(JSON.stringify({ continue: true }));
return;
}
try {
const groupId = getGroupId();
// Fetch memories (API returns old to new, we'll reverse and take latest)
const response = await getMemories({ pageSize: PAGE_SIZE });
const memories = transformGetMemoriesResults(response);
// Get last session summary from local storage
const lastSession = getLastSessionSummary(groupId);
if (memories.length === 0 && !lastSession) {
// No memories and no last session, skip
console.log(JSON.stringify({ continue: true }));
return;
}
// Take the most recent memories
const recentMemories = memories.slice(0, RECENT_MEMORY_COUNT);
// Build context message for Claude (no AI summarization)
let contextParts = [];
// Add last session info if available
if (lastSession) {
const timeAgo = formatRelativeTime(lastSession.timestamp);
contextParts.push(`Last session (${timeAgo}, ${lastSession.turnCount} turns): ${lastSession.summary}`);
}
// Add recent memories if available
if (recentMemories.length > 0) {
const memoriesText = recentMemories.map((m, i) => {
const date = new Date(m.timestamp).toLocaleDateString();
return `[${i + 1}] (${date}) ${m.subject}\n${m.text}`;
}).join('\n\n---\n\n');
contextParts.push(`Recent memories (${recentMemories.length}):\n\n${memoriesText}`);
}
const contextMessage = `<session-context>\n${contextParts.join('\n\n')}\n</session-context>`;
// Build display output - show meaningful content, concise but informative
let displayOutput;
if (lastSession) {
// Show last session: time, turns, summary
const truncatedSummary = lastSession.summary.length > 40
? lastSession.summary.substring(0, 40) + '...'
: lastSession.summary;
const timeAgo = formatRelativeTime(lastSession.timestamp);
displayOutput = `💡 EverMem: Last (${timeAgo}, ${lastSession.turnCount} turns): "${truncatedSummary}"`;
// Add memory preview if available
if (recentMemories.length > 0) {
const memorySubjects = recentMemories.slice(0, 2).map(m => {
const subj = m.subject || '';
return subj.length > 15 ? subj.substring(0, 15) + '..' : subj;
}).join(', ');
displayOutput += ` | ${recentMemories.length} memories: ${memorySubjects}`;
}
} else if (recentMemories.length > 0) {
// No last session, show recent memories with subjects
const memorySubjects = recentMemories.slice(0, 3).map(m => {
const subj = m.subject || '';
return subj.length > 20 ? subj.substring(0, 20) + '..' : subj;
}).join(', ');
displayOutput = `💡 EverMem: ${recentMemories.length} memories: ${memorySubjects}`;
} else {
displayOutput = `💡 EverMem: Ready`;
}
// Output: display to user and add to context
console.log(JSON.stringify({
continue: true,
systemMessage: displayOutput,
systemPrompt: contextMessage
}));
} catch (error) {
// Don't block session start on errors, but provide detailed error info
const errorDetails = {
message: error.message,
code: error.code,
name: error.name
};
// Provide user-friendly error messages
let userMessage = '⚠️ EverMem: ';
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
userMessage += `Network error - cannot reach EverMem server. Check your internet connection.`;
} else if (error.code === 'ETIMEDOUT') {
userMessage += `Request timeout - EverMem server is slow or unreachable.`;
} else if (error.message?.includes('401') || error.message?.includes('Unauthorized')) {
userMessage += `Authentication failed. Check your EVERMEM_API_KEY in .env file.`;
} else if (error.message?.includes('404')) {
userMessage += `API endpoint not found. Check EVERMEM_BASE_URL in .env file.`;
} else if (error.message?.includes('ENOENT')) {
userMessage += `File not found: ${error.path || 'unknown'}`;
} else {
userMessage += `${error.name}: ${error.message}`;
}
console.log(JSON.stringify({
continue: true,
systemMessage: userMessage
}));
}
}
// Top-level error handler for uncaught exceptions during module load
process.on('uncaughtException', (error) => {
let userMessage = '⚠️ EverMem SessionStart failed: ';
if (error.code === 'ERR_MODULE_NOT_FOUND') {
const moduleName = error.message.match(/Cannot find package '([^']+)'/)?.[1] || 'unknown';
userMessage += `Missing dependency '${moduleName}'. Run: cd ${process.cwd()} && npm install`;
} else if (error.code === 'ERR_REQUIRE_ESM') {
userMessage += `Module format error. Ensure package.json has "type": "module"`;
} else {
userMessage += `${error.name}: ${error.message}`;
}
console.log(JSON.stringify({
continue: true,
systemMessage: userMessage
}));
process.exit(0);
});
main();