Release v3.6.3

Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Alex Newman
2025-09-11 17:15:50 -04:00
parent c4eb2e2dc9
commit 97807494fd
43 changed files with 10632 additions and 286 deletions
+51
View File
@@ -0,0 +1,51 @@
import { readFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// <Block> 5.1 ====================================
// Default values
const DEFAULT_PACKAGE_NAME = 'claude-mem';
// This MUST be replaced by build process with --define flag
// @ts-ignore
// For development, use fallback
const DEFAULT_PACKAGE_VERSION = typeof __DEFAULT_PACKAGE_VERSION__ !== 'undefined'
? __DEFAULT_PACKAGE_VERSION__
: '3.5.6-dev';
const DEFAULT_PACKAGE_DESCRIPTION = 'Memory compression system for Claude Code - persist context across sessions';
let packageName = DEFAULT_PACKAGE_NAME;
let packageVersion = DEFAULT_PACKAGE_VERSION;
let packageDescription = DEFAULT_PACKAGE_DESCRIPTION;
// </Block> =======================================
// Try to read package.json if it exists (for development)
// <Block> 5.2 ====================================
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
// <Block> 5.2a ====================================
if (existsSync(packageJsonPath)) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
// <Block> 5.2b ====================================
packageName = packageJson.name || DEFAULT_PACKAGE_NAME;
packageVersion = packageJson.version || DEFAULT_PACKAGE_VERSION;
packageDescription = packageJson.description || DEFAULT_PACKAGE_DESCRIPTION;
// </Block> =======================================
}
// </Block> =======================================
} catch {
// Use defaults if package.json can't be read
}
// </Block> =======================================
// <Block> 5.3 ====================================
// Export package configuration
export const PACKAGE_NAME = packageName;
export const PACKAGE_VERSION = packageVersion;
export const PACKAGE_DESCRIPTION = packageDescription;
// Export commonly used names
export const CLI_NAME = PACKAGE_NAME; // The CLI command name
// </Block> =======================================
+200
View File
@@ -0,0 +1,200 @@
import { existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { HookError, CompressionError, Logger, FileLogger } from './types.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class ErrorHandler {
private logger: Logger;
private logDir: string;
// <Block> 7.1 ====================================
constructor(enableDebug = false) {
this.logDir = join(__dirname, '..', 'logs');
this.ensureLogDirectory();
const logFile = join(
this.logDir,
`claude-mem-${new Date().toISOString().slice(0, 10)}.log`
);
this.logger = new FileLogger(logFile, enableDebug);
}
// </Block> =======================================
// <Block> 7.2 ====================================
private ensureLogDirectory(): void {
if (!existsSync(this.logDir)) {
mkdirSync(this.logDir, { recursive: true });
}
}
// </Block> =======================================
// <Block> 7.3 ====================================
handleHookError(error: Error, hookType: string, payload?: unknown): never {
// <Block> 7.3a ====================================
const hookError =
error instanceof HookError
? error
: new HookError(
error.message,
hookType,
payload as any,
'HOOK_EXECUTION_ERROR'
);
// </Block> =======================================
this.logger.error(`Hook execution failed in ${hookType}`, hookError, {
hookType,
payload: payload ? JSON.stringify(payload) : undefined,
});
console.log(
JSON.stringify({
continue: false,
stopReason: `Hook error: ${hookError.message}`,
error: {
type: hookError.name,
message: hookError.message,
code: hookError.code,
},
})
);
process.exit(1);
}
// </Block> =======================================
// <Block> 7.4 ====================================
handleCompressionError(
error: Error,
transcriptPath: string,
stage: string
): never {
// <Block> 7.4a ====================================
const compressionError =
error instanceof CompressionError
? error
: new CompressionError(error.message, transcriptPath, stage as any);
// </Block> =======================================
this.logger.error(`Compression failed during ${stage}`, compressionError, {
transcriptPath,
stage,
});
console.error(`Compression error: ${compressionError.message}`);
console.error(`Stage: ${stage}`);
console.error(`Transcript: ${transcriptPath}`);
process.exit(1);
}
// </Block> =======================================
// <Block> 7.5 ====================================
handleValidationError(
message: string,
context?: Record<string, unknown>
): never {
this.logger.error('Validation error', undefined, { message, context });
console.error(`Validation error: ${message}`);
// <Block> 7.5a ====================================
if (context) {
console.error('Context:', JSON.stringify(context, null, 2));
}
// </Block> =======================================
process.exit(1);
}
// </Block> =======================================
// <Block> 7.6 ====================================
logSuccess(operation: string, details?: Record<string, unknown>): void {
this.logger.info(`Operation successful: ${operation}`, details);
}
// </Block> =======================================
// <Block> 7.7 ====================================
logWarning(message: string, details?: Record<string, unknown>): void {
this.logger.warn(message, details);
}
// </Block> =======================================
// <Block> 7.8 ====================================
logDebug(message: string, details?: Record<string, unknown>): void {
this.logger.debug(message, details);
}
// </Block> =======================================
}
// <Block> 7.9 ====================================
export function parseStdinJson<T = unknown>(input: string): T {
try {
return JSON.parse(input) as T;
} catch (error) {
throw new Error(
`Failed to parse JSON input: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
// </Block> =======================================
// <Block> 7.10 ===================================
export async function safeExecute<T>(
operation: () => Promise<T>,
errorHandler: ErrorHandler,
context: string
): Promise<T> {
try {
return await operation();
} catch (error) {
const message = `Safe execution failed in ${context}: ${error instanceof Error ? error.message : String(error)}`;
errorHandler.handleValidationError(message, { context, error });
throw error;
}
}
// </Block> =======================================
// <Block> 7.11 ===================================
export function validateFileExists(
filePath: string,
errorHandler: ErrorHandler
): void {
if (!existsSync(filePath)) {
errorHandler.handleValidationError(`File not found: ${filePath}`, {
filePath,
});
}
}
// </Block> =======================================
// <Block> 7.12 ===================================
/**
* Creates a standardized hook response using HookTemplates
* @deprecated Use HookTemplates.createHookSuccessResponse or createHookErrorResponse instead
* This function is maintained for backward compatibility but should be replaced with HookTemplates.
*/
export function createHookResponse(
success: boolean,
data?: Record<string, unknown>
): string {
// Log deprecation warning in development mode
if (process.env.NODE_ENV === 'development') {
console.warn('createHookResponse in error-handler.ts is deprecated. Use HookTemplates.createHookSuccessResponse or createHookErrorResponse instead.');
}
const response = {
continue: success,
suppressOutput: true, // Add standard suppressOutput field for Claude Code compatibility
...data,
};
return JSON.stringify(response);
}
// </Block> =======================================
export const globalErrorHandler = new ErrorHandler(
process.env.DEBUG === 'true'
);
+67
View File
@@ -0,0 +1,67 @@
/**
* 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();
+91
View File
@@ -0,0 +1,91 @@
import { sep, basename } from 'path';
import { PathDiscovery } from '../services/path-discovery.js';
/**
* PathResolver utility for managing claude-mem file system paths
* Now delegates to PathDiscovery service for centralized path management
*/
export class PathResolver {
private pathDiscovery: PathDiscovery;
// <Block> 1.1 ====================================
constructor() {
this.pathDiscovery = PathDiscovery.getInstance();
}
// </Block> =======================================
// <Block> 1.2 ====================================
getConfigDir(): string {
return this.pathDiscovery.getDataDirectory();
}
// </Block> =======================================
// <Block> 1.3 ====================================
getIndexDir(): string {
return this.pathDiscovery.getIndexDirectory();
}
// </Block> =======================================
// <Block> 1.4 ====================================
getIndexPath(): string {
return this.pathDiscovery.getIndexPath();
}
// </Block> =======================================
// <Block> 1.5 ====================================
getArchiveDir(): string {
return this.pathDiscovery.getArchivesDirectory();
}
// </Block> =======================================
// <Block> 1.6 ====================================
getProjectArchiveDir(projectName: string): string {
return this.pathDiscovery.getProjectArchiveDirectory(projectName);
}
// </Block> =======================================
// <Block> 1.7 ====================================
getLogsDir(): string {
return this.pathDiscovery.getLogsDirectory();
}
// </Block> =======================================
// <Block> 1.8 ====================================
static ensureDirectory(dirPath: string): void {
PathDiscovery.getInstance().ensureDirectory(dirPath);
}
// </Block> =======================================
// <Block> 1.9 ====================================
static ensureDirectories(dirPaths: string[]): void {
PathDiscovery.getInstance().ensureDirectories(dirPaths);
}
// </Block> =======================================
// <Block> 1.10 ===================================
static extractProjectName(transcriptPath: string): string {
return PathDiscovery.extractProjectName(transcriptPath);
}
// <Block> 1.11 ===================================
/**
* DRY utility function: Canonical source for getting the current project prefix
* Replaces all instances of path.basename(process.cwd()) across the codebase
* @returns The current project directory name, sanitized for use as a prefix
*/
static getCurrentProjectPrefix(): string {
return PathDiscovery.getCurrentProjectName();
}
// </Block> =======================================
// <Block> 1.12 ===================================
/**
* DRY utility function: Gets raw project name without sanitization
* For use in contexts where original directory name is needed (e.g., display)
* @returns The current project directory name as-is
*/
static getCurrentProjectName(): string {
return PathDiscovery.getCurrentProjectName();
}
// </Block> =======================================
}
+98
View File
@@ -0,0 +1,98 @@
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { PathResolver } from './paths.js';
import type { Settings } from './types.js';
/**
* Settings utilities for managing ~/.claude-mem/settings.json
*/
export class SettingsManager {
private static settingsPath: string;
private static cachedSettings: Settings | null = null;
static {
const pathResolver = new PathResolver();
this.settingsPath = join(pathResolver.getConfigDir(), 'settings.json');
}
/**
* Safely read settings.json with error handling
* Returns empty object if file doesn't exist or is malformed
*/
static readSettings(): Settings {
// Return cached settings if available
if (this.cachedSettings !== null) {
return this.cachedSettings;
}
try {
if (existsSync(this.settingsPath)) {
const content = readFileSync(this.settingsPath, 'utf-8');
const settings = JSON.parse(content) as Settings;
this.cachedSettings = settings;
return settings;
}
} catch {
// File is malformed or unreadable - return empty settings
}
// File doesn't exist or failed to read
const emptySettings: Settings = {};
this.cachedSettings = emptySettings;
return emptySettings;
}
/**
* Get a specific setting value with optional fallback
*/
static getSetting<K extends keyof Settings>(
key: K,
fallback?: Settings[K]
): Settings[K] | undefined {
const settings = this.readSettings();
return settings[key] ?? fallback;
}
/**
* Get the Claude binary path from settings
* Falls back to 'claude' if not found or settings don't exist
*/
static getClaudePath(): string {
const claudePath = this.getSetting('claudePath', 'claude');
return claudePath as string;
}
/**
* Clear cached settings (useful for testing or after settings changes)
*/
static clearCache(): void {
this.cachedSettings = null;
}
}
/**
* Convenience function to get Claude binary path
* Can be imported directly for simple use cases
*/
export function getClaudePath(): string {
return SettingsManager.getClaudePath();
}
/**
* Convenience function to read all settings
* Can be imported directly for simple use cases
*/
export function readSettings(): Settings {
return SettingsManager.readSettings();
}
/**
* Convenience function to get a specific setting
* Can be imported directly for simple use cases
*/
export function getSetting<K extends keyof Settings>(
key: K,
fallback?: Settings[K]
): Settings[K] | undefined {
return SettingsManager.getSetting(key, fallback);
}
+263
View File
@@ -0,0 +1,263 @@
export interface HookPayload {
session_id: string;
transcript_path: string;
hook_event_name: string;
}
export interface PreCompactPayload extends HookPayload {
hook_event_name: 'PreCompact';
trigger: 'manual' | 'auto';
custom_instructions?: string;
}
export interface SessionStartPayload extends HookPayload {
hook_event_name: 'SessionStart';
source: 'startup' | 'compact' | 'vscode' | 'web';
}
export interface UserPromptSubmitPayload extends HookPayload {
hook_event_name: 'UserPromptSubmit';
prompt: string;
cwd: string;
}
export interface PreToolUsePayload extends HookPayload {
hook_event_name: 'PreToolUse';
tool_name: string;
tool_input: Record<string, unknown>;
}
export interface PostToolUsePayload extends HookPayload {
hook_event_name: 'PostToolUse';
tool_name: string;
tool_input: Record<string, unknown>;
tool_response: Record<string, unknown> & {
success?: boolean;
};
}
export interface NotificationPayload extends HookPayload {
hook_event_name: 'Notification';
message: string;
title?: string;
}
export interface StopPayload extends HookPayload {
hook_event_name: 'Stop';
stop_hook_active: boolean;
}
export interface BaseHookResponse {
continue?: boolean;
stopReason?: string;
suppressOutput?: boolean;
}
export interface PreCompactResponse extends BaseHookResponse {
decision?: 'approve' | 'block';
reason?: string;
}
export interface SessionStartResponse extends BaseHookResponse {
hookSpecificOutput?: {
hookEventName: 'SessionStart';
additionalContext?: string;
};
}
export interface PreToolUseResponse extends BaseHookResponse {
permissionDecision?: 'allow' | 'deny' | 'ask';
permissionDecisionReason?: string;
}
export interface CompressionResult {
compressedLines: string[];
originalTokens: number;
compressedTokens: number;
compressionRatio: number;
memoryNodes: string[];
}
export interface MemoryNode {
id: string;
type: 'document';
content: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
export class HookError extends Error {
constructor(
message: string,
public hookType: string,
public payload?: HookPayload,
public code?: string
) {
super(message);
this.name = 'HookError';
}
}
export class CompressionError extends Error {
constructor(
message: string,
public transcriptPath: string,
public stage: 'reading' | 'analyzing' | 'compressing' | 'writing'
) {
super(message);
this.name = 'CompressionError';
}
}
export interface Logger {
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string, error?: Error, meta?: Record<string, unknown>): void;
debug(message: string, meta?: Record<string, unknown>): void;
}
export class FileLogger implements Logger {
constructor(
private logFile: string,
private enableDebug = false
) {}
info(message: string, meta?: Record<string, unknown>): void {
this.log('INFO', message, meta);
}
warn(message: string, meta?: Record<string, unknown>): void {
this.log('WARN', message, meta);
}
error(message: string, error?: Error, meta?: Record<string, unknown>): void {
const errorMeta = error ? { error: error.message, stack: error.stack } : {};
this.log('ERROR', message, { ...meta, ...errorMeta });
}
debug(message: string, meta?: Record<string, unknown>): void {
if (this.enableDebug) {
this.log('DEBUG', message, meta);
}
}
private log(
level: string,
message: string,
meta?: Record<string, unknown>
): void {
const timestamp = new Date().toISOString();
const metaStr = meta ? ` ${JSON.stringify(meta)}` : '';
const logLine = `[${timestamp}] ${level}: ${message}${metaStr}\n`;
console.error(logLine);
}
}
export function validateHookPayload(
payload: unknown,
expectedType: string
): HookPayload {
if (!payload || typeof payload !== 'object') {
throw new HookError(
`Invalid payload: expected object, got ${typeof payload}`,
expectedType
);
}
const hookPayload = payload as Record<string, unknown>;
if (!hookPayload.session_id || typeof hookPayload.session_id !== 'string') {
throw new HookError(
'Missing or invalid session_id',
expectedType,
hookPayload as unknown as HookPayload
);
}
if (
!hookPayload.transcript_path ||
typeof hookPayload.transcript_path !== 'string'
) {
throw new HookError(
'Missing or invalid transcript_path',
expectedType,
hookPayload as unknown as HookPayload
);
}
return hookPayload as unknown as HookPayload;
}
export function createSuccessResponse(
additionalData?: Record<string, unknown>
): BaseHookResponse {
return {
continue: true,
...additionalData,
};
}
export function createErrorResponse(
reason: string,
additionalData?: Record<string, unknown>
): BaseHookResponse {
return {
continue: false,
stopReason: reason,
...additionalData,
};
}
// =============================================================================
// SETTINGS AND CONFIGURATION TYPES
// =============================================================================
/**
* Main settings interface for claude-mem configuration
*/
export interface Settings {
autoCompress?: boolean;
projectName?: string;
installed?: boolean;
backend?: string;
embedded?: boolean;
saveMemoriesOnClear?: boolean;
claudePath?: string;
[key: string]: unknown; // Allow additional properties
}
// =============================================================================
// MCP CLIENT INTERFACE TYPES
// =============================================================================
/**
* Document structure for MCP operations
*/
export interface MCPDocument {
id: string;
content: string;
metadata?: Record<string, unknown>;
}
/**
* Search result structure from MCP operations
*/
export interface MCPSearchResult {
documents?: MCPDocument[];
ids?: string[];
metadatas?: Record<string, unknown>[];
distances?: number[];
[key: string]: unknown;
}
/**
* Interface for MCP client implementations (Chroma-based)
*/
export interface IMCPClient {
connect(): Promise<void>;
disconnect(): Promise<void>;
addDocuments(documents: MCPDocument[]): Promise<void>;
queryDocuments(query: string, limit?: number): Promise<MCPSearchResult>;
getDocuments(ids?: string[]): Promise<MCPSearchResult>;
}