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.
This commit is contained in:
Elliot Chen
2026-06-05 22:35:51 +08:00
commit 518b8eca85
636 changed files with 160553 additions and 0 deletions

View File

@ -0,0 +1,200 @@
#!/usr/bin/env node
/**
* EverMem SessionEnd Hook
* Saves session summary (first user prompt + stats) to local storage
* No AI summarization - just extracts key info from transcript
*/
import { readFileSync, appendFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { getGroupId, getConfig } from './utils/config.js';
import { debug, setDebugPrefix } from './utils/debug.js';
setDebugPrefix('session-end');
const __dirname = dirname(fileURLToPath(import.meta.url));
const SESSIONS_FILE = resolve(__dirname, '../../data/sessions.jsonl');
/**
* Read transcript and extract key content
* @param {string} transcriptPath - Path to the transcript JSONL file
* @returns {Object|null} Extracted content
*/
function extractTranscriptContent(transcriptPath) {
try {
if (!existsSync(transcriptPath)) {
return null;
}
const content = readFileSync(transcriptPath, 'utf8');
const lines = content.trim().split('\n').filter(Boolean);
let firstUserPrompt = null;
let lastUserPrompt = null;
let turnCount = 0;
let firstTimestamp = null;
let lastTimestamp = null;
for (const line of lines) {
try {
const entry = JSON.parse(line);
// Track timestamps
if (entry.timestamp) {
if (!firstTimestamp) firstTimestamp = entry.timestamp;
lastTimestamp = entry.timestamp;
}
// Count turns
if (entry.type === 'system' && entry.subtype === 'turn_duration') {
turnCount++;
}
// Extract user messages (not tool_result)
if (entry.type === 'user' && entry.message?.role === 'user') {
const msgContent = entry.message.content;
if (typeof msgContent === 'string' && msgContent.trim()) {
if (!firstUserPrompt) {
firstUserPrompt = msgContent.trim();
}
lastUserPrompt = msgContent.trim();
}
}
} catch {}
}
return {
firstUserPrompt: firstUserPrompt?.substring(0, 200) || '',
lastUserPrompt: lastUserPrompt?.substring(0, 200) || '',
turnCount,
firstTimestamp,
lastTimestamp
};
} catch {
return null;
}
}
/**
* Save session summary to local JSONL file
*/
function saveSummary(entry) {
try {
appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + '\n', 'utf8');
return true;
} catch {
return false;
}
}
/**
* Check if session already has a summary
*/
function alreadySummarized(sessionId) {
try {
if (!existsSync(SESSIONS_FILE)) {
return false;
}
const content = readFileSync(SESSIONS_FILE, 'utf8');
return content.includes(`"sessionId":"${sessionId}"`);
} catch {
return false;
}
}
async function main() {
// Read hook input
let hookInput = {};
try {
let input = '';
for await (const chunk of process.stdin) {
input += chunk;
}
if (input) {
hookInput = JSON.parse(input);
}
} catch {
process.exit(0);
}
const { session_id, transcript_path, cwd, reason } = hookInput;
// Skip if no transcript or already summarized
if (!transcript_path || !session_id) {
process.exit(0);
}
const wasAlreadySummarized = alreadySummarized(session_id);
// Set cwd for config
if (cwd) {
process.env.EVERMEM_CWD = cwd;
}
const config = getConfig();
if (!config.isConfigured) {
process.exit(0);
}
// Extract content from transcript
const content = extractTranscriptContent(transcript_path);
if (!content || content.turnCount === 0) {
process.exit(0);
}
// Use first user prompt as summary (truncated)
const summary = content.firstUserPrompt || 'Session with no text prompts';
// Calculate session duration
let durationStr = '';
if (content.firstTimestamp && content.lastTimestamp) {
const durationMs = new Date(content.lastTimestamp) - new Date(content.firstTimestamp);
const minutes = Math.floor(durationMs / 60000);
if (minutes < 1) {
durationStr = '<1min';
} else if (minutes < 60) {
durationStr = `${minutes}min`;
} else {
const hours = Math.floor(minutes / 60);
const remainMins = minutes % 60;
durationStr = remainMins > 0 ? `${hours}h${remainMins}m` : `${hours}h`;
}
}
// Truncate summary for display
const displaySummary = summary.length > 50
? summary.substring(0, 50) + '...'
: summary;
// Build output: turns, duration, summary
const parts = [`${content.turnCount} turns`];
if (durationStr) parts.push(durationStr);
// Save to local file (only if not already saved)
if (!wasAlreadySummarized) {
const entry = {
sessionId: session_id,
groupId: getGroupId(),
summary,
turnCount: content.turnCount,
reason: reason || 'unknown',
startTime: content.firstTimestamp,
endTime: content.lastTimestamp,
timestamp: new Date().toISOString()
};
saveSummary(entry);
}
// Always output session summary (whether saved or not)
const message = `📝 Session (${parts.join(', ')}): "${displaySummary}"`;
// Log to unified debug file
debug('output', message);
console.error(message); // Direct terminal output
console.log(JSON.stringify({ systemMessage: message }));
}
main().catch(() => process.exit(0));