Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# NotebookLM Skill - Environment Variables
# This skill primarily uses browser-based Google authentication (no API keys needed).
# The following are optional configuration overrides.

# Browser configuration (optional)
# PLAYWRIGHT_BROWSERS_PATH= # Custom path for Playwright browser binaries

# Debug mode (optional)
# NOTEBOOKLM_DEBUG=false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ scripts/*.pyc
# Environment
.env
*.env
!.env.example
.env.*

# Browser/Auth state (if accidentally placed outside data/)
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,18 @@ git clone https://github.com/PleasePrompto/notebooklm-skill notebooklm

---

## Troubleshooting

| Issue | Solution |
|-------|----------|
| Browser doesn't open | Ensure Chrome/Chromium is installed and `BROWSER_PATH` is set if non-standard |
| Auth fails repeatedly | Delete `~/.claude/skills/notebooklm/.browser_data` and re-authenticate |
| "Skill not found" | Verify the clone is at `~/.claude/skills/notebooklm/` (not nested) |
| Timeout on queries | Increase timeout in config; long documents may need 120s+ |
| Running in web UI | This skill requires **local** Claude Code — web UI sandboxes block browser automation |

---

<div align="center">

Built as a Claude Code Skill adaptation of my [NotebookLM MCP Server](https://github.com/PleasePrompto/notebooklm-mcp)
Expand Down
10 changes: 5 additions & 5 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ def ensure_venv_and_run():

# We need to set up or switch to our venv
if not venv_dir.exists():
print("🔧 First-time setup detected...")
print(" Creating isolated environment for NotebookLM skill...")
print(" This ensures clean dependency management...")
print("[SETUP] First-time setup detected...")
print(" Creating isolated environment for NotebookLM skill...")
print(" This ensures clean dependency management...")

# Create venv
import venv
Expand Down Expand Up @@ -68,11 +68,11 @@ def ensure_venv_and_run():
capture_output=True
)

print(" Environment ready! All dependencies isolated in .venv/")
print("[OK] Environment ready! All dependencies isolated in .venv/")

# If we're here and not in the venv, we should recommend using the venv
if not in_venv:
print("\n⚠️ Running outside virtual environment")
print("\n[WARN] Running outside virtual environment")
print(" Recommended: Use scripts/run.py to ensure clean execution")
print(" Or activate: source .venv/bin/activate")

Expand Down
38 changes: 19 additions & 19 deletions scripts/ask_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ def ask_notebooklm(question: str, notebook_url: str, headless: bool = True) -> s
auth = AuthManager()

if not auth.is_authenticated():
print("⚠️ Not authenticated. Run: python auth_manager.py setup")
print("[WARN] Not authenticated. Run: python auth_manager.py setup")
return None

print(f"💬 Asking: {question}")
print(f"📚 Notebook: {notebook_url}")
print(f"[ASK] Asking: {question}")
print(f"[NOTEBOOK] Notebook: {notebook_url}")

playwright = None
context = None
Expand All @@ -73,14 +73,14 @@ def ask_notebooklm(question: str, notebook_url: str, headless: bool = True) -> s

# Navigate to notebook
page = context.new_page()
print(" 🌐 Opening notebook...")
print(" [OPEN] Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")

# Wait for NotebookLM
page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=10000)

# Wait for query input (MCP approach)
print(" Waiting for query input...")
print(" [WAIT] Waiting for query input...")
query_element = None

for selector in QUERY_INPUT_SELECTORS:
Expand All @@ -91,31 +91,31 @@ def ask_notebooklm(question: str, notebook_url: str, headless: bool = True) -> s
state="visible" # Only check visibility, not disabled!
)
if query_element:
print(f" Found input: {selector}")
print(f" [OK] Found input: {selector}")
break
except:
continue

if not query_element:
print(" Could not find query input")
print(" [ERROR] Could not find query input")
return None

# Type question (human-like, fast)
print(" Typing question...")
print(" [WAIT] Typing question...")

# Use primary selector for typing
input_selector = QUERY_INPUT_SELECTORS[0]
StealthUtils.human_type(page, input_selector, question)

