feat: remove install, logs, restore, status, trash, and uninstall commands
- Deleted the install.ts command file, removing the installation logic for the Claude Memory System. - Removed logs.ts command file, eliminating the log viewing functionality. - Deleted restore.ts command file, which handled restoring files from trash. - Removed status.ts command file, which provided system status checks. - Deleted trash-empty.ts and trash-view.ts command files, removing trash management features. - Removed trash.ts command file, which handled moving files to trash. - Deleted uninstall.ts command file, eliminating the uninstallation process for the memory system. - Updated new.ts hook to enforce plugin mode for Claude Code integration. - Cleaned up config.ts by removing unused export for CLI_NAME.
This commit is contained in:
-248
@@ -1,248 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
// CLI Dependencies and Imports Setup
|
||||
// Natural pattern: Import what you need before using it
|
||||
import { Command } from 'commander';
|
||||
import { PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_DESCRIPTION } from '../shared/config.js';
|
||||
|
||||
// Import command handlers
|
||||
import { install } from '../commands/install.js';
|
||||
import { uninstall } from '../commands/uninstall.js';
|
||||
import { logs } from '../commands/logs.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 { doctor } from '../commands/doctor.js';
|
||||
import { status } from '../commands/status.js';
|
||||
|
||||
const program = new Command();
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.2 ====================================
|
||||
// Program Configuration
|
||||
// Natural pattern: Configure program metadata first
|
||||
program
|
||||
.name(PACKAGE_NAME)
|
||||
.description(PACKAGE_DESCRIPTION)
|
||||
.version(PACKAGE_VERSION);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
// Install Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Install command
|
||||
program
|
||||
.command('install')
|
||||
.description('Install Claude Code hooks for automatic compression')
|
||||
.option('--user', 'Install for current user (default)')
|
||||
.option('--project', 'Install for current project only')
|
||||
.option('--local', 'Install to custom local directory')
|
||||
.option('--path <path>', 'Custom installation path (with --local)')
|
||||
.option('--timeout <ms>', 'Hook execution timeout in milliseconds', '180000')
|
||||
.option('--skip-mcp', 'Skip Chroma MCP server installation')
|
||||
.option('--force', 'Force installation even if already installed')
|
||||
.action(install);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.5 ====================================
|
||||
// Uninstall Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Uninstall command
|
||||
program
|
||||
.command('uninstall')
|
||||
.description('Remove Claude Code hooks')
|
||||
.option('--user', 'Remove from user settings (default)')
|
||||
.option('--project', 'Remove from project settings')
|
||||
.option('--all', 'Remove from both user and project settings')
|
||||
.action(uninstall);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.6 ====================================
|
||||
// Logs Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Logs command
|
||||
program
|
||||
.command('logs')
|
||||
.description('View claude-mem operation logs')
|
||||
.option('--debug', 'Show debug logs only')
|
||||
.option('--error', 'Show error logs only')
|
||||
.option('--tail [n]', 'Show last n lines', '50')
|
||||
.option('--follow', 'Follow log output')
|
||||
.action(logs);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.8 ====================================
|
||||
// Trash and Restore Commands Definition
|
||||
// Natural pattern: Define commands for safe file operations
|
||||
|
||||
// Trash command with subcommands
|
||||
const trashCmd = program
|
||||
.command('trash')
|
||||
.description('Manage trash bin for safe file deletion')
|
||||
.argument('[files...]', 'Files to move to trash')
|
||||
.option('-r, --recursive', 'Remove directories recursively')
|
||||
.option('-R', 'Remove directories recursively (same as -r)')
|
||||
.option('-f, --force', 'Suppress errors for nonexistent files')
|
||||
.action(async (files: string[] | undefined, options: any) => {
|
||||
// If no files provided, show help
|
||||
if (!files || files.length === 0) {
|
||||
trashCmd.outputHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Map -R to recursive
|
||||
if (options.R) options.recursive = true;
|
||||
|
||||
await trash(files, {
|
||||
force: options.force,
|
||||
recursive: options.recursive
|
||||
});
|
||||
});
|
||||
|
||||
// Trash view subcommand
|
||||
trashCmd
|
||||
.command('view')
|
||||
.description('View contents of trash bin')
|
||||
.action(viewTrash);
|
||||
|
||||
// Trash empty subcommand
|
||||
trashCmd
|
||||
.command('empty')
|
||||
.description('Permanently delete all files in trash')
|
||||
.option('-f, --force', 'Skip confirmation prompt')
|
||||
.action(emptyTrash);
|
||||
|
||||
// Restore command
|
||||
program
|
||||
.command('restore')
|
||||
.description('Restore files from trash interactively')
|
||||
.action(restore);
|
||||
|
||||
// Doctor command
|
||||
program
|
||||
.command('doctor')
|
||||
.description('Run health checks on claude-mem installation')
|
||||
.option('--json', 'Output results as JSON')
|
||||
.action(doctor);
|
||||
|
||||
// Status command
|
||||
program
|
||||
.command('status')
|
||||
.description('Show claude-mem system status')
|
||||
.action(status);
|
||||
// </Block> =======================================
|
||||
|
||||
// <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')
|
||||
.description('SessionStart hook - show recent session context')
|
||||
.action(async () => {
|
||||
try {
|
||||
const { contextHook } = await import('../hooks/index.js');
|
||||
const input = await readStdin();
|
||||
const data = input.trim() ? JSON.parse(input) : undefined;
|
||||
contextHook(data);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem context] Error: ${error.message}`);
|
||||
process.exit(0); // Exit gracefully to avoid blocking Claude Code
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('new')
|
||||
.description('UserPromptSubmit hook - initialize SDK session')
|
||||
.action(async () => {
|
||||
try {
|
||||
const { newHook } = await import('../hooks/index.js');
|
||||
const input = await readStdin();
|
||||
const data = input.trim() ? JSON.parse(input) : undefined;
|
||||
newHook(data);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem new] Error: ${error.message}`);
|
||||
process.exit(0); // Exit gracefully to avoid blocking Claude Code
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('save')
|
||||
.description('PostToolUse hook - queue observation')
|
||||
.action(async () => {
|
||||
try {
|
||||
const { saveHook } = await import('../hooks/index.js');
|
||||
const input = await readStdin();
|
||||
const data = input.trim() ? JSON.parse(input) : undefined;
|
||||
saveHook(data);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem save] Error: ${error.message}`);
|
||||
process.exit(0); // Exit gracefully to avoid blocking Claude Code
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('summary')
|
||||
.description('Stop hook - finalize session')
|
||||
.action(async () => {
|
||||
try {
|
||||
const { summaryHook } = await import('../hooks/index.js');
|
||||
const input = await readStdin();
|
||||
const data = input.trim() ? JSON.parse(input) : undefined;
|
||||
summaryHook(data);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem summary] Error: ${error.message}`);
|
||||
process.exit(0); // Exit gracefully to avoid blocking Claude Code
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('worker <sessionId>')
|
||||
.description('Run SDK worker process (internal use)')
|
||||
.action(async (sessionId: string) => {
|
||||
try {
|
||||
// Import and run the worker main function
|
||||
const { main } = await import('../sdk/worker.js');
|
||||
// Set process.argv so worker can parse sessionId
|
||||
process.argv[2] = sessionId;
|
||||
await main();
|
||||
} catch (error: any) {
|
||||
console.error(`[SDK Worker] Fatal error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to read stdin (Bun-compatible)
|
||||
async function readStdin(): Promise<string> {
|
||||
// Use Bun's native stdin.text() if available, otherwise use Node.js streams
|
||||
if (typeof Bun !== 'undefined' && Bun.stdin) {
|
||||
return await Bun.stdin.text();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let data = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.11 ===================================
|
||||
// CLI Execution
|
||||
// Natural pattern: After defining all commands, parse and execute
|
||||
// Parse arguments and execute
|
||||
program.parse();
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.11 ===================================
|
||||
// Module Exports for Programmatic Use
|
||||
// Export database and utility classes for hooks and external consumers
|
||||
export { DatabaseManager, migrations, initializeDatabase, getDatabase } from '../services/sqlite/index.js';
|
||||
// </Block> =======================================
|
||||
@@ -1,90 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as paths from '../shared/paths.js';
|
||||
import { HooksDatabase } from '../services/sqlite/index.js';
|
||||
|
||||
type CheckStatus = 'pass' | 'fail' | 'warn';
|
||||
|
||||
interface CheckResult {
|
||||
name: string;
|
||||
status: CheckStatus;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
function printCheck(result: CheckResult): void {
|
||||
const icon =
|
||||
result.status === 'pass' ? '✅' : result.status === 'warn' ? '⚠️ ' : '❌';
|
||||
const message = result.details ? `${result.name}: ${result.details}` : result.name;
|
||||
console.log(`${icon} ${message}`);
|
||||
}
|
||||
|
||||
export async function doctor(options: OptionValues = {}): Promise<void> {
|
||||
const checks: CheckResult[] = [];
|
||||
|
||||
// Data directory
|
||||
try {
|
||||
if (!fs.existsSync(paths.DATA_DIR)) {
|
||||
fs.mkdirSync(paths.DATA_DIR, { recursive: true });
|
||||
checks.push({ name: `Data directory created at ${paths.DATA_DIR}`, status: 'warn' });
|
||||
} else {
|
||||
const stats = fs.statSync(paths.DATA_DIR);
|
||||
let writable = false;
|
||||
try {
|
||||
fs.accessSync(paths.DATA_DIR, fs.constants.W_OK);
|
||||
writable = true;
|
||||
} catch {}
|
||||
checks.push({
|
||||
name: `Data directory ${paths.DATA_DIR}`,
|
||||
status: stats.isDirectory() && writable ? 'pass' : 'fail',
|
||||
details: stats.isDirectory() && writable ? 'accessible' : 'not writable'
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Data directory',
|
||||
status: 'fail',
|
||||
details: error?.message || String(error)
|
||||
});
|
||||
}
|
||||
|
||||
// SQLite connectivity
|
||||
try {
|
||||
const db = new HooksDatabase();
|
||||
checks.push({
|
||||
name: 'SQLite database',
|
||||
status: 'pass',
|
||||
details: 'connected'
|
||||
});
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'SQLite database',
|
||||
status: 'fail',
|
||||
details: error?.message || String(error)
|
||||
});
|
||||
}
|
||||
|
||||
// Chroma connectivity
|
||||
try {
|
||||
const chromaExists = fs.existsSync(paths.CHROMA_DIR);
|
||||
checks.push({
|
||||
name: 'Chroma vector store',
|
||||
status: chromaExists ? 'pass' : 'warn',
|
||||
details: chromaExists ? `data dir ${path.resolve(paths.CHROMA_DIR)}` : 'Not yet initialized'
|
||||
});
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Chroma vector store',
|
||||
status: 'warn',
|
||||
details: error?.message || 'Unable to check Chroma directory'
|
||||
});
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ checks }, null, 2));
|
||||
} else {
|
||||
console.log('claude-mem doctor');
|
||||
console.log('=================');
|
||||
checks.forEach(printCheck);
|
||||
}
|
||||
}
|
||||
@@ -1,519 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'fs';
|
||||
import { join, resolve, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as p from '@clack/prompts';
|
||||
import gradient from 'gradient-string';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { PACKAGE_NAME } from '../shared/config.js';
|
||||
import type { Settings } from '../shared/types.js';
|
||||
import { Platform } from '../utils/platform.js';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
|
||||
// Enhanced animation utilities
|
||||
function createLoadingAnimation(message: string) {
|
||||
let interval: NodeJS.Timeout;
|
||||
let frame = 0;
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
|
||||
return {
|
||||
start() {
|
||||
interval = setInterval(() => {
|
||||
process.stdout.write(`\r${chalk.cyan(frames[frame % frames.length])} ${message}`);
|
||||
frame++;
|
||||
}, 50); // Faster spinner animation (was 80ms)
|
||||
},
|
||||
stop(result: string, success: boolean = true) {
|
||||
clearInterval(interval);
|
||||
const icon = success ? chalk.green('✓') : chalk.red('✗');
|
||||
process.stdout.write(`\r${icon} ${result}\n`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Fast rainbow gradient preset with tighter color transitions
|
||||
const fastRainbow = gradient(['#ff0000', '#ff4500', '#ffa500', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#8b00ff']);
|
||||
const vibrantRainbow = gradient(['#ff006e', '#fb5607', '#ffbe0b', '#8338ec', '#3a86ff']);
|
||||
|
||||
// Installation scope type
|
||||
type InstallScope = 'user' | 'project' | 'local';
|
||||
|
||||
// Installation configuration from wizard
|
||||
interface InstallConfig {
|
||||
scope: InstallScope;
|
||||
customPath?: string;
|
||||
hookTimeout: number;
|
||||
forceReinstall: boolean;
|
||||
enableSmartTrash?: boolean;
|
||||
saveMemoriesOnClear?: boolean;
|
||||
}
|
||||
|
||||
|
||||
function installUv(): void {
|
||||
Platform.installUv();
|
||||
process.env.PATH = `${homedir()}/.cargo/bin:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
|
||||
function hasExistingInstallation(): boolean {
|
||||
const settingsPath = paths.CLAUDE_SETTINGS_PATH;
|
||||
|
||||
if (!existsSync(settingsPath)) return false;
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
||||
return !!(settings.hooks?.SessionStart || settings.hooks?.Stop || settings.hooks?.PostToolUse);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstallationWizard(existingInstall: boolean): Promise<InstallConfig | null> {
|
||||
const config: Partial<InstallConfig> = {};
|
||||
|
||||
if (existingInstall) {
|
||||
const shouldReinstall = await p.confirm({
|
||||
message: '🧠 Existing claude-mem installation detected. Your memories and data are safe!\n\nReinstall to update hooks and configuration?',
|
||||
initialValue: true
|
||||
});
|
||||
|
||||
if (p.isCancel(shouldReinstall) || !shouldReinstall) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.forceReinstall = true;
|
||||
} else {
|
||||
config.forceReinstall = false;
|
||||
}
|
||||
|
||||
// Select installation scope
|
||||
const scope = await p.select({
|
||||
message: 'Select installation scope',
|
||||
options: [
|
||||
{
|
||||
value: 'user',
|
||||
label: 'User (Recommended)',
|
||||
hint: 'Install for current user (~/.claude)'
|
||||
},
|
||||
{
|
||||
value: 'project',
|
||||
label: 'Project',
|
||||
hint: 'Install for current project only (./.mcp.json)'
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Local',
|
||||
hint: 'Custom local installation'
|
||||
}
|
||||
],
|
||||
initialValue: 'user'
|
||||
});
|
||||
|
||||
if (p.isCancel(scope)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.scope = scope as InstallScope;
|
||||
|
||||
// If local scope, ask for custom path
|
||||
if (scope === 'local') {
|
||||
const customPath = await p.text({
|
||||
message: 'Enter custom installation directory',
|
||||
placeholder: join(process.cwd(), '.claude-mem'),
|
||||
validate: (value) => {
|
||||
if (!value) return 'Path is required';
|
||||
if (!value.startsWith('/') && !value.startsWith('~')) {
|
||||
return 'Please provide an absolute path';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (p.isCancel(customPath)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.customPath = customPath as string;
|
||||
}
|
||||
|
||||
// Use default hook timeout (3 minutes)
|
||||
config.hookTimeout = 180000;
|
||||
|
||||
// Always install/reinstall Chroma MCP - it's required for claude-mem to work
|
||||
|
||||
// Ask about smart trash alias
|
||||
const enableSmartTrash = await p.confirm({
|
||||
message: 'Enable Smart Trash? This creates an alias for "rm" that moves files to ~/.claude-mem/trash instead of permanently deleting them. You can restore files anytime by typing "claude-mem restore".',
|
||||
initialValue: true
|
||||
});
|
||||
|
||||
if (p.isCancel(enableSmartTrash)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.enableSmartTrash = enableSmartTrash;
|
||||
|
||||
// Ask about save-on-clear
|
||||
const saveMemoriesOnClear = await p.confirm({
|
||||
message: 'Would you like to save memories when you type "/clear" in Claude Code? When running /clear with this on, it takes about a minute to save memories before your new session starts.',
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(saveMemoriesOnClear)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.saveMemoriesOnClear = saveMemoriesOnClear;
|
||||
|
||||
return config as InstallConfig;
|
||||
}
|
||||
// </Block>
|
||||
|
||||
|
||||
// <Block> Directory structure creation - natural setup flow
|
||||
function ensureDirectoryStructure(): void {
|
||||
// Create all data directories
|
||||
paths.ensureAllDataDirs();
|
||||
|
||||
// Create all Claude integration directories
|
||||
paths.ensureAllClaudeDirs();
|
||||
|
||||
// Create package.json in .claude-mem to fix ESM module issues
|
||||
const packageJsonPath = join(paths.DATA_DIR, 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
const packageJson = {
|
||||
name: "claude-mem-data",
|
||||
type: "module"
|
||||
};
|
||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
}
|
||||
}
|
||||
// </Block>
|
||||
|
||||
function copyFileRecursively(src: string, dest: string): void {
|
||||
const stat = statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
const files = readdirSync(src);
|
||||
files.forEach((file: string) => {
|
||||
copyFileRecursively(join(src, file), join(dest, file));
|
||||
});
|
||||
} else {
|
||||
copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ensureClaudeMdInstructions(): void {
|
||||
const claudeMdPath = paths.CLAUDE_MD_PATH;
|
||||
const claudeMdDir = dirname(claudeMdPath);
|
||||
|
||||
// Ensure .claude directory exists
|
||||
mkdirSync(claudeMdDir, { recursive: true });
|
||||
|
||||
const instructions = `
|
||||
<!-- CLAUDE-MEM QUICK REFERENCE -->
|
||||
## 🧠 Memory System Quick Reference
|
||||
|
||||
### Search Your Memories (SIMPLE & POWERFUL)
|
||||
- **Semantic search is king**: \`mcp__claude-mem__chroma_query_documents(["search terms"])\`
|
||||
- **🔒 ALWAYS include project name in query**: \`["claude-mem feature authentication"]\` not just \`["feature authentication"]\`
|
||||
- **Include dates for temporal search**: \`["project-name 2025-09-09 bug fix"]\` finds memories from that date
|
||||
- **Get specific memory**: \`mcp__claude-mem__chroma_get_documents(ids: ["document_id"])\`
|
||||
|
||||
### Search Tips That Actually Work
|
||||
- **Project isolation**: Always prefix queries with project name to avoid cross-contamination
|
||||
- **Temporal search**: Include dates (YYYY-MM-DD) in query text to find memories from specific times
|
||||
- **Intent-based**: "implementing oauth" > "oauth implementation code function"
|
||||
- **Multiple queries**: Search with different phrasings for better coverage
|
||||
- **Session-specific**: Include session ID in query when you know it
|
||||
|
||||
### What Doesn't Work (Don't Do This!)
|
||||
- ❌ Complex where filters with $and/$or - they cause errors
|
||||
- ❌ Timestamp comparisons ($gte/$lt) - Chroma stores timestamps as strings
|
||||
- ❌ Mixing project filters in where clause - causes "Error finding id"
|
||||
|
||||
### Storage
|
||||
- Collection: "claude_memories"
|
||||
- Archives: ~/.claude-mem/archives/
|
||||
<!-- /CLAUDE-MEM QUICK REFERENCE -->`;
|
||||
|
||||
// Check if file exists and read content
|
||||
let content = '';
|
||||
if (existsSync(claudeMdPath)) {
|
||||
content = readFileSync(claudeMdPath, 'utf8');
|
||||
|
||||
// Check if instructions already exist (handle both old and new format)
|
||||
const hasOldInstructions = content.includes('<!-- CLAUDE-MEM INSTRUCTIONS -->');
|
||||
const hasNewInstructions = content.includes('<!-- CLAUDE-MEM QUICK REFERENCE -->');
|
||||
|
||||
if (hasOldInstructions || hasNewInstructions) {
|
||||
// Replace existing instructions (handle both old and new markers)
|
||||
let startMarker, endMarker;
|
||||
if (hasOldInstructions) {
|
||||
startMarker = '<!-- CLAUDE-MEM INSTRUCTIONS -->';
|
||||
endMarker = '<!-- /CLAUDE-MEM INSTRUCTIONS -->';
|
||||
} else {
|
||||
startMarker = '<!-- CLAUDE-MEM QUICK REFERENCE -->';
|
||||
endMarker = '<!-- /CLAUDE-MEM QUICK REFERENCE -->';
|
||||
}
|
||||
|
||||
const startIndex = content.indexOf(startMarker);
|
||||
const endIndex = content.indexOf(endMarker) + endMarker.length;
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1) {
|
||||
content = content.substring(0, startIndex) + instructions.trim() + content.substring(endIndex);
|
||||
}
|
||||
} else {
|
||||
// Append instructions to the end
|
||||
content = content.trim() + '\n' + instructions;
|
||||
}
|
||||
} else {
|
||||
// Create new file with instructions
|
||||
content = instructions.trim();
|
||||
}
|
||||
|
||||
// Write the updated content
|
||||
writeFileSync(claudeMdPath, content);
|
||||
}
|
||||
|
||||
function installChromaMcp(forceReinstall: boolean = false): void {
|
||||
const uvPath = `${homedir()}/.cargo/bin`;
|
||||
if (existsSync(uvPath) && !process.env.PATH?.includes(uvPath)) {
|
||||
process.env.PATH = `${uvPath}:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
if (forceReinstall) {
|
||||
try {
|
||||
execSync('claude mcp remove claude-mem', { stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
// Ignore errors if claude-mem doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
const chromaMcpCommand = `claude mcp add claude-mem -- uvx chroma-mcp --client-type persistent --data-dir ${paths.CHROMA_DIR}`;
|
||||
execSync(chromaMcpCommand, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
function createHookConfig(command: string, timeout: number, matcher?: string) {
|
||||
const config: any = {
|
||||
hooks: [{ type: "command", command, timeout }]
|
||||
};
|
||||
if (matcher) config.matcher = matcher;
|
||||
return config;
|
||||
}
|
||||
|
||||
function configureHooks(settingsPath: string): void {
|
||||
let settings: any = existsSync(settingsPath)
|
||||
? JSON.parse(readFileSync(settingsPath, 'utf8'))
|
||||
: { hooks: {} };
|
||||
|
||||
mkdirSync(dirname(settingsPath), { recursive: true });
|
||||
|
||||
if (!settings.hooks) settings.hooks = {};
|
||||
|
||||
// Remove any existing claude-mem hooks
|
||||
const hookTypes = ['SessionStart', 'Stop', 'UserPromptSubmit', 'PostToolUse'];
|
||||
hookTypes.forEach(type => {
|
||||
if (settings.hooks[type]) {
|
||||
settings.hooks[type] = settings.hooks[type].filter(
|
||||
(cfg: any) => !cfg.hooks?.some((h: any) => h.command?.includes(PACKAGE_NAME))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Configure hooks to use CLI commands directly
|
||||
// claude-mem should be in PATH after npm installation
|
||||
settings.hooks.SessionStart = [createHookConfig(`${PACKAGE_NAME} context`, 180)];
|
||||
settings.hooks.Stop = [createHookConfig(`${PACKAGE_NAME} summary`, 60)];
|
||||
settings.hooks.UserPromptSubmit = [createHookConfig(`${PACKAGE_NAME} new`, 60)];
|
||||
settings.hooks.PostToolUse = [createHookConfig(`${PACKAGE_NAME} save`, 180, "*")];
|
||||
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
|
||||
function getSettingsPath(config: InstallConfig): string {
|
||||
if (config.scope === 'local' && config.customPath) {
|
||||
return join(config.customPath, 'settings.local.json');
|
||||
} else if (config.scope === 'project') {
|
||||
return join(process.cwd(), '.claude', 'settings.json');
|
||||
} else {
|
||||
return paths.CLAUDE_SETTINGS_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
function configureUserSettings(config: InstallConfig): void {
|
||||
const userSettingsPath = paths.USER_SETTINGS_PATH;
|
||||
|
||||
let userSettings: Settings = existsSync(userSettingsPath)
|
||||
? JSON.parse(readFileSync(userSettingsPath, 'utf8'))
|
||||
: {};
|
||||
|
||||
userSettings.backend = 'chroma';
|
||||
userSettings.installed = true;
|
||||
userSettings.embedded = true;
|
||||
userSettings.saveMemoriesOnClear = config.saveMemoriesOnClear || false;
|
||||
|
||||
writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2));
|
||||
}
|
||||
|
||||
function configureSmartTrashAlias(): void {
|
||||
const shellConfigs = Platform.getShellConfigPaths();
|
||||
const aliasDefinition = Platform.getAliasDefinition('rm', 'claude-mem trash');
|
||||
const commentLine = '# claude-mem smart trash alias';
|
||||
|
||||
for (const configPath of shellConfigs) {
|
||||
if (!existsSync(configPath)) {
|
||||
// Create the file if it doesn't exist (especially for PowerShell profiles)
|
||||
const dir = dirname(configPath);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(configPath, '');
|
||||
}
|
||||
|
||||
let content = readFileSync(configPath, 'utf8');
|
||||
if (content.includes(aliasDefinition)) continue;
|
||||
|
||||
const aliasBlock = `\n${commentLine}\n${aliasDefinition}\n`;
|
||||
content += aliasBlock;
|
||||
writeFileSync(configPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function installClaudeCommands(): void {
|
||||
const claudeCommandsDir = paths.CLAUDE_COMMANDS_DIR;
|
||||
const packageCommandsDir = paths.getPackageCommandsDir();
|
||||
|
||||
mkdirSync(claudeCommandsDir, { recursive: true });
|
||||
|
||||
const commandFiles = ['save.md', 'remember.md', 'claude-mem.md'];
|
||||
|
||||
for (const fileName of commandFiles) {
|
||||
const sourcePath = join(packageCommandsDir, fileName);
|
||||
const destPath = join(claudeCommandsDir, fileName);
|
||||
if (existsSync(sourcePath)) {
|
||||
copyFileSync(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function install(options: OptionValues = {}): Promise<void> {
|
||||
console.log(fastRainbow('\n═══════════════════════════════════════'));
|
||||
console.log(fastRainbow(' CLAUDE-MEM INSTALLER '));
|
||||
console.log(fastRainbow('═══════════════════════════════════════'));
|
||||
|
||||
console.log(boxen(vibrantRainbow('🧠 Persistent Memory System for Claude Code\n\n✨ Transform your Claude experience with seamless context preservation\n🚀 Never lose your conversation history again'), {
|
||||
padding: 2,
|
||||
margin: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'magenta',
|
||||
textAlignment: 'center'
|
||||
}));
|
||||
|
||||
installUv();
|
||||
|
||||
const isNonInteractive = options.user || options.project || options.local || options.force;
|
||||
|
||||
let config: InstallConfig;
|
||||
|
||||
if (isNonInteractive) {
|
||||
config = {
|
||||
scope: options.local ? 'local' : options.project ? 'project' : 'user',
|
||||
customPath: options.path,
|
||||
hookTimeout: options.timeout ? parseInt(options.timeout) : 180,
|
||||
forceReinstall: !!options.force,
|
||||
enableSmartTrash: false,
|
||||
saveMemoriesOnClear: false
|
||||
};
|
||||
} else {
|
||||
const existingInstall = hasExistingInstallation();
|
||||
const wizardConfig = await runInstallationWizard(existingInstall);
|
||||
if (!wizardConfig) {
|
||||
process.exit(0);
|
||||
}
|
||||
config = wizardConfig;
|
||||
}
|
||||
|
||||
console.log(vibrantRainbow('\n🚀 Beginning Installation Process\n'));
|
||||
|
||||
const steps = [
|
||||
{ name: 'Creating directory structure', fn: () => ensureDirectoryStructure() },
|
||||
{ name: 'Installing Chroma MCP server', fn: () => installChromaMcp(config.forceReinstall) },
|
||||
{ name: 'Adding CLAUDE.md instructions', fn: () => ensureClaudeMdInstructions() },
|
||||
{ name: 'Installing Claude commands', fn: () => installClaudeCommands() },
|
||||
{ name: 'Configuring Claude settings', fn: () => configureHooks(getSettingsPath(config)) },
|
||||
{ name: 'Configuring user settings', fn: () => configureUserSettings(config) }
|
||||
];
|
||||
|
||||
if (config.enableSmartTrash) {
|
||||
steps.push({ name: 'Configuring Smart Trash alias', fn: () => configureSmartTrashAlias() });
|
||||
}
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
const progress = `[${i + 1}/${steps.length}]`;
|
||||
|
||||
const loader = createLoadingAnimation(`${chalk.gray(progress)} ${step.name}...`);
|
||||
loader.start();
|
||||
|
||||
step.fn();
|
||||
loader.stop(`${chalk.gray(progress)} ${step.name} ${vibrantRainbow('completed! ✨')}`);
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Beautiful success message
|
||||
const successTitle = fastRainbow('🎉 INSTALLATION COMPLETE! 🎉');
|
||||
|
||||
const successMessage = `
|
||||
${chalk.bold('How your new memory system works:')}
|
||||
|
||||
${chalk.green('•')} When you start Claude Code, claude-mem loads your latest memories automatically
|
||||
${chalk.green('•')} Memories are saved automatically as you work
|
||||
${chalk.green('•')} Ask Claude to search your memories anytime with natural language
|
||||
${chalk.green('•')} Instructions added to ${chalk.cyan('~/.claude/CLAUDE.md')} teach Claude how to use the system
|
||||
|
||||
${chalk.bold('Slash Commands Available:')}
|
||||
${chalk.cyan('/claude-mem help')} - Show all memory commands and features
|
||||
${chalk.cyan('/save')} - Quick save of current conversation overview
|
||||
${chalk.cyan('/remember')} - Search your saved memories
|
||||
|
||||
${chalk.bold('Quick Start:')}
|
||||
${chalk.yellow('1.')} Restart Claude Code to activate your memory system
|
||||
${chalk.yellow('2.')} Start using Claude normally - memories save automatically
|
||||
${chalk.yellow('3.')} Search memories by asking: ${chalk.italic('"Search my memories for X"')}`;
|
||||
|
||||
|
||||
const finalSmartTrashNote = config.enableSmartTrash ?
|
||||
`\n\n${chalk.blue('🗑️ Smart Trash Enabled:')}
|
||||
${chalk.gray(' • rm commands now move files to ~/.claude-mem/trash')}
|
||||
${chalk.gray(' • View trash:')} ${chalk.cyan('claude-mem trash view')}
|
||||
${chalk.gray(' • Restore files:')} ${chalk.cyan('claude-mem restore')}
|
||||
${chalk.gray(' • Empty trash:')} ${chalk.cyan('claude-mem trash empty')}
|
||||
${chalk.yellow(' • Restart terminal for alias to activate')}` : '';
|
||||
|
||||
const finalClearHookNote = config.saveMemoriesOnClear ?
|
||||
`\n\n${chalk.magenta('💾 Save-on-clear enabled:')}
|
||||
${chalk.gray(' • /clear now saves memories automatically (takes ~1 minute)')}` : '';
|
||||
|
||||
console.log(boxen(successTitle + successMessage + finalSmartTrashNote + finalClearHookNote, {
|
||||
padding: 2,
|
||||
margin: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'green',
|
||||
backgroundColor: '#001122'
|
||||
}));
|
||||
|
||||
// Final flourish
|
||||
console.log(fastRainbow('\n✨ Welcome to the future of persistent AI conversations! ✨\n'));
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
async function showLog(logPath: string, logType: string, tail: number): Promise<void> {
|
||||
// <Block> 1.2 ====================================
|
||||
try {
|
||||
const content = readFileSync(logPath, 'utf8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
const displayLines = lines.slice(-tail);
|
||||
|
||||
console.log(`📋 ${logType} Logs (last ${tail} lines):`);
|
||||
console.log(` File: ${logPath}`);
|
||||
console.log('');
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
if (displayLines.length === 0) {
|
||||
console.log(' No log entries found');
|
||||
// </Block> =======================================
|
||||
} else {
|
||||
displayLines.forEach(line => {
|
||||
console.log(` ${line}`);
|
||||
});
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
console.log('');
|
||||
// </Block> =======================================
|
||||
} catch (error) {
|
||||
// <Block> 1.4 ====================================
|
||||
console.log(`❌ Could not read ${logType.toLowerCase()} log: ${logPath}`);
|
||||
// </Block> =======================================
|
||||
}
|
||||
// </Block> =======================================
|
||||
}
|
||||
|
||||
// <Block> 2.1 ====================================
|
||||
export async function logs(options: OptionValues = {}): Promise<void> {
|
||||
// <Block> 2.2 ====================================
|
||||
const logsDir = paths.LOGS_DIR;
|
||||
const tail = parseInt(options.tail) || 20;
|
||||
// </Block> =======================================
|
||||
|
||||
// Find most recent log file
|
||||
try {
|
||||
const files = readdirSync(logsDir);
|
||||
const logFiles = files
|
||||
.filter(f => f.startsWith('claude-mem-') && f.endsWith('.log'))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: join(logsDir, f),
|
||||
mtime: statSync(join(logsDir, f)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
if (logFiles.length === 0) {
|
||||
console.log('❌ No log files found in ~/.claude-mem/logs/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show most recent log
|
||||
await showLog(logFiles[0].path, 'Most Recent', tail);
|
||||
|
||||
if (options.all && logFiles.length > 1) {
|
||||
console.log(`📚 Found ${logFiles.length} total log files`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Could not read logs directory: ~/.claude-mem/logs/');
|
||||
console.log(' Run a compression first to generate logs');
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readdirSync, renameSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
export async function restore(): Promise<void> {
|
||||
const trashDir = paths.TRASH_DIR;
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('Trash is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await p.select({
|
||||
message: 'Select file to restore:',
|
||||
options: files.map(f => ({ value: f, label: f }))
|
||||
});
|
||||
|
||||
if (p.isCancel(file)) return;
|
||||
|
||||
renameSync(join(trashDir, file), join(process.cwd(), file));
|
||||
console.log(`Restored ${file}`);
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join, resolve, dirname } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as paths from '../shared/paths.js';
|
||||
import { HooksDatabase } from '../services/sqlite/index.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export async function status(): Promise<void> {
|
||||
console.log('🔍 Claude Memory System Status Check');
|
||||
console.log('=====================================\n');
|
||||
|
||||
// paths imported
|
||||
|
||||
console.log('⚙️ Settings Configuration:');
|
||||
|
||||
const checkSettings = (name: string, path: string) => {
|
||||
if (!existsSync(path)) {
|
||||
console.log(` ⏭️ ${name}: No settings file`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` 📋 ${name}: ${path}`);
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(path, 'utf8'));
|
||||
|
||||
const hasSessionStart = settings.hooks?.SessionStart?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) => hook.command?.includes('claude-mem'))
|
||||
);
|
||||
|
||||
const hasStop = settings.hooks?.Stop?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) => hook.command?.includes('claude-mem'))
|
||||
);
|
||||
|
||||
const hasUserPrompt = settings.hooks?.UserPromptSubmit?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) => hook.command?.includes('claude-mem'))
|
||||
);
|
||||
|
||||
const hasPostTool = settings.hooks?.PostToolUse?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) => hook.command?.includes('claude-mem'))
|
||||
);
|
||||
|
||||
console.log(` SessionStart (claude-mem context): ${hasSessionStart ? '✅' : '❌'}`);
|
||||
console.log(` Stop (claude-mem summary): ${hasStop ? '✅' : '❌'}`);
|
||||
console.log(` UserPromptSubmit (claude-mem new): ${hasUserPrompt ? '✅' : '❌'}`);
|
||||
console.log(` PostToolUse (claude-mem save): ${hasPostTool ? '✅' : '❌'}`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(` ⚠️ Could not parse settings`);
|
||||
}
|
||||
};
|
||||
|
||||
checkSettings('Global', paths.ClaudeSettingsPath());
|
||||
checkSettings('Project', join(process.cwd(), '.claude', 'settings.json'));
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('📦 Compressed Transcripts:');
|
||||
const claudeProjectsDir = join(paths.ClaudeConfigDirectory(), 'projects');
|
||||
|
||||
if (existsSync(claudeProjectsDir)) {
|
||||
try {
|
||||
let compressedCount = 0;
|
||||
let archiveCount = 0;
|
||||
|
||||
const searchDir = (dir: string, depth = 0) => {
|
||||
if (depth > 3) return;
|
||||
|
||||
const files = readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = join(dir, file);
|
||||
const stats = statSync(fullPath);
|
||||
|
||||
if (stats.isDirectory() && !file.startsWith('.')) {
|
||||
searchDir(fullPath, depth + 1);
|
||||
} else if (file.endsWith('.jsonl.compressed')) {
|
||||
compressedCount++;
|
||||
} else if (file.endsWith('.jsonl.archive')) {
|
||||
archiveCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchDir(claudeProjectsDir);
|
||||
|
||||
console.log(` Compressed files: ${compressedCount}`);
|
||||
console.log(` Archive files: ${archiveCount}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not scan projects directory`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ℹ️ No Claude projects directory found`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🔧 Runtime Environment:');
|
||||
|
||||
const checkCommand = (cmd: string, name: string) => {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, { encoding: 'utf8' }).trim();
|
||||
console.log(` ✅ ${name}: ${version}`);
|
||||
} catch {
|
||||
console.log(` ❌ ${name}: Not found`);
|
||||
}
|
||||
};
|
||||
|
||||
checkCommand('node', 'Node.js');
|
||||
checkCommand('bun', 'Bun');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🧠 Chroma Storage Status:');
|
||||
console.log(' ✅ Storage backend: Chroma MCP');
|
||||
console.log(` 📍 Data location: ${paths.ChromaDirectory()}`);
|
||||
console.log(' 🔍 Features: Vector search, semantic similarity, document storage');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('📊 Summary:');
|
||||
const globalPath = paths.ClaudeSettingsPath();
|
||||
const projectPath = join(process.cwd(), '.claude', 'settings.json');
|
||||
|
||||
let isInstalled = false;
|
||||
let installLocation = 'Not installed';
|
||||
|
||||
try {
|
||||
if (existsSync(globalPath)) {
|
||||
const settings = JSON.parse(readFileSync(globalPath, 'utf8'));
|
||||
if (settings.hooks?.SessionStart || settings.hooks?.Stop || settings.hooks?.PostToolUse) {
|
||||
isInstalled = true;
|
||||
installLocation = 'Global';
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(projectPath)) {
|
||||
const settings = JSON.parse(readFileSync(projectPath, 'utf8'));
|
||||
if (settings.hooks?.SessionStart || settings.hooks?.Stop || settings.hooks?.PostToolUse) {
|
||||
isInstalled = true;
|
||||
installLocation = installLocation === 'Global' ? 'Global + Project' : 'Project';
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (isInstalled) {
|
||||
console.log(` ✅ Claude Memory System is installed (${installLocation})`);
|
||||
console.log('');
|
||||
console.log('💡 To test: Use /compact in Claude Code');
|
||||
} else {
|
||||
console.log(` ❌ Claude Memory System is not installed`);
|
||||
console.log('');
|
||||
console.log('💡 To install: claude-mem install');
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { rmSync, readdirSync, existsSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
export async function emptyTrash(options: { force?: boolean } = {}): Promise<void> {
|
||||
const trashDir = paths.TRASH_DIR;
|
||||
|
||||
// Check if trash directory exists
|
||||
if (!existsSync(trashDir)) {
|
||||
p.log.info('🗑️ Trash is already empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
p.log.info('🗑️ Trash is already empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Count items
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(trashDir, file);
|
||||
const stats = statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm deletion unless --force flag is used
|
||||
if (!options.force) {
|
||||
const confirm = await p.confirm({
|
||||
message: `Permanently delete ${folderCount} folders and ${fileCount} files from trash?`,
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(confirm) || !confirm) {
|
||||
p.log.info('Cancelled - trash not emptied');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all files in trash
|
||||
const s = p.spinner();
|
||||
s.start('Emptying trash...');
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(trashDir, file);
|
||||
rmSync(filePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
s.stop(`🗑️ Trash emptied - permanently deleted ${folderCount} folders and ${fileCount} files`);
|
||||
|
||||
} catch (error) {
|
||||
p.log.error('Failed to empty trash');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
interface TrashItem {
|
||||
originalName: string;
|
||||
trashedName: string;
|
||||
size: number;
|
||||
trashedAt: Date;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
|
||||
function parseTrashName(filename: string): { name: string; timestamp: number } {
|
||||
const lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex === -1) return { name: filename, timestamp: 0 };
|
||||
|
||||
const timestamp = parseInt(filename.substring(lastDotIndex + 1));
|
||||
if (isNaN(timestamp)) return { name: filename, timestamp: 0 };
|
||||
|
||||
return {
|
||||
name: filename.substring(0, lastDotIndex),
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
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(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getDirectorySize(dirPath: string): number {
|
||||
let size = 0;
|
||||
const files = readdirSync(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(dirPath, file);
|
||||
const stats = statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
size += getDirectorySize(filePath);
|
||||
} else {
|
||||
size += stats.size;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export async function viewTrash(): Promise<void> {
|
||||
const trashDir = paths.TRASH_DIR;
|
||||
|
||||
try {
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
p.log.info('🗑️ Trash is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const items: TrashItem[] = files.map(file => {
|
||||
const filePath = join(trashDir, file);
|
||||
const stats = statSync(filePath);
|
||||
const { name, timestamp } = parseTrashName(file);
|
||||
|
||||
const size = stats.isDirectory() ? getDirectorySize(filePath) : stats.size;
|
||||
|
||||
return {
|
||||
originalName: name,
|
||||
trashedName: file,
|
||||
size,
|
||||
trashedAt: new Date(timestamp),
|
||||
isDirectory: stats.isDirectory()
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by date, newest first
|
||||
items.sort((a, b) => b.trashedAt.getTime() - a.trashedAt.getTime());
|
||||
|
||||
// Display header
|
||||
console.log('\n🗑️ Trash Contents\n');
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
// Display items
|
||||
let totalSize = 0;
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const item of items) {
|
||||
totalSize += item.size;
|
||||
if (item.isDirectory) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
const type = item.isDirectory ? '📁' : '📄';
|
||||
const date = item.trashedAt.toLocaleString();
|
||||
const size = formatSize(item.size);
|
||||
|
||||
console.log(`${type} ${item.originalName}`);
|
||||
console.log(` Size: ${size} | Trashed: ${date}`);
|
||||
console.log(` ID: ${item.trashedName}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Display summary
|
||||
console.log('─'.repeat(80));
|
||||
console.log(`Total: ${folderCount} folders, ${fileCount} files (${formatSize(totalSize)})`);
|
||||
console.log('\nTo restore files: claude-mem restore');
|
||||
console.log('To empty trash: claude-mem trash empty');
|
||||
|
||||
} catch (error) {
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
p.log.info('🗑️ Trash is empty');
|
||||
} else {
|
||||
p.log.error('Failed to read trash directory');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { renameSync, existsSync, mkdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import { glob } from 'glob';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
interface TrashOptions {
|
||||
force?: boolean;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export async function trash(filePaths: string | string[], options: TrashOptions = {}): Promise<void> {
|
||||
const trashDir = paths.TRASH_DIR;
|
||||
if (!existsSync(trashDir)) mkdirSync(trashDir, { recursive: true });
|
||||
|
||||
// Handle single string or array of paths
|
||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||
|
||||
for (const filePath of paths) {
|
||||
// Handle glob patterns
|
||||
const expandedPaths = await glob(filePath);
|
||||
const actualPaths = expandedPaths.length > 0 ? expandedPaths : [filePath];
|
||||
|
||||
for (const actualPath of actualPaths) {
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!existsSync(actualPath)) {
|
||||
if (!options.force) {
|
||||
console.error(`trash: ${actualPath}: No such file or directory`);
|
||||
continue;
|
||||
}
|
||||
// With -f, silently skip missing files
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a directory and we need recursive
|
||||
const stats = statSync(actualPath);
|
||||
if (stats.isDirectory() && !options.recursive) {
|
||||
if (!options.force) {
|
||||
console.error(`trash: ${actualPath}: is a directory`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique destination name to avoid conflicts
|
||||
const fileName = basename(actualPath);
|
||||
const timestamp = Date.now();
|
||||
const destination = join(trashDir, `${fileName}.${timestamp}`);
|
||||
|
||||
renameSync(actualPath, destination);
|
||||
console.log(`Moved ${fileName} to trash`);
|
||||
|
||||
} catch (error) {
|
||||
if (!options.force) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`trash: ${actualPath}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import * as paths from '../shared/paths.js';
|
||||
|
||||
async function removeSmartTrashAlias(): Promise<boolean> {
|
||||
const homeDir = homedir();
|
||||
const shellConfigs = [
|
||||
join(homeDir, '.bashrc'),
|
||||
join(homeDir, '.zshrc'),
|
||||
join(homeDir, '.bash_profile')
|
||||
];
|
||||
|
||||
const aliasLine = 'alias rm="claude-mem trash"';
|
||||
// Handle both variations of the comment line
|
||||
const commentPatterns = [
|
||||
'# claude-mem smart trash alias',
|
||||
'# claude-mem trash bin alias'
|
||||
];
|
||||
let removedFromAny = false;
|
||||
|
||||
for (const configPath of shellConfigs) {
|
||||
if (!existsSync(configPath)) continue;
|
||||
|
||||
let content = readFileSync(configPath, 'utf8');
|
||||
|
||||
// Check if alias exists
|
||||
if (!content.includes(aliasLine)) {
|
||||
continue; // Not configured in this file
|
||||
}
|
||||
|
||||
// Remove the alias and its comment
|
||||
const lines = content.split('\n');
|
||||
const filteredLines = lines.filter((line, index) => {
|
||||
// Skip the alias line
|
||||
if (line.trim() === aliasLine) return false;
|
||||
// Skip any claude-mem comment line if it's right before the alias
|
||||
for (const commentPattern of commentPatterns) {
|
||||
if (line.trim() === commentPattern &&
|
||||
index + 1 < lines.length &&
|
||||
lines[index + 1].trim() === aliasLine) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const newContent = filteredLines.join('\n');
|
||||
|
||||
// Only write if content actually changed
|
||||
if (newContent !== content) {
|
||||
// Create backup
|
||||
const backupPath = configPath + '.backup.' + Date.now();
|
||||
writeFileSync(backupPath, content);
|
||||
|
||||
// Write updated content
|
||||
writeFileSync(configPath, newContent);
|
||||
console.log(`✅ Removed Smart Trash alias from ${configPath.replace(homeDir, '~')}`);
|
||||
removedFromAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
return removedFromAny;
|
||||
}
|
||||
|
||||
export async function uninstall(options: OptionValues = {}): Promise<void> {
|
||||
console.log('🔄 Uninstalling Claude Memory System hooks...');
|
||||
|
||||
const locations = [];
|
||||
if (options.all) {
|
||||
locations.push({
|
||||
name: 'User',
|
||||
path: paths.CLAUDE_SETTINGS_PATH
|
||||
});
|
||||
locations.push({
|
||||
name: 'Project',
|
||||
path: join(process.cwd(), '.claude', 'settings.json')
|
||||
});
|
||||
} else {
|
||||
const isProject = options.project;
|
||||
locations.push({
|
||||
name: isProject ? 'Project' : 'User',
|
||||
path: isProject ? join(process.cwd(), '.claude', 'settings.json') : paths.CLAUDE_SETTINGS_PATH
|
||||
});
|
||||
}
|
||||
|
||||
let removedCount = 0;
|
||||
|
||||
for (const location of locations) {
|
||||
if (!existsSync(location.path)) {
|
||||
console.log(`⏭️ No settings found at ${location.name} location`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = readFileSync(location.path, 'utf8');
|
||||
const settings = JSON.parse(content);
|
||||
|
||||
if (!settings.hooks) {
|
||||
console.log(`⏭️ No hooks configured in ${location.name} settings`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
|
||||
// Remove claude-mem hooks (CLI commands)
|
||||
const hookTypes = ['SessionStart', 'Stop', 'UserPromptSubmit', 'PostToolUse'];
|
||||
|
||||
for (const hookType of hookTypes) {
|
||||
if (settings.hooks[hookType]) {
|
||||
const filteredHooks = settings.hooks[hookType].filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) => hook.command?.includes('claude-mem'))
|
||||
);
|
||||
|
||||
if (filteredHooks.length !== settings.hooks[hookType].length) {
|
||||
settings.hooks[hookType] = filteredHooks.length ? filteredHooks : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed ${hookType} hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up undefined hooks
|
||||
hookTypes.forEach(hookType => {
|
||||
if (settings.hooks[hookType] === undefined) delete settings.hooks[hookType];
|
||||
});
|
||||
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
||||
|
||||
if (modified) {
|
||||
const backupPath = location.path + '.backup.' + Date.now();
|
||||
writeFileSync(backupPath, content);
|
||||
console.log(`📋 Created backup: ${backupPath}`);
|
||||
|
||||
writeFileSync(location.path, JSON.stringify(settings, null, 2));
|
||||
removedCount++;
|
||||
console.log(`✅ Updated ${location.name} settings: ${location.path}`);
|
||||
} else {
|
||||
console.log(`ℹ️ No Claude Memory System hooks found in ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Smart Trash alias from shell configs
|
||||
const removedAlias = await removeSmartTrashAlias();
|
||||
|
||||
console.log('');
|
||||
if (removedCount > 0 || removedAlias) {
|
||||
console.log('✨ Uninstallation complete!');
|
||||
if (removedCount > 0) {
|
||||
console.log('The Claude Memory System hooks have been removed from your settings.');
|
||||
}
|
||||
if (removedAlias) {
|
||||
console.log('The Smart Trash alias has been removed from your shell configuration.');
|
||||
console.log('⚠️ Restart your terminal for the alias removal to take effect.');
|
||||
}
|
||||
console.log('');
|
||||
console.log('Note: Your compressed transcripts and archives are preserved.');
|
||||
console.log('To reinstall: claude-mem install');
|
||||
} else {
|
||||
console.log('ℹ️ No Claude Memory System hooks or aliases were found to remove.');
|
||||
}
|
||||
}
|
||||
+9
-15
@@ -48,25 +48,19 @@ export function newHook(input?: UserPromptSubmitInput): void {
|
||||
db.close();
|
||||
|
||||
// Start SDK worker in background as detached process
|
||||
// In plugin mode, use bundled worker; otherwise use global CLI
|
||||
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
let child;
|
||||
|
||||
if (pluginRoot) {
|
||||
// Plugin mode: use bundled worker
|
||||
const workerPath = path.join(pluginRoot, 'scripts', 'hooks', 'worker.js');
|
||||
child = spawn('bun', [workerPath, sessionId.toString()], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
} else {
|
||||
// Traditional mode: use global CLI
|
||||
child = spawn('claude-mem', ['worker', sessionId.toString()], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
if (!pluginRoot) {
|
||||
throw new Error('CLAUDE_PLUGIN_ROOT not set - claude-mem must be installed as a Claude Code plugin');
|
||||
}
|
||||
|
||||
// Use bundled worker
|
||||
const workerPath = path.join(pluginRoot, 'scripts', 'hooks', 'worker.js');
|
||||
const child = spawn('bun', [workerPath, sessionId.toString()], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
child.unref();
|
||||
|
||||
// Output hook response
|
||||
|
||||
@@ -45,7 +45,4 @@ try {
|
||||
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> =======================================
|
||||
Reference in New Issue
Block a user