Release v3.9.9

Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Alex Newman
2025-10-03 18:20:47 -04:00
parent 4d5b307a74
commit 85ed7c3d2f
85 changed files with 11156 additions and 7458 deletions
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
/**
* Shared configuration loader utility for Claude Memory hooks
* Loads CLI command name from config.json with proper fallback handling
*/
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync, existsSync } from 'fs';
/**
* Loads the CLI command name from the hooks config.json file
* @returns {string} The CLI command name (defaults to 'claude-mem')
*/
export function loadCliCommand() {
const __dirname = dirname(fileURLToPath(import.meta.url));
const configPath = join(__dirname, '..', 'config.json');
if (existsSync(configPath)) {
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
return config.cliCommand || 'claude-mem';
}
return 'claude-mem';
}
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env node
/**
* Hook Helper Functions
*
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
*/
import { spawn } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Creates a standardized hook response using the HookTemplates system
* @param {string} hookType - Type of hook ('PreCompact' or 'SessionStart')
* @param {boolean} success - Whether the operation was successful
* @param {Object} options - Additional options
* @returns {Object} Formatted hook response
*/
export function createHookResponse(hookType, success, options = {}) {
if (hookType === 'PreCompact') {
if (success) {
return {
continue: true,
suppressOutput: true
};
} else {
return {
continue: false,
stopReason: options.reason || 'Pre-compact operation failed',
suppressOutput: true
};
}
}
if (hookType === 'SessionStart') {
if (success && options.context) {
return {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: options.context
}
};
} else {
return {
continue: true,
suppressOutput: true
};
}
}
if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') {
return {
continue: true,
suppressOutput: true
};
}
if (hookType === 'Stop') {
return {
continue: true,
suppressOutput: true
};
}
// Generic response for unknown hook types
return {
continue: success,
suppressOutput: true,
...(options.reason && !success ? { stopReason: options.reason } : {})
};
}
/**
* Formats a session start context message using standardized templates
* @param {Object} contextData - Context information
* @returns {string} Formatted context message
*/
export function formatSessionStartContext(contextData) {
const {
projectName = 'unknown project',
memoryCount = 0,
lastSessionTime,
recentComponents = [],
recentDecisions = []
} = contextData;
const timeInfo = lastSessionTime ? ` (last worked: ${lastSessionTime})` : '';
const contextParts = [];
contextParts.push(`🧠 Loaded ${memoryCount} memories from previous sessions for ${projectName}${timeInfo}`);
if (recentComponents.length > 0) {
contextParts.push(`\n🎯 Recent components: ${recentComponents.slice(0, 3).join(', ')}`);
}
if (recentDecisions.length > 0) {
contextParts.push(`\n🔄 Recent decisions: ${recentDecisions.slice(0, 2).join(', ')}`);
}
if (memoryCount > 0) {
contextParts.push('\n💡 Use search_nodes("keywords") to find related work or open_nodes(["entity_name"]) to load specific components');
}
return contextParts.join('');
}
/**
* Executes a CLI command and returns the result
* @param {string} command - CLI command to execute
* @param {Array} args - Command arguments
* @param {Object} options - Spawn options
* @returns {Promise<{stdout: string, stderr: string, success: boolean}>}
*/
export async function executeCliCommand(command, args = [], options = {}) {
return new Promise((resolve) => {
const { input, ...spawnOptions } = options;
const process = spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'],
...spawnOptions
});
let stdout = '';
let stderr = '';
if (process.stdout) {
process.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (process.stderr) {
process.stderr.on('data', (data) => {
stderr += data.toString();
});
}
if (input && process.stdin) {
process.stdin.write(input);
process.stdin.end();
} else if (process.stdin) {
process.stdin.end();
}
process.on('close', (code) => {
resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
success: code === 0
});
});
process.on('error', (error) => {
resolve({
stdout: '',
stderr: error.message,
success: false
});
});
});
}
/**
* Parses context data from CLI output
* @param {string} output - Raw CLI output
* @returns {Object} Parsed context data
*/
export function parseContextData(output) {
if (!output || !output.trim()) {
return {
memoryCount: 0,
recentComponents: [],
recentDecisions: []
};
}
// Try to parse as JSON first (if CLI outputs structured data)
try {
const parsed = JSON.parse(output);
return {
memoryCount: parsed.memoryCount || 0,
recentComponents: parsed.recentComponents || [],
recentDecisions: parsed.recentDecisions || [],
lastSessionTime: parsed.lastSessionTime
};
} catch (e) {
// If not JSON, treat as plain text context
const lines = output.split('\n').filter(line => line.trim());
return {
memoryCount: lines.length,
recentComponents: [],
recentDecisions: [],
rawContext: output
};
}
}
/**
* Validates hook payload structure
* @param {Object} payload - Hook payload to validate
* @param {string} expectedHookType - Expected hook event name
* @returns {{valid: boolean, error?: string}} Validation result
*/
export function validateHookPayload(payload, expectedHookType) {
if (!payload || typeof payload !== 'object') {
return { valid: false, error: 'Payload must be a valid object' };
}
if (!payload.session_id || typeof payload.session_id !== 'string') {
return { valid: false, error: 'Missing or invalid session_id' };
}
if (!payload.transcript_path || typeof payload.transcript_path !== 'string') {
return { valid: false, error: 'Missing or invalid transcript_path' };
}
if (expectedHookType && payload.hook_event_name !== expectedHookType) {
return { valid: false, error: `Expected hook_event_name to be ${expectedHookType}` };
}
return { valid: true };
}
/**
* Logs debug information if debug mode is enabled
* @param {string} message - Debug message
* @param {Object} data - Additional data to log
*/
export function debugLog(message, data = {}) {
if (process.env.DEBUG === 'true' || process.env.CLAUDE_MEM_DEBUG === 'true') {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
}
}
@@ -0,0 +1,278 @@
// src/prompts/hook-prompts.config.ts
var HOOK_CONFIG = {
maxUserPromptLength: 200,
maxToolResponseLength: 20000,
sdk: {
model: "claude-sonnet-4-5",
allowedTools: ["Bash"],
maxTokensSystem: 8192,
maxTokensTool: 8192,
maxTokensEnd: 2048
}
};
var 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.`;
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
Tool: {{toolName}}
User Context: "{{userPrompt}}"
\`\`\`
{{toolResponse}}
\`\`\`
Analyze and store if meaningful.`;
var 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.`;
var PROMPTS = {
system: SYSTEM_PROMPT,
tool: TOOL_MESSAGE,
end: END_MESSAGE
};
// src/prompts/hook-prompt-renderer.ts
function substituteVariables(template, variables) {
let result = template;
for (const [key, value] of Object.entries(variables)) {
const placeholder = `{{${key}}}`;
result = result.split(placeholder).join(value);
}
return result;
}
function truncate(text, maxLength) {
if (text.length <= maxLength)
return text;
return text.slice(0, maxLength) + (text.length > maxLength ? "..." : "");
}
function formatTime(timestamp) {
const timePart = timestamp.split("T")[1];
if (!timePart)
return "";
return timePart.slice(0, 8);
}
function renderSystemPrompt(variables) {
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
return substituteVariables(PROMPTS.system, {
project: variables.project,
sessionId: variables.sessionId,
date: variables.date,
userPrompt: userPromptTruncated
});
}
function renderToolMessage(variables) {
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
const toolResponseTruncated = truncate(variables.toolResponse, HOOK_CONFIG.maxToolResponseLength);
const timeFormatted = formatTime(variables.timestamp);
return substituteVariables(PROMPTS.tool, {
toolName: variables.toolName,
toolResponse: toolResponseTruncated,
userPrompt: userPromptTruncated,
timestamp: variables.timestamp,
timeFormatted
});
}
function renderEndMessage(variables) {
return substituteVariables(PROMPTS.end, {
project: variables.project,
sessionId: variables.sessionId
});
}
function renderPrompt(type, variables) {
switch (type) {
case "system":
return renderSystemPrompt(variables);
case "tool":
return renderToolMessage(variables);
case "end":
return renderEndMessage(variables);
default:
throw new Error(`Unknown prompt type: ${type}`);
}
}
export {
renderToolMessage,
renderSystemPrompt,
renderPrompt,
renderEndMessage,
PROMPTS,
HOOK_CONFIG
};
@@ -0,0 +1,217 @@
// src/prompts/hook-prompts.config.ts
var HOOK_CONFIG = {
maxUserPromptLength: 200,
maxToolResponseLength: 20000,
sdk: {
model: "claude-sonnet-4-5",
allowedTools: ["Bash"],
maxTokensSystem: 8192,
maxTokensTool: 8192,
maxTokensEnd: 2048
}
};
var 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.`;
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
Tool: {{toolName}}
User Context: "{{userPrompt}}"
\`\`\`
{{toolResponse}}
\`\`\`
Analyze and store if meaningful.`;
var 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.`;
var PROMPTS = {
system: SYSTEM_PROMPT,
tool: TOOL_MESSAGE,
end: END_MESSAGE
};
export {
TOOL_MESSAGE,
SYSTEM_PROMPT,
PROMPTS,
HOOK_CONFIG,
END_MESSAGE
};
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* Path resolver utility for Claude Memory hooks
* Provides proper path handling using environment variables
*/
import { join, basename } from 'path';
import { homedir } from 'os';
/**
* Gets the base data directory for claude-mem
* @returns {string} Data directory path
*/
export function getDataDir() {
return process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
}
/**
* Gets the settings file path
* @returns {string} Settings file path
*/
export function getSettingsPath() {
return join(getDataDir(), 'settings.json');
}
/**
* Gets the archives directory path
* @returns {string} Archives directory path
*/
export function getArchivesDir() {
return process.env.CLAUDE_MEM_ARCHIVES_DIR || join(getDataDir(), 'archives');
}
/**
* Gets the logs directory path
* @returns {string} Logs directory path
*/
export function getLogsDir() {
return process.env.CLAUDE_MEM_LOGS_DIR || join(getDataDir(), 'logs');
}
/**
* Gets the compact flag file path
* @returns {string} Compact flag file path
*/
export function getCompactFlagPath() {
return join(getDataDir(), '.compact-running');
}
/**
* Gets the claude-mem package root directory
* @returns {Promise<string>} Package root path
*/
export async function getPackageRoot() {
// Method 1: Check if we're running from development
const devPath = join(homedir(), 'Scripts', 'claude-mem-source');
const { existsSync } = await import('fs');
if (existsSync(join(devPath, 'package.json'))) {
return devPath;
}
// Method 2: Follow the binary symlink
try {
const { execSync } = await import('child_process');
const { realpathSync } = await import('fs');
const binPath = execSync('which claude-mem', { encoding: 'utf8' }).trim();
const realBinPath = realpathSync(binPath);
// Binary is typically at package_root/dist/claude-mem.min.js
return join(realBinPath, '../..');
} catch {}
throw new Error('Cannot locate claude-mem package root');
}
/**
* Gets the project root directory
* Uses CLAUDE_PROJECT_DIR environment variable if available, otherwise falls back to cwd
* @returns {string} Project root path
*/
export function getProjectRoot() {
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
}
/**
* Derives project name from CLAUDE_PROJECT_DIR or current working directory
* Priority: CLAUDE_PROJECT_DIR > cwd parameter > process.cwd()
* @param {string} [cwd] - Optional current working directory from hook payload
* @returns {string} Project name (basename of project directory)
*/
export function getProjectName(cwd) {
const projectRoot = process.env.CLAUDE_PROJECT_DIR || cwd || process.cwd();
return basename(projectRoot);
}
/**
* Gets all common paths used by hooks
* @returns {Object} Object containing all common paths
*/
export function getPaths() {
return {
dataDir: getDataDir(),
settingsPath: getSettingsPath(),
archivesDir: getArchivesDir(),
logsDir: getLogsDir(),
compactFlagPath: getCompactFlagPath()
};
}