This is page 2 of 2. Use http://codebase.md/pleaseprompto/notebooklm-mcp?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── CHANGELOG.md
├── docs
│ ├── configuration.md
│ ├── tools.md
│ ├── troubleshooting.md
│ └── usage-guide.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── auth
│ │ └── auth-manager.ts
│ ├── config.ts
│ ├── errors.ts
│ ├── index.ts
│ ├── library
│ │ ├── notebook-library.ts
│ │ └── types.ts
│ ├── resources
│ │ └── resource-handlers.ts
│ ├── session
│ │ ├── browser-session.ts
│ │ ├── session-manager.ts
│ │ └── shared-context-manager.ts
│ ├── tools
│ │ ├── definitions
│ │ │ ├── ask-question.ts
│ │ │ ├── notebook-management.ts
│ │ │ ├── session-management.ts
│ │ │ └── system.ts
│ │ ├── definitions.ts
│ │ ├── handlers.ts
│ │ └── index.ts
│ ├── types.ts
│ └── utils
│ ├── cleanup-manager.ts
│ ├── cli-handler.ts
│ ├── logger.ts
│ ├── page-utils.ts
│ ├── settings-manager.ts
│ └── stealth-utils.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/src/utils/cleanup-manager.ts:
--------------------------------------------------------------------------------
```typescript
/**
* 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(),
};
}
}
```
--------------------------------------------------------------------------------
/src/tools/handlers.ts:
--------------------------------------------------------------------------------
```typescript
/**
* MCP Tool Handlers
*
* Implements the logic for all MCP tools.
*/
import { SessionManager } from "../session/session-manager.js";
import { AuthManager } from "../auth/auth-manager.js";
import { NotebookLibrary } from "../library/notebook-library.js";
import type { AddNotebookInput, UpdateNotebookInput } from "../library/types.js";
import { CONFIG, applyBrowserOptions, type BrowserOptions } from "../config.js";
import { log } from "../utils/logger.js";
import type {
AskQuestionResult,
ToolResult,
ProgressCallback,
} from "../types.js";
import { RateLimitError } from "../errors.js";
import { CleanupManager } from "../utils/cleanup-manager.js";
const FOLLOW_UP_REMINDER =
"\n\nEXTREMELY IMPORTANT: Is that ALL you need to know? You can always ask another question using the same session ID! Think about it carefully: before you reply to the user, review their original request and this answer. If anything is still unclear or missing, ask me another question first.";
/**
* MCP Tool Handlers
*/
export class ToolHandlers {
private sessionManager: SessionManager;
private authManager: AuthManager;
private library: NotebookLibrary;
constructor(sessionManager: SessionManager, authManager: AuthManager, library: NotebookLibrary) {
this.sessionManager = sessionManager;
this.authManager = authManager;
this.library = library;
}
/**
* Handle ask_question tool
*/
async handleAskQuestion(
args: {
question: string;
session_id?: string;
notebook_id?: string;
notebook_url?: string;
show_browser?: boolean;
browser_options?: BrowserOptions;
},
sendProgress?: ProgressCallback
): Promise<ToolResult<AskQuestionResult>> {
const { question, session_id, notebook_id, notebook_url, show_browser, browser_options } = args;
log.info(`🔧 [TOOL] ask_question called`);
log.info(` Question: "${question.substring(0, 100)}"...`);
if (session_id) {
log.info(` Session ID: ${session_id}`);
}
if (notebook_id) {
log.info(` Notebook ID: ${notebook_id}`);
}
if (notebook_url) {
log.info(` Notebook URL: ${notebook_url}`);
}
try {
// Resolve notebook URL
let resolvedNotebookUrl = notebook_url;
if (!resolvedNotebookUrl && notebook_id) {
const notebook = this.library.incrementUseCount(notebook_id);
if (!notebook) {
throw new Error(`Notebook not found in library: ${notebook_id}`);
}
resolvedNotebookUrl = notebook.url;
log.info(` Resolved notebook: ${notebook.name}`);
} else if (!resolvedNotebookUrl) {
const active = this.library.getActiveNotebook();
if (active) {
const notebook = this.library.incrementUseCount(active.id);
if (!notebook) {
throw new Error(`Active notebook not found: ${active.id}`);
}
resolvedNotebookUrl = notebook.url;
log.info(` Using active notebook: ${notebook.name}`);
}
}
// Progress: Getting or creating session
await sendProgress?.("Getting or creating browser session...", 1, 5);
// Apply browser options temporarily
const originalConfig = { ...CONFIG };
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
Object.assign(CONFIG, effectiveConfig);
// Calculate overrideHeadless parameter for session manager
// show_browser takes precedence over browser_options.headless
let overrideHeadless: boolean | undefined = undefined;
if (show_browser !== undefined) {
overrideHeadless = show_browser;
} else if (browser_options?.show !== undefined) {
overrideHeadless = browser_options.show;
} else if (browser_options?.headless !== undefined) {
overrideHeadless = !browser_options.headless;
}
try {
// Get or create session (with headless override to handle mode changes)
const session = await this.sessionManager.getOrCreateSession(
session_id,
resolvedNotebookUrl,
overrideHeadless
);
// Progress: Asking question
await sendProgress?.("Asking question to NotebookLM...", 2, 5);
// Ask the question (pass progress callback)
const rawAnswer = await session.ask(question, sendProgress);
const answer = `${rawAnswer.trimEnd()}${FOLLOW_UP_REMINDER}`;
// Get session info
const sessionInfo = session.getInfo();
const result: AskQuestionResult = {
status: "success",
question,
answer,
session_id: session.sessionId,
notebook_url: session.notebookUrl,
session_info: {
age_seconds: sessionInfo.age_seconds,
message_count: sessionInfo.message_count,
last_activity: sessionInfo.last_activity,
},
};
// Progress: Complete
await sendProgress?.("Question answered successfully!", 5, 5);
log.success(`✅ [TOOL] ask_question completed successfully`);
return {
success: true,
data: result,
};
} finally {
// Restore original CONFIG
Object.assign(CONFIG, originalConfig);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
// Special handling for rate limit errors
if (error instanceof RateLimitError || errorMessage.toLowerCase().includes("rate limit")) {
log.error(`🚫 [TOOL] Rate limit detected`);
return {
success: false,
error:
"NotebookLM rate limit reached (50 queries/day for free accounts).\n\n" +
"You can:\n" +
"1. Use the 're_auth' tool to login with a different Google account\n" +
"2. Wait until tomorrow for the quota to reset\n" +
"3. Upgrade to Google AI Pro/Ultra for 5x higher limits\n\n" +
`Original error: ${errorMessage}`,
};
}
log.error(`❌ [TOOL] ask_question failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle list_sessions tool
*/
async handleListSessions(): Promise<
ToolResult<{
active_sessions: number;
max_sessions: number;
session_timeout: number;
oldest_session_seconds: number;
total_messages: number;
sessions: Array<{
id: string;
created_at: number;
last_activity: number;
age_seconds: number;
inactive_seconds: number;
message_count: number;
notebook_url: string;
}>;
}>
> {
log.info(`🔧 [TOOL] list_sessions called`);
try {
const stats = this.sessionManager.getStats();
const sessions = this.sessionManager.getAllSessionsInfo();
const result = {
active_sessions: stats.active_sessions,
max_sessions: stats.max_sessions,
session_timeout: stats.session_timeout,
oldest_session_seconds: stats.oldest_session_seconds,
total_messages: stats.total_messages,
sessions: sessions.map((info) => ({
id: info.id,
created_at: info.created_at,
last_activity: info.last_activity,
age_seconds: info.age_seconds,
inactive_seconds: info.inactive_seconds,
message_count: info.message_count,
notebook_url: info.notebook_url,
})),
};
log.success(
`✅ [TOOL] list_sessions completed (${result.active_sessions} sessions)`
);
return {
success: true,
data: result,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] list_sessions failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle close_session tool
*/
async handleCloseSession(args: { session_id: string }): Promise<
ToolResult<{ status: string; message: string; session_id: string }>
> {
const { session_id } = args;
log.info(`🔧 [TOOL] close_session called`);
log.info(` Session ID: ${session_id}`);
try {
const closed = await this.sessionManager.closeSession(session_id);
if (closed) {
log.success(`✅ [TOOL] close_session completed`);
return {
success: true,
data: {
status: "success",
message: `Session ${session_id} closed successfully`,
session_id,
},
};
} else {
log.warning(`⚠️ [TOOL] Session ${session_id} not found`);
return {
success: false,
error: `Session ${session_id} not found`,
};
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] close_session failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle reset_session tool
*/
async handleResetSession(args: { session_id: string }): Promise<
ToolResult<{ status: string; message: string; session_id: string }>
> {
const { session_id } = args;
log.info(`🔧 [TOOL] reset_session called`);
log.info(` Session ID: ${session_id}`);
try {
const session = this.sessionManager.getSession(session_id);
if (!session) {
log.warning(`⚠️ [TOOL] Session ${session_id} not found`);
return {
success: false,
error: `Session ${session_id} not found`,
};
}
await session.reset();
log.success(`✅ [TOOL] reset_session completed`);
return {
success: true,
data: {
status: "success",
message: `Session ${session_id} reset successfully`,
session_id,
},
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] reset_session failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle get_health tool
*/
async handleGetHealth(): Promise<
ToolResult<{
status: string;
authenticated: boolean;
notebook_url: string;
active_sessions: number;
max_sessions: number;
session_timeout: number;
total_messages: number;
headless: boolean;
auto_login_enabled: boolean;
stealth_enabled: boolean;
troubleshooting_tip?: string;
}>
> {
log.info(`🔧 [TOOL] get_health called`);
try {
// Check authentication status
const statePath = await this.authManager.getValidStatePath();
const authenticated = statePath !== null;
// Get session stats
const stats = this.sessionManager.getStats();
const result = {
status: "ok",
authenticated,
notebook_url: CONFIG.notebookUrl || "not configured",
active_sessions: stats.active_sessions,
max_sessions: stats.max_sessions,
session_timeout: stats.session_timeout,
total_messages: stats.total_messages,
headless: CONFIG.headless,
auto_login_enabled: CONFIG.autoLoginEnabled,
stealth_enabled: CONFIG.stealthEnabled,
// Add troubleshooting tip if not authenticated
...((!authenticated) && {
troubleshooting_tip:
"For fresh start with clean browser session: Close all Chrome instances → " +
"cleanup_data(confirm=true, preserve_library=true) → setup_auth"
}),
};
log.success(`✅ [TOOL] get_health completed`);
return {
success: true,
data: result,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] get_health failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle setup_auth tool
*
* Opens a browser window for manual login with live progress updates.
* The operation waits synchronously for login completion (up to 10 minutes).
*/
async handleSetupAuth(
args: {
show_browser?: boolean;
browser_options?: BrowserOptions;
},
sendProgress?: ProgressCallback
): Promise<
ToolResult<{
status: string;
message: string;
authenticated: boolean;
duration_seconds?: number;
}>
> {
const { show_browser, browser_options } = args;
// CRITICAL: Send immediate progress to reset timeout from the very start
await sendProgress?.("Initializing authentication setup...", 0, 10);
log.info(`🔧 [TOOL] setup_auth called`);
if (show_browser !== undefined) {
log.info(` Show browser: ${show_browser}`);
}
const startTime = Date.now();
// Apply browser options temporarily
const originalConfig = { ...CONFIG };
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
Object.assign(CONFIG, effectiveConfig);
try {
// Progress: Starting
await sendProgress?.("Preparing authentication browser...", 1, 10);
log.info(` 🌐 Opening browser for interactive login...`);
// Progress: Opening browser
await sendProgress?.("Opening browser window...", 2, 10);
// Perform setup with progress updates (uses CONFIG internally)
const success = await this.authManager.performSetup(sendProgress);
const durationSeconds = (Date.now() - startTime) / 1000;
if (success) {
// Progress: Complete
await sendProgress?.("Authentication saved successfully!", 10, 10);
log.success(`✅ [TOOL] setup_auth completed (${durationSeconds.toFixed(1)}s)`);
return {
success: true,
data: {
status: "authenticated",
message: "Successfully authenticated and saved browser state",
authenticated: true,
duration_seconds: durationSeconds,
},
};
} else {
log.error(`❌ [TOOL] setup_auth failed (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: "Authentication failed or was cancelled",
};
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const durationSeconds = (Date.now() - startTime) / 1000;
log.error(`❌ [TOOL] setup_auth failed: ${errorMessage} (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: errorMessage,
};
} finally {
// Restore original CONFIG
Object.assign(CONFIG, originalConfig);
}
}
/**
* Handle re_auth tool
*
* Performs a complete re-authentication:
* 1. Closes all active browser sessions
* 2. Deletes all saved authentication data (cookies, Chrome profile)
* 3. Opens browser for fresh Google login
*
* Use for switching Google accounts or recovering from rate limits.
*/
async handleReAuth(
args: {
show_browser?: boolean;
browser_options?: BrowserOptions;
},
sendProgress?: ProgressCallback
): Promise<
ToolResult<{
status: string;
message: string;
authenticated: boolean;
duration_seconds?: number;
}>
> {
const { show_browser, browser_options } = args;
await sendProgress?.("Preparing re-authentication...", 0, 12);
log.info(`🔧 [TOOL] re_auth called`);
if (show_browser !== undefined) {
log.info(` Show browser: ${show_browser}`);
}
const startTime = Date.now();
// Apply browser options temporarily
const originalConfig = { ...CONFIG };
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
Object.assign(CONFIG, effectiveConfig);
try {
// 1. Close all active sessions
await sendProgress?.("Closing all active sessions...", 1, 12);
log.info(" 🛑 Closing all sessions...");
await this.sessionManager.closeAllSessions();
log.success(" ✅ All sessions closed");
// 2. Clear all auth data
await sendProgress?.("Clearing authentication data...", 2, 12);
log.info(" 🗑️ Clearing all auth data...");
await this.authManager.clearAllAuthData();
log.success(" ✅ Auth data cleared");
// 3. Perform fresh setup
await sendProgress?.("Starting fresh authentication...", 3, 12);
log.info(" 🌐 Starting fresh authentication setup...");
const success = await this.authManager.performSetup(sendProgress);
const durationSeconds = (Date.now() - startTime) / 1000;
if (success) {
await sendProgress?.("Re-authentication complete!", 12, 12);
log.success(`✅ [TOOL] re_auth completed (${durationSeconds.toFixed(1)}s)`);
return {
success: true,
data: {
status: "authenticated",
message:
"Successfully re-authenticated with new account. All previous sessions have been closed.",
authenticated: true,
duration_seconds: durationSeconds,
},
};
} else {
log.error(`❌ [TOOL] re_auth failed (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: "Re-authentication failed or was cancelled",
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const durationSeconds = (Date.now() - startTime) / 1000;
log.error(
`❌ [TOOL] re_auth failed: ${errorMessage} (${durationSeconds.toFixed(1)}s)`
);
return {
success: false,
error: errorMessage,
};
} finally {
// Restore original CONFIG
Object.assign(CONFIG, originalConfig);
}
}
/**
* Handle add_notebook tool
*/
async handleAddNotebook(args: AddNotebookInput): Promise<ToolResult<{ notebook: any }>> {
log.info(`🔧 [TOOL] add_notebook called`);
log.info(` Name: ${args.name}`);
try {
const notebook = this.library.addNotebook(args);
log.success(`✅ [TOOL] add_notebook completed: ${notebook.id}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] add_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle list_notebooks tool
*/
async handleListNotebooks(): Promise<ToolResult<{ notebooks: any[] }>> {
log.info(`🔧 [TOOL] list_notebooks called`);
try {
const notebooks = this.library.listNotebooks();
log.success(`✅ [TOOL] list_notebooks completed (${notebooks.length} notebooks)`);
return {
success: true,
data: { notebooks },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] list_notebooks failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle get_notebook tool
*/
async handleGetNotebook(args: { id: string }): Promise<ToolResult<{ notebook: any }>> {
log.info(`🔧 [TOOL] get_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.getNotebook(args.id);
if (!notebook) {
log.warning(`⚠️ [TOOL] Notebook not found: ${args.id}`);
return {
success: false,
error: `Notebook not found: ${args.id}`,
};
}
log.success(`✅ [TOOL] get_notebook completed: ${notebook.name}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] get_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle select_notebook tool
*/
async handleSelectNotebook(args: { id: string }): Promise<ToolResult<{ notebook: any }>> {
log.info(`🔧 [TOOL] select_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.selectNotebook(args.id);
log.success(`✅ [TOOL] select_notebook completed: ${notebook.name}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] select_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle update_notebook tool
*/
async handleUpdateNotebook(args: UpdateNotebookInput): Promise<ToolResult<{ notebook: any }>> {
log.info(`🔧 [TOOL] update_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.updateNotebook(args);
log.success(`✅ [TOOL] update_notebook completed: ${notebook.name}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] update_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle remove_notebook tool
*/
async handleRemoveNotebook(args: { id: string }): Promise<ToolResult<{ removed: boolean; closed_sessions: number }>> {
log.info(`🔧 [TOOL] remove_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.getNotebook(args.id);
if (!notebook) {
log.warning(`⚠️ [TOOL] Notebook not found: ${args.id}`);
return {
success: false,
error: `Notebook not found: ${args.id}`,
};
}
const removed = this.library.removeNotebook(args.id);
if (removed) {
const closedSessions = await this.sessionManager.closeSessionsForNotebook(
notebook.url
);
log.success(`✅ [TOOL] remove_notebook completed`);
return {
success: true,
data: { removed: true, closed_sessions: closedSessions },
};
} else {
log.warning(`⚠️ [TOOL] Notebook not found: ${args.id}`);
return {
success: false,
error: `Notebook not found: ${args.id}`,
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] remove_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle search_notebooks tool
*/
async handleSearchNotebooks(args: { query: string }): Promise<ToolResult<{ notebooks: any[] }>> {
log.info(`🔧 [TOOL] search_notebooks called`);
log.info(` Query: "${args.query}"`);
try {
const notebooks = this.library.searchNotebooks(args.query);
log.success(`✅ [TOOL] search_notebooks completed (${notebooks.length} results)`);
return {
success: true,
data: { notebooks },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] search_notebooks failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle get_library_stats tool
*/
async handleGetLibraryStats(): Promise<ToolResult<any>> {
log.info(`🔧 [TOOL] get_library_stats called`);
try {
const stats = this.library.getStats();
log.success(`✅ [TOOL] get_library_stats completed`);
return {
success: true,
data: stats,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] get_library_stats failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle cleanup_data tool
*
* ULTRATHINK Deep Cleanup - scans entire system for ALL NotebookLM MCP files
*/
async handleCleanupData(
args: { confirm: boolean; preserve_library?: boolean }
): Promise<
ToolResult<{
status: string;
mode: string;
preview?: {
categories: Array<{ name: string; description: string; paths: string[]; totalBytes: number; optional: boolean }>;
totalPaths: number;
totalSizeBytes: number;
};
result?: {
deletedPaths: string[];
failedPaths: string[];
totalSizeBytes: number;
categorySummary: Record<string, { count: number; bytes: number }>;
};
}>
> {
const { confirm, preserve_library = false } = args;
log.info(`🔧 [TOOL] cleanup_data called`);
log.info(` Confirm: ${confirm}`);
log.info(` Preserve Library: ${preserve_library}`);
const cleanupManager = new CleanupManager();
try {
// Always run in deep mode
const mode = "deep";
if (!confirm) {
// Preview mode - show what would be deleted
log.info(` 📋 Generating cleanup preview (mode: ${mode})...`);
const preview = await cleanupManager.getCleanupPaths(mode, preserve_library);
const platformInfo = cleanupManager.getPlatformInfo();
log.info(` Found ${preview.totalPaths.length} items (${cleanupManager.formatBytes(preview.totalSizeBytes)})`);
log.info(` Platform: ${platformInfo.platform}`);
return {
success: true,
data: {
status: "preview",
mode,
preview: {
categories: preview.categories,
totalPaths: preview.totalPaths.length,
totalSizeBytes: preview.totalSizeBytes,
},
},
};
} else {
// Cleanup mode - actually delete files
log.info(` 🗑️ Performing cleanup (mode: ${mode})...`);
const result = await cleanupManager.performCleanup(mode, preserve_library);
if (result.success) {
log.success(`✅ [TOOL] cleanup_data completed - deleted ${result.deletedPaths.length} items`);
} else {
log.warning(`⚠️ [TOOL] cleanup_data completed with ${result.failedPaths.length} errors`);
}
return {
success: result.success,
data: {
status: result.success ? "completed" : "partial",
mode,
result: {
deletedPaths: result.deletedPaths,
failedPaths: result.failedPaths,
totalSizeBytes: result.totalSizeBytes,
categorySummary: result.categorySummary,
},
},
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] cleanup_data failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Cleanup all resources (called on server shutdown)
*/
async cleanup(): Promise<void> {
log.info(`🧹 Cleaning up tool handlers...`);
await this.sessionManager.closeAllSessions();
log.success(`✅ Tool handlers cleanup complete`);
}
}
```
--------------------------------------------------------------------------------
/src/auth/auth-manager.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Authentication Manager for NotebookLM
*
* Handles:
* - Interactive login (headful browser for setup)
* - Auto-login with credentials (email/password from ENV)
* - Browser state persistence (cookies + localStorage + sessionStorage)
* - Cookie expiry validation
* - State expiry checks (24h file age)
* - Hard reset for clean start
*
* Based on the Python implementation from auth.py
*/
import type { BrowserContext, Page } from "patchright";
import fs from "fs/promises";
import { existsSync } from "fs";
import path from "path";
import { CONFIG, NOTEBOOKLM_AUTH_URL } from "../config.js";
import { log } from "../utils/logger.js";
import {
humanType,
randomDelay,
realisticClick,
randomMouseMovement,
} from "../utils/stealth-utils.js";
import type { ProgressCallback } from "../types.js";
/**
* Critical cookie names for Google authentication
*/
const CRITICAL_COOKIE_NAMES = [
"SID",
"HSID",
"SSID", // Google session
"APISID",
"SAPISID", // API auth
"OSID",
"__Secure-OSID", // NotebookLM-specific
"__Secure-1PSID",
"__Secure-3PSID", // Secure variants
];
export class AuthManager {
private stateFilePath: string;
private sessionFilePath: string;
constructor() {
this.stateFilePath = path.join(CONFIG.browserStateDir, "state.json");
this.sessionFilePath = path.join(CONFIG.browserStateDir, "session.json");
}
// ============================================================================
// Browser State Management
// ============================================================================
/**
* Save entire browser state (cookies + localStorage)
*/
async saveBrowserState(context: BrowserContext, page?: Page): Promise<boolean> {
try {
// Save storage state (cookies + localStorage + IndexedDB)
await context.storageState({ path: this.stateFilePath });
// Also save sessionStorage if page is provided
if (page) {
try {
const sessionStorageData: string = await page.evaluate((): string => {
// Properly extract sessionStorage as a plain object
const storage: Record<string, string> = {};
// @ts-expect-error - sessionStorage exists in browser context
for (let i = 0; i < sessionStorage.length; i++) {
// @ts-expect-error - sessionStorage exists in browser context
const key = sessionStorage.key(i);
if (key) {
// @ts-expect-error - sessionStorage exists in browser context
storage[key] = sessionStorage.getItem(key) || '';
}
}
return JSON.stringify(storage);
});
await fs.writeFile(this.sessionFilePath, sessionStorageData, {
encoding: "utf-8",
});
const entries = Object.keys(JSON.parse(sessionStorageData)).length;
log.success(`✅ Browser state saved (incl. sessionStorage: ${entries} entries)`);
} catch (error) {
log.warning(`⚠️ State saved, but sessionStorage failed: ${error}`);
}
} else {
log.success("✅ Browser state saved");
}
return true;
} catch (error) {
log.error(`❌ Failed to save browser state: ${error}`);
return false;
}
}
/**
* Check if saved browser state exists
*/
async hasSavedState(): Promise<boolean> {
try {
await fs.access(this.stateFilePath);
return true;
} catch {
return false;
}
}
/**
* Get path to saved browser state
*/
getStatePath(): string | null {
// Synchronous check using imported existsSync
if (existsSync(this.stateFilePath)) {
return this.stateFilePath;
}
return null;
}
/**
* Get valid state path (checks expiry)
*/
async getValidStatePath(): Promise<string | null> {
const statePath = this.getStatePath();
if (!statePath) {
return null;
}
if (await this.isStateExpired()) {
log.warning("⚠️ Saved state is expired (>24h old)");
log.info("💡 Run setup_auth tool to re-authenticate");
return null;
}
return statePath;
}
/**
* Load sessionStorage from file
*/
async loadSessionStorage(): Promise<Record<string, string> | null> {
try {
const data = await fs.readFile(this.sessionFilePath, { encoding: "utf-8" });
const sessionData = JSON.parse(data);
log.success(`✅ Loaded sessionStorage (${Object.keys(sessionData).length} entries)`);
return sessionData;
} catch (error) {
log.warning(`⚠️ Failed to load sessionStorage: ${error}`);
return null;
}
}
// ============================================================================
// Cookie Validation
// ============================================================================
/**
* Validate if saved state is still valid
*/
async validateState(context: BrowserContext): Promise<boolean> {
try {
const cookies = await context.cookies();
if (cookies.length === 0) {
log.warning("⚠️ No cookies found in state");
return false;
}
// Check for Google auth cookies
const googleCookies = cookies.filter((c) =>
c.domain.includes("google.com")
);
if (googleCookies.length === 0) {
log.warning("⚠️ No Google cookies found");
return false;
}
// Check if important cookies are expired
const currentTime = Date.now() / 1000;
for (const cookie of googleCookies) {
const expires = cookie.expires ?? -1;
if (expires !== -1 && expires < currentTime) {
log.warning(`⚠️ Cookie '${cookie.name}' has expired`);
return false;
}
}
log.success("✅ State validation passed");
return true;
} catch (error) {
log.warning(`⚠️ State validation failed: ${error}`);
return false;
}
}
/**
* Validate if critical authentication cookies are still valid
*/
async validateCookiesExpiry(context: BrowserContext): Promise<boolean> {
try {
const cookies = await context.cookies();
if (cookies.length === 0) {
log.warning("⚠️ No cookies found");
return false;
}
// Find critical cookies
const criticalCookies = cookies.filter((c) =>
CRITICAL_COOKIE_NAMES.includes(c.name)
);
if (criticalCookies.length === 0) {
log.warning("⚠️ No critical auth cookies found");
return false;
}
// Check expiration for each critical cookie
const currentTime = Date.now() / 1000;
const expiredCookies: string[] = [];
for (const cookie of criticalCookies) {
const expires = cookie.expires ?? -1;
// -1 means session cookie (valid until browser closes)
if (expires === -1) {
continue;
}
// Check if cookie is expired
if (expires < currentTime) {
expiredCookies.push(cookie.name);
}
}
if (expiredCookies.length > 0) {
log.warning(`⚠️ Expired cookies: ${expiredCookies.join(", ")}`);
return false;
}
log.success(`✅ All ${criticalCookies.length} critical cookies are valid`);
return true;
} catch (error) {
log.warning(`⚠️ Cookie validation failed: ${error}`);
return false;
}
}
/**
* Check if the saved state file is too old (>24 hours)
*/
async isStateExpired(): Promise<boolean> {
try {
const stats = await fs.stat(this.stateFilePath);
const fileAgeSeconds = (Date.now() - stats.mtimeMs) / 1000;
const maxAgeSeconds = 24 * 60 * 60; // 24 hours
if (fileAgeSeconds > maxAgeSeconds) {
const hoursOld = fileAgeSeconds / 3600;
log.warning(`⚠️ Saved state is ${hoursOld.toFixed(1)}h old (max: 24h)`);
return true;
}
return false;
} catch {
return true; // File doesn't exist = expired
}
}
// ============================================================================
// Interactive Login
// ============================================================================
/**
* Perform interactive login
* User will see a browser window and login manually
*
* SIMPLE & RELIABLE: Just wait for URL to change to notebooklm.google.com
*/
async performLogin(page: Page, sendProgress?: ProgressCallback): Promise<boolean> {
try {
log.info("🌐 Opening Google login page...");
log.warning("📝 Please login to your Google account");
log.warning("⏳ Browser will close automatically once you reach NotebookLM");
log.info("");
// Progress: Navigating
await sendProgress?.("Navigating to Google login...", 3, 10);
// Navigate to Google login (redirects to NotebookLM after auth)
await page.goto(NOTEBOOKLM_AUTH_URL, { timeout: 60000 });
// Progress: Waiting for login
await sendProgress?.("Waiting for manual login (up to 10 minutes)...", 4, 10);
// Wait for user to complete login
log.warning("⏳ Waiting for login (up to 10 minutes)...");
const checkIntervalMs = 1000; // Check every 1 second
const maxAttempts = 600; // 10 minutes total
let lastProgressUpdate = 0;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const currentUrl = page.url();
const elapsedSeconds = Math.floor(attempt * (checkIntervalMs / 1000));
// Send progress every 10 seconds
if (elapsedSeconds - lastProgressUpdate >= 10) {
lastProgressUpdate = elapsedSeconds;
const progressStep = Math.min(8, 4 + Math.floor(elapsedSeconds / 60));
await sendProgress?.(
`Waiting for login... (${elapsedSeconds}s elapsed)`,
progressStep,
10
);
}
// ✅ SIMPLE: Check if we're on NotebookLM (any path!)
if (currentUrl.startsWith("https://notebooklm.google.com/")) {
await sendProgress?.("Login successful! NotebookLM detected!", 9, 10);
log.success("✅ Login successful! NotebookLM URL detected.");
log.success(`✅ Current URL: ${currentUrl}`);
// Short wait to ensure page is loaded
await page.waitForTimeout(2000);
return true;
}
// Still on accounts.google.com - log periodically
if (currentUrl.includes("accounts.google.com") && attempt % 30 === 0 && attempt > 0) {
log.warning(`⏳ Still waiting... (${elapsedSeconds}s elapsed)`);
}
await page.waitForTimeout(checkIntervalMs);
} catch {
await page.waitForTimeout(checkIntervalMs);
continue;
}
}
// Timeout reached - final check
const currentUrl = page.url();
if (currentUrl.startsWith("https://notebooklm.google.com/")) {
await sendProgress?.("Login successful (detected on timeout check)!", 9, 10);
log.success("✅ Login successful (detected on timeout check)");
return true;
}
log.error("❌ Login verification failed - timeout reached");
log.warning(`Current URL: ${currentUrl}`);
return false;
} catch (error) {
log.error(`❌ Login failed: ${error}`);
return false;
}
}
// ============================================================================
// Auto-Login with Credentials
// ============================================================================
/**
* Attempt to authenticate using configured credentials
*/
async loginWithCredentials(
context: BrowserContext,
page: Page,
email: string,
password: string
): Promise<boolean> {
const maskedEmail = this.maskEmail(email);
log.warning(`🔁 Attempting automatic login for ${maskedEmail}...`);
// Log browser visibility
if (!CONFIG.headless) {
log.info(" 👁️ Browser is VISIBLE for debugging");
} else {
log.info(" 🙈 Browser is HEADLESS (invisible)");
}
log.info(` 🌐 Navigating to Google login...`);
try {
await page.goto(NOTEBOOKLM_AUTH_URL, {
waitUntil: "domcontentloaded",
timeout: CONFIG.browserTimeout,
});
log.success(` ✅ Page loaded: ${page.url().slice(0, 80)}...`);
} catch (error) {
log.warning(` ⚠️ Page load timeout (continuing anyway)`);
}
const deadline = Date.now() + CONFIG.autoLoginTimeoutMs;
log.info(` ⏰ Auto-login timeout: ${CONFIG.autoLoginTimeoutMs / 1000}s`);
// Already on NotebookLM?
log.info(" 🔍 Checking if already authenticated...");
if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) {
log.success("✅ Already authenticated");
await this.saveBrowserState(context, page);
return true;
}
log.warning(" ❌ Not authenticated yet, proceeding with login...");
// Handle possible account chooser
log.info(" 🔍 Checking for account chooser...");
if (await this.handleAccountChooser(page, email)) {
log.success(" ✅ Account selected from chooser");
if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) {
log.success("✅ Automatic login successful");
await this.saveBrowserState(context, page);
return true;
}
}
// Email step
log.info(" 📧 Entering email address...");
if (!(await this.fillIdentifier(page, email))) {
if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) {
log.success("✅ Automatic login successful");
await this.saveBrowserState(context, page);
return true;
}
log.warning("⚠️ Email input not detected");
}
// Password step (wait until visible)
let waitAttempts = 0;
log.warning(" ⏳ Waiting for password page to load...");
while (Date.now() < deadline && !(await this.fillPassword(page, password))) {
waitAttempts++;
// Log every 10 seconds (20 attempts * 0.5s)
if (waitAttempts % 20 === 0) {
const secondsWaited = waitAttempts * 0.5;
const secondsRemaining = (deadline - Date.now()) / 1000;
log.warning(
` ⏳ Still waiting for password field... (${secondsWaited}s elapsed, ${secondsRemaining.toFixed(0)}s remaining)`
);
log.info(` 📍 Current URL: ${page.url().slice(0, 100)}`);
}
if (page.url().includes("challenge")) {
log.warning("⚠️ Additional verification required (Google challenge page).");
return false;
}
await page.waitForTimeout(500);
}
// Wait for Google redirect after login
log.info(" 🔄 Waiting for Google redirect to NotebookLM...");
if (await this.waitForRedirectAfterLogin(page, deadline)) {
log.success("✅ Automatic login successful");
await this.saveBrowserState(context, page);
return true;
}
// Login failed - diagnose
log.error("❌ Automatic login timed out");
// Take screenshot for debugging
try {
const screenshotPath = path.join(
CONFIG.dataDir,
`login_failed_${Date.now()}.png`
);
await page.screenshot({ path: screenshotPath });
log.info(` 📸 Screenshot saved: ${screenshotPath}`);
} catch (error) {
log.warning(` ⚠️ Could not save screenshot: ${error}`);
}
// Diagnose specific failure reason
const currentUrl = page.url();
log.warning(" 🔍 Diagnosing failure...");
if (currentUrl.includes("accounts.google.com")) {
if (currentUrl.includes("/signin/identifier")) {
log.error(" ❌ Still on email page - email input might have failed");
log.info(" 💡 Check if email is correct in .env");
} else if (currentUrl.includes("/challenge")) {
log.error(
" ❌ Google requires additional verification (2FA, CAPTCHA, suspicious login)"
);
log.info(" 💡 Try logging in manually first: use setup_auth tool");
} else if (currentUrl.includes("/pwd") || currentUrl.includes("/password")) {
log.error(" ❌ Still on password page - password input might have failed");
log.info(" 💡 Check if password is correct in .env");
} else {
log.error(` ❌ Stuck on Google accounts page: ${currentUrl.slice(0, 80)}...`);
}
} else if (currentUrl.includes("notebooklm.google.com")) {
log.warning(" ⚠️ Reached NotebookLM but couldn't detect successful login");
log.info(" 💡 This might be a timing issue - try again");
} else {
log.error(` ❌ Unexpected page: ${currentUrl.slice(0, 80)}...`);
}
return false;
}
// ============================================================================
// Helper Methods
// ============================================================================
/**
* Wait for Google to redirect to NotebookLM after successful login (SIMPLE & RELIABLE)
*
* Just checks if URL changes to notebooklm.google.com - no complex UI element searching!
* Matches the simplified approach used in performLogin().
*/
private async waitForRedirectAfterLogin(
page: Page,
deadline: number
): Promise<boolean> {
log.info(" ⏳ Waiting for redirect to NotebookLM...");
while (Date.now() < deadline) {
try {
const currentUrl = page.url();
// Simple check: Are we on NotebookLM?
if (currentUrl.startsWith("https://notebooklm.google.com/")) {
log.success(" ✅ NotebookLM URL detected!");
// Short wait to ensure page is loaded
await page.waitForTimeout(2000);
return true;
}
} catch {
// Ignore errors
}
await page.waitForTimeout(500);
}
log.error(" ❌ Redirect timeout - NotebookLM URL not reached");
return false;
}
/**
* Wait for NotebookLM to load (SIMPLE & RELIABLE)
*
* Just checks if URL starts with notebooklm.google.com - no complex UI element searching!
* Matches the simplified approach used in performLogin().
*/
private async waitForNotebook(page: Page, timeoutMs: number): Promise<boolean> {
const endTime = Date.now() + timeoutMs;
while (Date.now() < endTime) {
try {
const currentUrl = page.url();
// Simple check: Are we on NotebookLM?
if (currentUrl.startsWith("https://notebooklm.google.com/")) {
log.success(" ✅ NotebookLM URL detected");
return true;
}
} catch {
// Ignore errors
}
await page.waitForTimeout(1000);
}
return false;
}
/**
* Handle possible account chooser
*/
private async handleAccountChooser(page: Page, email: string): Promise<boolean> {
try {
const chooser = await page.$$("div[data-identifier], li[data-identifier]");
if (chooser.length > 0) {
for (const item of chooser) {
const identifier = (await item.getAttribute("data-identifier"))?.toLowerCase() || "";
if (identifier === email.toLowerCase()) {
await item.click();
await randomDelay(150, 320);
await page.waitForTimeout(500);
return true;
}
}
// Click "Use another account"
await this.clickText(page, [
"Use another account",
"Weiteres Konto hinzufügen",
"Anderes Konto verwenden",
]);
await randomDelay(150, 320);
return false;
}
return false;
} catch {
return false;
}
}
/**
* Fill email identifier field with human-like typing
*/
private async fillIdentifier(page: Page, email: string): Promise<boolean> {
log.info(" 📧 Looking for email field...");
const emailSelectors = [
"input#identifierId",
"input[name='identifier']",
"input[type='email']",
];
let emailSelector: string | null = null;
let emailField: any = null;
for (const selector of emailSelectors) {
try {
const candidate = await page.waitForSelector(selector, {
state: "attached",
timeout: 3000,
});
if (!candidate) continue;
try {
if (!(await candidate.isVisible())) {
continue; // Hidden field
}
} catch {
continue;
}
emailField = candidate;
emailSelector = selector;
log.success(` ✅ Email field visible: ${selector}`);
break;
} catch {
continue;
}
}
if (!emailField || !emailSelector) {
log.warning(" ℹ️ No visible email field found (likely pre-filled)");
log.info(` 📍 Current URL: ${page.url().slice(0, 100)}`);
return false;
}
// Human-like mouse movement to field
try {
const box = await emailField.boundingBox();
if (box) {
const targetX = box.x + box.width / 2;
const targetY = box.y + box.height / 2;
await randomMouseMovement(page, targetX, targetY);
await randomDelay(200, 500);
}
} catch {
// Ignore errors
}
// Click to focus
try {
await realisticClick(page, emailSelector, false);
} catch (error) {
log.warning(` ⚠️ Could not click email field (${error}); trying direct focus`);
try {
await emailField.focus();
} catch {
log.error(" ❌ Failed to focus email field");
return false;
}
}
// ✅ FASTER: Programmer typing speed (90-120 WPM from config)
log.info(` ⌨️ Typing email: ${this.maskEmail(email)}`);
try {
const wpm = CONFIG.typingWpmMin + Math.floor(Math.random() * (CONFIG.typingWpmMax - CONFIG.typingWpmMin + 1));
await humanType(page, emailSelector, email, { wpm, withTypos: false });
log.success(" ✅ Email typed successfully");
} catch (error) {
log.error(` ❌ Typing failed: ${error}`);
try {
await page.fill(emailSelector, email);
log.success(" ✅ Filled email using fallback");
} catch {
return false;
}
}
// Human "thinking" pause before clicking Next
await randomDelay(400, 1200);
// Click Next button
log.info(" 🔘 Looking for Next button...");
const nextSelectors = [
"button:has-text('Next')",
"button:has-text('Weiter')",
"#identifierNext",
];
let nextClicked = false;
for (const selector of nextSelectors) {
try {
const button = await page.locator(selector);
if ((await button.count()) > 0) {
await realisticClick(page, selector, true);
log.success(` ✅ Next button clicked: ${selector}`);
nextClicked = true;
break;
}
} catch {
continue;
}
}
if (!nextClicked) {
log.warning(" ⚠️ Button not found, pressing Enter");
await emailField.press("Enter");
}
// Variable delay
await randomDelay(800, 1500);
log.success(" ✅ Email step complete");
return true;
}
/**
* Fill password field with human-like typing
*/
private async fillPassword(page: Page, password: string): Promise<boolean> {
log.info(" 🔐 Looking for password field...");
const passwordSelectors = ["input[name='Passwd']", "input[type='password']"];
let passwordSelector: string | null = null;
let passwordField: any = null;
for (const selector of passwordSelectors) {
try {
passwordField = await page.$(selector);
if (passwordField) {
passwordSelector = selector;
log.success(` ✅ Password field found: ${selector}`);
break;
}
} catch {
continue;
}
}
if (!passwordField) {
// Not found yet, but don't fail - this is called in a loop
return false;
}
// Human-like mouse movement to field
try {
const box = await passwordField.boundingBox();
if (box) {
const targetX = box.x + box.width / 2;
const targetY = box.y + box.height / 2;
await randomMouseMovement(page, targetX, targetY);
await randomDelay(300, 700);
}
} catch {
// Ignore errors
}
// Click to focus
if (passwordSelector) {
await realisticClick(page, passwordSelector, false);
}
// ✅ FASTER: Programmer typing speed (90-120 WPM from config)
log.info(" ⌨️ Typing password...");
try {
const wpm = CONFIG.typingWpmMin + Math.floor(Math.random() * (CONFIG.typingWpmMax - CONFIG.typingWpmMin + 1));
if (passwordSelector) {
await humanType(page, passwordSelector, password, { wpm, withTypos: false });
}
log.success(" ✅ Password typed successfully");
} catch (error) {
log.error(` ❌ Typing failed: ${error}`);
return false;
}
// Human "review" pause before submitting password
await randomDelay(300, 1000);
// Click Next button
log.info(" 🔘 Looking for Next button...");
const pwdNextSelectors = [
"button:has-text('Next')",
"button:has-text('Weiter')",
"#passwordNext",
];
let pwdNextClicked = false;
for (const selector of pwdNextSelectors) {
try {
const button = await page.locator(selector);
if ((await button.count()) > 0) {
await realisticClick(page, selector, true);
log.success(` ✅ Next button clicked: ${selector}`);
pwdNextClicked = true;
break;
}
} catch {
continue;
}
}
if (!pwdNextClicked) {
log.warning(" ⚠️ Button not found, pressing Enter");
await passwordField.press("Enter");
}
// Variable delay
await randomDelay(800, 1500);
log.success(" ✅ Password step complete");
return true;
}
/**
* Click text element
*/
private async clickText(page: Page, texts: string[]): Promise<boolean> {
for (const text of texts) {
const selector = `text="${text}"`;
try {
const locator = page.locator(selector);
if ((await locator.count()) > 0) {
await realisticClick(page, selector, true);
await randomDelay(120, 260);
return true;
}
} catch {
continue;
}
}
return false;
}
/**
* Mask email for logging
*/
private maskEmail(email: string): string {
if (!email.includes("@")) {
return "***";
}
const [name, domain] = email.split("@");
if (name.length <= 2) {
return `${"*".repeat(name.length)}@${domain}`;
}
return `${name[0]}${"*".repeat(name.length - 2)}${name[name.length - 1]}@${domain}`;
}
// ============================================================================
// Additional Helper Methods
// ============================================================================
/**
* Load authentication state from a specific file path
*/
async loadAuthState(context: BrowserContext, statePath: string): Promise<boolean> {
try {
// Read state.json
const stateData = await fs.readFile(statePath, { encoding: "utf-8" });
const state = JSON.parse(stateData);
// Add cookies to context
if (state.cookies) {
await context.addCookies(state.cookies);
log.success(`✅ Loaded ${state.cookies.length} cookies from ${statePath}`);
return true;
}
log.warning(`⚠️ No cookies found in state file`);
return false;
} catch (error) {
log.error(`❌ Failed to load auth state: ${error}`);
return false;
}
}
/**
* Perform interactive setup (for setup_auth tool)
* Opens a PERSISTENT browser for manual login
*
* CRITICAL: Uses the SAME persistent context as runtime!
* This ensures cookies are automatically saved to the Chrome profile.
*
* Benefits over temporary browser:
* - Session cookies persist correctly (Playwright bug workaround)
* - Same fingerprint as runtime
* - No need for addCookies() workarounds
* - Automatic cookie persistence via Chrome profile
*
* @param sendProgress Optional progress callback
* @param overrideHeadless Optional override for headless mode (true = visible, false = headless)
* If not provided, defaults to true (visible) for setup
*/
async performSetup(sendProgress?: ProgressCallback, overrideHeadless?: boolean): Promise<boolean> {
const { chromium } = await import("patchright");
// Determine headless mode: override or default to true (visible for setup)
// overrideHeadless contains show_browser value (true = show, false = hide)
const shouldShowBrowser = overrideHeadless !== undefined ? overrideHeadless : true;
try {
// CRITICAL: Clear ALL old auth data FIRST (for account switching)
log.info("🔄 Preparing for new account authentication...");
await sendProgress?.("Clearing old authentication data...", 1, 10);
await this.clearAllAuthData();
log.info("🚀 Launching persistent browser for interactive setup...");
log.info(` 📍 Profile: ${CONFIG.chromeProfileDir}`);
await sendProgress?.("Launching persistent browser...", 2, 10);
// ✅ CRITICAL FIX: Use launchPersistentContext (same as runtime!)
// This ensures session cookies persist correctly
const context = await chromium.launchPersistentContext(
CONFIG.chromeProfileDir,
{
headless: !shouldShowBrowser, // Use override or default to visible for setup
channel: "chrome" as const,
viewport: CONFIG.viewport,
locale: "en-US",
timezoneId: "Europe/Berlin",
args: [
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-first-run",
"--no-default-browser-check",
],
}
);
// Get or create a page
const pages = context.pages();
const page = pages.length > 0 ? pages[0] : await context.newPage();
// Perform login with progress updates
const loginSuccess = await this.performLogin(page, sendProgress);
if (loginSuccess) {
// ✅ Save browser state to state.json (for validation & backup)
// Chrome ALSO saves everything to the persistent profile automatically!
await sendProgress?.("Saving authentication state...", 9, 10);
await this.saveBrowserState(context, page);
log.success("✅ Setup complete - authentication saved to:");
log.success(` 📄 State file: ${this.stateFilePath}`);
log.success(` 📁 Chrome profile: ${CONFIG.chromeProfileDir}`);
log.info("💡 Session cookies will now persist across restarts!");
}
// Close persistent context
await context.close();
return loginSuccess;
} catch (error) {
log.error(`❌ Setup failed: ${error}`);
return false;
}
}
// ============================================================================
// Cleanup
// ============================================================================
/**
* Clear ALL authentication data for account switching
*
* CRITICAL: This deletes EVERYTHING to ensure only ONE account is active:
* - All state.json files (cookies, localStorage)
* - sessionStorage files
* - Chrome profile directory (browser fingerprint, cache, etc.)
*
* Use this BEFORE authenticating a new account!
*/
async clearAllAuthData(): Promise<void> {
log.warning("🗑️ Clearing ALL authentication data for account switch...");
let deletedCount = 0;
// 1. Delete all state files in browser_state_dir
try {
const files = await fs.readdir(CONFIG.browserStateDir);
for (const file of files) {
if (file.endsWith(".json")) {
await fs.unlink(path.join(CONFIG.browserStateDir, file));
log.info(` ✅ Deleted: ${file}`);
deletedCount++;
}
}
} catch (error) {
log.warning(` ⚠️ Could not delete state files: ${error}`);
}
// 2. Delete Chrome profile (THE KEY for account switching!)
// This removes ALL browser data: cookies, cache, fingerprint, etc.
try {
const chromeProfileDir = CONFIG.chromeProfileDir;
if (existsSync(chromeProfileDir)) {
await fs.rm(chromeProfileDir, { recursive: true, force: true });
log.success(` ✅ Deleted Chrome profile: ${chromeProfileDir}`);
deletedCount++;
}
} catch (error) {
log.warning(` ⚠️ Could not delete Chrome profile: ${error}`);
}
if (deletedCount === 0) {
log.info(" ℹ️ No old auth data found (already clean)");
} else {
log.success(`✅ All auth data cleared (${deletedCount} items) - ready for new account!`);
}
}
/**
* Clear all saved authentication state
*/
async clearState(): Promise<boolean> {
try {
try {
await fs.unlink(this.stateFilePath);
} catch {
// File doesn't exist
}
try {
await fs.unlink(this.sessionFilePath);
} catch {
// File doesn't exist
}
log.success("✅ Authentication state cleared");
return true;
} catch (error) {
log.error(`❌ Failed to clear state: ${error}`);
return false;
}
}
/**
* HARD RESET: Completely delete ALL authentication state
*/
async hardResetState(): Promise<boolean> {
try {
log.warning("🧹 Performing HARD RESET of all authentication state...");
let deletedCount = 0;
// Delete state file
try {
await fs.unlink(this.stateFilePath);
log.info(` 🗑️ Deleted: ${this.stateFilePath}`);
deletedCount++;
} catch {
// File doesn't exist
}
// Delete session file
try {
await fs.unlink(this.sessionFilePath);
log.info(` 🗑️ Deleted: ${this.sessionFilePath}`);
deletedCount++;
} catch {
// File doesn't exist
}
// Delete entire browser_state_dir
try {
const files = await fs.readdir(CONFIG.browserStateDir);
for (const file of files) {
await fs.unlink(path.join(CONFIG.browserStateDir, file));
deletedCount++;
}
log.info(` 🗑️ Deleted: ${CONFIG.browserStateDir}/ (${files.length} files)`);
} catch {
// Directory doesn't exist or empty
}
if (deletedCount === 0) {
log.info(" ℹ️ No state to delete (already clean)");
} else {
log.success(`✅ Hard reset complete: ${deletedCount} items deleted`);
}
return true;
} catch (error) {
log.error(`❌ Hard reset failed: ${error}`);
return false;
}
}
}
```