# Submit
print(" 📤 Submitting...")
print(" [SUBMIT] Submitting...")
page.keyboard.press("Enter")

# Small pause
StealthUtils.random_delay(500, 1500)

# Wait for response (MCP approach: poll for stable text)
print(" Waiting for answer...")
print(" [WAIT] Waiting for answer...")

answer = None
stable_count = 0
Expand Down Expand Up @@ -159,15 +159,15 @@ def ask_notebooklm(question: str, notebook_url: str, headless: bool = True) -> s
time.sleep(1)

if not answer:
print(" Timeout waiting for answer")
print(" [ERROR] Timeout waiting for answer")
return None

print(" Got answer!")
print(" [OK] Got answer!")
# Add follow-up reminder to encourage Claude to ask more questions
return answer + FOLLOW_UP_REMINDER

except Exception as e:
print(f" ❌ Error: {e}")
print(f" [ERROR] {e}")
import traceback
traceback.print_exc()
return None
Expand Down Expand Up @@ -206,7 +206,7 @@ def main():
if notebook:
notebook_url = notebook['url']
else:
print(f" Notebook '{args.notebook_id}' not found")
print(f"[ERROR] Notebook '{args.notebook_id}' not found")
return 1

if not notebook_url:
Expand All @@ -215,19 +215,19 @@ def main():
active = library.get_active_notebook()
if active:
notebook_url = active['url']
print(f"📚 Using active notebook: {active['name']}")
print(f"[NOTEBOOK] Using active notebook: {active['name']}")
else:
# Show available notebooks
notebooks = library.list_notebooks()
if notebooks:
print("\n📚 Available notebooks:")
print("\n[NOTEBOOKS] Available notebooks:")
for nb in notebooks:
mark = " [ACTIVE]" if nb.get('id') == library.active_notebook_id else ""
print(f" {nb['id']}: {nb['name']}{mark}")
print(f" {nb['id']}: {nb['name']}{mark}")
print("\nSpecify with --notebook-id or set active:")
print("python scripts/run.py notebook_manager.py activate --id ID")
else:
print(" No notebooks in library. Add one first:")
print("[ERROR] No notebooks in library. Add one first:")
print("python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS")
return 1

Expand All @@ -248,7 +248,7 @@ def main():
print("=" * 60)
return 0
else:
print("\n Failed to get answer")
print("\n[ERROR] Failed to get answer")
return 1


Expand Down
60 changes: 30 additions & 30 deletions scripts/auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def is_authenticated(self) -> bool:
# Check if state file is not too old (7 days)
age_days = (time.time() - self.state_file.stat().st_mtime) / 86400
if age_days > 7:
print(f"⚠️ Browser state is {age_days:.1f} days old, may need re-authentication")
print(f"[WARN] Browser state is {age_days:.1f} days old, may need re-authentication")

return True

Expand Down Expand Up @@ -94,8 +94,8 @@ def setup_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
Returns:
True if authentication successful
"""
print("🔐 Starting authentication setup...")
print(f" Timeout: {timeout_minutes} minutes")
print("[AUTH] Starting authentication setup...")
print(f" Timeout: {timeout_minutes} minutes")

playwright = None
context = None
Expand All @@ -115,32 +115,32 @@ def setup_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:

# Check if already authenticated
if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url:
print(" Already authenticated!")
print(" [OK] Already authenticated!")
self._save_browser_state(context)
return True

# Wait for manual login
print("\n Please log in to your Google account...")
print(f" ⏱️ Waiting up to {timeout_minutes} minutes for login...")
print("\n [WAIT] Please log in to your Google account...")
print(f" [WAIT] Waiting up to {timeout_minutes} minutes for login...")

try:
# Wait for URL to change to NotebookLM (regex ensures it's the actual domain, not a parameter)
timeout_ms = int(timeout_minutes * 60 * 1000)
page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=timeout_ms)

print(f" Login successful!")
print(f" [OK] Login successful!")

# Save authentication state
self._save_browser_state(context)
self._save_auth_info()
return True

except Exception as e:
print(f" Authentication timeout: {e}")
print(f" [ERROR] Authentication timeout: {e}")
return False

except Exception as e:
print(f" ❌ Error: {e}")
print(f" [ERROR] {e}")
return False

finally:
Expand All @@ -162,9 +162,9 @@ def _save_browser_state(self, context: BrowserContext):
try:
# Save storage state (cookies, localStorage)
context.storage_state(path=str(self.state_file))
print(f" 💾 Saved browser state to: {self.state_file}")
print(f" [SAVE] Saved browser state to: {self.state_file}")
except Exception as e:
print(f" Failed to save browser state: {e}")
print(f" [ERROR] Failed to save browser state: {e}")
raise

def _save_auth_info(self):
Expand All @@ -186,29 +186,29 @@ def clear_auth(self) -> bool:
Returns:
True if cleared successfully
"""
print("🗑️ Clearing authentication data...")
print("[CLEAR] Clearing authentication data...")

