Release v3.9.9
Published from npm package build Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
@@ -1,200 +0,0 @@
|
||||
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'
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
}
|
||||
+19
-234
@@ -1,103 +1,17 @@
|
||||
export interface HookPayload {
|
||||
session_id: string;
|
||||
transcript_path: string;
|
||||
hook_event_name: string;
|
||||
}
|
||||
/**
|
||||
* Core Type Definitions
|
||||
*
|
||||
* Minimal type definitions for the claude-mem system.
|
||||
* Only includes types that are actively imported and used.
|
||||
*/
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// ERROR CLASSES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Custom error class for compression failures
|
||||
*/
|
||||
export class CompressionError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -109,108 +23,8 @@ export class CompressionError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// CONFIGURATION TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
@@ -224,40 +38,11 @@ export interface Settings {
|
||||
embedded?: boolean;
|
||||
saveMemoriesOnClear?: boolean;
|
||||
claudePath?: string;
|
||||
rollingCaptureEnabled?: boolean;
|
||||
rollingSummaryEnabled?: boolean;
|
||||
rollingSessionStartEnabled?: boolean;
|
||||
rollingChunkTokens?: number;
|
||||
rollingChunkOverlapTokens?: number;
|
||||
rollingSummaryTurnLimit?: number;
|
||||
[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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user