/**
* Cleanup Manager for NotebookLM MCP Server
*
* ULTRATHINK EDITION - Complete cleanup across all platforms!
*
* Handles safe removal of:
* - Legacy data from notebooklm-mcp-nodejs
* - Current installation data
* - Browser profiles and session data
* - NPM/NPX cache
* - Claude CLI MCP logs
* - Claude Projects cache
* - Temporary backups
* - Editor logs (Cursor, VSCode)
* - Trash files (optional)
*
* Platform support: Linux, Windows, macOS
*/
import fs from "fs/promises";
import path from "path";
import { globby } from "globby";
import envPaths from "env-paths";
import os from "os";
import { log } from "./logger.js";
export type CleanupMode = "legacy" | "all" | "deep";
export interface CleanupResult {
success: boolean;
mode: CleanupMode;
deletedPaths: string[];
failedPaths: string[];
totalSizeBytes: number;
categorySummary: Record<string, { count: number; bytes: number }>;
}
export interface CleanupCategory {
name: string;
description: string;
paths: string[];
totalBytes: number;
optional: boolean;
}
interface Paths {
data: string;
config: string;
cache: string;
log: string;
temp: string;
}
export class CleanupManager {
private legacyPaths: Paths;
private currentPaths: Paths;
private homeDir: string;
private tempDir: string;
constructor() {
// envPaths() does NOT create directories - it just returns path strings
// IMPORTANT: envPaths() has a default suffix 'nodejs', so we must explicitly disable it!
// Legacy paths with -nodejs suffix (using default suffix behavior)
this.legacyPaths = envPaths("notebooklm-mcp"); // This becomes notebooklm-mcp-nodejs by default
// Current paths without suffix (disable the default suffix with empty string)
this.currentPaths = envPaths("notebooklm-mcp", {suffix: ""});
// Platform-agnostic paths
this.homeDir = os.homedir();
this.tempDir = os.tmpdir();
}
// ============================================================================
// Platform-Specific Path Resolution
// ============================================================================
/**
* Get NPM cache directory (platform-specific)
*/
private getNpmCachePath(): string {
return path.join(this.homeDir, ".npm");
}
/**
* Get Claude CLI cache directory (platform-specific)
*/
private getClaudeCliCachePath(): string {
const platform = process.platform;
if (platform === "win32") {
const localAppData = process.env.LOCALAPPDATA || path.join(this.homeDir, "AppData", "Local");
return path.join(localAppData, "claude-cli-nodejs");
} else if (platform === "darwin") {
return path.join(this.homeDir, "Library", "Caches", "claude-cli-nodejs");
} else {
// Linux and others
return path.join(this.homeDir, ".cache", "claude-cli-nodejs");
}
}
/**
* Get Claude projects directory (platform-specific)
*/
private getClaudeProjectsPath(): string {
const platform = process.platform;
if (platform === "win32") {
const appData = process.env.APPDATA || path.join(this.homeDir, "AppData", "Roaming");
return path.join(appData, ".claude", "projects");
} else if (platform === "darwin") {
return path.join(this.homeDir, "Library", "Application Support", "claude", "projects");
} else {
// Linux and others
return path.join(this.homeDir, ".claude", "projects");
}
}
/**
* Get editor config paths (Cursor, VSCode)
*/
private getEditorConfigPaths(): string[] {
const platform = process.platform;
const paths: string[] = [];
if (platform === "win32") {
const appData = process.env.APPDATA || path.join(this.homeDir, "AppData", "Roaming");
paths.push(
path.join(appData, "Cursor", "logs"),
path.join(appData, "Code", "logs")
);
} else if (platform === "darwin") {
paths.push(
path.join(this.homeDir, "Library", "Application Support", "Cursor", "logs"),
path.join(this.homeDir, "Library", "Application Support", "Code", "logs")
);
} else {
// Linux
paths.push(
path.join(this.homeDir, ".config", "Cursor", "logs"),
path.join(this.homeDir, ".config", "Code", "logs")
);
}
return paths;
}
/**
* Get trash directory (platform-specific)
*/
private getTrashPath(): string | null {
const platform = process.platform;
if (platform === "darwin") {
return path.join(this.homeDir, ".Trash");
} else if (platform === "linux") {
return path.join(this.homeDir, ".local", "share", "Trash");
} else {
// Windows Recycle Bin is complex, skip for now
return null;
}
}
/**
* Get manual legacy config paths that might not be caught by envPaths
* This ensures we catch ALL legacy installations including old config.json files
*/
private getManualLegacyPaths(): string[] {
const paths: string[] = [];
const platform = process.platform;
if (platform === "linux") {
// Linux-specific paths
paths.push(
path.join(this.homeDir, ".config", "notebooklm-mcp"),
path.join(this.homeDir, ".config", "notebooklm-mcp-nodejs"),
path.join(this.homeDir, ".local", "share", "notebooklm-mcp"),
path.join(this.homeDir, ".local", "share", "notebooklm-mcp-nodejs"),
path.join(this.homeDir, ".cache", "notebooklm-mcp"),
path.join(this.homeDir, ".cache", "notebooklm-mcp-nodejs"),
path.join(this.homeDir, ".local", "state", "notebooklm-mcp"),
path.join(this.homeDir, ".local", "state", "notebooklm-mcp-nodejs")
);
} else if (platform === "darwin") {
// macOS-specific paths
paths.push(
path.join(this.homeDir, "Library", "Application Support", "notebooklm-mcp"),
path.join(this.homeDir, "Library", "Application Support", "notebooklm-mcp-nodejs"),
path.join(this.homeDir, "Library", "Preferences", "notebooklm-mcp"),
path.join(this.homeDir, "Library", "Preferences", "notebooklm-mcp-nodejs"),
path.join(this.homeDir, "Library", "Caches", "notebooklm-mcp"),
path.join(this.homeDir, "Library", "Caches", "notebooklm-mcp-nodejs"),
path.join(this.homeDir, "Library", "Logs", "notebooklm-mcp"),
path.join(this.homeDir, "Library", "Logs", "notebooklm-mcp-nodejs")
);
} else if (platform === "win32") {
// Windows-specific paths
const localAppData = process.env.LOCALAPPDATA || path.join(this.homeDir, "AppData", "Local");
const appData = process.env.APPDATA || path.join(this.homeDir, "AppData", "Roaming");
paths.push(
path.join(localAppData, "notebooklm-mcp"),
path.join(localAppData, "notebooklm-mcp-nodejs"),
path.join(appData, "notebooklm-mcp"),
path.join(appData, "notebooklm-mcp-nodejs")
);
}
return paths;
}
// ============================================================================
// Search Methods for Different File Types
// ============================================================================
/**
* Find NPM/NPX cache files
*/
private async findNpmCache(): Promise<string[]> {
const found: string[] = [];
try {
const npmCachePath = this.getNpmCachePath();
const npxPath = path.join(npmCachePath, "_npx");
if (!(await this.pathExists(npxPath))) {
return found;
}
// Search for notebooklm-mcp in npx cache
const pattern = path.join(npxPath, "*/node_modules/notebooklm-mcp");
const matches = await globby(pattern, { onlyDirectories: true, absolute: true });
found.push(...matches);
} catch (error) {
log.warning(`⚠️ Error searching NPM cache: ${error}`);
}
return found;
}
/**
* Find Claude CLI MCP logs
*/
private async findClaudeCliLogs(): Promise<string[]> {
const found: string[] = [];
try {
const claudeCliPath = this.getClaudeCliCachePath();
if (!(await this.pathExists(claudeCliPath))) {
return found;
}
// Search for notebooklm MCP logs
const patterns = [
path.join(claudeCliPath, "*/mcp-logs-notebooklm"),
path.join(claudeCliPath, "*notebooklm-mcp*"),
];
for (const pattern of patterns) {
const matches = await globby(pattern, { onlyDirectories: true, absolute: true });
found.push(...matches);
}
} catch (error) {
log.warning(`⚠️ Error searching Claude CLI cache: ${error}`);
}
return found;
}
/**
* Find Claude projects cache
*/
private async findClaudeProjects(): Promise<string[]> {
const found: string[] = [];
try {
const projectsPath = this.getClaudeProjectsPath();
if (!(await this.pathExists(projectsPath))) {
return found;
}
// Search for notebooklm-mcp projects
const pattern = path.join(projectsPath, "*notebooklm-mcp*");
const matches = await globby(pattern, { onlyDirectories: true, absolute: true });
found.push(...matches);
} catch (error) {
log.warning(`⚠️ Error searching Claude projects: ${error}`);
}
return found;
}
/**
* Find temporary backups
*/
private async findTemporaryBackups(): Promise<string[]> {
const found: string[] = [];
try {
// Search for notebooklm backup directories in temp
const pattern = path.join(this.tempDir, "notebooklm-backup-*");
const matches = await globby(pattern, { onlyDirectories: true, absolute: true });
found.push(...matches);
} catch (error) {
log.warning(`⚠️ Error searching temp backups: ${error}`);
}
return found;
}
/**
* Find editor logs (Cursor, VSCode)
*/
private async findEditorLogs(): Promise<string[]> {
const found: string[] = [];
try {
const editorPaths = this.getEditorConfigPaths();
for (const editorPath of editorPaths) {
if (!(await this.pathExists(editorPath))) {
continue;
}
// Search for MCP notebooklm logs
const pattern = path.join(editorPath, "**/exthost/**/*notebooklm*.log");
const matches = await globby(pattern, { onlyFiles: true, absolute: true });
found.push(...matches);
}
} catch (error) {
log.warning(`⚠️ Error searching editor logs: ${error}`);
}
return found;
}
/**
* Find trash files
*/
private async findTrashFiles(): Promise<string[]> {
const found: string[] = [];
try {
const trashPath = this.getTrashPath();
if (!trashPath || !(await this.pathExists(trashPath))) {
return found;
}
// Search for notebooklm files in trash
const patterns = [
path.join(trashPath, "**/*notebooklm*"),
];
for (const pattern of patterns) {
const matches = await globby(pattern, { absolute: true });
found.push(...matches);
}
} catch (error) {
log.warning(`⚠️ Error searching trash: ${error}`);
}
return found;
}
// ============================================================================
// Main Cleanup Methods
// ============================================================================
/**
* Get all paths that would be deleted for a given mode (with categorization)
*/
async getCleanupPaths(
mode: CleanupMode,
preserveLibrary: boolean = false
): Promise<{
categories: CleanupCategory[];
totalPaths: string[];
totalSizeBytes: number;
}> {
const categories: CleanupCategory[] = [];
const allPaths: Set<string> = new Set();
let totalSizeBytes = 0;
// Category 1: Legacy Paths (notebooklm-mcp-nodejs & manual legacy paths)
if (mode === "legacy" || mode === "all" || mode === "deep") {
const legacyPaths: string[] = [];
let legacyBytes = 0;
// Check envPaths-based legacy directories
const legacyDirs = [
this.legacyPaths.data,
this.legacyPaths.config,
this.legacyPaths.cache,
this.legacyPaths.log,
this.legacyPaths.temp,
];
for (const dir of legacyDirs) {
if (await this.pathExists(dir)) {
const size = await this.getDirectorySize(dir);
legacyPaths.push(dir);
legacyBytes += size;
allPaths.add(dir);
}
}
// CRITICAL: Also check manual legacy paths to catch old config.json files
// and any paths that envPaths might miss
const manualLegacyPaths = this.getManualLegacyPaths();
for (const dir of manualLegacyPaths) {
if (await this.pathExists(dir) && !allPaths.has(dir)) {
const size = await this.getDirectorySize(dir);
legacyPaths.push(dir);
legacyBytes += size;
allPaths.add(dir);
}
}
if (legacyPaths.length > 0) {
categories.push({
name: "Legacy Installation (notebooklm-mcp-nodejs)",
description: "Old installation data with -nodejs suffix and legacy config files",
paths: legacyPaths,
totalBytes: legacyBytes,
optional: false,
});
totalSizeBytes += legacyBytes;
}
}
// Category 2: Current Installation
if (mode === "all" || mode === "deep") {
const currentPaths: string[] = [];
let currentBytes = 0;
// If preserveLibrary is true, don't delete the data directory itself
// Instead, only delete subdirectories
const currentDirs = preserveLibrary
? [
// Don't include data directory to preserve library.json
this.currentPaths.config,
this.currentPaths.cache,
this.currentPaths.log,
this.currentPaths.temp,
// Only delete subdirectories, not the parent
path.join(this.currentPaths.data, "browser_state"),
path.join(this.currentPaths.data, "chrome_profile"),
path.join(this.currentPaths.data, "chrome_profile_instances"),
]
: [
// Delete everything including data directory
this.currentPaths.data,
this.currentPaths.config,
this.currentPaths.cache,
this.currentPaths.log,
this.currentPaths.temp,
// Specific subdirectories (only if parent doesn't exist)
path.join(this.currentPaths.data, "browser_state"),
path.join(this.currentPaths.data, "chrome_profile"),
path.join(this.currentPaths.data, "chrome_profile_instances"),
];
for (const dir of currentDirs) {
if (await this.pathExists(dir) && !allPaths.has(dir)) {
const size = await this.getDirectorySize(dir);
currentPaths.push(dir);
currentBytes += size;
allPaths.add(dir);
}
}
if (currentPaths.length > 0) {
const description = preserveLibrary
? "Active installation data and browser profiles (library.json will be preserved)"
: "Active installation data and browser profiles";
categories.push({
name: "Current Installation (notebooklm-mcp)",
description,
paths: currentPaths,
totalBytes: currentBytes,
optional: false,
});
totalSizeBytes += currentBytes;
}
}
// Category 3: NPM Cache
if (mode === "all" || mode === "deep") {
const npmPaths = await this.findNpmCache();
if (npmPaths.length > 0) {
let npmBytes = 0;
for (const p of npmPaths) {
if (!allPaths.has(p)) {
npmBytes += await this.getDirectorySize(p);
allPaths.add(p);
}
}
if (npmBytes > 0) {
categories.push({
name: "NPM/NPX Cache",
description: "NPX cached installations of notebooklm-mcp",
paths: npmPaths,
totalBytes: npmBytes,
optional: false,
});
totalSizeBytes += npmBytes;
}
}
}
// Category 4: Claude CLI Logs
if (mode === "all" || mode === "deep") {
const claudeCliPaths = await this.findClaudeCliLogs();
if (claudeCliPaths.length > 0) {
let claudeCliBytes = 0;
for (const p of claudeCliPaths) {
if (!allPaths.has(p)) {
claudeCliBytes += await this.getDirectorySize(p);
allPaths.add(p);
}
}
if (claudeCliBytes > 0) {
categories.push({
name: "Claude CLI MCP Logs",
description: "MCP server logs from Claude CLI",
paths: claudeCliPaths,
totalBytes: claudeCliBytes,
optional: false,
});
totalSizeBytes += claudeCliBytes;
}
}
}
// Category 5: Temporary Backups
if (mode === "all" || mode === "deep") {
const backupPaths = await this.findTemporaryBackups();
if (backupPaths.length > 0) {
let backupBytes = 0;
for (const p of backupPaths) {
if (!allPaths.has(p)) {
backupBytes += await this.getDirectorySize(p);
allPaths.add(p);
}
}
if (backupBytes > 0) {
categories.push({
name: "Temporary Backups",
description: "Temporary backup directories in system temp",
paths: backupPaths,
totalBytes: backupBytes,
optional: false,
});
totalSizeBytes += backupBytes;
}
}
}
// Category 6: Claude Projects (deep mode only)
if (mode === "deep") {
const projectPaths = await this.findClaudeProjects();
if (projectPaths.length > 0) {
let projectBytes = 0;
for (const p of projectPaths) {
if (!allPaths.has(p)) {
projectBytes += await this.getDirectorySize(p);
allPaths.add(p);
}
}
if (projectBytes > 0) {
categories.push({
name: "Claude Projects Cache",
description: "Project-specific cache in Claude config",
paths: projectPaths,
totalBytes: projectBytes,
optional: true,
});
totalSizeBytes += projectBytes;
}
}
}
// Category 7: Editor Logs (deep mode only)
if (mode === "deep") {
const editorPaths = await this.findEditorLogs();
if (editorPaths.length > 0) {
let editorBytes = 0;
for (const p of editorPaths) {
if (!allPaths.has(p)) {
editorBytes += await this.getFileSize(p);
allPaths.add(p);
}
}
if (editorBytes > 0) {
categories.push({
name: "Editor Logs (Cursor/VSCode)",
description: "MCP logs from code editors",
paths: editorPaths,
totalBytes: editorBytes,
optional: true,
});
totalSizeBytes += editorBytes;
}
}
}
// Category 8: Trash Files (deep mode only)
if (mode === "deep") {
const trashPaths = await this.findTrashFiles();
if (trashPaths.length > 0) {
let trashBytes = 0;
for (const p of trashPaths) {
if (!allPaths.has(p)) {
trashBytes += await this.getFileSize(p);
allPaths.add(p);
}
}
if (trashBytes > 0) {
categories.push({
name: "Trash Files",
description: "Deleted notebooklm files in system trash",
paths: trashPaths,
totalBytes: trashBytes,
optional: true,
});
totalSizeBytes += trashBytes;
}
}
}
return {
categories,
totalPaths: Array.from(allPaths),
totalSizeBytes,
};
}
/**
* Perform cleanup with safety checks and detailed reporting
*/
async performCleanup(
mode: CleanupMode,
preserveLibrary: boolean = false
): Promise<CleanupResult> {
log.info(`🧹 Starting cleanup in "${mode}" mode...`);
if (preserveLibrary) {
log.info(`📚 Library preservation enabled - library.json will be kept!`);
}
const { categories, totalSizeBytes } = await this.getCleanupPaths(mode, preserveLibrary);
const deletedPaths: string[] = [];
const failedPaths: string[] = [];
const categorySummary: Record<string, { count: number; bytes: number }> = {};
// Delete by category
for (const category of categories) {
log.info(`\n📦 ${category.name} (${category.paths.length} items, ${this.formatBytes(category.totalBytes)})`);
if (category.optional) {
log.warning(` ⚠️ Optional category - ${category.description}`);
}
let categoryDeleted = 0;
let categoryBytes = 0;
for (const itemPath of category.paths) {
try {
if (await this.pathExists(itemPath)) {
const size = await this.getDirectorySize(itemPath);
log.info(` 🗑️ Deleting: ${itemPath}`);
await fs.rm(itemPath, { recursive: true, force: true });
deletedPaths.push(itemPath);
categoryDeleted++;
categoryBytes += size;
log.success(` ✅ Deleted: ${itemPath} (${this.formatBytes(size)})`);
}
} catch (error) {
log.error(` ❌ Failed to delete: ${itemPath} - ${error}`);
failedPaths.push(itemPath);
}
}
categorySummary[category.name] = {
count: categoryDeleted,
bytes: categoryBytes,
};
}
const success = failedPaths.length === 0;
if (success) {
log.success(`\n✅ Cleanup complete! Deleted ${deletedPaths.length} items (${this.formatBytes(totalSizeBytes)})`);
} else {
log.warning(`\n⚠️ Cleanup completed with ${failedPaths.length} errors`);
log.success(` Deleted: ${deletedPaths.length} items`);
log.error(` Failed: ${failedPaths.length} items`);
}
return {
success,
mode,
deletedPaths,
failedPaths,
totalSizeBytes,
categorySummary,
};
}
// ============================================================================
// Helper Methods
// ============================================================================
/**
* Check if a path exists
*/
private async pathExists(dirPath: string): Promise<boolean> {
try {
await fs.access(dirPath);
return true;
} catch {
return false;
}
}
/**
* Get the size of a single file
*/
private async getFileSize(filePath: string): Promise<number> {
try {
const stats = await fs.stat(filePath);
return stats.size;
} catch {
return 0;
}
}
/**
* Get the total size of a directory (recursive)
*/
private async getDirectorySize(dirPath: string): Promise<number> {
try {
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
return stats.size;
}
let totalSize = 0;
const files = await fs.readdir(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const fileStats = await fs.stat(filePath);
if (fileStats.isDirectory()) {
totalSize += await this.getDirectorySize(filePath);
} else {
totalSize += fileStats.size;
}
}
return totalSize;
} catch {
return 0;
}
}
/**
* Format bytes to human-readable string
*/
formatBytes(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
* Get platform-specific path info
*/
getPlatformInfo(): {
platform: string;
legacyBasePath: string;
currentBasePath: string;
npmCachePath: string;
claudeCliCachePath: string;
claudeProjectsPath: string;
} {
const platform = process.platform;
let platformName = "Unknown";
switch (platform) {
case "win32":
platformName = "Windows";
break;
case "darwin":
platformName = "macOS";
break;
case "linux":
platformName = "Linux";
break;
}
return {
platform: platformName,
legacyBasePath: this.legacyPaths.data,
currentBasePath: this.currentPaths.data,
npmCachePath: this.getNpmCachePath(),
claudeCliCachePath: this.getClaudeCliCachePath(),
claudeProjectsPath: this.getClaudeProjectsPath(),
};
}
}