try:
# Remove browser state
if self.state_file.exists():
self.state_file.unlink()
print(" Removed browser state")
print(" [OK] Removed browser state")

# Remove auth info
if self.auth_info_file.exists():
self.auth_info_file.unlink()
print(" Removed auth info")
print(" [OK] Removed auth info")

# Clear entire browser state directory
if self.browser_state_dir.exists():
shutil.rmtree(self.browser_state_dir)
self.browser_state_dir.mkdir(parents=True, exist_ok=True)
print(" Cleared browser data")
print(" [OK] Cleared browser data")

return True

except Exception as e:
print(f" Error clearing auth: {e}")
print(f" [ERROR] Error clearing auth: {e}")
return False

def re_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
Expand All @@ -222,7 +222,7 @@ def re_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
Returns:
True if successful
"""
print("🔄 Starting re-authentication...")
print("[REAUTH] Starting re-authentication...")

# Clear existing auth
self.clear_auth()
Expand All @@ -241,7 +241,7 @@ def validate_auth(self) -> bool:
if not self.is_authenticated():
return False

print("🔍 Validating authentication...")
print("[VALIDATE] Validating authentication...")

playwright = None
context = None
Expand All @@ -261,14 +261,14 @@ def validate_auth(self) -> bool:

# Check if we can access NotebookLM
if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url:
print(" Authentication is valid")
print(" [OK] Authentication is valid")
return True
else:
print(" Authentication is invalid (redirected to login)")
print(" [ERROR] Authentication is invalid (redirected to login)")
return False

except Exception as e:
print(f" Validation failed: {e}")
print(f" [ERROR] Validation failed: {e}")
return False

finally:
Expand Down Expand Up @@ -316,21 +316,21 @@ def main():
# Execute command
if args.command == 'setup':
if auth.setup_auth(headless=args.headless, timeout_minutes=args.timeout):
print("\n Authentication setup complete!")
print("\n[OK] Authentication setup complete!")
print("You can now use ask_question.py to query NotebookLM")
else:
print("\n Authentication setup failed")
print("\n[ERROR] Authentication setup failed")
exit(1)

elif args.command == 'status':
info = auth.get_auth_info()
print("\n🔐 Authentication Status:")
print(f" Authenticated: {'Yes' if info['authenticated'] else 'No'}")
print("\n[STATUS] Authentication Status:")
print(f" Authenticated: {'Yes' if info['authenticated'] else 'No'}")
if info.get('state_age_hours'):
print(f" State age: {info['state_age_hours']:.1f} hours")
print(f" State age: {info['state_age_hours']:.1f} hours")
if info.get('authenticated_at_iso'):
print(f" Last auth: {info['authenticated_at_iso']}")
print(f" State file: {info['state_file']}")
print(f" Last auth: {info['authenticated_at_iso']}")
print(f" State file: {info['state_file']}")

elif args.command == 'validate':
if auth.validate_auth():
Expand All @@ -345,9 +345,9 @@ def main():

elif args.command == 'reauth':
if auth.re_auth(timeout_minutes=args.timeout):
print("\n Re-authentication complete!")
print("\n[OK] Re-authentication complete!")
else:
print("\n Re-authentication failed")
print("\n[ERROR] Re-authentication failed")
exit(1)

else:
Expand Down
Loading