Refactor: Remove legacy data access objects and logging utilities

- Deleted MemoryStore, OverviewStore, SessionStore, StreamingSessionStore, and TranscriptEventStore classes to streamline database interactions.
- Removed logger and rolling log utilities to simplify logging mechanisms.
- Updated index file to reflect the removal of stores and logging functionalities.
This commit is contained in:
Alex Newman
2025-10-15 20:02:15 -04:00
parent 047298a183
commit 7307563cfe
24 changed files with 165 additions and 5111 deletions
+160 -512
View File
File diff suppressed because one or more lines are too long
+5 -106
View File
@@ -9,19 +9,11 @@ import { PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_DESCRIPTION } from '../shared/co
// Import command handlers
import { install } from '../commands/install.js';
import { uninstall } from '../commands/uninstall.js';
import { status } from '../commands/status.js';
import { logs } from '../commands/logs.js';
import { loadContext } from '../commands/load-context.js';
import { trash } from '../commands/trash.js';
import { viewTrash } from '../commands/trash-view.js';
import { emptyTrash } from '../commands/trash-empty.js';
import { restore } from '../commands/restore.js';
import { changelog } from '../commands/changelog.js';
import { doctor } from '../commands/doctor.js';
import { storeMemory } from '../commands/store-memory.js';
import { storeOverview } from '../commands/store-overview.js';
import { updateSessionMetadata } from '../commands/update-session-metadata.js';
import { generateTitle } from '../commands/generate-title.js';
const program = new Command();
// </Block> =======================================
@@ -65,32 +57,7 @@ program
.action(uninstall);
// </Block> =======================================
// <Block> 1.6 ====================================
// Status Command Definition
// Natural pattern: Define command with its handler
// Status command
program
.command('status')
.description('Check installation status of Claude Memory System')
.action(status);
// Doctor command
program
.command('doctor')
.description('Run environment and pipeline diagnostics for rolling memory')
.option('--json', 'Output JSON instead of text')
.action(async (options: any) => {
try {
await doctor(options);
} catch (error: any) {
console.error(`doctor failed: ${error.message || error}`);
process.exitCode = 1;
}
});
// </Block> =======================================
// <Block> 1.7 ====================================
// Logs Command Definition
// Natural pattern: Define command with its options and handler
// Logs command
@@ -105,20 +72,6 @@ program
// </Block> =======================================
// <Block> 1.8 ====================================
// Load-Context Command Definition
// Natural pattern: Define command with its options and handler
// Load-context command
program
.command('load-context')
.description('Load compressed memories for current session')
.option('--project <name>', 'Filter by project name')
.option('--count <n>', 'Number of memories to load', '10')
.option('--raw', 'Output raw JSON instead of formatted text')
.option('--format <type>', 'Output format: json, session-start, or default')
.action(loadContext);
// </Block> =======================================
// <Block> 1.9 ====================================
// Trash and Restore Commands Definition
// Natural pattern: Define commands for safe file operations
@@ -166,63 +119,9 @@ program
.action(restore);
// </Block> =======================================
// Store memory command (for SDK streaming)
program
.command('store-memory')
.description('Store a memory to all storage layers (used by SDK)')
.requiredOption('--id <id>', 'Memory ID')
.requiredOption('--project <project>', 'Project name')
.requiredOption('--session <session>', 'Session ID')
.requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
.requiredOption('--title <title>', 'Memory title (3-8 words)')
.requiredOption('--subtitle <subtitle>', 'Memory subtitle (max 24 words)')
.requiredOption('--facts <json>', 'Atomic facts as JSON array')
.option('--concepts <json>', 'Concept tags as JSON array')
.option('--files <json>', 'Files touched as JSON array')
.action(storeMemory);
// Store overview command (for SDK streaming)
program
.command('store-overview')
.description('Store a session overview (used by SDK)')
.requiredOption('--project <project>', 'Project name')
.requiredOption('--session <session>', 'Session ID')
.requiredOption('--content <content>', 'Overview content')
.action(storeOverview);
// Update session metadata command (for SDK streaming)
program
.command('update-session-metadata')
.description('Update session title and subtitle (used by SDK)')
.requiredOption('--project <project>', 'Project name')
.requiredOption('--session <session>', 'Session ID')
.requiredOption('--title <title>', 'Session title (3-6 words)')
.option('--subtitle <subtitle>', 'Session subtitle (max 20 words)')
.action(updateSessionMetadata);
// Changelog command
program
.command('changelog')
.description('Generate CHANGELOG.md from claude-mem memories')
.option('--historical <n>', 'Number of versions to search (default: current version only)')
.option('--generate <version>', 'Generate changelog for a specific version')
.option('--start <time>', 'Start time for memory search (ISO format)')
.option('--end <time>', 'End time for memory search (ISO format)')
.option('--update', 'Update CHANGELOG.md from JSONL entries')
.option('--preview', 'Preview the generated changelog')
.option('-v, --verbose', 'Show detailed output')
.action(changelog);
// Generate title command
program
.command('generate-title <prompt>')
.description('Generate a session title and subtitle from a prompt')
.option('--json', 'Output as JSON')
.option('--oneline', 'Output as single line (title - subtitle)')
.option('--session-id <id>', 'Claude session ID to update')
.option('--save', 'Save the generated title to the database (requires --session-id)')
.action(generateTitle);
// <Block> 1.9 ====================================
// Hook Commands Definition
// Natural pattern: Define hook commands for Claude Code integration
// Hook commands (for Claude Code hook integration)
program
.command('context')
@@ -282,8 +181,8 @@ async function readStdin(): Promise<string> {
program.parse();
// </Block> =======================================
// <Block> 1.12 ===================================
// <Block> 1.11 ===================================
// Module Exports for Programmatic Use
// Export database and utility classes for hooks and external consumers
export { DatabaseManager, StreamingSessionStore, migrations, initializeDatabase, getDatabase } from '../services/sqlite/index.js';
export { DatabaseManager, migrations, initializeDatabase, getDatabase } from '../services/sqlite/index.js';
// </Block> =======================================
-744
View File
@@ -1,744 +0,0 @@
import { OptionValues } from 'commander';
import { query } from '@anthropic-ai/claude-agent-sdk';
import fs from 'fs';
import path from 'path';
import { getClaudePath } from '../shared/settings.js';
import { execSync } from 'child_process';
interface ChangelogEntry {
version: string;
date: string;
type: 'Added' | 'Changed' | 'Fixed' | 'Removed' | 'Deprecated' | 'Security';
description: string;
timestamp: string;
generatedAt?: string; // When this changelog entry was created
}
interface MemorySearchResult {
version: string;
text: string;
metadata: any;
}
export async function changelog(options: OptionValues): Promise<void> {
try {
// Handle --update flag to regenerate CHANGELOG.md from JSONL
if (options.update) {
await updateChangelogFromJsonl(options);
return;
}
// Get current version and project name from package.json
const packageJsonPath = path.join(process.cwd(), 'package.json');
let currentVersion = 'unknown';
let projectName = 'unknown';
if (fs.existsSync(packageJsonPath)) {
try {
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
currentVersion = packageData.version || 'unknown';
projectName = packageData.name || path.basename(process.cwd());
} catch (e) {
projectName = path.basename(process.cwd());
}
}
// Calculate versions to search for based on flags
const versionsToSearch: string[] = [];
let historicalCount = options.historical || 1; // Default to current version only
// Handle --generate flag for specific version
if (options.generate) {
versionsToSearch.push(options.generate);
historicalCount = 1; // Single version mode
console.log(`🎯 Generating changelog for specific version: ${options.generate}`);
} else if (currentVersion !== 'unknown') {
// Normal mode: use current version or historical versions
const parts = currentVersion.split('.');
if (parts.length === 3) {
let major = parseInt(parts[0]);
let minor = parseInt(parts[1]);
let patch = parseInt(parts[2]);
for (let i = 0; i < historicalCount; i++) {
versionsToSearch.push(`${major}.${minor}.${patch}`);
// Decrement version
if (patch === 0) {
if (minor === 0) {
// Can't go lower than x.0.0
break;
}
minor--;
patch = 9;
} else {
patch--;
}
}
}
}
if (versionsToSearch.length === 0) {
console.log('⚠️ Could not determine versions to search. Please check package.json');
process.exit(1);
}
// Check if current version already has a changelog entry
const projectChangelogDir = path.join(
process.env.HOME || process.env.USERPROFILE || '',
'.claude-mem',
'projects'
);
const changelogJsonlPath = path.join(projectChangelogDir, `${projectName}-changelog.jsonl`);
let hasCurrentVersion = false;
if (fs.existsSync(changelogJsonlPath)) {
const existingLines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
for (const line of existingLines) {
try {
const entry = JSON.parse(line);
if (entry.version === currentVersion) {
hasCurrentVersion = true;
}
} catch (e) {
// Skip invalid lines
}
}
if (!options.historical && !options.generate && historicalCount === 1) {
if (hasCurrentVersion) {
console.log(`❌ Version ${currentVersion} already has changelog entries.`);
console.log('\n📝 Workflow:');
console.log(' 1. Make your code updates');
console.log(' 2. Build and test: bun run build');
console.log(' 3. Bump version: npm version patch');
console.log(' 4. Generate changelog: claude-mem changelog');
console.log(' 5. Commit and push\n');
console.log(`💡 Or use --historical 1 to regenerate this version's changelog`);
process.exit(1);
}
}
}
// Get npm publish times for all versions we need
let versionTimeRanges: Array<{version: string, startTime: string, endTime: string}> = [];
// Check if custom time range is provided
if (options.start && options.end) {
// Use custom time range for the specified version
const version = options.generate || currentVersion;
versionTimeRanges.push({
version,
startTime: options.start,
endTime: options.end
});
console.log(`📅 Using custom time range for ${version}:`);
console.log(` Start: ${new Date(options.start).toLocaleString()}`);
console.log(` End: ${new Date(options.end).toLocaleString()}`);
} else {
try {
const npmTimeData = execSync(`npm view ${projectName} time --json`, {
encoding: 'utf-8',
timeout: 5000
});
const publishTimes = JSON.parse(npmTimeData);
// For historical mode, we need one extra previous version to get proper time ranges
// E.g., for 3 versions, we need 4 timestamps to create 3 ranges
let extraPrevVersion = '';
if (historicalCount > 1) {
// Get the version before our oldest version in the search list
const oldestVersion = versionsToSearch[versionsToSearch.length - 1];
const parts = oldestVersion.split('.');
const major = parseInt(parts[0]);
const minor = parseInt(parts[1]);
const patch = parseInt(parts[2]);
if (patch > 0) {
extraPrevVersion = `${major}.${minor}.${patch - 1}`;
} else if (minor > 0) {
// Look for highest patch of previous minor
const prevMinorPrefix = `${major}.${minor - 1}.`;
const prevMinorVersions = Object.keys(publishTimes)
.filter(v => v.startsWith(prevMinorPrefix))
.sort((a, b) => {
const aPatch = parseInt(a.split('.')[2] || '0');
const bPatch = parseInt(b.split('.')[2] || '0');
return bPatch - aPatch;
});
if (prevMinorVersions.length > 0) {
extraPrevVersion = prevMinorVersions[0];
}
} else if (major > 0) {
// Look for highest version of previous major
const prevMajorPrefix = `${major - 1}.`;
const prevMajorVersions = Object.keys(publishTimes)
.filter(v => v.startsWith(prevMajorPrefix))
.sort((a, b) => {
const [, aMinor, aPatch] = a.split('.').map(Number);
const [, bMinor, bPatch] = b.split('.').map(Number);
if (aMinor !== bMinor) return bMinor - aMinor;
return bPatch - aPatch;
});
if (prevMajorVersions.length > 0) {
extraPrevVersion = prevMajorVersions[0];
}
}
if (options.verbose && extraPrevVersion && publishTimes[extraPrevVersion]) {
console.log(`📍 Using ${extraPrevVersion} as start boundary for time ranges`);
}
}
// Build time ranges for each version
for (let i = 0; i < versionsToSearch.length; i++) {
const version = versionsToSearch[i];
// Start time:
// - For the first (newest) version, use the publish time of the version before it
// - For middle versions, use the publish time of the next version in our list
// - For the last (oldest) version, use the extra previous version we found
let startTime = '2000-01-01T00:00:00Z'; // Default to old date
if (i === 0) {
// First (newest) version - find its immediate predecessor
const versionParts = version.split('.');
const major = parseInt(versionParts[0]);
const minor = parseInt(versionParts[1]);
const patch = parseInt(versionParts[2]);
let prevVersion = '';
if (patch > 0) {
prevVersion = `${major}.${minor}.${patch - 1}`;
} else if (minor > 0) {
// Look for highest patch of previous minor
const prevMinorPrefix = `${major}.${minor - 1}.`;
const prevMinorVersions = Object.keys(publishTimes)
.filter(v => v.startsWith(prevMinorPrefix))
.sort((a, b) => {
const aPatch = parseInt(a.split('.')[2] || '0');
const bPatch = parseInt(b.split('.')[2] || '0');
return bPatch - aPatch;
});
if (prevMinorVersions.length > 0) {
prevVersion = prevMinorVersions[0];
}
}
if (publishTimes[prevVersion]) {
startTime = publishTimes[prevVersion];
}
} else if (i < versionsToSearch.length - 1) {
// Middle versions - use the next version in our list
const prevVersionInList = versionsToSearch[i + 1];
if (publishTimes[prevVersionInList]) {
startTime = publishTimes[prevVersionInList];
}
} else {
// Last (oldest) version - use the extra previous version
if (extraPrevVersion && publishTimes[extraPrevVersion]) {
startTime = publishTimes[extraPrevVersion];
}
}
// End time is this version's publish time (or now for unreleased)
let endTime = publishTimes[version] || new Date().toISOString();
versionTimeRanges.push({ version, startTime, endTime });
if (options.verbose) {
console.log(`📅 Version ${version}: ${new Date(startTime).toLocaleString()} - ${new Date(endTime).toLocaleString()}`);
}
}
// Always log what we're doing for single version
if (historicalCount === 1) {
const latestRange = versionTimeRanges[0];
if (latestRange) {
console.log(`📦 Using npm time range for ${latestRange.version}: ${new Date(latestRange.startTime).toLocaleString()} - ${new Date(latestRange.endTime).toLocaleString()}`);
}
}
} catch (e) {
console.log('❌ Could not fetch npm publish times. Cannot proceed without time ranges.');
process.exit(1);
}
}
console.log(`🔍 Searching memories for versions: ${versionsToSearch.join(', ')}`);
console.log(`📦 Project: ${projectName}\n`);
// Phase 1: Search for version-related memories using MCP tools
// ALWAYS use time range search - no other method
const searchPrompt = versionTimeRanges.length > 0 ?
`You are helping generate a changelog by searching for memories within specific time ranges for multiple versions.
PROJECT: ${projectName}
VERSION TIME RANGES:
${versionTimeRanges.map(r => `- Version ${r.version}: ${new Date(r.startTime).toLocaleDateString()} to ${new Date(r.endTime).toLocaleDateString()}`).join('\n')}
YOUR TASK:
Use mcp__claude-mem__chroma_query_documents to search for memories for each version time range.
SEARCH STRATEGY:
${versionTimeRanges.map(r => {
const startDate = new Date(r.startTime);
const endDate = new Date(r.endTime);
// Generate all date prefixes between start and end
const datePrefixes: string[] = [];
const currentDate = new Date(startDate);
while (currentDate <= endDate) {
// Add day prefix like "2025-09-09"
const dayPrefix = currentDate.toISOString().split('T')[0];
datePrefixes.push(dayPrefix);
currentDate.setDate(currentDate.getDate() + 1);
}
return `
Version ${r.version} (${new Date(r.startTime).toLocaleDateString()} to ${new Date(r.endTime).toLocaleDateString()}):
1. Search for memories from these dates: ${datePrefixes.join(', ')}
2. Make multiple calls to mcp__claude-mem__chroma_query_documents:
- collection_name: "claude_memories"
- query_texts: Include the project name AND date in each query:
* "${projectName} ${datePrefixes[0]} feature"
* "${projectName} ${datePrefixes[0]} fix"
* "${projectName} ${datePrefixes[0]} change"
* "${projectName} ${datePrefixes[0]} improvement"
* "${projectName} ${datePrefixes[0]} refactor"
- n_results: 50
3. The date in the query text helps semantic search find memories from that day
4. Assign memories to this version if their timestamp falls within:
- Start: ${r.startTime}
- End: ${r.endTime}`;
}).join('\n')}
IMPORTANT:
- Always include project name and date in query_texts for best results
- Semantic search will naturally find memories near those dates
- Group returned memories by version based on their timestamp metadata
Return a JSON object with this structure:
{
"memories": [
{
"version": "version_number",
"text": "memory content",
"metadata": {metadata object with timestamp},
"relevance": "high/medium/low"
}
]
}
Group memories by the version they belong to based on timestamp.
Start searching now.` :
`ERROR: No time ranges available. This should never happen.`;
if (versionTimeRanges.length === 0) {
console.log('❌ No time ranges available. Cannot search memories.');
process.exit(1);
}
if (options.verbose) {
console.log('📝 Calling Claude to search memories...');
}
// Call Claude with MCP tools to search memories
const searchResponse = await query({
prompt: searchPrompt,
options: {
allowedTools: [
'mcp__claude-mem__chroma_query_documents',
'mcp__claude-mem__chroma_get_documents'
],
pathToClaudeCodeExecutable: getClaudePath()
}
});
// Extract memories from response
let memoriesJson = '';
if (searchResponse && typeof searchResponse === 'object' && Symbol.asyncIterator in searchResponse) {
for await (const message of searchResponse) {
if (message?.type === 'assistant' && message?.message?.content) {
const content = message.message.content;
if (typeof content === 'string') {
memoriesJson += content;
} else if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text' && block.text) {
memoriesJson += block.text;
}
}
}
}
}
}
// Parse memories
let memories: MemorySearchResult[] = [];
try {
// Extract JSON from response (might be wrapped in markdown)
const jsonMatch = memoriesJson.match(/```json\n([\s\S]*?)\n```/) ||
memoriesJson.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
if (parsed.memories && Array.isArray(parsed.memories)) {
memories = parsed.memories;
}
}
} catch (e) {
console.error('⚠️ Could not parse memory search results:', e);
}
if (memories.length === 0) {
console.log('\n⚠️ No version-related memories found for this version.');
console.log(' This is normal for the first release or when no changes were tracked.');
console.log(' Creating a placeholder changelog entry...');
// Create a minimal placeholder entry
const placeholderEntry: ChangelogEntry = {
version: versionsToSearch[0], // Use the first (current) version
date: todayStr,
type: 'Changed',
description: 'Initial release or minor updates',
timestamp: new Date().toISOString(),
generatedAt: new Date().toISOString()
};
// Save the placeholder entry
if (!fs.existsSync(projectChangelogDir)) {
fs.mkdirSync(projectChangelogDir, { recursive: true });
}
const jsonlContent = JSON.stringify(placeholderEntry) + '\n';
fs.appendFileSync(changelogJsonlPath, jsonlContent);
console.log(`✅ Created placeholder changelog entry for v${versionsToSearch[0]}`);
// Generate the CHANGELOG.md with the placeholder
await updateChangelogFromJsonl(options);
return; // Exit successfully
}
console.log(`✅ Found ${memories.length} version-related memories\n`);
// Get system date for accuracy
const systemDate = execSync('date "+%Y-%m-%d %H:%M:%S %Z"').toString().trim();
const todayStr = systemDate.split(' ')[0]; // YYYY-MM-DD format
// Phase 2: Generate changelog entries from memories
const changelogPrompt = `Analyze these memories and generate changelog entries.
PROJECT: ${projectName}
DATE: ${todayStr}
MEMORIES BY VERSION:
${versionsToSearch.map(version => {
const versionMemories = memories.filter(m => m.version === version);
if (versionMemories.length === 0) return `### Version ${version}\nNo memories found.`;
return `### Version ${version} (${versionMemories.length} memories):
${versionMemories.map((m, i) => `${i + 1}. ${m.text}`).join('\n')}`;
}).join('\n\n')}
INSTRUCTIONS:
1. Extract concrete changes, fixes, and additions from the memories
2. Categorize each change as: Added, Changed, Fixed, Removed, Deprecated, or Security
3. Write clear, user-facing descriptions
4. Start each entry with an action verb
5. Focus on what matters to users, not internal implementation details
Return ONLY a JSON array with this structure:
[
{
"version": "3.6.1",
"type": "Added",
"description": "New feature description"
},
{
"version": "3.6.1",
"type": "Fixed",
"description": "Bug fix description"
}
]`;
console.log('🔄 Generating changelog entries...');
// Call Claude to generate changelog entries
const changelogResponse = await query({
prompt: changelogPrompt,
options: {
allowedTools: [],
pathToClaudeCodeExecutable: getClaudePath()
}
});
// Extract JSON from response
let entriesJson = '';
if (changelogResponse && typeof changelogResponse === 'object' && Symbol.asyncIterator in changelogResponse) {
for await (const message of changelogResponse) {
if (message?.type === 'assistant' && message?.message?.content) {
const content = message.message.content;
if (typeof content === 'string') {
entriesJson += content;
} else if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text' && block.text) {
entriesJson += block.text;
}
}
}
}
}
}
// Parse changelog entries
let entries: ChangelogEntry[] = [];
try {
// Extract JSON (might be wrapped in markdown)
const jsonMatch = entriesJson.match(/```json\n([\s\S]*?)\n```/) ||
entriesJson.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
if (Array.isArray(parsed)) {
const generatedAt = new Date().toISOString();
entries = parsed.map(e => ({
...e,
date: todayStr,
timestamp: e.timestamp || generatedAt, // Memory timestamp if available
generatedAt: generatedAt // When this changelog was generated
}));
}
}
} catch (e) {
console.error('⚠️ Could not parse changelog entries:', e);
}
if (entries.length === 0) {
console.log('⚠️ No changelog entries generated.');
process.exit(1);
}
// Ensure project changelog directory exists
if (!fs.existsSync(projectChangelogDir)) {
fs.mkdirSync(projectChangelogDir, { recursive: true });
}
// Save entries to project JSONL file
console.log(`\n💾 Saving ${entries.length} changelog entries to ${path.basename(changelogJsonlPath)}`);
// When using --historical or --generate, remove old entries for the versions being regenerated
if ((options.historical && historicalCount > 1) || options.generate) {
let existingEntries: ChangelogEntry[] = [];
if (fs.existsSync(changelogJsonlPath)) {
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
for (const line of lines) {
try {
const entry = JSON.parse(line);
// Keep entries that are NOT in the versions we're regenerating
if (!versionsToSearch.includes(entry.version)) {
existingEntries.push(entry);
}
} catch (e) {
// Skip invalid lines
}
}
}
// Rewrite the file with filtered entries plus new ones
const allEntries = [...existingEntries, ...entries];
const jsonlContent = allEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
fs.writeFileSync(changelogJsonlPath, jsonlContent);
console.log(`🔄 Regenerated entries for versions: ${versionsToSearch.join(', ')}`);
} else {
// Append new entries to JSONL
const jsonlContent = entries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
fs.appendFileSync(changelogJsonlPath, jsonlContent);
}
// Now generate markdown from all JSONL entries
console.log('\n📝 Generating CHANGELOG.md from entries...');
// Read all entries from JSONL
let allEntries: ChangelogEntry[] = [];
if (fs.existsSync(changelogJsonlPath)) {
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
for (const line of lines) {
try {
allEntries.push(JSON.parse(line));
} catch (e) {
// Skip invalid lines
}
}
}
// Group entries by version
const entriesByVersion = new Map<string, ChangelogEntry[]>();
for (const entry of allEntries) {
if (!entriesByVersion.has(entry.version)) {
entriesByVersion.set(entry.version, []);
}
entriesByVersion.get(entry.version)!.push(entry);
}
// Generate markdown
let markdown = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n';
// Sort versions in descending order
const sortedVersions = Array.from(entriesByVersion.keys()).sort((a, b) => {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
}
return 0;
});
for (const version of sortedVersions) {
const versionEntries = entriesByVersion.get(version)!;
const date = versionEntries[0].date || todayStr;
markdown += `\n## [${version}] - ${date}\n\n`;
// Group by type
const types: Array<ChangelogEntry['type']> = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
for (const type of types) {
const typeEntries = versionEntries.filter(e => e.type === type);
if (typeEntries.length > 0) {
markdown += `### ${type}\n`;
for (const entry of typeEntries) {
markdown += `- ${entry.description}\n`;
}
markdown += '\n';
}
}
}
// Write the CHANGELOG.md
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
fs.writeFileSync(changelogPath, markdown);
console.log(`✅ Generated CHANGELOG.md with ${allEntries.length} total entries across ${entriesByVersion.size} versions!`);
if (options.preview) {
console.log('\n📄 Preview:\n');
console.log(markdown.split('\n').slice(0, 30).join('\n'));
if (markdown.split('\n').length > 30) {
console.log('\n... (truncated for preview)');
}
}
} catch (error) {
console.error('❌ Error generating changelog:', error instanceof Error ? error.message : error);
if (error instanceof Error && error.stack) {
console.error('Stack:', error.stack);
}
process.exit(1);
}
}
async function updateChangelogFromJsonl(options: OptionValues): Promise<void> {
try {
// Get project name from package.json
const packageJsonPath = path.join(process.cwd(), 'package.json');
let projectName = 'unknown';
if (fs.existsSync(packageJsonPath)) {
try {
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
projectName = packageData.name || path.basename(process.cwd());
} catch (e) {
projectName = path.basename(process.cwd());
}
}
const projectChangelogDir = path.join(
process.env.HOME || process.env.USERPROFILE || '',
'.claude-mem',
'projects'
);
const changelogJsonlPath = path.join(projectChangelogDir, `${projectName}-changelog.jsonl`);
if (!fs.existsSync(changelogJsonlPath)) {
console.log('❌ No changelog entries found. Generate some first with: claude-mem changelog');
process.exit(1);
}
console.log('📝 Updating CHANGELOG.md from JSONL entries...');
// Read all entries from JSONL
let allEntries: ChangelogEntry[] = [];
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
for (const line of lines) {
try {
allEntries.push(JSON.parse(line));
} catch (e) {
// Skip invalid lines
}
}
if (allEntries.length === 0) {
console.log('❌ No valid entries found in JSONL file');
process.exit(1);
}
// Group entries by version
const entriesByVersion = new Map<string, ChangelogEntry[]>();
for (const entry of allEntries) {
if (!entriesByVersion.has(entry.version)) {
entriesByVersion.set(entry.version, []);
}
entriesByVersion.get(entry.version)!.push(entry);
}
// Generate markdown
let markdown = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n';
// Sort versions in descending order
const sortedVersions = Array.from(entriesByVersion.keys()).sort((a, b) => {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
}
return 0;
});
for (const version of sortedVersions) {
const versionEntries = entriesByVersion.get(version)!;
const date = versionEntries[0].date;
markdown += `\n## [${version}] - ${date}\n\n`;
// Group by type
const types: Array<ChangelogEntry['type']> = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
for (const type of types) {
const typeEntries = versionEntries.filter(e => e.type === type);
if (typeEntries.length > 0) {
markdown += `### ${type}\n`;
for (const entry of typeEntries) {
markdown += `- ${entry.description}\n`;
}
markdown += '\n';
}
}
}
// Write the CHANGELOG.md
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
fs.writeFileSync(changelogPath, markdown);
console.log(`✅ Updated CHANGELOG.md with ${allEntries.length} entries across ${entriesByVersion.size} versions!`);
if (options.preview) {
console.log('\n📄 Preview:\n');
console.log(markdown.split('\n').slice(0, 30).join('\n'));
if (markdown.split('\n').length > 30) {
console.log('\n... (truncated for preview)');
}
}
} catch (error) {
console.error('❌ Error updating changelog:', error instanceof Error ? error.message : error);
process.exit(1);
}
}
-181
View File
@@ -1,181 +0,0 @@
import { OptionValues } from 'commander';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { getClaudePath } from '../shared/settings.js';
import { DatabaseManager } from '../services/sqlite/Database.js';
import { StreamingSessionStore } from '../services/sqlite/StreamingSessionStore.js';
import { migrations } from '../services/sqlite/migrations.js';
/**
* Generate a session title and subtitle from a user prompt
* CLI command that uses Agent SDK (like changelog.ts)
*
* Can be called in two modes:
* 1. Standalone: generate-title "user prompt" --json
* 2. With session: generate-title "user prompt" --session-id <id> --save
*/
export async function generateTitle(prompt: string, options: OptionValues): Promise<void> {
if (!prompt || prompt.trim().length === 0) {
console.error(JSON.stringify({
success: false,
error: 'Prompt is required'
}));
process.exit(1);
}
// If --session-id provided, validate that session exists
let streamingStore: StreamingSessionStore | null = null;
let sessionRecord = null;
if (options.sessionId) {
try {
const dbManager = DatabaseManager.getInstance();
for (const migration of migrations) {
dbManager.registerMigration(migration);
}
const db = await dbManager.initialize();
streamingStore = new StreamingSessionStore(db);
sessionRecord = streamingStore.getByClaudeSessionId(options.sessionId);
if (!sessionRecord) {
console.error(JSON.stringify({
success: false,
error: `Session not found: ${options.sessionId}`
}));
process.exit(1);
}
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: `Database error: ${error.message}`
}));
process.exit(1);
}
}
const systemPrompt = `You are a title and subtitle generator for claude-mem session metadata.
Your job is to analyze a user's request and generate:
1. A concise title (3-8 words)
2. A one-sentence subtitle (max 20 words)
TITLE GUIDELINES:
- 3-8 words maximum
- Scannable and clear
- Captures the core action or topic
- Professional and informative
- Examples:
* "Dark Mode Implementation"
* "Authentication Bug Fix"
* "API Rate Limiting Setup"
* "React Component Refactoring"
SUBTITLE GUIDELINES:
- One sentence, max 20 words
- Descriptive and specific
- Focus on the outcome or benefit
- Use active voice when possible
- Examples:
* "Adding theme toggle and dark color scheme support to the application"
* "Resolving login timeout issue affecting user session persistence"
* "Implementing request throttling to prevent API quota exhaustion"
OUTPUT FORMAT:
You must output EXACTLY two lines:
Line 1: Title only (no prefix, no quotes)
Line 2: Subtitle only (no prefix, no quotes)
EXAMPLE:
User request: "Help me add dark mode to my app"
Output:
Dark Mode Implementation
Adding theme toggle and dark color scheme support to the application
USER REQUEST:
${prompt}
Now generate the title and subtitle (two lines exactly):`;
try {
const response = await query({
prompt: systemPrompt,
options: {
allowedTools: [],
pathToClaudeCodeExecutable: getClaudePath()
}
});
// Extract text from response (same pattern as changelog.ts)
let fullResponse = '';
if (response && typeof response === 'object' && Symbol.asyncIterator in response) {
for await (const message of response) {
if (message?.type === 'assistant' && message?.message?.content) {
const content = message.message.content;
if (typeof content === 'string') {
fullResponse += content;
} else if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text' && block.text) {
fullResponse += block.text;
}
}
}
}
}
}
// Parse the response - expecting exactly 2 lines
const lines = fullResponse.trim().split('\n').filter(line => line.trim().length > 0);
if (lines.length < 2) {
console.error(JSON.stringify({
success: false,
error: 'Could not generate title and subtitle',
response: fullResponse
}));
process.exit(1);
}
const title = lines[0].trim();
const subtitle = lines[1].trim();
// If --save and we have a session, update the database
if (options.save && streamingStore && sessionRecord) {
try {
streamingStore.update(sessionRecord.id, {
title,
subtitle
});
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: `Failed to save title: ${error.message}`
}));
process.exit(1);
}
}
// Output format depends on options
if (options.json) {
console.log(JSON.stringify({
success: true,
title,
subtitle,
sessionId: sessionRecord?.claude_session_id
}, null, 2));
} else if (options.oneline) {
console.log(`${title} - ${subtitle}`);
} else {
console.log(title);
console.log(subtitle);
}
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: error.message || 'Unknown error generating title'
}));
process.exit(1);
}
}
-461
View File
@@ -1,461 +0,0 @@
import { OptionValues } from 'commander';
import fs from 'fs';
import { join } from 'path';
import { PathDiscovery } from '../services/path-discovery.js';
import {
createCompletionMessage,
createContextualError,
createUserFriendlyError,
formatTimeAgo,
outputSessionStartContent
} from '../prompts/templates/context/ContextTemplates.js';
import { getStorageProvider, needsMigration } from '../shared/storage.js';
import { MemoryRow, OverviewRow } from '../services/sqlite/types.js';
import { createStores } from '../services/sqlite/index.js';
import { getRollingSettings } from '../shared/rolling-settings.js';
import { rollingLog } from '../shared/rolling-log.js';
interface TrashStatus {
folderCount: number;
fileCount: number;
totalSize: number;
isEmpty: boolean;
}
function formatDateHeader(date = new Date()): string {
return date.toLocaleString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
});
}
function wordWrap(text: string, maxWidth: number, prefix: string): string {
const words = text.split(' ');
const lines: string[] = [];
let currentLine = prefix;
const continuationPrefix = ' '.repeat(prefix.length);
for (const word of words) {
const needsSpace = currentLine !== prefix && currentLine !== continuationPrefix;
const testLine = currentLine + (needsSpace ? ' ' : '') + word;
if (testLine.length <= maxWidth) {
currentLine = testLine;
} else {
if (currentLine.trim()) {
lines.push(currentLine);
}
currentLine = continuationPrefix + word;
}
}
if (currentLine.trim()) {
lines.push(currentLine);
}
return lines.join('\n');
}
function buildProjectMatcher(projectName: string): (value?: string) => boolean {
const aliases = new Set<string>();
aliases.add(projectName);
aliases.add(projectName.replace(/-/g, '_'));
aliases.add(projectName.replace(/_/g, '-'));
return (value?: string) => !!value && aliases.has(value);
}
function formatSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function getTrashStatus(): TrashStatus {
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
if (!fs.existsSync(trashDir)) {
return { folderCount: 0, fileCount: 0, totalSize: 0, isEmpty: true };
}
const items = fs.readdirSync(trashDir);
if (items.length === 0) {
return { folderCount: 0, fileCount: 0, totalSize: 0, isEmpty: true };
}
let folderCount = 0;
let fileCount = 0;
let totalSize = 0;
for (const item of items) {
const itemPath = join(trashDir, item);
const stats = fs.statSync(itemPath);
if (stats.isDirectory()) {
folderCount++;
} else {
fileCount++;
}
totalSize += stats.size;
}
return { folderCount, fileCount, totalSize, isEmpty: false };
}
async function renderRollingSessionStart(projectOverride?: string): Promise<void> {
const settings = getRollingSettings();
if (!settings.sessionStartEnabled) {
console.log('Rolling session-start output disabled in settings.');
rollingLog('info', 'session-start output skipped (disabled)', {
project: projectOverride
});
return;
}
const stores = await createStores();
const projectName = projectOverride || PathDiscovery.getCurrentProjectName();
// Get all overviews for this project (oldest to newest)
const allOverviews = stores.overviews.getAllForProject(projectName);
// Limit to last 10 overviews
const recentOverviews = allOverviews.slice(-10);
// If no data at all, show friendly message
if (recentOverviews.length === 0) {
console.log('===============================================================================');
console.log(`What's new | ${formatDateHeader()}`);
console.log('===============================================================================');
console.log('No previous sessions found for this project.');
console.log('Start working and claude-mem will automatically capture context for future sessions.');
console.log('===============================================================================');
const trashStatus = getTrashStatus();
if (!trashStatus.isEmpty) {
const formattedSize = formatSize(trashStatus.totalSize);
console.log(
`🗑️ Trash ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} use \`claude-mem restore\``
);
console.log('===============================================================================');
}
return;
}
// Output header
console.log('===============================================================================');
console.log(`What's new | ${formatDateHeader()}`);
console.log('===============================================================================');
// Output each overview with timestamp, memory names, and files touched (oldest to newest)
recentOverviews.forEach((overview) => {
const date = new Date(overview.created_at);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, '0');
const ampm = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
console.log(`[${year}-${month}-${day} at ${displayHours}:${minutes} ${ampm}]`);
// Get memories for this session to show titles, subtitles, files, and keywords
const sessionMemories = stores.memories.getBySessionId(overview.session_id);
// Extract memory titles and subtitles
const memories = sessionMemories
.map(m => ({ title: m.title, subtitle: m.subtitle }))
.filter(m => m.title);
// Extract unique files touched across all memories
const allFilesTouched = new Set<string>();
const allKeywords = new Set<string>();
sessionMemories.forEach(m => {
if (m.files_touched) {
try {
const files = JSON.parse(m.files_touched);
if (Array.isArray(files)) {
files.forEach(f => allFilesTouched.add(f));
}
} catch (e) {
// Skip invalid JSON
}
}
if (m.keywords) {
// Keywords are comma-separated
m.keywords.split(',').forEach(k => allKeywords.add(k.trim()));
}
});
console.log('');
// Always show overview content
console.log(wordWrap(overview.content, 80, ''));
// Display files touched if any
if (allFilesTouched.size > 0) {
console.log('');
console.log(wordWrap(`- ${Array.from(allFilesTouched).join(', ')}`, 80, ''));
}
// Display keywords/tags if any
if (allKeywords.size > 0) {
console.log('');
console.log(wordWrap(`Tags: ${Array.from(allKeywords).join(', ')}`, 80, ''));
}
console.log('');
});
console.log('===============================================================================');
const trashStatus = getTrashStatus();
if (!trashStatus.isEmpty) {
const formattedSize = formatSize(trashStatus.totalSize);
console.log(
`🗑️ Trash ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} use \`claude-mem restore\``
);
console.log('===============================================================================');
}
}
export async function loadContext(options: OptionValues = {}): Promise<void> {
try {
// Check if migration is needed and warn the user
if (await needsMigration()) {
console.warn('⚠️ JSONL to SQLite migration recommended. Run: claude-mem migrate-index');
}
const storage = await getStorageProvider();
// If using JSONL fallback, use original implementation
if (storage.backend === 'jsonl') {
return await loadContextFromJSONL(options);
}
// SQLite implementation - fetch data using storage provider
let recentMemories: MemoryRow[] = [];
let recentOverviews: OverviewRow[] = [];
// Auto-detect current project for session-start format if no project specified
let projectToUse = options.project;
if (!projectToUse && options.format === 'session-start') {
projectToUse = PathDiscovery.getCurrentProjectName();
}
if (options.format === 'session-start') {
await renderRollingSessionStart(projectToUse);
return;
}
const overviewLimit = options.format === 'json' ? 5 : 3;
if (projectToUse) {
recentMemories = await storage.getRecentMemoriesForProject(projectToUse, 10);
recentOverviews = await storage.getRecentOverviewsForProject(projectToUse, overviewLimit);
} else {
recentMemories = await storage.getRecentMemories(10);
recentOverviews = await storage.getRecentOverviews(overviewLimit);
}
// Convert SQLite rows to JSONL format for compatibility with existing output functions
const memoriesAsJSON = recentMemories.map(row => ({
type: 'memory',
text: row.text,
document_id: row.document_id,
keywords: row.keywords,
session_id: row.session_id,
project: row.project,
timestamp: row.created_at,
archive: row.archive_basename
}));
const overviewsAsJSON = recentOverviews.map(row => ({
type: 'overview',
content: row.content,
session_id: row.session_id,
project: row.project,
timestamp: row.created_at
}));
// If no data found, show appropriate messages
if (memoriesAsJSON.length === 0 && overviewsAsJSON.length === 0) {
return;
}
if (options.format === 'json') {
// For JSON format, combine last 10 of each type
const recentObjects = [...memoriesAsJSON, ...overviewsAsJSON];
console.log(JSON.stringify(recentObjects));
} else {
// Default format - show last 10 memories and last 3 overviews
const totalCount = memoriesAsJSON.length + overviewsAsJSON.length;
console.log(createCompletionMessage('Context loading', totalCount, 'recent entries found'));
// Show memories first
memoriesAsJSON.forEach((obj) => {
console.log(`${obj.text} | ${obj.document_id} | ${obj.keywords}`);
});
// Then show overviews
overviewsAsJSON.forEach((obj) => {
console.log(`**Overview:** ${obj.content}`);
});
}
// Display trash status if not empty (except for JSON format to avoid breaking JSON parsing)
if (options.format !== 'json') {
const trashStatus = getTrashStatus();
if (!trashStatus.isEmpty) {
const formattedSize = formatSize(trashStatus.totalSize);
console.log(`🗑️ Trash ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} use \`claude-mem restore\``);
console.log('');
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (options.format === 'session-start') {
console.log(createContextualError('CONNECTION_FAILED', errorMessage));
} else {
console.log(createUserFriendlyError('Context loading', errorMessage, 'Check file permissions and try again'));
}
}
}
/**
* Original JSONL-based implementation for fallback compatibility
*/
async function loadContextFromJSONL(options: OptionValues = {}): Promise<void> {
const pathDiscovery = PathDiscovery.getInstance();
const indexPath = pathDiscovery.getIndexPath();
// Auto-detect current project for session-start format if no project specified
let projectToUse = options.project;
if (!projectToUse && options.format === 'session-start') {
projectToUse = PathDiscovery.getCurrentProjectName();
}
// Check if index file exists
if (!fs.existsSync(indexPath)) {
if (options.format === 'session-start') {
console.log(createContextualError('NO_MEMORIES', projectToUse || 'this project'));
}
return;
}
const content = fs.readFileSync(indexPath, 'utf-8');
const lines = content.trim().split('\n').filter(line => line.trim());
if (lines.length === 0) {
if (options.format === 'session-start') {
console.log(createContextualError('NO_MEMORIES', projectToUse || 'this project'));
}
return;
}
// Parse JSONL format - each line is a JSON object
const jsonObjects: any[] = [];
for (const line of lines) {
try {
// Skip lines that don't look like JSON (could be legacy format)
if (!line.trim().startsWith('{')) {
continue;
}
const obj = JSON.parse(line);
jsonObjects.push(obj);
} catch (e) {
// Skip malformed JSON lines
continue;
}
}
if (jsonObjects.length === 0) {
if (options.format === 'session-start') {
console.log(createContextualError('NO_MEMORIES', projectToUse || 'this project'));
}
return;
}
// Separate memories, overviews, and other types
const memories = jsonObjects.filter(obj => obj.type === 'memory');
const overviews = jsonObjects.filter(obj => obj.type === 'overview');
const sessions = jsonObjects.filter(obj => obj.type === 'session');
// Filter each type by project if specified
// Handle both hyphen and underscore formats since index has mixed entries
let filteredMemories = memories;
let filteredOverviews = overviews;
let filteredSessions = sessions;
if (projectToUse) {
const matchesProject = buildProjectMatcher(projectToUse);
filteredMemories = memories.filter(obj => matchesProject(obj.project));
filteredOverviews = overviews.filter(obj => matchesProject(obj.project));
filteredSessions = sessions.filter(obj => matchesProject(obj.project));
}
if (options.format === 'session-start') {
// Get last 10 memories and last 10 overviews for session-start
const recentMemories = filteredMemories.slice(-10);
const recentOverviews = filteredOverviews.slice(-10);
const recentSessions = filteredSessions.slice(-10);
// Combine them for the display
const recentObjects = [...recentSessions, ...recentMemories, ...recentOverviews];
// Find most recent timestamp for last session info
let lastSessionTime = 'recently';
const timestamps = recentObjects
.map(obj => {
// Get timestamp from JSON object
return obj.timestamp ? new Date(obj.timestamp) : null;
})
.filter(date => date !== null)
.sort((a, b) => b.getTime() - a.getTime());
if (timestamps.length > 0) {
lastSessionTime = formatTimeAgo(timestamps[0]);
}
// Use dual-stream output for session start formatting
outputSessionStartContent({
projectName: projectToUse || 'your project',
memoryCount: recentMemories.length,
lastSessionTime,
recentObjects
});
} else if (options.format === 'json') {
// For JSON format, combine last 10 of each type
const recentMemories = filteredMemories.slice(-10);
const recentOverviews = filteredOverviews.slice(-3);
const recentObjects = [...recentMemories, ...recentOverviews];
console.log(JSON.stringify(recentObjects));
} else {
// Default format - show last 10 memories and last 3 overviews
const recentMemories = filteredMemories.slice(-10);
const recentOverviews = filteredOverviews.slice(-3);
const totalCount = recentMemories.length + recentOverviews.length;
console.log(createCompletionMessage('Context loading', totalCount, 'recent entries found'));
// Show memories first
recentMemories.forEach((obj) => {
console.log(`${obj.text} | ${obj.document_id} | ${obj.keywords}`);
});
// Then show overviews
recentOverviews.forEach((obj) => {
console.log(`**Overview:** ${obj.content}`);
});
}
}
-154
View File
@@ -1,154 +0,0 @@
import { OptionValues } from 'commander';
import { spawnSync } from 'child_process';
import { createStores } from '../services/sqlite/index.js';
/**
* Store a memory to all three storage layers
* Called by SDK via bash during streaming memory capture
*/
export async function storeMemory(options: OptionValues): Promise<void> {
const { id, project, session, date, title, subtitle, facts, concepts, files } = options;
// Validate required fields
if (!id || !project || !session || !date) {
console.error('Error: All fields required: --id, --project, --session, --date');
process.exit(1);
}
// Validate hierarchical fields (required for v2 format)
if (!title || !subtitle || !facts) {
console.error('Error: Hierarchical format required: --title, --subtitle, --facts');
process.exit(1);
}
try {
const stores = await createStores();
const timestamp = new Date().toISOString();
// Ensure session exists
const sessionExists = await stores.sessions.has(session);
if (!sessionExists) {
await stores.sessions.create({
session_id: session,
project,
created_at: timestamp,
source: 'save'
});
}
// Parse JSON arrays if provided as strings
let factsArray: string | undefined;
let conceptsArray: string | undefined;
let filesArray: string | undefined;
try {
factsArray = facts ? JSON.stringify(JSON.parse(facts)) : undefined;
} catch (e) {
factsArray = facts; // Store as-is if not valid JSON
}
try {
conceptsArray = concepts ? JSON.stringify(JSON.parse(concepts)) : undefined;
} catch (e) {
conceptsArray = concepts; // Store as-is if not valid JSON
}
try {
filesArray = files ? JSON.stringify(JSON.parse(files)) : undefined;
} catch (e) {
filesArray = files; // Store as-is if not valid JSON
}
// Layer 1: SQLite Memory Index
const memoryExists = stores.memories.hasDocumentId(id);
if (!memoryExists) {
stores.memories.create({
document_id: id,
text: '', // Deprecated: hierarchical fields replace narrative text
keywords: '',
session_id: session,
project,
created_at: timestamp,
origin: 'streaming-sdk',
// Hierarchical fields (v2)
title: title || undefined,
subtitle: subtitle || undefined,
facts: factsArray,
concepts: conceptsArray,
files_touched: filesArray
});
}
// Layer 2: ChromaDB - Store hierarchical memory
if (factsArray) {
const factsJson = JSON.parse(factsArray);
const conceptsJson = conceptsArray ? JSON.parse(conceptsArray) : [];
const filesJson = filesArray ? JSON.parse(filesArray) : [];
// Store each atomic fact as a separate ChromaDB document
factsJson.forEach((fact: string, idx: number) => {
spawnSync('claude-mem', [
'chroma_add_documents',
'--collection_name', 'claude_memories',
'--documents', JSON.stringify([fact]),
'--ids', JSON.stringify([`${id}_fact_${String(idx).padStart(3, '0')}`]),
'--metadatas', JSON.stringify([{
type: 'fact',
parent_id: id,
fact_index: idx,
title,
subtitle,
project,
session_id: session,
created_at: timestamp,
created_at_epoch: Date.parse(timestamp),
keywords: '',
concepts: JSON.stringify(conceptsJson),
files_touched: JSON.stringify(filesJson),
origin: 'streaming-sdk'
}])
]);
});
// Store full narrative with hierarchical metadata
spawnSync('claude-mem', [
'chroma_add_documents',
'--collection_name', 'claude_memories',
'--documents', JSON.stringify([`${title}\n${subtitle}\n\n${factsJson.join('\n')}`]),
'--ids', JSON.stringify([id]),
'--metadatas', JSON.stringify([{
type: 'narrative',
title,
subtitle,
facts_count: factsJson.length,
project,
session_id: session,
created_at: timestamp,
created_at_epoch: Date.parse(timestamp),
keywords: '',
concepts: JSON.stringify(conceptsJson),
files_touched: JSON.stringify(filesJson),
origin: 'streaming-sdk'
}])
]);
}
// Success output (SDK will see this)
console.log(JSON.stringify({
success: true,
memory_id: id,
project,
session,
date,
timestamp,
hierarchical: !!(title && subtitle && facts)
}));
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: error.message || 'Unknown error storing memory'
}));
process.exit(1);
}
}
-45
View File
@@ -1,45 +0,0 @@
import { OptionValues } from 'commander';
import { createStores } from '../services/sqlite/index.js';
/**
* Store a session overview
* Called by SDK via bash at session end
*/
export async function storeOverview(options: OptionValues): Promise<void> {
const { project, session, content } = options;
// Validate required fields
if (!project || !session || !content) {
console.error('Error: All fields required: --project, --session, --content');
process.exit(1);
}
try {
const stores = await createStores();
const timestamp = new Date().toISOString();
// Create one overview per session (rolling log architecture)
stores.overviews.upsert({
session_id: session,
content,
created_at: timestamp,
project,
origin: 'streaming-sdk'
});
// Success output (SDK will see this)
console.log(JSON.stringify({
success: true,
project,
session,
timestamp
}));
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: error.message || 'Unknown error storing overview'
}));
process.exit(1);
}
}
-80
View File
@@ -1,80 +0,0 @@
import { OptionValues } from 'commander';
import path from 'path';
import fs from 'fs';
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
/**
* Update session metadata (title/subtitle) in the streaming session JSON file
* Called by SDK when generating session title at the start
*/
export async function updateSessionMetadata(options: OptionValues): Promise<void> {
const { project, session, title, subtitle } = options;
// Validate required fields
if (!project || !session) {
console.error(JSON.stringify({
success: false,
error: 'Missing required fields: --project, --session'
}));
process.exit(1);
}
if (!title) {
console.error(JSON.stringify({
success: false,
error: 'Missing required field: --title'
}));
process.exit(1);
}
try {
// Load existing session file
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
if (!fs.existsSync(sessionFile)) {
console.error(JSON.stringify({
success: false,
error: `Session file not found: ${sessionFile}`
}));
process.exit(1);
}
let sessionData: any = {};
try {
sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
} catch (e) {
console.error(JSON.stringify({
success: false,
error: 'Failed to parse session file'
}));
process.exit(1);
}
// Update metadata
sessionData.promptTitle = title;
if (subtitle) {
sessionData.promptSubtitle = subtitle;
}
sessionData.updatedAt = new Date().toISOString();
// Write back to file
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
// Output success
console.log(JSON.stringify({
success: true,
title,
subtitle: subtitle || null,
project,
session
}));
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: error.message || 'Unknown error updating session metadata'
}));
process.exit(1);
}
}
-25
View File
@@ -1,25 +0,0 @@
/**
* Claude Memory System - Core Constants
*
* This file contains debug logging templates used throughout the application.
*/
// =============================================================================
// DEBUG AND LOGGING TEMPLATES
// =============================================================================
/**
* Debug logging message templates
*/
export const DEBUG_MESSAGES = {
COMPRESSION_STARTED: '🚀 COMPRESSION STARTED',
TRANSCRIPT_PATH: (path: string) => `📁 Transcript Path: ${path}`,
SESSION_ID: (id: string) => `🔍 Session ID: ${id}`,
PROJECT_NAME: (name: string) => `📝 PROJECT NAME: ${name}`,
CLAUDE_SDK_CALL: '🤖 Calling Claude SDK to analyze and populate memory database...',
TRANSCRIPT_STATS: (size: number, count: number) =>
`📊 Transcript size: ${size} characters, ${count} messages`,
COMPRESSION_COMPLETE: (count: number) => `✅ COMPRESSION COMPLETE\n Total summaries extracted: ${count}`,
CLAUDE_PATH_FOUND: (path: string) => `🎯 Found Claude Code at: ${path}`,
MCP_CONFIG_USED: (path: string) => `📋 Using MCP config: ${path}`
} as const;
-64
View File
@@ -1,64 +0,0 @@
/**
* Time utilities for formatting relative timestamps
*/
export function formatRelativeTime(timestamp: string | Date): string {
try {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSeconds = Math.floor(diffMs / 1000);
const diffMinutes = Math.floor(diffSeconds / 60);
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
if (diffSeconds < 60) {
return 'Just now';
} else if (diffMinutes < 60) {
return diffMinutes === 1 ? '1 minute ago' : `${diffMinutes} minutes ago`;
} else if (diffHours < 24) {
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
} else if (diffDays === 1) {
return 'Yesterday';
} else if (diffDays < 7) {
return `${diffDays} days ago`;
} else if (diffWeeks === 1) {
return '1 week ago';
} else if (diffWeeks < 4) {
return `${diffWeeks} weeks ago`;
} else if (diffMonths === 1) {
return '1 month ago';
} else if (diffMonths < 12) {
return `${diffMonths} months ago`;
} else {
const diffYears = Math.floor(diffMonths / 12);
return diffYears === 1 ? '1 year ago' : `${diffYears} years ago`;
}
} catch (error) {
// Return a fallback for invalid timestamps
return 'Recently';
}
}
export function parseTimestamp(entry: any): Date | null {
// Try multiple timestamp fields that might exist
const possibleFields = ['timestamp', 'created_at', 'date', 'time'];
for (const field of possibleFields) {
if (entry[field]) {
try {
const date = new Date(entry[field]);
if (!isNaN(date.getTime())) {
return date;
}
} catch {
continue;
}
}
}
// If no valid timestamp found, return null
return null;
}
-224
View File
@@ -1,224 +0,0 @@
# Hook Prompts System
This directory contains the centralized prompt configuration for all streaming hooks.
## Quick Edit Guide
**Want to change hook prompts?** Edit this file:
```
hook-prompts.config.ts
```
Then rebuild and reinstall:
```bash
bun run build
bun run dev:install
```
## Files in This Directory
### hook-prompts.config.ts
**EDIT THIS FILE** to change prompt content.
Contains:
- `SYSTEM_PROMPT` - Initial instructions for SDK (190 lines)
- `TOOL_MESSAGE` - Format for tool responses (10 lines)
- `END_MESSAGE` - Session completion request (10 lines)
- `HOOK_CONFIG` - Shared settings (truncation limits, SDK options)
Uses `{{variableName}}` template syntax.
### hook-prompt-renderer.ts
**DON'T EDIT** unless adding new variables or changing rendering logic.
Contains:
- `renderSystemPrompt()` - Processes system prompt template
- `renderToolMessage()` - Processes tool message template
- `renderEndMessage()` - Processes end message template
- Template substitution and auto-truncation logic
### templates/context/ContextTemplates.ts
Session start message formatting (separate from hook prompts).
## Template Variables Reference
### SYSTEM_PROMPT Variables
```typescript
{
project: string; // Project name (e.g., "claude-mem-source")
sessionId: string; // Claude Code session ID
date: string; // YYYY-MM-DD format
userPrompt: string; // Auto-truncated to 200 chars
}
```
### TOOL_MESSAGE Variables
```typescript
{
toolName: string; // Tool name (e.g., "Read", "Bash")
toolResponse: string; // Auto-truncated to 20000 chars
userPrompt: string; // Auto-truncated to 200 chars
timestamp: string; // Full ISO timestamp
timeFormatted: string; // HH:MM:SS format (auto-generated)
}
```
### END_MESSAGE Variables
```typescript
{
project: string; // Project name
sessionId: string; // Claude Code session ID
}
```
## Usage in Hooks
### user-prompt-submit-streaming.js
```javascript
import { renderSystemPrompt, HOOK_CONFIG } from '../src/prompts/hook-prompt-renderer.js';
const prompt = renderSystemPrompt({
project,
sessionId: session_id,
date,
userPrompt: prompt || ''
});
query({
prompt,
options: {
model: HOOK_CONFIG.sdk.model,
allowedTools: HOOK_CONFIG.sdk.allowedTools,
maxTokens: HOOK_CONFIG.sdk.maxTokensSystem
}
});
```
### post-tool-use-streaming.js
```javascript
import { renderToolMessage, HOOK_CONFIG } from '../src/prompts/hook-prompt-renderer.js';
const message = renderToolMessage({
toolName: tool_name,
toolResponse: toolResponseStr,
userPrompt: prompt || '',
timestamp: timestamp || new Date().toISOString()
});
query({
prompt: message,
options: {
model: HOOK_CONFIG.sdk.model,
maxTokens: HOOK_CONFIG.sdk.maxTokensTool
}
});
```
### stop-streaming.js
```javascript
import { renderEndMessage, HOOK_CONFIG } from '../src/prompts/hook-prompt-renderer.js';
const message = renderEndMessage({
project,
sessionId: claudeSessionId
});
query({
prompt: message,
options: {
model: HOOK_CONFIG.sdk.model,
maxTokens: HOOK_CONFIG.sdk.maxTokensEnd
}
});
```
## Configuration Options
Edit `HOOK_CONFIG` in `hook-prompts.config.ts`:
```typescript
export const HOOK_CONFIG = {
// Truncation limits for template variables
maxUserPromptLength: 200, // Increase to show more context
maxToolResponseLength: 20000, // Increase for larger outputs
// SDK configuration
sdk: {
model: 'claude-sonnet-4-5', // Change model version
allowedTools: ['Bash'], // Add more tools if needed
maxTokensSystem: 8192, // Token limit for system prompt
maxTokensTool: 8192, // Token limit for tool messages
maxTokensEnd: 2048, // Token limit for end message
},
};
```
## Example: Editing a Prompt
### Before
```typescript
export const TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
Tool: {{toolName}}
User Context: "{{userPrompt}}"
\`\`\`
{{toolResponse}}
\`\`\`
Analyze and store if meaningful.`;
```
### After
```typescript
export const TOOL_MESSAGE = `# Analysis Request {{timeFormatted}}
Executed: {{toolName}}
Context: "{{userPrompt}}"
Priority: High
Output:
\`\`\`
{{toolResponse}}
\`\`\`
IMPORTANT: Only store if this contains:
- New code patterns or logic
- Architecture decisions
- Error messages with solutions
- Configuration changes
Skip trivial operations.`;
```
### Apply Changes
```bash
bun run build && bun run dev:install
```
## Benefits
### DRY Compliance
- **Before**: 3 files with 188 lines of hardcoded prompts
- **After**: 1 config file with all prompts centralized
### Maintainability
- Change prompts without touching hook implementation
- Type-safe template variables
- Consistent formatting across all hooks
- Version-controlled prompt history
### Flexibility
- Easy A/B testing of different instructions
- Simple to adjust truncation limits
- Quick model/token configuration changes
- Template variables prevent copy-paste errors
## Full Documentation
See `/Users/alexnewman/Scripts/claude-mem-source/docs/HOOK_PROMPTS.md` for:
- Detailed editing guide
- Troubleshooting common issues
- Adding new template variables
- Advanced customization
- Migration notes
-159
View File
@@ -1,159 +0,0 @@
/**
* Hook Prompt Renderer
*
* Simple template rendering for hook prompts.
* Handles variable substitution and auto-truncation.
*/
import {
PROMPTS,
HOOK_CONFIG,
type SystemPromptVariables,
type ToolMessageVariables,
type EndMessageVariables,
} from './hook-prompts.config.js';
// =============================================================================
// TEMPLATE RENDERING
// =============================================================================
/**
* Simple template variable substitution
* Replaces {{variableName}} with actual values
*/
function substituteVariables(
template: string,
variables: Record<string, string>
): string {
let result = template;
for (const [key, value] of Object.entries(variables)) {
const placeholder = `{{${key}}}`;
// Replace all occurrences of this placeholder
result = result.split(placeholder).join(value);
}
return result;
}
/**
* Truncate text with ellipsis if it exceeds maxLength
*/
function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + (text.length > maxLength ? '...' : '');
}
/**
* Format timestamp for tool message header
* Extracts HH:MM:SS from ISO timestamp
*/
function formatTime(timestamp: string): string {
const timePart = timestamp.split('T')[1];
if (!timePart) return '';
return timePart.slice(0, 8); // HH:MM:SS
}
// =============================================================================
// PUBLIC RENDERING FUNCTIONS
// =============================================================================
/**
* Render system prompt for SDK session initialization
*/
export function renderSystemPrompt(
variables: SystemPromptVariables
): string {
// Auto-truncate userPrompt
const userPromptTruncated = truncate(
variables.userPrompt,
HOOK_CONFIG.maxUserPromptLength
);
return substituteVariables(PROMPTS.system, {
project: variables.project,
sessionId: variables.sessionId,
date: variables.date,
userPrompt: userPromptTruncated,
});
}
/**
* Render tool message for SDK processing
*/
export function renderToolMessage(
variables: ToolMessageVariables
): string {
// Auto-truncate userPrompt and toolResponse
const userPromptTruncated = truncate(
variables.userPrompt,
HOOK_CONFIG.maxUserPromptLength
);
const toolResponseTruncated = truncate(
variables.toolResponse,
HOOK_CONFIG.maxToolResponseLength
);
// Format timestamp
const timeFormatted = formatTime(variables.timestamp);
return substituteVariables(PROMPTS.tool, {
toolName: variables.toolName,
toolResponse: toolResponseTruncated,
userPrompt: userPromptTruncated,
timestamp: variables.timestamp,
timeFormatted,
});
}
/**
* Render end message for session completion
*/
export function renderEndMessage(
variables: EndMessageVariables
): string {
return substituteVariables(PROMPTS.end, {
project: variables.project,
sessionId: variables.sessionId,
});
}
// =============================================================================
// GENERIC RENDERER (for convenience)
// =============================================================================
export type PromptType = 'system' | 'tool' | 'end';
export type PromptVariables<T extends PromptType> = T extends 'system'
? SystemPromptVariables
: T extends 'tool'
? ToolMessageVariables
: T extends 'end'
? EndMessageVariables
: never;
/**
* Generic prompt renderer - dispatches to specific renderer based on type
*/
export function renderPrompt<T extends PromptType>(
type: T,
variables: PromptVariables<T>
): string {
switch (type) {
case 'system':
return renderSystemPrompt(variables as SystemPromptVariables);
case 'tool':
return renderToolMessage(variables as ToolMessageVariables);
case 'end':
return renderEndMessage(variables as EndMessageVariables);
default:
throw new Error(`Unknown prompt type: ${type}`);
}
}
// =============================================================================
// EXPORTS
// =============================================================================
export { HOOK_CONFIG, PROMPTS };
-306
View File
@@ -1,306 +0,0 @@
/**
* Hook Prompts Configuration
*
* Centralized configuration for all streaming hook prompts.
* This is the SINGLE SOURCE OF TRUTH for hook prompt content.
*
* EDITING GUIDE:
* - Use {{variableName}} for template variables
* - Available variables are listed in each prompt's interface
* - All prompts are processed through renderPrompt() function
* - Changes here apply to all hooks automatically after rebuild
*
* LIFECYCLE FLOW:
* 1. user-prompt-submit: Initializes SDK session with systemPrompt
* 2. post-tool-use: Feeds tool responses using toolMessage (repeats N times)
* 3. stop: Ends session and requests overview using endMessage
*/
// =============================================================================
// TYPE DEFINITIONS
// =============================================================================
export interface SystemPromptVariables {
project: string;
sessionId: string;
date: string;
userPrompt: string; // Auto-truncated to maxUserPromptLength
}
export interface ToolMessageVariables {
toolName: string;
toolResponse: string; // Auto-truncated to maxToolResponseLength
userPrompt: string; // Auto-truncated to maxUserPromptLength
timestamp: string; // Full ISO timestamp
timeFormatted: string; // HH:MM:SS format
}
export interface EndMessageVariables {
project: string;
sessionId: string;
}
// =============================================================================
// SHARED CONFIGURATION
// =============================================================================
export const HOOK_CONFIG = {
// Truncation limits for template variables
maxUserPromptLength: 200,
maxToolResponseLength: 20000,
// SDK configuration (used by hooks)
sdk: {
model: 'claude-sonnet-4-5',
allowedTools: ['Bash'],
maxTokensSystem: 8192,
maxTokensTool: 8192,
maxTokensEnd: 2048,
},
} as const;
// =============================================================================
// PHASE 1: SYSTEM PROMPT (user-prompt-submit-streaming.js)
// =============================================================================
/**
* System prompt that initializes the SDK session.
* Instructs the SDK on how to process tool responses and store memories.
*
* Variables:
* - {{project}}: Project name (from cwd basename)
* - {{sessionId}}: Claude Code session ID
* - {{date}}: Current date (YYYY-MM-DD)
* - {{userPrompt}}: User's initial prompt (truncated to 200 chars)
*/
export const SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
# SESSION CONTEXT
- Project: {{project}}
- Session: {{sessionId}}
- Date: {{date}}
- User Request: "{{userPrompt}}"
# YOUR JOB
## FIRST: Generate Session Title
IMMEDIATELY generate a title and subtitle for this session based on the user request.
Use this bash command:
\`\`\`bash
claude-mem update-session-metadata \\
--project "{{project}}" \\
--session "{{sessionId}}" \\
--title "Short title (3-6 words)" \\
--subtitle "One sentence description (max 20 words)"
\`\`\`
Example for "Help me add dark mode to my app":
- Title: "Dark Mode Implementation"
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
## THEN: Process Tool Responses
You will receive a stream of tool responses. For each one:
1. ANALYZE: Does this contain information worth remembering?
2. DECIDE: Should I store this or skip it?
3. EXTRACT: What are the key semantic concepts?
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
5. STORE: Use bash to save the hierarchical memory
6. TRACK: Keep count of stored memories (001, 002, 003...)
# WHAT TO STORE
Store these:
- File contents with logic, algorithms, or patterns
- Search results revealing project structure
- Build errors or test failures with context
- Code revealing architecture or design decisions
- Git diffs with significant changes
- Command outputs showing system state
Skip these:
- Simple status checks (git status with no changes)
- Trivial edits (one-line config changes)
- Repeated operations
- Binary data or noise
- Anything without semantic value
# HIERARCHICAL MEMORY FORMAT
Each memory has FOUR components:
## 1. TITLE (3-8 words)
A scannable headline that captures the core action or topic.
Examples:
- "SDK Transcript Cleanup Implementation"
- "Hook System Architecture Analysis"
- "ChromaDB Migration Planning"
## 2. SUBTITLE (max 24 words)
A concise, memorable summary that captures the essence of the change.
Examples:
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
Guidelines:
- Clear and descriptive
- Focus on the outcome or benefit
- Use active voice when possible
- Keep it professional and informative
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
Individual, searchable statements that can be vector-embedded separately.
Each fact is ONE specific piece of information.
Examples:
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
- "Uses fs.unlink with graceful error handling for missing files"
- "Checks two transcript path formats for backward compatibility"
Guidelines:
- Start with filename or component when relevant
- Be specific: include paths, function names, actual values
- Each fact stands alone (no pronouns like "it" or "this")
- 50-150 characters target
- Focus on searchable technical details
## 4. NARRATIVE (512-1024 tokens, same as current format)
The full contextual story for deep dives:
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
This is the detailed explanation for when someone needs full context.
# STORAGE COMMAND FORMAT
Store using this EXACT bash command structure:
\`\`\`bash
claude-mem store-memory \\
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
--title "Your Title Here" \\
--subtitle "Your concise subtitle here" \\
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
--concepts '["concept1", "concept2", "concept3"]' \\
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
--project "{{project}}" \\
--session "{{sessionId}}" \\
--date "{{date}}"
\`\`\`
CRITICAL FORMATTING RULES:
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
- Use double quotes inside the JSON arrays: "item"
- Use double quotes around simple string values: --title "Title"
- Escape any quotes in the content properly
- Sequential numbering: 001, 002, 003, etc.
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
# EXAMPLE MEMORY
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
Your storage command:
\`\`\`bash
claude-mem store-memory \\
--id "claude-mem_abc123_2025-10-01_001" \\
--title "SDK Transcript Auto-Cleanup" \\
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
--files '["hooks/stop-streaming.js"]' \\
--project "claude-mem" \\
--session "abc123" \\
--date "2025-10-01"
\`\`\`
# STATE TRACKING
CRITICAL: Keep track of your memory counter across all tool messages.
- Start at 001
- Increment for each stored memory
- Never repeat numbers
- Each session has separate numbering
# SESSION END
At the end (when I send "SESSION ENDING"), generate an overview using:
\`\`\`bash
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
\`\`\`
# IMPORTANT REMINDERS
- You're processing a DIFFERENT Claude Code session (not your own)
- Use Bash tool to call claude-mem commands
- Keep subtitles clear and informative (max 24 words)
- Each fact is ONE specific thing (not multiple ideas)
- Be selective - quality over quantity
- Always increment memory numbers
- Facts should be searchable (specific file names, paths, functions)
Ready for tool responses.`;
// =============================================================================
// PHASE 2: TOOL MESSAGE (post-tool-use-streaming.js)
// =============================================================================
/**
* Message format for each tool response sent to the SDK.
* The SDK analyzes this and decides whether to store a memory.
*
* Variables:
* - {{timeFormatted}}: Time portion of timestamp (HH:MM:SS)
* - {{toolName}}: Name of the tool that was used
* - {{userPrompt}}: User's original prompt (truncated to 200 chars)
* - {{toolResponse}}: Full tool response (truncated to 20000 chars)
*/
export const TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
Tool: {{toolName}}
User Context: "{{userPrompt}}"
\`\`\`
{{toolResponse}}
\`\`\`
Analyze and store if meaningful.`;
// =============================================================================
// PHASE 3: END MESSAGE (stop-streaming.js)
// =============================================================================
/**
* Message sent to SDK when session ends.
* Requests the SDK to generate and store a session overview.
*
* Variables:
* - {{project}}: Project name
* - {{sessionId}}: Claude Code session ID
*/
export const END_MESSAGE = `# SESSION ENDING
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
Store it using Bash:
\`\`\`bash
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
\`\`\`
Focus on: what was done, current state, key decisions, outcomes.`;
// =============================================================================
// EXPORTS
// =============================================================================
export const PROMPTS = {
system: SYSTEM_PROMPT,
tool: TOOL_MESSAGE,
end: END_MESSAGE,
} as const;
@@ -1,491 +0,0 @@
/**
* Context Templates for Human-Readable Formatting
*
* Essential templates for user-facing messages in the memory system.
* Focused on session start messages, error handling, and operation feedback.
* Previously included Handlebars templates for session start formatting; current
* version renders directly via console for clarity and performance.
*/
import { formatRelativeTime, parseTimestamp } from '../../../lib/time-utils.js';
// =============================================================================
// TERMINAL WIDTH & WORD WRAPPING
// =============================================================================
/**
* Determines target wrap width based on:
* 1) CLAUDE_MEM_WRAP_WIDTH env override
* 2) TTY columns (capped at 120)
* 3) Fallback default of 80
*/
function getWrapWidth(): number {
const env = process.env.CLAUDE_MEM_WRAP_WIDTH;
if (env) {
const n = parseInt(env, 10);
if (!Number.isNaN(n) && n > 40 && n <= 200) return n;
}
// Default to classic 80 columns unless overridden
return 80;
}
/**
* Wrap a single logical line to the given width, preserving leading indentation.
* Also avoids wrapping pure separator lines (====, ----, etc.).
*/
function wrapSingleLine(line: string, width: number): string {
if (!line) return '';
// Don't wrap long separator lines
if (/^[\-=\u2014_\u2500\u2550]{5,}$/.test(line.trim())) return line;
// If already short enough, return as-is
if (line.length <= width) return line;
const indentMatch = line.match(/^\s*/);
const indent = indentMatch ? indentMatch[0] : '';
const content = line.slice(indent.length);
const avail = Math.max(10, width - indent.length); // keep some minimum
const words = content.split(/(\s+)/); // keep whitespace tokens
const out: string[] = [];
let current = '';
const pushLine = () => {
out.push(indent + current.trimEnd());
current = '';
};
for (const token of words) {
if (token === '') continue;
// If token itself is longer than available width, hard-break it
if (!/\s/.test(token) && token.length > avail) {
if (current.trim().length > 0) pushLine();
let start = 0;
while (start < token.length) {
const chunk = token.slice(start, start + avail);
out.push(indent + chunk);
start += avail;
}
current = '';
continue;
}
if (indent.length + current.length + token.length > width) {
pushLine();
}
current += token;
}
if (current.trim().length > 0 || out.length === 0) pushLine();
return out.join('\n');
}
/**
* Wrap a block of text (possibly multi-line) to the given width.
* Preserves blank lines and wraps each line independently.
*/
function wrapText(text: string, width: number): string {
if (!text) return '';
return text
.split('\n')
.map((line) => wrapSingleLine(line, width))
.join('\n');
}
/** Create a full-width horizontal line with the given character */
function makeLine(char: string = '─', width: number = getWrapWidth()): string {
if (!char || char.length === 0) char = '-';
// Repeat and slice to exact width to avoid multi-column surprises
return char.repeat(width).slice(0, width);
}
// =============================================================================
// SESSION START MESSAGES
// =============================================================================
/**
* Creates a completion message after context operations
*/
export function createCompletionMessage(
operation: string,
count?: number,
details?: string
): string {
const countInfo = count !== undefined ? ` (${count} items)` : '';
const detailInfo = details ? `\n${details}` : '';
const width = getWrapWidth();
return wrapText(
`${operation} completed successfully${countInfo}${detailInfo}`,
width
);
}
// =============================================================================
// ERROR MESSAGES (USER-FRIENDLY)
// =============================================================================
/**
* Creates user-friendly error messages with helpful suggestions
*/
export function createUserFriendlyError(
operation: string,
error: string,
suggestion?: string
): string {
const suggestionText = suggestion ? `\n\n💡 ${suggestion}` : '';
const width = getWrapWidth();
return wrapText(
`${operation} encountered an issue: ${error}${suggestionText}`,
width
);
}
/**
* Common error scenarios with built-in suggestions
*/
export const ERROR_SCENARIOS = {
NO_MEMORIES: (projectName: string) => ({
message: `No previous memories found for ${projectName}`,
suggestion:
'This appears to be your first session. Memories will be created as you work.',
}),
CONNECTION_FAILED: () => ({
message: 'Could not connect to memory system',
suggestion:
'Try restarting Claude Code or check if the MCP server is properly configured.',
}),
SEARCH_FAILED: (query: string) => ({
message: `Search for "${query}" didn't return any results`,
suggestion:
'Try using different keywords or check if memories exist for this project.',
}),
LOAD_TIMEOUT: () => ({
message: 'Memory loading timed out',
suggestion:
'The operation is taking longer than expected. You can continue without loaded context.',
}),
};
/**
* Creates contextual error messages based on common scenarios
*/
export function createContextualError(
scenario: keyof typeof ERROR_SCENARIOS,
...args: string[]
): string {
const errorInfo = (ERROR_SCENARIOS[scenario] as any)(...args);
return createUserFriendlyError(
'Memory system',
errorInfo.message,
errorInfo.suggestion
);
}
// =============================================================================
// TIME AND DATE FORMATTING
// =============================================================================
/**
* Formats timestamps into human-readable "time ago" format
*/
export function formatTimeAgo(timestamp: string | Date): string {
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
if (days < 7) return `${days} day${days > 1 ? 's' : ''} ago`;
return date.toLocaleDateString();
}
// =============================================================================
// SESSION START TEMPLATE SYSTEM (data processing only)
// =============================================================================
/**
* Interface for memory entry data structure
*/
interface MemoryEntry {
summary: string;
keywords?: string;
location?: string;
sessionId?: string;
number?: number;
}
/**
* Interface for grouped session memories
*/
interface SessionGroup {
sessionId: string;
memories: MemoryEntry[];
}
/**
* Interface for overview with timestamp
*/
interface OverviewEntry {
content: string;
timestamp?: Date;
timeAgo?: string;
sessionId?: string;
}
/**
* Interface for session-grouped overviews
*/
interface SessionOverviewGroup {
sessionId: string;
overviews: OverviewEntry[];
earliestTimestamp?: Date;
timeAgo?: string;
}
/**
* Pure data processing function - converts JSON objects into structured memory entries
* No formatting is done here, only data parsing and cleaning
*/
function processMemoryEntries(recentObjects: any[]): MemoryEntry[] {
if (recentObjects.length === 0) {
return [];
}
// Filter only memory type objects and convert to MemoryEntry format
return recentObjects
.filter((obj) => obj.type === 'memory')
.map((obj) => {
const entry: MemoryEntry = {
summary: obj.text || '',
sessionId: obj.session_id || '',
};
// Add optional fields if present
if (obj.keywords) {
entry.keywords = obj.keywords;
}
if (obj.document_id && !obj.document_id.includes('Session:')) {
entry.location = obj.document_id;
}
return entry;
})
.filter((entry) => entry.summary.length > 0);
}
/**
* Groups memories by session ID and adds numbering
*/
function groupMemoriesBySession(memories: MemoryEntry[]): SessionGroup[] {
const sessionMap = new Map<string, MemoryEntry[]>();
// Group memories by session ID
memories.forEach((memory) => {
const sessionId = memory.sessionId;
if (sessionId) {
if (!sessionMap.has(sessionId)) {
sessionMap.set(sessionId, []);
}
sessionMap.get(sessionId)!.push(memory);
}
});
// Convert to session groups with numbering
return Array.from(sessionMap.entries()).map(
([sessionId, sessionMemories]) => {
const numberedMemories = sessionMemories.map((memory, index) => ({
...memory,
number: index + 1,
}));
return {
sessionId,
memories: numberedMemories,
};
}
);
}
/**
* Groups overviews by session ID and calculates session timestamps
*/
function groupOverviewsBySession(
overviews: OverviewEntry[]
): SessionOverviewGroup[] {
const sessionMap = new Map<string, OverviewEntry[]>();
// Group overviews by session ID
overviews.forEach((overview) => {
const sessionId = overview.sessionId || 'unknown';
if (!sessionMap.has(sessionId)) {
sessionMap.set(sessionId, []);
}
sessionMap.get(sessionId)!.push(overview);
});
// Convert to session groups with timestamps
return Array.from(sessionMap.entries()).map(
([sessionId, sessionOverviews]) => {
// Find the earliest timestamp in this session's overviews
const timestamps = sessionOverviews
.map((o) => o.timestamp)
.filter((t): t is Date => t !== undefined)
.sort((a, b) => a.getTime() - b.getTime());
const group: SessionOverviewGroup = {
sessionId,
overviews: sessionOverviews,
};
// Add session-level timestamp if available
if (timestamps.length > 0) {
group.earliestTimestamp = timestamps[0];
group.timeAgo = formatRelativeTime(timestamps[0]);
}
return group;
}
);
}
/**
* Renders the complete session start template with provided data using Handlebars
* Data processing is separated from presentation - template controls the format
*/
// Intentionally removed Handlebars-based renderer; console output is handled by
// outputSessionStartContent() below.
/**
* Outputs session start content using dual streams:
* - stdout (console.log) -> Claude's context only (granular memories)
* - stderr (console.error) -> User visible (clean overviews)
*/
export function outputSessionStartContent(params: {
projectName: string;
memoryCount: number;
lastSessionTime?: string;
recentObjects: any[];
}): void {
const { projectName, memoryCount, lastSessionTime, recentObjects } = params;
const width = getWrapWidth();
// Start with current date and time at the top
const now = new Date();
const dateTimeFormatted = now.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
});
console.log('');
console.log(wrapText(`📅 ${dateTimeFormatted}`, width));
console.log(makeLine('─', width));
// Extract overviews for user display - get more to show session grouping
const overviews = extractOverviews(recentObjects, 10, projectName);
// Debug: Log what we're getting
console.error(`[DEBUG] recentObjects has ${recentObjects.length} items`);
console.error(`[DEBUG] overviews extracted: ${overviews.length}`);
// Process memory entries for Claude context
const memories = processMemoryEntries(recentObjects);
// Helper to split and normalize keywords into a map (lowercased -> original)
const splitKeywordsInto = (kw: string, dest: Map<string, string>) => {
const tokens =
kw.includes(',') || kw.includes('\n') ? kw.split(/[\n,]+/) : [kw];
for (const t of tokens) {
const trimmed = t.trim();
if (!trimmed) continue;
const key = trimmed.toLowerCase();
if (!dest.has(key)) dest.set(key, trimmed);
}
};
// Output memories first, then overviews at bottom, all sorted oldest to newest
if (memories.length > 0) {
const sessionGroups = groupMemoriesBySession(memories);
console.log('');
console.log('');
console.log(wrapText('📚 Memories', width));
sessionGroups.forEach((group) => {
console.log(makeLine('─', width));
console.log('');
console.log(wrapText(`🔍 ${group.sessionId}`, width));
// Collect keywords for this session as we iterate its memories
const groupKeywordMap = new Map<string, string>();
group.memories.forEach((memory) => {
console.log('');
console.log(wrapText(`${memory.number}. ${memory.summary}`, width));
if (memory.keywords)
splitKeywordsInto(memory.keywords, groupKeywordMap);
});
// Print this session's aggregated keywords under the session block
const groupKeywords = Array.from(groupKeywordMap.values());
if (groupKeywords.length > 0) {
console.log('');
console.log(wrapText(`🏷️ ${groupKeywords.join(', ')}`, width));
}
console.log('');
});
}
// Overview section at bottom with session grouping
if (overviews.length > 0) {
const sessionGroups = groupOverviewsBySession(overviews);
// Sort groups by timestamp, oldest first for chronological reading order
sessionGroups.sort((a, b) => {
const timeA = a.earliestTimestamp?.getTime() || 0;
const timeB = b.earliestTimestamp?.getTime() || 0;
return timeA - timeB; // Ascending order (oldest first)
});
console.log('');
console.log(wrapText('🧠 Overviews', width));
console.log(makeLine('─', width));
// Match the memories section layout: session header, numbered items, per-session separator
sessionGroups.forEach((group) => {
console.log('');
console.log(wrapText(`🔍 ${group.sessionId}`, width));
group.overviews.forEach((overview, index) => {
console.log('');
console.log(wrapText(`${index + 1}. ${overview.content}`, width));
console.log('');
if (overview.timeAgo) {
console.log(wrapText(`📅 ${overview.timeAgo}`, width));
}
});
console.log('');
console.log(makeLine('─', width));
});
} else if (memories.length === 0) {
console.log(
wrapText(`🧠 No recent context found for ${projectName}`, width)
);
}
}
-229
View File
@@ -1,229 +0,0 @@
import { Database } from 'bun:sqlite';
import { getDatabase } from './Database.js';
import { DiagnosticRow, DiagnosticInput, normalizeTimestamp } from './types.js';
/**
* Data Access Object for diagnostic records
*/
export class DiagnosticsStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new diagnostic record
*/
create(input: DiagnosticInput): DiagnosticRow {
const { isoString, epoch } = normalizeTimestamp(input.created_at);
const stmt = this.db.query(`
INSERT INTO diagnostics (
session_id, message, severity, created_at, created_at_epoch, project, origin
) VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const info = stmt.run(
input.session_id || null,
input.message,
input.severity || 'warn',
isoString,
epoch,
input.project,
input.origin || 'compressor'
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Get diagnostic by primary key
*/
getById(id: number): DiagnosticRow | null {
const stmt = this.db.query('SELECT * FROM diagnostics WHERE id = ?');
return stmt.get(id) as DiagnosticRow || null;
}
/**
* Get diagnostics for a specific session
*/
getBySessionId(sessionId: string): DiagnosticRow[] {
const stmt = this.db.query(`
SELECT * FROM diagnostics
WHERE session_id = ?
ORDER BY created_at_epoch DESC
`);
return stmt.all(sessionId) as DiagnosticRow[];
}
/**
* Get recent diagnostics for a project
*/
getRecentForProject(project: string, limit = 10): DiagnosticRow[] {
const stmt = this.db.query(`
SELECT * FROM diagnostics
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, limit) as DiagnosticRow[];
}
/**
* Get recent diagnostics across all projects
*/
getRecent(limit = 10): DiagnosticRow[] {
const stmt = this.db.query(`
SELECT * FROM diagnostics
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as DiagnosticRow[];
}
/**
* Get diagnostics by severity level
*/
getBySeverity(severity: 'info' | 'warn' | 'error', limit?: number): DiagnosticRow[] {
const query = limit
? 'SELECT * FROM diagnostics WHERE severity = ? ORDER BY created_at_epoch DESC LIMIT ?'
: 'SELECT * FROM diagnostics WHERE severity = ? ORDER BY created_at_epoch DESC';
const stmt = this.db.query(query);
const params = limit ? [severity, limit] : [severity];
return stmt.all(...params) as DiagnosticRow[];
}
/**
* Get diagnostics by origin
*/
getByOrigin(origin: string, limit?: number): DiagnosticRow[] {
const query = limit
? 'SELECT * FROM diagnostics WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
: 'SELECT * FROM diagnostics WHERE origin = ? ORDER BY created_at_epoch DESC';
const stmt = this.db.query(query);
const params = limit ? [origin, limit] : [origin];
return stmt.all(...params) as DiagnosticRow[];
}
/**
* Search diagnostics by message content
*/
searchByMessage(query: string, project?: string, limit = 20): DiagnosticRow[] {
let sql = 'SELECT * FROM diagnostics WHERE message LIKE ?';
const params: any[] = [`%${query}%`];
if (project) {
sql += ' AND project = ?';
params.push(project);
}
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
params.push(limit);
const stmt = this.db.query(sql);
return stmt.all(...params) as DiagnosticRow[];
}
/**
* Count total diagnostics
*/
count(): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM diagnostics');
const result = stmt.get() as { count: number };
return result.count;
}
/**
* Count diagnostics by project
*/
countByProject(project: string): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM diagnostics WHERE project = ?');
const result = stmt.get(project) as { count: number };
return result.count;
}
/**
* Count diagnostics by severity
*/
countBySeverity(severity: 'info' | 'warn' | 'error'): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM diagnostics WHERE severity = ?');
const result = stmt.get(severity) as { count: number };
return result.count;
}
/**
* Update a diagnostic record
*/
update(id: number, input: Partial<DiagnosticInput>): DiagnosticRow {
const existing = this.getById(id);
if (!existing) {
throw new Error(`Diagnostic with id ${id} not found`);
}
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
const stmt = this.db.query(`
UPDATE diagnostics SET
message = ?, severity = ?, created_at = ?, created_at_epoch = ?, project = ?, origin = ?
WHERE id = ?
`);
stmt.run(
input.message || existing.message,
input.severity || existing.severity,
isoString,
epoch,
input.project || existing.project,
input.origin || existing.origin,
id
);
return this.getById(id)!;
}
/**
* Delete a diagnostic by ID
*/
deleteById(id: number): boolean {
const stmt = this.db.query('DELETE FROM diagnostics WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete diagnostics by session_id
*/
deleteBySessionId(sessionId: string): number {
const stmt = this.db.query('DELETE FROM diagnostics WHERE session_id = ?');
const info = stmt.run(sessionId);
return info.changes;
}
/**
* Get unique projects from diagnostics
*/
getUniqueProjects(): string[] {
const stmt = this.db.query('SELECT DISTINCT project FROM diagnostics ORDER BY project');
const rows = stmt.all() as { project: string }[];
return rows.map(row => row.project);
}
/**
* Get diagnostic summary stats
*/
getStats(): { total: number; info: number; warn: number; error: number } {
const stmt = this.db.query(`
SELECT
COUNT(*) as total,
COUNT(CASE WHEN severity = 'info' THEN 1 END) as info,
COUNT(CASE WHEN severity = 'warn' THEN 1 END) as warn,
COUNT(CASE WHEN severity = 'error' THEN 1 END) as error
FROM diagnostics
`);
return stmt.get() as { total: number; info: number; warn: number; error: number };
}
}
-287
View File
@@ -1,287 +0,0 @@
import { Database } from 'bun:sqlite';
import { getDatabase } from './Database.js';
import { MemoryRow, MemoryInput, normalizeTimestamp } from './types.js';
/**
* Data Access Object for memory records
*/
export class MemoryStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new memory record
*/
create(input: MemoryInput): MemoryRow {
const { isoString, epoch } = normalizeTimestamp(input.created_at);
const stmt = this.db.query(`
INSERT INTO memories (
session_id, text, document_id, keywords, created_at, created_at_epoch,
project, archive_basename, origin, title, subtitle, facts, concepts, files_touched
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const info = stmt.run(
input.session_id,
input.text,
input.document_id || null,
input.keywords || null,
isoString,
epoch,
input.project,
input.archive_basename || null,
input.origin || 'transcript',
input.title || null,
input.subtitle || null,
input.facts || null,
input.concepts || null,
input.files_touched || null
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Create multiple memory records in a transaction
*/
createMany(inputs: MemoryInput[]): MemoryRow[] {
const transaction = this.db.transaction((memories: MemoryInput[]) => {
const results: MemoryRow[] = [];
for (const memory of memories) {
results.push(this.create(memory));
}
return results;
});
return transaction(inputs);
}
/**
* Get memory by primary key
*/
getById(id: number): MemoryRow | null {
const stmt = this.db.query('SELECT * FROM memories WHERE id = ?');
return stmt.get(id) as MemoryRow || null;
}
/**
* Get memory by document_id
*/
getByDocumentId(documentId: string): MemoryRow | null {
const stmt = this.db.query('SELECT * FROM memories WHERE document_id = ?');
return stmt.get(documentId) as MemoryRow || null;
}
/**
* Check if a document_id already exists
*/
hasDocumentId(documentId: string): boolean {
const stmt = this.db.query('SELECT 1 FROM memories WHERE document_id = ? LIMIT 1');
return Boolean(stmt.get(documentId));
}
/**
* Get memories for a specific session
*/
getBySessionId(sessionId: string): MemoryRow[] {
const stmt = this.db.query(`
SELECT * FROM memories
WHERE session_id = ?
ORDER BY created_at_epoch DESC
`);
return stmt.all(sessionId) as MemoryRow[];
}
/**
* Get recent memories for a project
*/
getRecentForProject(project: string, limit = 10): MemoryRow[] {
const stmt = this.db.query(`
SELECT * FROM memories
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, limit) as MemoryRow[];
}
/**
* Get recent memories across all projects
*/
getRecent(limit = 10): MemoryRow[] {
const stmt = this.db.query(`
SELECT * FROM memories
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as MemoryRow[];
}
/**
* Search memories by text content
*/
searchByText(query: string, project?: string, limit = 20): MemoryRow[] {
let sql = 'SELECT * FROM memories WHERE text LIKE ?';
const params: any[] = [`%${query}%`];
if (project) {
sql += ' AND project = ?';
params.push(project);
}
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
params.push(limit);
const stmt = this.db.query(sql);
return stmt.all(...params) as MemoryRow[];
}
/**
* Search memories by keywords
*/
searchByKeywords(keywords: string, project?: string, limit = 20): MemoryRow[] {
let sql = 'SELECT * FROM memories WHERE keywords LIKE ?';
const params: any[] = [`%${keywords}%`];
if (project) {
sql += ' AND project = ?';
params.push(project);
}
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
params.push(limit);
const stmt = this.db.query(sql);
return stmt.all(...params) as MemoryRow[];
}
/**
* Get memories by origin type
*/
getByOrigin(origin: string, limit?: number): MemoryRow[] {
const query = limit
? 'SELECT * FROM memories WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
: 'SELECT * FROM memories WHERE origin = ? ORDER BY created_at_epoch DESC';
const stmt = this.db.query(query);
const params = limit ? [origin, limit] : [origin];
return stmt.all(...params) as MemoryRow[];
}
/**
* Get recent memories for a project filtered by origin
*/
getRecentForProjectByOrigin(project: string, origin: string, limit = 10): MemoryRow[] {
const stmt = this.db.query(`
SELECT * FROM memories
WHERE project = ? AND origin = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, origin, limit) as MemoryRow[];
}
/**
* Get last N memories for a project, sorted oldest to newest
*/
getLastNForProject(project: string, limit = 10): MemoryRow[] {
const stmt = this.db.query(`
SELECT * FROM (
SELECT * FROM memories
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
)
ORDER BY created_at_epoch ASC
`);
return stmt.all(project, limit) as MemoryRow[];
}
/**
* Count total memories
*/
count(): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM memories');
const result = stmt.get() as { count: number };
return result.count;
}
/**
* Count memories by project
*/
countByProject(project: string): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM memories WHERE project = ?');
const result = stmt.get(project) as { count: number };
return result.count;
}
/**
* Update a memory record
*/
update(id: number, input: Partial<MemoryInput>): MemoryRow {
const existing = this.getById(id);
if (!existing) {
throw new Error(`Memory with id ${id} not found`);
}
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
const stmt = this.db.query(`
UPDATE memories SET
text = ?, document_id = ?, keywords = ?, created_at = ?, created_at_epoch = ?,
project = ?, archive_basename = ?, origin = ?, title = ?, subtitle = ?, facts = ?,
concepts = ?, files_touched = ?
WHERE id = ?
`);
stmt.run(
input.text || existing.text,
input.document_id !== undefined ? input.document_id : existing.document_id,
input.keywords !== undefined ? input.keywords : existing.keywords,
isoString,
epoch,
input.project || existing.project,
input.archive_basename !== undefined ? input.archive_basename : existing.archive_basename,
input.origin || existing.origin,
input.title !== undefined ? input.title : existing.title,
input.subtitle !== undefined ? input.subtitle : existing.subtitle,
input.facts !== undefined ? input.facts : existing.facts,
input.concepts !== undefined ? input.concepts : existing.concepts,
input.files_touched !== undefined ? input.files_touched : existing.files_touched,
id
);
return this.getById(id)!;
}
/**
* Delete a memory by ID
*/
deleteById(id: number): boolean {
const stmt = this.db.query('DELETE FROM memories WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete memories by session_id
*/
deleteBySessionId(sessionId: string): number {
const stmt = this.db.query('DELETE FROM memories WHERE session_id = ?');
const info = stmt.run(sessionId);
return info.changes;
}
/**
* Get unique projects from memories
*/
getUniqueProjects(): string[] {
const stmt = this.db.query('SELECT DISTINCT project FROM memories ORDER BY project');
const rows = stmt.all() as { project: string }[];
return rows.map(row => row.project);
}
}
-241
View File
@@ -1,241 +0,0 @@
import { Database } from 'bun:sqlite';
import { getDatabase } from './Database.js';
import { OverviewRow, OverviewInput, normalizeTimestamp } from './types.js';
/**
* Data Access Object for overview records
*/
export class OverviewStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new overview record
*/
create(input: OverviewInput): OverviewRow {
const { isoString, epoch } = normalizeTimestamp(input.created_at);
const stmt = this.db.query(`
INSERT INTO overviews (
session_id, content, created_at, created_at_epoch, project, origin
) VALUES (?, ?, ?, ?, ?, ?)
`);
const info = stmt.run(
input.session_id,
input.content,
isoString,
epoch,
input.project,
input.origin || 'claude'
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Create or replace an overview for a session (since one session should have one overview)
*/
upsert(input: OverviewInput): OverviewRow {
const existing = this.getBySessionId(input.session_id);
if (existing) {
return this.update(existing.id, input);
}
return this.create(input);
}
/**
* Get overview by primary key
*/
getById(id: number): OverviewRow | null {
const stmt = this.db.query('SELECT * FROM overviews WHERE id = ?');
return stmt.get(id) as OverviewRow || null;
}
/**
* Get overview by session_id
*/
getBySessionId(sessionId: string): OverviewRow | null {
const stmt = this.db.query('SELECT * FROM overviews WHERE session_id = ?');
return stmt.get(sessionId) as OverviewRow || null;
}
/**
* Get recent overviews for a project
*/
getRecentForProject(project: string, limit = 5): OverviewRow[] {
const stmt = this.db.query(`
SELECT * FROM overviews
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, limit) as OverviewRow[];
}
/**
* Get all overviews for a project (oldest to newest)
*/
getAllForProject(project: string): OverviewRow[] {
const stmt = this.db.query(`
SELECT * FROM overviews
WHERE project = ?
ORDER BY created_at_epoch ASC
`);
return stmt.all(project) as OverviewRow[];
}
/**
* Get recent overviews across all projects
*/
getRecent(limit = 5): OverviewRow[] {
const stmt = this.db.query(`
SELECT * FROM overviews
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as OverviewRow[];
}
/**
* Search overviews by content
*/
searchByContent(query: string, project?: string, limit = 10): OverviewRow[] {
let sql = 'SELECT * FROM overviews WHERE content LIKE ?';
const params: any[] = [`%${query}%`];
if (project) {
sql += ' AND project = ?';
params.push(project);
}
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
params.push(limit);
const stmt = this.db.query(sql);
return stmt.all(...params) as OverviewRow[];
}
/**
* Get overviews by origin type
*/
getByOrigin(origin: string, limit?: number): OverviewRow[] {
const query = limit
? 'SELECT * FROM overviews WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
: 'SELECT * FROM overviews WHERE origin = ? ORDER BY created_at_epoch DESC';
const stmt = this.db.query(query);
const params = limit ? [origin, limit] : [origin];
return stmt.all(...params) as OverviewRow[];
}
/**
* Count total overviews
*/
count(): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM overviews');
const result = stmt.get() as { count: number };
return result.count;
}
/**
* Count overviews by project
*/
countByProject(project: string): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM overviews WHERE project = ?');
const result = stmt.get(project) as { count: number };
return result.count;
}
/**
* Update an overview record
*/
update(id: number, input: Partial<OverviewInput>): OverviewRow {
const existing = this.getById(id);
if (!existing) {
throw new Error(`Overview with id ${id} not found`);
}
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
const stmt = this.db.query(`
UPDATE overviews SET
content = ?, created_at = ?, created_at_epoch = ?, project = ?, origin = ?
WHERE id = ?
`);
stmt.run(
input.content || existing.content,
isoString,
epoch,
input.project || existing.project,
input.origin || existing.origin,
id
);
return this.getById(id)!;
}
/**
* Delete an overview by ID
*/
deleteById(id: number): boolean {
const stmt = this.db.query('DELETE FROM overviews WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete overview by session_id
*/
deleteBySessionId(sessionId: string): boolean {
const stmt = this.db.query('DELETE FROM overviews WHERE session_id = ?');
const info = stmt.run(sessionId);
return info.changes > 0;
}
/**
* Get unique projects from overviews
*/
getUniqueProjects(): string[] {
const stmt = this.db.query('SELECT DISTINCT project FROM overviews ORDER BY project');
const rows = stmt.all() as { project: string }[];
return rows.map(row => row.project);
}
/**
* Get most recent overview for a specific project
*/
getByProject(project: string): OverviewRow | null {
const stmt = this.db.query(`
SELECT * FROM overviews
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT 1
`);
return stmt.get(project) as OverviewRow || null;
}
/**
* Create or update overview for a project (keeps only most recent)
*/
upsertByProject(input: OverviewInput): OverviewRow {
const existing = this.getByProject(input.project);
if (existing) {
return this.update(existing.id, input);
}
return this.create(input);
}
/**
* Delete overview by project name
*/
deleteByProject(project: string): boolean {
const stmt = this.db.query('DELETE FROM overviews WHERE project = ?');
const info = stmt.run(project);
return info.changes > 0;
}
}
-195
View File
@@ -1,195 +0,0 @@
import { Database } from 'bun:sqlite';
import { getDatabase } from './Database.js';
import { SessionRow, SessionInput, normalizeTimestamp } from './types.js';
/**
* Data Access Object for session records
*/
export class SessionStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new session record
*/
create(input: SessionInput): SessionRow {
const { isoString, epoch } = normalizeTimestamp(input.created_at);
const stmt = this.db.query(`
INSERT INTO sessions (
session_id, project, created_at, created_at_epoch, source,
archive_path, archive_bytes, archive_checksum, archived_at, metadata_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const info = stmt.run(
input.session_id,
input.project,
isoString,
epoch,
input.source || 'compress',
input.archive_path || null,
input.archive_bytes || null,
input.archive_checksum || null,
input.archived_at || null,
input.metadata_json || null
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Upsert a session record (insert or update if session_id exists)
*/
upsert(input: SessionInput): SessionRow {
const existing = this.getBySessionId(input.session_id);
if (existing) {
return this.update(existing.id, input);
}
return this.create(input);
}
/**
* Update an existing session record
*/
update(id: number, input: Partial<SessionInput>): SessionRow {
const existing = this.getById(id);
if (!existing) {
throw new Error(`Session with id ${id} not found`);
}
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
const stmt = this.db.query(`
UPDATE sessions SET
project = ?, created_at = ?, created_at_epoch = ?, source = ?,
archive_path = ?, archive_bytes = ?, archive_checksum = ?, archived_at = ?, metadata_json = ?
WHERE id = ?
`);
stmt.run(
input.project || existing.project,
isoString,
epoch,
input.source || existing.source,
input.archive_path !== undefined ? input.archive_path : existing.archive_path,
input.archive_bytes !== undefined ? input.archive_bytes : existing.archive_bytes,
input.archive_checksum !== undefined ? input.archive_checksum : existing.archive_checksum,
input.archived_at !== undefined ? input.archived_at : existing.archived_at,
input.metadata_json !== undefined ? input.metadata_json : existing.metadata_json,
id
);
return this.getById(id)!;
}
/**
* Get session by primary key
*/
getById(id: number): SessionRow | null {
const stmt = this.db.query('SELECT * FROM sessions WHERE id = ?');
return stmt.get(id) as SessionRow || null;
}
/**
* Get session by session_id
*/
getBySessionId(sessionId: string): SessionRow | null {
const stmt = this.db.query('SELECT * FROM sessions WHERE session_id = ?');
return stmt.get(sessionId) as SessionRow || null;
}
/**
* Check if a session exists by session_id
*/
has(sessionId: string): boolean {
const stmt = this.db.query('SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1');
return Boolean(stmt.get(sessionId));
}
/**
* Get all session_ids as a Set (useful for import-history)
*/
getAllSessionIds(): Set<string> {
const stmt = this.db.query('SELECT session_id FROM sessions');
const rows = stmt.all() as { session_id: string }[];
return new Set(rows.map(row => row.session_id));
}
/**
* Get recent sessions for a project
*/
getRecentForProject(project: string, limit = 5): SessionRow[] {
const stmt = this.db.query(`
SELECT * FROM sessions
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, limit) as SessionRow[];
}
/**
* Get recent sessions across all projects
*/
getRecent(limit = 5): SessionRow[] {
const stmt = this.db.query(`
SELECT * FROM sessions
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as SessionRow[];
}
/**
* Get sessions by source type
*/
getBySource(source: 'compress' | 'save' | 'legacy-jsonl', limit?: number): SessionRow[] {
const query = limit
? 'SELECT * FROM sessions WHERE source = ? ORDER BY created_at_epoch DESC LIMIT ?'
: 'SELECT * FROM sessions WHERE source = ? ORDER BY created_at_epoch DESC';
const stmt = this.db.query(query);
const params = limit ? [source, limit] : [source];
return stmt.all(...params) as SessionRow[];
}
/**
* Count total sessions
*/
count(): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM sessions');
const result = stmt.get() as { count: number };
return result.count;
}
/**
* Count sessions by project
*/
countByProject(project: string): number {
const stmt = this.db.query('SELECT COUNT(*) as count FROM sessions WHERE project = ?');
const result = stmt.get(project) as { count: number };
return result.count;
}
/**
* Delete a session by ID (cascades to related records)
*/
deleteById(id: number): boolean {
const stmt = this.db.query('DELETE FROM sessions WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete a session by session_id (cascades to related records)
*/
deleteBySessionId(sessionId: string): boolean {
const stmt = this.db.query('DELETE FROM sessions WHERE session_id = ?');
const info = stmt.run(sessionId);
return info.changes > 0;
}
}
@@ -1,266 +0,0 @@
import { Database } from 'bun:sqlite';
import { getDatabase } from './Database.js';
import { normalizeTimestamp } from './types.js';
/**
* Represents a streaming session row in the database
*/
export interface StreamingSessionRow {
id: number;
claude_session_id: string;
sdk_session_id?: string;
project: string;
title?: string;
subtitle?: string;
user_prompt?: string;
started_at: string;
started_at_epoch: number;
updated_at?: string;
updated_at_epoch?: number;
completed_at?: string;
completed_at_epoch?: number;
status: 'active' | 'completed' | 'failed';
}
/**
* Input type for creating a new streaming session
*/
export interface StreamingSessionInput {
claude_session_id: string;
project: string;
user_prompt?: string;
started_at?: string | Date | number;
}
/**
* Input type for updating a streaming session
*/
export interface StreamingSessionUpdate {
sdk_session_id?: string;
title?: string;
subtitle?: string;
status?: 'active' | 'completed' | 'failed';
completed_at?: string | Date | number;
}
/**
* Data Access Object for streaming session records
* Handles real-time session tracking during SDK compression
*/
export class StreamingSessionStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new streaming session record
* This should be called immediately when the hook receives a user prompt
*/
create(input: StreamingSessionInput): StreamingSessionRow {
const { isoString, epoch } = normalizeTimestamp(input.started_at);
const stmt = this.db.query(`
INSERT INTO streaming_sessions (
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
) VALUES (?, ?, ?, ?, ?, 'active')
`);
const info = stmt.run(
input.claude_session_id,
input.project,
input.user_prompt || null,
isoString,
epoch
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Update a streaming session by internal ID
* Uses atomic transaction to prevent race conditions
*/
update(id: number, updates: StreamingSessionUpdate): StreamingSessionRow {
const { isoString: updatedAt, epoch: updatedEpoch } = normalizeTimestamp(new Date());
const existing = this.getById(id);
if (!existing) {
throw new Error(`Streaming session with id ${id} not found`);
}
const parts: string[] = [];
const values: any[] = [];
if (updates.sdk_session_id !== undefined) {
parts.push('sdk_session_id = ?');
values.push(updates.sdk_session_id);
}
if (updates.title !== undefined) {
parts.push('title = ?');
values.push(updates.title);
}
if (updates.subtitle !== undefined) {
parts.push('subtitle = ?');
values.push(updates.subtitle);
}
if (updates.status !== undefined) {
parts.push('status = ?');
values.push(updates.status);
}
if (updates.completed_at !== undefined) {
const { isoString, epoch } = normalizeTimestamp(updates.completed_at);
parts.push('completed_at = ?', 'completed_at_epoch = ?');
values.push(isoString, epoch);
}
// Always update the updated_at timestamp
parts.push('updated_at = ?', 'updated_at_epoch = ?');
values.push(updatedAt, updatedEpoch);
values.push(id);
const stmt = this.db.query(`
UPDATE streaming_sessions
SET ${parts.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id)!;
}
/**
* Update a streaming session by Claude session ID
* Convenience method for hooks that only have the Claude session ID
*/
updateByClaudeSessionId(claudeSessionId: string, updates: StreamingSessionUpdate): StreamingSessionRow | null {
const session = this.getByClaudeSessionId(claudeSessionId);
if (!session) {
return null;
}
return this.update(session.id, updates);
}
/**
* Get streaming session by internal ID
*/
getById(id: number): StreamingSessionRow | null {
const stmt = this.db.query('SELECT * FROM streaming_sessions WHERE id = ?');
return stmt.get(id) as StreamingSessionRow || null;
}
/**
* Get streaming session by Claude session ID
*/
getByClaudeSessionId(claudeSessionId: string): StreamingSessionRow | null {
const stmt = this.db.query('SELECT * FROM streaming_sessions WHERE claude_session_id = ?');
return stmt.get(claudeSessionId) as StreamingSessionRow || null;
}
/**
* Get streaming session by SDK session ID
*/
getBySdkSessionId(sdkSessionId: string): StreamingSessionRow | null {
const stmt = this.db.query('SELECT * FROM streaming_sessions WHERE sdk_session_id = ?');
return stmt.get(sdkSessionId) as StreamingSessionRow || null;
}
/**
* Check if a streaming session exists by Claude session ID
*/
has(claudeSessionId: string): boolean {
const stmt = this.db.query('SELECT 1 FROM streaming_sessions WHERE claude_session_id = ? LIMIT 1');
return Boolean(stmt.get(claudeSessionId));
}
/**
* Get active streaming sessions for a project
*/
getActiveForProject(project: string): StreamingSessionRow[] {
const stmt = this.db.query(`
SELECT * FROM streaming_sessions
WHERE project = ? AND status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all(project) as StreamingSessionRow[];
}
/**
* Get all active streaming sessions
*/
getAllActive(): StreamingSessionRow[] {
const stmt = this.db.query(`
SELECT * FROM streaming_sessions
WHERE status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all() as StreamingSessionRow[];
}
/**
* Get recent streaming sessions (completed or failed)
*/
getRecent(limit = 10): StreamingSessionRow[] {
const stmt = this.db.query(`
SELECT * FROM streaming_sessions
ORDER BY started_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as StreamingSessionRow[];
}
/**
* Mark a session as completed
*/
markCompleted(id: number): StreamingSessionRow {
return this.update(id, {
status: 'completed',
completed_at: new Date()
});
}
/**
* Mark a session as failed
*/
markFailed(id: number): StreamingSessionRow {
return this.update(id, {
status: 'failed',
completed_at: new Date()
});
}
/**
* Delete a streaming session by ID
*/
deleteById(id: number): boolean {
const stmt = this.db.query('DELETE FROM streaming_sessions WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete a streaming session by Claude session ID
*/
deleteByClaudeSessionId(claudeSessionId: string): boolean {
const stmt = this.db.query('DELETE FROM streaming_sessions WHERE claude_session_id = ?');
const info = stmt.run(claudeSessionId);
return info.changes > 0;
}
/**
* Clean up old completed/failed sessions (older than N days)
*/
cleanupOldSessions(daysOld = 30): number {
const cutoffEpoch = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
const stmt = this.db.query(`
DELETE FROM streaming_sessions
WHERE status IN ('completed', 'failed')
AND completed_at_epoch < ?
`);
const info = stmt.run(cutoffEpoch);
return info.changes;
}
}
-107
View File
@@ -1,107 +0,0 @@
import { Database } from 'bun:sqlite';
import { getDatabase } from './Database.js';
import {
TranscriptEventInput,
TranscriptEventRow,
normalizeTimestamp
} from './types.js';
/**
* Data access for transcript_events table
*/
export class TranscriptEventStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Insert or update a transcript event
*/
upsert(event: TranscriptEventInput): TranscriptEventRow {
const { isoString, epoch } = normalizeTimestamp(event.captured_at);
const stmt = this.db.query(`
INSERT INTO transcript_events (
session_id,
project,
event_index,
event_type,
raw_json,
captured_at,
captured_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id, event_index) DO UPDATE SET
project = excluded.project,
event_type = excluded.event_type,
raw_json = excluded.raw_json,
captured_at = excluded.captured_at,
captured_at_epoch = excluded.captured_at_epoch
`);
stmt.run(
event.session_id,
event.project || null,
event.event_index,
event.event_type || null,
event.raw_json,
isoString,
epoch
);
return this.getBySessionAndIndex(event.session_id, event.event_index)!;
}
/**
* Bulk upsert events in a single transaction
*/
upsertMany(events: TranscriptEventInput[]): TranscriptEventRow[] {
const transaction = this.db.transaction((rows: TranscriptEventInput[]) => {
const results: TranscriptEventRow[] = [];
for (const row of rows) {
results.push(this.upsert(row));
}
return results;
});
return transaction(events);
}
/**
* Get event by session and index
*/
getBySessionAndIndex(sessionId: string, eventIndex: number): TranscriptEventRow | null {
const stmt = this.db.query(`
SELECT * FROM transcript_events
WHERE session_id = ? AND event_index = ?
`);
return stmt.get(sessionId, eventIndex) as TranscriptEventRow | null;
}
/**
* Get highest event_index stored for a session
*/
getMaxEventIndex(sessionId: string): number {
const stmt = this.db.query(`
SELECT MAX(event_index) as max_event_index
FROM transcript_events
WHERE session_id = ?
`);
const row = stmt.get(sessionId) as { max_event_index: number | null } | undefined;
return row?.max_event_index ?? -1;
}
/**
* List recent events for a session
*/
listBySession(sessionId: string, limit = 200, offset = 0): TranscriptEventRow[] {
const stmt = this.db.query(`
SELECT * FROM transcript_events
WHERE session_id = ?
ORDER BY event_index ASC
LIMIT ? OFFSET ?
`);
return stmt.all(sessionId, limit, offset) as TranscriptEventRow[];
}
}
-38
View File
@@ -1,14 +1,6 @@
// Export main components
export { DatabaseManager, getDatabase, initializeDatabase } from './Database.js';
// Export store classes
export { SessionStore } from './SessionStore.js';
export { MemoryStore } from './MemoryStore.js';
export { OverviewStore } from './OverviewStore.js';
export { DiagnosticsStore } from './DiagnosticsStore.js';
export { TranscriptEventStore } from './TranscriptEventStore.js';
export { StreamingSessionStore } from './StreamingSessionStore.js';
// Export hooks database
export { HooksDatabase } from './HooksDatabase.js';
@@ -17,33 +9,3 @@ export * from './types.js';
// Export migrations
export { migrations } from './migrations.js';
// Convenience function to get all stores
export async function createStores() {
const { DatabaseManager } = await import('./Database.js');
const { migrations } = await import('./migrations.js');
// Register migrations before initialization
const manager = DatabaseManager.getInstance();
for (const migration of migrations) {
manager.registerMigration(migration);
}
const db = await manager.initialize();
const { SessionStore } = await import('./SessionStore.js');
const { MemoryStore } = await import('./MemoryStore.js');
const { OverviewStore } = await import('./OverviewStore.js');
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
const { StreamingSessionStore } = await import('./StreamingSessionStore.js');
return {
sessions: new SessionStore(db),
memories: new MemoryStore(db),
overviews: new OverviewStore(db),
diagnostics: new DiagnosticsStore(db),
transcriptEvents: new TranscriptEventStore(db),
streamingSessions: new StreamingSessionStore(db)
};
}
-67
View File
@@ -1,67 +0,0 @@
/**
* Simple logging utility for claude-mem
*/
export interface LogLevel {
DEBUG: number;
INFO: number;
WARN: number;
ERROR: number;
}
const LOG_LEVELS: LogLevel = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
class Logger {
// <Block> 2.1 ====================================
private level: number = LOG_LEVELS.INFO;
setLevel(level: keyof LogLevel): void {
this.level = LOG_LEVELS[level];
}
// </Block> =======================================
// <Block> 2.2 ====================================
debug(message: string, ...args: any[]): void {
if (this.level <= LOG_LEVELS.DEBUG) {
console.debug(`[DEBUG] ${message}`, ...args);
}
}
// </Block> =======================================
// <Block> 2.3 ====================================
info(message: string, ...args: any[]): void {
if (this.level <= LOG_LEVELS.INFO) {
console.info(`[INFO] ${message}`, ...args);
}
}
// </Block> =======================================
// <Block> 2.4 ====================================
warn(message: string, ...args: any[]): void {
if (this.level <= LOG_LEVELS.WARN) {
console.warn(`[WARN] ${message}`, ...args);
}
}
// </Block> =======================================
// <Block> 2.5 ====================================
error(message: string, error?: any, context?: any): void {
if (this.level <= LOG_LEVELS.ERROR) {
console.error(`[ERROR] ${message}`);
if (error) {
console.error(error);
}
if (context) {
console.error('Context:', context);
}
}
}
// </Block> =======================================
}
export const log = new Logger();
-42
View File
@@ -1,42 +0,0 @@
import { appendFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { PathDiscovery } from '../services/path-discovery.js';
let logPath: string | null = null;
function ensureLogPath(): string {
if (logPath) {
return logPath;
}
const discovery = PathDiscovery.getInstance();
const logsDir = discovery.getLogsDirectory();
if (!existsSync(logsDir)) {
mkdirSync(logsDir, { recursive: true });
}
logPath = join(logsDir, 'rolling-memory.log');
return logPath;
}
export type RollingLogLevel = 'debug' | 'info' | 'warn' | 'error';
export function rollingLog(
level: RollingLogLevel,
message: string,
payload: Record<string, unknown> = {}
): void {
try {
const file = ensureLogPath();
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...payload
};
appendFileSync(file, `${JSON.stringify(entry)}\n`, 'utf8');
} catch {
// Logging should never throw user-facing errors
}
}
-87
View File
@@ -1,87 +0,0 @@
import { readSettings } from './settings.js';
export interface RollingSettings {
captureEnabled: boolean;
summaryEnabled: boolean;
sessionStartEnabled: boolean;
chunkTokenLimit: number;
chunkOverlapTokens: number;
summaryTurnLimit: number;
}
const DEFAULTS: RollingSettings = {
captureEnabled: true,
summaryEnabled: true,
sessionStartEnabled: true,
chunkTokenLimit: 600,
chunkOverlapTokens: 200,
summaryTurnLimit: 20
};
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
const lowered = value.toLowerCase();
if (lowered === 'true') return true;
if (lowered === 'false') return false;
}
return fallback;
}
function normalizeNumber(value: unknown, fallback: number): number {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
return parsed;
}
}
return fallback;
}
export function getRollingSettings(): RollingSettings {
const settings = readSettings();
return {
captureEnabled: normalizeBoolean(
settings.rollingCaptureEnabled,
DEFAULTS.captureEnabled
),
summaryEnabled: normalizeBoolean(
settings.rollingSummaryEnabled,
DEFAULTS.summaryEnabled
),
sessionStartEnabled: normalizeBoolean(
settings.rollingSessionStartEnabled,
DEFAULTS.sessionStartEnabled
),
chunkTokenLimit: normalizeNumber(
settings.rollingChunkTokens,
DEFAULTS.chunkTokenLimit
),
chunkOverlapTokens: normalizeNumber(
settings.rollingChunkOverlapTokens,
DEFAULTS.chunkOverlapTokens
),
summaryTurnLimit: normalizeNumber(
settings.rollingSummaryTurnLimit,
DEFAULTS.summaryTurnLimit
)
};
}
export function isRollingCaptureEnabled(): boolean {
return getRollingSettings().captureEnabled;
}
export function isRollingSummaryEnabled(): boolean {
return getRollingSettings().summaryEnabled;
}
export function isRollingSessionStartEnabled(): boolean {
return getRollingSettings().sessionStartEnabled;
}