Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35b7aab174 | |||
| 2601215c91 | |||
| 4ebf0cad6b | |||
| 98d959112c | |||
| d01c2afaa6 | |||
| 8ebcb55b0d |
@@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.10] - 2025-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Claude Code statusline integration for real-time memory status
|
||||||
|
- MCP memory tools server providing compress, stats, search, and overview commands
|
||||||
|
- Concept documentation explaining memory compression and context loading
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Corrected integration architecture to use hooks instead of MCP SDK
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.9] - 2025-09-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Display current date and time at the top of session-start hook output for better temporal context
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Enhanced session-start hook formatting with emoji icons and separator lines for improved readability
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.8] - 2025-09-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed publish command failing when no version-related memories exist for changelog generation
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.6] - 2025-09-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Resolved compaction errors when processing large conversation histories by reducing chunk size limits to stay within Claude's context window
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.5] - 2025-09-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Session groups now display in chronological order (most recent first)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Improved CLI path detection for cross-platform compatibility
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.4] - 2025-09-13
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update save documentation to include allowed-tools and description metadata fields
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Remove deprecated markdown to JSONL migration script
|
||||||
|
|
||||||
|
|
||||||
## [3.6.3] - 2025-09-11
|
## [3.6.3] - 2025-09-11
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+7
-3
@@ -1,3 +1,7 @@
|
|||||||
Write an overview of the current conversation context and:
|
---
|
||||||
1. Add it to claude-mem using the chroma MCP tools
|
allowed-tools: Bash
|
||||||
2. Save the overview using the claude-mem CLI tool: `claude-mem save "your overview message"`
|
description: Write an overview and save with claude-mem
|
||||||
|
---
|
||||||
|
**Write an overview** of the current conversation context and:
|
||||||
|
1. **Add it to claude-mem** using the chroma MCP tools. Always use primitive types (strings, numbers, booleans) when calling MCP Chroma tools directly. Arrays should be comma-separated strings, and nested objects should be flattened.
|
||||||
|
2. **Save the overview to index** using the claude-mem CLI tool: `claude-mem save "your overview message"`
|
||||||
Vendored
+117
-115
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "3.6.3",
|
"version": "3.6.10",
|
||||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
@@ -393,8 +393,34 @@ Start searching now.` :
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (memories.length === 0) {
|
if (memories.length === 0) {
|
||||||
console.log('\n⚠️ No version-related memories found. Try compressing more sessions first.');
|
console.log('\n⚠️ No version-related memories found for this version.');
|
||||||
process.exit(1);
|
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`);
|
console.log(`✅ Found ${memories.length} version-related memories\n`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OptionValues } from 'commander';
|
import { OptionValues } from 'commander';
|
||||||
import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'fs';
|
import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'fs';
|
||||||
import { join, resolve, dirname } from 'path';
|
import { join, resolve, dirname } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir, platform } from 'os';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
@@ -95,7 +95,8 @@ async function validatePrerequisites(): Promise<boolean> {
|
|||||||
name: 'Claude Code CLI',
|
name: 'Claude Code CLI',
|
||||||
check: async () => {
|
check: async () => {
|
||||||
try {
|
try {
|
||||||
execSync('which claude', { stdio: 'ignore' });
|
const command = platform() === 'win32' ? 'where claude' : 'which claude';
|
||||||
|
execSync(command, { stdio: 'ignore' });
|
||||||
return { success: true, message: '' };
|
return { success: true, message: '' };
|
||||||
} catch {
|
} catch {
|
||||||
return { success: false, message: 'Claude Code CLI not found. Please install: https://docs.anthropic.com/claude/docs/claude-code' };
|
return { success: false, message: 'Claude Code CLI not found. Please install: https://docs.anthropic.com/claude/docs/claude-code' };
|
||||||
@@ -184,7 +185,8 @@ async function validatePrerequisites(): Promise<boolean> {
|
|||||||
// <Block> Claude binary path detection
|
// <Block> Claude binary path detection
|
||||||
function detectClaudePath(): string | null {
|
function detectClaudePath(): string | null {
|
||||||
try {
|
try {
|
||||||
const path = execSync('which claude', {
|
const command = platform() === 'win32' ? 'where claude' : 'which claude';
|
||||||
|
const path = execSync(command, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'ignore']
|
stdio: ['ignore', 'pipe', 'ignore']
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ import {
|
|||||||
outputSessionStartContent
|
outputSessionStartContent
|
||||||
} from '../prompts/templates/context/ContextTemplates.js';
|
} from '../prompts/templates/context/ContextTemplates.js';
|
||||||
|
|
||||||
interface IndexEntry {
|
|
||||||
summary: string;
|
|
||||||
entity: string;
|
|
||||||
keywords: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrashStatus {
|
interface TrashStatus {
|
||||||
folderCount: number;
|
folderCount: number;
|
||||||
fileCount: number;
|
fileCount: number;
|
||||||
@@ -23,6 +17,14 @@ interface TrashStatus {
|
|||||||
isEmpty: boolean;
|
isEmpty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
function formatSize(bytes: number): string {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -115,20 +117,25 @@ export async function loadContext(options: OptionValues = {}): Promise<void> {
|
|||||||
const sessions = jsonObjects.filter(obj => obj.type === 'session');
|
const sessions = jsonObjects.filter(obj => obj.type === 'session');
|
||||||
|
|
||||||
// Filter each type by project if specified
|
// Filter each type by project if specified
|
||||||
|
// Handle both hyphen and underscore formats since index has mixed entries
|
||||||
let filteredMemories = memories;
|
let filteredMemories = memories;
|
||||||
let filteredOverviews = overviews;
|
let filteredOverviews = overviews;
|
||||||
|
let filteredSessions = sessions;
|
||||||
if (options.project) {
|
if (options.project) {
|
||||||
filteredMemories = memories.filter(obj => obj.project === options.project);
|
const matchesProject = buildProjectMatcher(options.project);
|
||||||
filteredOverviews = overviews.filter(obj => obj.project === options.project);
|
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') {
|
if (options.format === 'session-start') {
|
||||||
// Get last 10 memories and last 5 overviews for session-start
|
// Get last 10 memories and last 5 overviews for session-start
|
||||||
const recentMemories = filteredMemories.slice(-10);
|
const recentMemories = filteredMemories.slice(-10);
|
||||||
const recentOverviews = filteredOverviews.slice(-5);
|
const recentOverviews = filteredOverviews.slice(-5);
|
||||||
|
const recentSessions = filteredSessions.slice(-5);
|
||||||
|
|
||||||
// Combine them for the display
|
// Combine them for the display
|
||||||
const recentObjects = [...recentMemories, ...recentOverviews];
|
const recentObjects = [...recentSessions, ...recentMemories, ...recentOverviews];
|
||||||
|
|
||||||
// Find most recent timestamp for last session info
|
// Find most recent timestamp for last session info
|
||||||
let lastSessionTime = 'recently';
|
let lastSessionTime = 'recently';
|
||||||
@@ -195,4 +202,4 @@ export async function loadContext(options: OptionValues = {}): Promise<void> {
|
|||||||
console.log(createUserFriendlyError('Context loading', errorMessage, 'Check file permissions and try again'));
|
console.log(createUserFriendlyError('Context loading', errorMessage, 'Check file permissions and try again'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-time migration script to convert claude-mem-index.md to claude-mem-index.jsonl
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { PathDiscovery } from '../services/path-discovery.js';
|
|
||||||
|
|
||||||
export function migrateToJSONL(): void {
|
|
||||||
const pathDiscovery = PathDiscovery.getInstance();
|
|
||||||
const oldIndexPath = path.join(pathDiscovery.getDataDirectory(), 'claude-mem-index.md');
|
|
||||||
const newIndexPath = pathDiscovery.getIndexPath();
|
|
||||||
|
|
||||||
// Check if old index exists
|
|
||||||
if (!fs.existsSync(oldIndexPath)) {
|
|
||||||
console.log('No markdown index found to migrate');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if new index already exists
|
|
||||||
if (fs.existsSync(newIndexPath)) {
|
|
||||||
console.log('JSONL index already exists, skipping migration');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Starting migration from MD to JSONL...');
|
|
||||||
|
|
||||||
const content = fs.readFileSync(oldIndexPath, 'utf-8');
|
|
||||||
const lines = content.split('\n').filter(line => line.trim());
|
|
||||||
|
|
||||||
const jsonlLines: string[] = [];
|
|
||||||
let currentSessionId = '';
|
|
||||||
let currentSessionTimestamp = '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// Parse session headers: # Session: <id> [<timestamp>]
|
|
||||||
const sessionMatch = line.match(/^# Session:\s*([^\[]+)(?:\s*\[([^\]]+)\])?/);
|
|
||||||
if (sessionMatch) {
|
|
||||||
currentSessionId = sessionMatch[1].trim();
|
|
||||||
currentSessionTimestamp = sessionMatch[2]?.trim() || new Date().toISOString();
|
|
||||||
|
|
||||||
// Extract project from session ID (assuming format like <project>_<uuid>)
|
|
||||||
const projectMatch = currentSessionId.match(/^([^_]+)_/);
|
|
||||||
const project = projectMatch ? projectMatch[1] : 'unknown';
|
|
||||||
|
|
||||||
jsonlLines.push(JSON.stringify({
|
|
||||||
type: 'session',
|
|
||||||
session_id: currentSessionId,
|
|
||||||
timestamp: currentSessionTimestamp,
|
|
||||||
project
|
|
||||||
}));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse overviews: **Overview:** <text>
|
|
||||||
const overviewMatch = line.match(/^\*\*Overview:\*\*\s*(.+)/);
|
|
||||||
if (overviewMatch) {
|
|
||||||
const overviewText = overviewMatch[1].trim();
|
|
||||||
|
|
||||||
// Extract project from current session ID
|
|
||||||
const projectMatch = currentSessionId.match(/^([^_]+)_/);
|
|
||||||
const project = projectMatch ? projectMatch[1] : 'unknown';
|
|
||||||
|
|
||||||
jsonlLines.push(JSON.stringify({
|
|
||||||
type: 'overview',
|
|
||||||
content: overviewText,
|
|
||||||
session_id: currentSessionId,
|
|
||||||
project,
|
|
||||||
timestamp: currentSessionTimestamp
|
|
||||||
}));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip certain lines
|
|
||||||
if (line.startsWith('# NO SUMMARIES EXTRACTED')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse memory entries (pipe-separated)
|
|
||||||
if (line.includes(' | ')) {
|
|
||||||
const parts = line.split(' | ').map(p => p.trim());
|
|
||||||
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
const [text, document_id, keywords, timestamp, archive] = parts;
|
|
||||||
|
|
||||||
// Extract project from document_id (format: <project>_<session>_<number>)
|
|
||||||
const projectMatch = document_id?.match(/^([^_]+)_/);
|
|
||||||
const project = projectMatch ? projectMatch[1] : 'unknown';
|
|
||||||
|
|
||||||
jsonlLines.push(JSON.stringify({
|
|
||||||
type: 'memory',
|
|
||||||
text,
|
|
||||||
document_id: document_id || `${currentSessionId}_${Date.now()}`,
|
|
||||||
keywords: keywords || '',
|
|
||||||
session_id: currentSessionId,
|
|
||||||
project,
|
|
||||||
timestamp: timestamp || currentSessionTimestamp,
|
|
||||||
archive: archive || `${currentSessionId}.jsonl.archive`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write JSONL file
|
|
||||||
fs.writeFileSync(newIndexPath, jsonlLines.join('\n') + '\n');
|
|
||||||
|
|
||||||
// Backup old index
|
|
||||||
const backupPath = oldIndexPath + '.backup';
|
|
||||||
fs.renameSync(oldIndexPath, backupPath);
|
|
||||||
|
|
||||||
console.log(`✅ Migration complete!`);
|
|
||||||
console.log(` - Converted ${jsonlLines.length} entries`);
|
|
||||||
console.log(` - New index: ${newIndexPath}`);
|
|
||||||
console.log(` - Backup: ${backupPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run if called directly
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
||||||
migrateToJSONL();
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,11 @@ function generateSessionId(message: string): string {
|
|||||||
* Save command - stores a message to both Chroma collection and JSONL index
|
* Save command - stores a message to both Chroma collection and JSONL index
|
||||||
*/
|
*/
|
||||||
export async function save(message: string, options: OptionValues = {}): Promise<void> {
|
export async function save(message: string, options: OptionValues = {}): Promise<void> {
|
||||||
|
// Debug: Log what we receive
|
||||||
|
appendFileSync('/Users/alexnewman/.claude-mem/save-debug.log',
|
||||||
|
`[${new Date().toISOString()}] Received message: "${message}" (type: ${typeof message}, length: ${message?.length})\n`,
|
||||||
|
'utf8');
|
||||||
|
|
||||||
if (!message || message.trim() === '') {
|
if (!message || message.trim() === '') {
|
||||||
console.error('Error: Message is required');
|
console.error('Error: Message is required');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export interface ChunkedMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ChunkManager {
|
export class ChunkManager {
|
||||||
private static readonly DEFAULT_MAX_TOKENS = 28000;
|
private static readonly DEFAULT_MAX_TOKENS = 22400; // Reduced by 20% from 28000
|
||||||
private static readonly DEFAULT_MAX_BYTES = 98000;
|
private static readonly DEFAULT_MAX_BYTES = 78400; // Reduced by 20% from 98000
|
||||||
private static readonly DEFAULT_CONTEXT_OVERLAP = 2;
|
private static readonly DEFAULT_CONTEXT_OVERLAP = 2;
|
||||||
private static readonly CHARS_PER_TOKEN_ESTIMATE = 3.5;
|
private static readonly CHARS_PER_TOKEN_ESTIMATE = 3.5;
|
||||||
|
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ export class PromptOrchestrator {
|
|||||||
projectName = this.projectName,
|
projectName = this.projectName,
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
// Extract project prefix from project name (convert to snake_case)
|
// Use project name as-is for consistency with directory names
|
||||||
const projectPrefix = projectName.replace(/[-\s]/g, '_').toLowerCase();
|
const projectPrefix = projectName;
|
||||||
|
|
||||||
// Use the simple prompt with the transcript included
|
// Use the simple prompt with the transcript included
|
||||||
return createAnalysisPrompt(
|
return createAnalysisPrompt(
|
||||||
|
|||||||
@@ -559,9 +559,29 @@ export function outputSessionStartContent(params: {
|
|||||||
const { projectName, memoryCount, lastSessionTime, recentObjects } = params;
|
const { projectName, memoryCount, lastSessionTime, recentObjects } = params;
|
||||||
const width = getWrapWidth();
|
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
|
// Extract overviews for user display - get more to show session grouping
|
||||||
const overviews = extractOverviews(recentObjects, 10, projectName);
|
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
|
// Process memory entries for Claude context
|
||||||
const memories = processMemoryEntries(recentObjects);
|
const memories = processMemoryEntries(recentObjects);
|
||||||
// Helper to split and normalize keywords into a map (lowercased -> original)
|
// Helper to split and normalize keywords into a map (lowercased -> original)
|
||||||
@@ -613,6 +633,14 @@ export function outputSessionStartContent(params: {
|
|||||||
// Overview section at bottom with session grouping
|
// Overview section at bottom with session grouping
|
||||||
if (overviews.length > 0) {
|
if (overviews.length > 0) {
|
||||||
const sessionGroups = groupOverviewsBySession(overviews);
|
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('');
|
||||||
|
|
||||||
console.log(wrapText('🧠 Overviews', width));
|
console.log(wrapText('🧠 Overviews', width));
|
||||||
|
|||||||
Reference in New Issue
Block a user