
Nblm
- 6.3k installs
- 33 repo stars
- Updated February 28, 2026
- magicseek/nblm
nblm is an agent skill for Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management,
About
Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded citation-backed answers from Gemini Browser automation library management persistent auth Drastically reduced hallucinations through document-only responses name nblm description Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded citation-backed answers from Gemini Browser automation library management persistent auth Drastically reduced hallucinations through document-only responses NotebookLM Quick Commands Query Google NotebookLM for source-grounded citation-backed answers Environment All dependencies and authentication are handled automatically by run py First run creates venv and installs Python Node js dependencies If Google auth is missing or expired a browser window opens automatically No manual pre-flight steps required Usage nblm command args Commands Notebook Management Command Description login Authenticate with Google status Show auth and library status accounts List all Google accounts accounts add Add a new Google account accounts switch id Switch active account by index or email accounts remove id Remove a Google.
- NotebookLM Quick Commands
- First run creates `.venv` and installs Python/Node.js dependencies
- If Google auth is missing or expired, a browser window opens automatically
- No manual pre-flight steps required
- `--use-active` - Upload to the currently active notebook
Nblm by the numbers
- 6,323 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #118 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
nblm capabilities & compatibility
- Capabilities
- notebooklm quick commands · first run creates `.venv` and installs python/no · if google auth is missing or expired, a browser · no manual pre flight steps required · ` use active` upload to the currently active
- Use cases
- documentation
What nblm says it does
--- name: nblm description: Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini.
Browser automation, library management, persistent auth.
Drastically reduced hallucinations through document-only responses.
npx skills add https://github.com/magicseek/nblm --skill nblmAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.3k |
|---|---|
| repo stars | ★ 33 |
| Security audit | 1 / 3 scanners passed |
| Last updated | February 28, 2026 |
| Repository | magicseek/nblm ↗ |
When should developers use nblm and what problem does it solve?
Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Dr
Who is it for?
Developers working with nblm patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Dr
What you get
Grounded guidance and workflows from SKILL.md for nblm.
- citation-backed answer text
- notebook query results
- authenticated session state
Files
NotebookLM Quick Commands
Query Google NotebookLM for source-grounded, citation-backed answers.
Environment
All dependencies and authentication are handled automatically by run.py:
- First run creates
.venvand installs Python/Node.js dependencies - If Google auth is missing or expired, a browser window opens automatically
- No manual pre-flight steps required
---
Usage
/nblm <command> [args]
Commands
Notebook Management
| Command | Description |
|---|---|
login | Authenticate with Google |
status | Show auth and library status |
accounts | List all Google accounts |
accounts add | Add a new Google account |
accounts switch <id> | Switch active account (by index or email) |
accounts remove <id> | Remove a Google account |
accounts use <id> | Set agent-specific active account (OpenClaw isolation) |
accounts clear | Clear agent-specific account override |
local | List notebooks in local library |
remote | List all notebooks from NotebookLM API |
create <name> | Create a new notebook |
delete [--id ID] | Delete a notebook |
rename <name> [--id ID] | Rename a notebook |
summary [--id ID] | Get AI-generated summary |
describe [--id ID] | Get description and suggested topics |
add <url-or-id> | Add notebook to local library (auto-detects URL vs notebook ID) |
activate <id> | Set active notebook |
Source Management
| Command | Description |
|---|---|
sources [--id ID] | List sources in notebook |
upload <file> | Upload a single file |
upload <folder> | Sync a folder of files to NotebookLM |
upload-zlib <url> | Download from Z-Library and upload |
upload-url <url> | Add URL as source |
upload-youtube <url> | Add YouTube video as source |
upload-text <title> [--content TEXT] | Add text as source |
source-text <source-id> | Get full indexed text |
source-guide <source-id> | Get AI summary and keywords |
source-rename <source-id> <name> | Rename a source |
source-refresh <source-id> | Re-fetch URL content |
source-delete <source-id> | Delete a source |
Upload options:
--use-active- Upload to the currently active notebook--create-new- Create a new notebook named after the file/folder--notebook-id <id>- Upload to a specific notebook--dry-run- Show sync plan without executing (folder sync)--rebuild- Force rebuild tracking file (folder sync)
Important: When user runs upload without specifying a target, ASK them first:
"Would you like to upload to the active notebook, or create a new notebook?"
Then pass the appropriate flag (--use-active or --create-new).
Chat & Audio/Media
| Command | Description |
|---|---|
ask <question> | Query NotebookLM |
podcast [--instructions TEXT] | Generate audio podcast |
podcast-status <task-id> | Check podcast generation status |
podcast-download [output-path] | Download latest podcast |
briefing [--instructions TEXT] | Generate brief audio summary |
debate [--instructions TEXT] | Generate debate-style audio |
slides [--instructions TEXT] | Generate slide deck |
slides-download [output-path] | Download slide deck as PDF |
infographic [--instructions TEXT] | Generate infographic |
infographic-download [output-path] | Download infographic |
media-list [--type TYPE] | List generated media (audio/video/slides/infographic) |
media-delete <id> | Delete a generated media item |
Command Routing
Based on $ARGUMENTS, execute the appropriate command:
$IF($ARGUMENTS, Parse the command from: "$ARGUMENTS"
login → python scripts/run.py auth_manager.py setup --service google
accounts → python scripts/run.py auth_manager.py accounts list
accounts add → python scripts/run.py auth_manager.py accounts add
accounts switch <id> → python scripts/run.py auth_manager.py accounts switch "<id>"
accounts remove <id> → python scripts/run.py auth_manager.py accounts remove "<id>"
accounts use <id> → python scripts/run.py auth_manager.py accounts use "<id>"
accounts clear → python scripts/run.py auth_manager.py accounts clear
status → Run both:
python scripts/run.py auth_manager.py statuspython scripts/run.py notebook_manager.py list
local → python scripts/run.py notebook_manager.py list
remote → python scripts/run.py nblm_cli.py notebooks
create <name> → python scripts/run.py nblm_cli.py create "<name>"
delete [--id ID] → python scripts/run.py nblm_cli.py delete <args>
rename <name> [--id ID] → python scripts/run.py nblm_cli.py rename "<name>" <args>
summary [--id ID] → python scripts/run.py nblm_cli.py summary <args>
describe [--id ID] → python scripts/run.py nblm_cli.py describe <args>
add <url-or-id> → Smart add workflow (auto-detects URL vs notebook ID)
activate <id> → python scripts/run.py notebook_manager.py activate --id "<id>"
sources [--id ID] → python scripts/run.py nblm_cli.py sources <args>
upload <file> → First ASK user: "Upload to active notebook or create new?" Then:
- Active:
python scripts/run.py source_manager.py add --file "<file>" --use-active - New:
python scripts/run.py source_manager.py add --file "<file>" --create-new
upload <folder> → Sync a folder:
- First ASK user: "Sync to active notebook, create new, or specify notebook?"
- Active:
python scripts/run.py source_manager.py sync "<folder>" --use-active - New:
python scripts/run.py source_manager.py sync "<folder>" --create-new - Specific:
python scripts/run.py source_manager.py sync "<folder>" --notebook-id ID - Dry-run:
python scripts/run.py source_manager.py sync "<folder>" --dry-run - Rebuild:
python scripts/run.py source_manager.py sync "<folder>" --rebuild
upload-zlib <url> → First ASK user: "Upload to active notebook or create new?" Then:
- Active:
python scripts/run.py source_manager.py add --url "<url>" --use-active - New:
python scripts/run.py source_manager.py add --url "<url>" --create-new
upload-url <url> → python scripts/run.py nblm_cli.py upload-url "<url>"
upload-youtube <url> → python scripts/run.py nblm_cli.py upload-youtube "<url>"
upload-text <title> → python scripts/run.py nblm_cli.py upload-text "<title>" <args>
source-text <id> → python scripts/run.py nblm_cli.py source-text "<id>"
source-guide <id> → python scripts/run.py nblm_cli.py source-guide "<id>"
source-rename <id> <name> → python scripts/run.py nblm_cli.py source-rename "<id>" "<name>"
source-refresh <id> → python scripts/run.py nblm_cli.py source-refresh "<id>"
source-delete <id> → python scripts/run.py nblm_cli.py source-delete "<id>"
ask <question> → python scripts/run.py nblm_cli.py ask "<question>"
podcast → python scripts/run.py artifact_manager.py generate --format DEEP_DIVE <args>
podcast-status <task-id> → python scripts/run.py artifact_manager.py status --task-id "<task-id>"
podcast-download [output-path] → python scripts/run.py artifact_manager.py download "<output-path>"
briefing → python scripts/run.py artifact_manager.py generate --format BRIEF <args>
debate → python scripts/run.py artifact_manager.py generate --format DEBATE <args>
slides → python scripts/run.py artifact_manager.py generate-slides <args>
slides-download [output-path] → python scripts/run.py artifact_manager.py download "<output-path>" --type slide-deck
infographic → python scripts/run.py artifact_manager.py generate-infographic <args>
infographic-download [output-path] → python scripts/run.py artifact_manager.py download "<output-path>" --type infographic
media-list [--type TYPE] → python scripts/run.py artifact_manager.py list <args>
media-delete <id> → python scripts/run.py artifact_manager.py delete "<id>"
If command not recognized, show usage help.,
Show available commands with /nblm (no arguments) )
Podcast Options
/nblm podcast --length DEFAULT --wait --output ./podcast.mp3
/nblm podcast --instructions "Focus on the key findings"
/nblm briefing --wait --output ./summary.mp3
/nblm debate --instructions "Compare the two approaches"| Option | Values |
|---|---|
--length | SHORT, DEFAULT, LONG |
--instructions | Custom instructions for the content |
--wait | Wait for generation to complete |
--output | Download path (requires --wait) |
Slide Deck Options
/nblm slides --format DETAILED_DECK --wait --output ./presentation.pdf
/nblm slides --instructions "Focus on key diagrams" --format PRESENTER_SLIDES| Option | Values |
|---|---|
--format | DETAILED_DECK, PRESENTER_SLIDES |
--length | SHORT, DEFAULT |
--instructions | Custom instructions for the content |
--wait | Wait for generation to complete |
--output | Download path (requires --wait) |
Infographic Options
/nblm infographic --orientation LANDSCAPE --wait --output ./visual.png
/nblm infographic --instructions "Highlight comparison" --detail-level DETAILED| Option | Values |
|---|---|
--orientation | LANDSCAPE, PORTRAIT, SQUARE |
--detail-level | CONCISE, STANDARD, DETAILED |
--instructions | Custom instructions for the content |
--wait | Wait for generation to complete |
--output | Download path (requires --wait) |
Media Generation
| Command | Description | Output |
|---|---|---|
/nblm podcast | Deep-dive audio discussion | MP3 |
/nblm briefing | Brief audio summary | MP3 |
/nblm debate | Debate-style audio | MP3 |
/nblm slides | Slide deck presentation | |
/nblm infographic | Visual infographic | PNG |
Examples
/nblm podcast --wait --output ./deep-dive.mp3
/nblm briefing --instructions "Focus on chapter 3" --wait
/nblm debate --length LONG --wait --output ./debate.mp3
/nblm slides --instructions "Include key diagrams" --format DETAILED_DECK --wait --output ./presentation.pdf
/nblm infographic --orientation LANDSCAPE --detail-level DETAILED --wait --output ./summary.pngDownload & Manage
/nblm podcast-download ./my-podcast.mp3
/nblm slides-download ./presentation.pdf
/nblm infographic-download ./visual.png
/nblm media-list # List all generated media
/nblm media-list --type audio # List only audio
/nblm media-delete <id> # Delete a media item---
Extended Documentation
When to Use This Skill
Trigger when user:
- Mentions NotebookLM explicitly
- Shares NotebookLM URL (
https://notebooklm.google.com/notebook/...) - Asks to query their notebooks/documentation
- Wants to add documentation to NotebookLM library
- Uses phrases like "ask my NotebookLM", "check my docs", "query my notebook"
⚠️ CRITICAL: Add Command - Smart Discovery
The add command now automatically discovers metadata from the notebook:
# Smart Add (auto-discovers name, description, topics)
python scripts/run.py notebook_manager.py add <notebook-id-or-url>
# With optional overrides
python scripts/run.py notebook_manager.py add <id> --name "Custom Name" --topics "custom,topics"What Smart Add does: 1. Fetches notebook title from NotebookLM API 2. Queries the notebook content to generate description and topics 3. Adds to local library with discovered metadata
Supported input formats:
- Notebook ID:
5fd9f36b-8000-401d-a7a0-7aa3f7832644 - Full URL:
https://notebooklm.google.com/notebook/5fd9f36b-8000-401d-a7a0-7aa3f7832644
NEVER manually specify --name, --description, or --topics unless the user explicitly provides them.
Critical: Always Use run.py Wrapper
NEVER call scripts directly. ALWAYS use `python scripts/run.py [script]`:
# ✅ CORRECT - Always use run.py:
python scripts/run.py auth_manager.py status
python scripts/run.py notebook_manager.py list
python scripts/run.py ask_question.py --question "..."
# ❌ WRONG - Never call directly:
python scripts/auth_manager.py status # Fails without venv!The run.py wrapper automatically: 1. Creates .venv if needed 2. Installs all dependencies 3. Activates environment 4. Executes script properly
Core Workflow
Step 1: Check Authentication Status
python scripts/run.py auth_manager.py statusIf not authenticated, proceed to setup.
Step 2: Authenticate (One-Time Setup)
# Browser MUST be visible for manual Google login
python scripts/run.py auth_manager.py setupImportant:
- Browser is VISIBLE for authentication
- Browser window opens automatically
- User must manually log in to Google
- Tell user: "A browser window will open for Google login"
Step 3: Manage Notebook Library
# List all notebooks
python scripts/run.py notebook_manager.py list
# BEFORE ADDING: Ask user for metadata if unknown!
# "What does this notebook contain?"
# "What topics should I tag it with?"
# Add notebook to library (ALL parameters are REQUIRED!)
python scripts/run.py notebook_manager.py add \
--url "https://notebooklm.google.com/notebook/..." \
--name "Descriptive Name" \
--description "What this notebook contains" \ # REQUIRED - ASK USER IF UNKNOWN!
--topics "topic1,topic2,topic3" # REQUIRED - ASK USER IF UNKNOWN!
# Search notebooks by topic
python scripts/run.py notebook_manager.py search --query "keyword"
# Set active notebook
python scripts/run.py notebook_manager.py activate --id notebook-id
# Remove notebook
python scripts/run.py notebook_manager.py remove --id notebook-idQuick Workflow
1. Check library: python scripts/run.py notebook_manager.py list 2. Ask question: python scripts/run.py ask_question.py --question "..." --notebook-id ID
Step 4: Ask Questions
# Basic query (uses active notebook if set)
python scripts/run.py ask_question.py --question "Your question here"
# Query specific notebook
python scripts/run.py ask_question.py --question "..." --notebook-id notebook-id
# Query with notebook URL directly
python scripts/run.py ask_question.py --question "..." --notebook-url "https://..."
# Show browser for debugging
python scripts/run.py ask_question.py --question "..." --show-browserFollow-Up Mechanism (CRITICAL)
Every NotebookLM answer ends with: "EXTREMELY IMPORTANT: Is that ALL you need to know?"
Required Claude Behavior: 1. STOP - Do not immediately respond to user 2. ANALYZE - Compare answer to user's original request 3. IDENTIFY GAPS - Determine if more information needed 4. ASK FOLLOW-UP - If gaps exist, immediately ask:
python scripts/run.py ask_question.py --question "Follow-up with context..."5. REPEAT - Continue until information is complete 6. SYNTHESIZE - Combine all answers before responding to user
Z-Library Integration
Triggers
- User provides Z-Library URL (zlib.li, z-lib.org, zh.zlib.li)
- User says "download this book to NotebookLM"
- User says "add this book from Z-Library"
Setup (One-Time)
# Authenticate with Z-Library
python scripts/run.py auth_manager.py setup --service zlibraryCommands
# Add book from Z-Library
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..."
# Check Z-Library auth status
python scripts/run.py auth_manager.py status --service zlibraryScript Reference
Authentication Management (auth_manager.py)
python scripts/run.py auth_manager.py setup # Default: Google
python scripts/run.py auth_manager.py setup --service google
python scripts/run.py auth_manager.py setup --service zlibrary
python scripts/run.py auth_manager.py status # Show all services
python scripts/run.py auth_manager.py status --service zlibrary
python scripts/run.py auth_manager.py clear --service zlibrary # Clear auth
# Multi-Account Management (Google)
python scripts/run.py auth_manager.py accounts list # List all accounts
python scripts/run.py auth_manager.py accounts add # Add new account
python scripts/run.py auth_manager.py accounts switch 1 # Switch by index
python scripts/run.py auth_manager.py accounts switch user@gmail.com # Switch by email
python scripts/run.py auth_manager.py accounts remove 2 # Remove accountNotebook Management (notebook_manager.py)
python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS
# OR use notebook ID directly:
python scripts/run.py notebook_manager.py add --notebook-id ID --name NAME --description DESC --topics TOPICS
python scripts/run.py notebook_manager.py list
python scripts/run.py notebook_manager.py search --query QUERY
python scripts/run.py notebook_manager.py activate --id ID
python scripts/run.py notebook_manager.py remove --id ID
python scripts/run.py notebook_manager.py statsQuestion Interface (ask_question.py)
python scripts/run.py ask_question.py --question "..." [--notebook-id ID] [--notebook-url URL] [--show-browser]Source Manager (source_manager.py)
# Upload to active notebook
python scripts/run.py source_manager.py add --file "/path/to/book.pdf" --use-active
# Create new notebook for upload
python scripts/run.py source_manager.py add --file "/path/to/book.pdf" --create-new
# Upload to specific notebook
python scripts/run.py source_manager.py add --file "/path/to/book.pdf" --notebook-id NOTEBOOK_ID
# Z-Library download and upload
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..." --use-active
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..." --create-new
# Sync a folder (new!)
python scripts/run.py source_manager.py sync "/path/to/docs" --use-active
python scripts/run.py source_manager.py sync "/path/to/docs" --create-new
python scripts/run.py source_manager.py sync "/path/to/docs" --notebook-id NOTEBOOK_ID
# Sync options (new!)
python scripts/run.py source_manager.py sync "/path/to/docs" --dry-run # Preview only
python scripts/run.py source_manager.py sync "/path/to/docs" --rebuild # Force re-hash all filesFolder Sync:
- Scans folder for supported types: PDF, TXT, MD, DOCX, HTML, EPUB
- Tracks sync state internally (no per-folder tracking file to manage)
- Sync strategy: add new, update modified (delete + re-upload), skip unchanged
- Multi-account aware (tracks which Google account was used)
Note: One of --use-active, --create-new, or --notebook-id is REQUIRED. Uploads wait for NotebookLM processing and print progress as Ready: N/T. Press Ctrl+C to stop waiting. Local file uploads use browser automation and require Google authentication. If browser automation is unavailable, set NOTEBOOKLM_UPLOAD_MODE=text to upload extracted text instead (PDFs require pypdf).
Data Cleanup (cleanup_manager.py)
python scripts/run.py cleanup_manager.py # Preview cleanup
python scripts/run.py cleanup_manager.py --confirm # Execute cleanup
python scripts/run.py cleanup_manager.py --preserve-library # Keep notebooksWatchdog Status (auth_manager.py)
python scripts/run.py auth_manager.py watchdog-statusEnvironment Management
The virtual environment is automatically managed:
- First run creates
.venvautomatically - Dependencies install automatically
- Node.js dependencies install automatically
- agent-browser daemon starts on demand and keeps browser state in memory
- daemon stops after 10 minutes of inactivity (any agent-browser command resets the timer)
- set
AGENT_BROWSER_OWNER_PIDto auto-stop when the agent process exits scripts/run.pysetsAGENT_BROWSER_OWNER_PIDto its parent PID by default- Everything isolated in skill directory
Manual setup (only if automatic fails):
python -m venv .venv
source .venv/bin/activate # Linux/Mac
pip install -r requirements.txt
npm install
npm run install-browsersData Storage
All data stored in ~/.claude/skills/notebooklm/data/:
library.json- Notebook metadata (with account associations)auth/google/- Multi-account Google authindex.json- Account index (active account, list)<n>-<email>.json- Per-account credentialsauth/zlibrary.json- Z-Library auth stateagent_browser/session_id- Current daemon session IDagent_browser/last_activity.json- Last activity timestamp for idle shutdownagent_browser/watchdog.pid- Idle watchdog process ID
Security: Protected by .gitignore, never commit to git.
Configuration
Optional .env file in skill directory:
HEADLESS=false # Browser visibility
SHOW_BROWSER=false # Default browser display
STEALTH_ENABLED=true # Human-like behavior
TYPING_WPM_MIN=160 # Typing speed
TYPING_WPM_MAX=240
DEFAULT_NOTEBOOK_ID= # Default notebookDecision Flow
User mentions NotebookLM
↓
Check auth → python scripts/run.py auth_manager.py status
↓
If not authenticated → python scripts/run.py auth_manager.py setup
↓
Check/Add notebook → python scripts/run.py notebook_manager.py list/add (with --description)
↓
Activate notebook → python scripts/run.py notebook_manager.py activate --id ID
↓
Ask question → python scripts/run.py ask_question.py --question "..."
↓
See "Is that ALL you need?" → Ask follow-ups until complete
↓
Synthesize and respond to userTroubleshooting
| Problem | Solution |
|---|---|
| ModuleNotFoundError | Use run.py wrapper |
| Authentication fails | Browser must be visible for setup! --show-browser |
| DAEMON_UNAVAILABLE | Ensure Node.js/npm installed, run npm install, retry |
| AUTH_REQUIRED | Run python scripts/run.py auth_manager.py setup |
| ELEMENT_NOT_FOUND | Verify notebook URL and re-run with fresh page load |
| Rate limit (50/day) | Wait or add another Google account with accounts add |
| Browser crashes | python scripts/run.py cleanup_manager.py --preserve-library |
| Notebook not found | Check with notebook_manager.py list |
Best Practices
1. Always use run.py - Handles environment automatically 2. Check auth first - Before any operations 3. Follow-up questions - Don't stop at first answer 4. Browser visible for auth - Required for manual login 5. Include context - Each question is independent 6. Synthesize answers - Combine multiple responses
Limitations
- No session persistence (each question = new browser)
- Rate limits on free Google accounts (50 queries/day per account; use multiple accounts to increase)
- Manual upload required (user must add docs to NotebookLM)
- Browser overhead (few seconds per question)
Resources (Skill Structure)
Important directories and files:
scripts/- All automation scripts (ask_question.py, notebook_manager.py, etc.)data/- Local storage for authentication and notebook libraryreferences/- Extended documentation:api_reference.md- Detailed API documentation for all scriptstroubleshooting.md- Common issues and solutionsusage_patterns.md- Best practices and workflow examples.venv/- Isolated Python environment (auto-created on first run).gitignore- Protects sensitive data from being committed
# Worktrees
.worktrees/
# Virtual Environment
.venv/
venv/
env/
*.venv
# Skill Data (NEVER commit - contains auth and personal notebooks!)
data/
data/*
data/**/*
# Claude-specific
.claude/
.claude-plugin/
*.claude
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
scripts/__pycache__/
scripts/*.pyc
# Environment
.env
*.env
.env.*
# Browser/Auth state (if accidentally placed outside data/)
browser_state/
auth/
auth_info.json
library.json
notebooks.json
state.json
cookies.json
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
Thumbs.db
desktop.ini
ehthumbs.db
# Logs
*.log
logs/
*.debug
# Backups
*.backup
*.bak
*.tmp
*.temp
# Test artifacts
.coverage
htmlcov/
.pytest_cache/
.tox/
# Package artifacts
dist/
build/
*.egg-info/
# Node
node_modules/
prpm.lock
# Documentation plans (internal)
docs/plans/
AGENTS.md
This file provides guidance to AI coding agents when working with code in this repository. It mirrors CLAUDE.md to ensure all AI agents have the same context.
⚠️ IRON RULE - NotebookLM Usage
When working in this repository and needing to reference or query NotebookLM, you MUST use the skill provided by this repo itself. Do not use external NotebookLM tools or services - always use the scripts and tooling defined here.
This is a non-negotiable project law.
Project Overview
nblm - enables AI coding agents to query Google NotebookLM for source-grounded, citation-backed answers. Uses the agent-browser daemon (Node.js) and a Unix socket protocol for automation.
Session Model: Stateless per question; the daemon keeps browser state in memory until it is stopped.
Development Commands
Running Scripts (Always use run.py wrapper)
# CORRECT - Always use run.py:
python scripts/run.py auth_manager.py status
python scripts/run.py notebook_manager.py list
python scripts/run.py ask_question.py --question "..."
# WRONG - Will fail without venv:
python scripts/auth_manager.py statusThe run.py wrapper automatically creates .venv, installs Python deps, and installs Node.js deps if needed.
Manual Environment Setup (if automatic fails)
python -m venv .venv
source .venv/bin/activate # Linux/Mac
pip install -r requirements.txt
npm install
npm run install-browsersCommon Script Commands
# Authentication
python scripts/run.py auth_manager.py setup # Default: Google
python scripts/run.py auth_manager.py setup --service zlibrary
python scripts/run.py auth_manager.py status # Show all services
python scripts/run.py auth_manager.py status --service zlibrary
python scripts/run.py auth_manager.py clear --service zlibrary # Clear auth data
# Notebook Library
python scripts/run.py notebook_manager.py list
python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS
python scripts/run.py notebook_manager.py search --query KEYWORD
python scripts/run.py notebook_manager.py activate --id ID
python scripts/run.py notebook_manager.py remove --id ID
# Query
python scripts/run.py ask_question.py --question "..." [--notebook-id ID] [--notebook-url URL] [--show-browser]
# Source Manager
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..."
python scripts/run.py source_manager.py add --file "/path/to/book.pdf"
# Cleanup
python scripts/run.py cleanup_manager.py # Preview
python scripts/run.py cleanup_manager.py --confirm # Execute
python scripts/run.py cleanup_manager.py --preserve-library # Keep notebooksArchitecture
scripts/
├── run.py # Entry point wrapper - handles venv and npm deps
├── ask_question.py # Core query logic - uses agent-browser client
├── auth_manager.py # Multi-service authentication and session persistence
├── notebook_manager.py # CRUD operations for notebook library (library.json)
├── source_manager.py # Source ingestion (file/Z-Library)
├── agent_browser_client.py # Unix socket client for agent-browser daemon
├── cleanup_manager.py # Data cleanup with preservation options
├── config.py # Configuration management
└── setup_environment.py # Automatic venv and dependency installation
scripts/zlibrary/
├── downloader.py # Z-Library download automation
└── epub_converter.py # EPUB to Markdown conversion
data/ # Git-ignored local storage
├── library.json # Notebook metadata
├── auth/ # Per-service auth state
│ ├── google.json
│ └── zlibrary.json
└── agent_browser/ # Session metadata (session_id)
references/ # Extended documentation
├── api_reference.md
├── troubleshooting.md
└── usage_patterns.mdKey Flow: run.py → ensures Python/Node deps → scripts use NotebookLMWrapper (async) → notebooklm-py API → agent-browser fallback
Key Dependencies
Foundation Libraries (Project Decision)
This skill uses two foundation libraries for NotebookLM integration:
1. agent-browser (npm - vercel-labs/agent-browser)
- Headless browser automation CLI for AI agents
- Used for: Authentication, token refresh, browser fallback for uploads, Z-Library automation
- Key commands:
snapshot,click,fill,upload,navigate,evaluate
2. notebooklm-py (pip - teng-lin/notebooklm-py)
- Python async API client for Google NotebookLM
- Used for: All NotebookLM API operations (notebooks, sources, chat)
- Key APIs:
client.notebooks.create(name)- Create notebooksclient.notebooks.list()- List notebooksclient.sources.add_file(notebook_id, Path(...))- Upload filesclient.sources.add_url(notebook_id, url)- Add URL sourcesclient.sources.add_youtube(notebook_id, url)- Add YouTubeclient.sources.add_text(notebook_id, title, content)- Add textclient.sources.list(notebook_id)- List sourcesclient.chat(notebook_id, message)- Query notebook
Architecture:
- API-first: All NotebookLM operations go through notebooklm-py
- Browser fallback: File uploads fall back to agent-browser on API failure
- Auth extraction: agent-browser extracts csrf_token/session_id for notebooklm-py
- Token refresh: Silent refresh on auth errors before retry
Other Dependencies
- python-dotenv==1.0.0: Environment configuration
- ebooklib / beautifulsoup4 / lxml: EPUB conversion
- Node.js: Required to run the agent-browser daemon
Testing
No automated test suite. Testing is manual/functional via the scripts.
# Auth (Google + Z-Library)
python scripts/run.py auth_manager.py setup --service zlibrary
python scripts/run.py auth_manager.py status
# Download + upload
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..."Important Notes
- Authentication requires a visible browser session (
--show-browser) - Free tier rate limit: 50 queries/day
data/directory contains sensitive auth data - never commitdata/auth/google.jsonincludes NotebookLM API token + cookiesNOTEBOOKLM_AUTH_TOKEN+NOTEBOOKLM_COOKIESallow API fallback if the daemon cannot start- Each question is independent (stateless model)
- Answers include follow-up prompt to encourage comprehensive research
Authentication Notes
This skill uses the agent-browser daemon to automate NotebookLM. Authentication is handled by a visible browser session and kept in memory by the daemon while it runs. NotebookLM API credentials (token + cookie header) are persisted to data/auth/google.json on demand, or supplied via environment variables.
How Authentication Works
auth_manager.py setuplaunches a headed browser via the daemon.- You log in to Google manually.
- The daemon keeps cookies/storage in memory for subsequent commands.
- Cookies and local storage are cached to
data/agent_browser/storage_state.jsonfor reuse after daemon restarts. - NotebookLM API credentials are stored in
data/auth/google.json: notebooklm_auth_tokennotebooklm_cookies- If cached NotebookLM credentials are older than 10 days, the skill attempts an HTTP refresh using stored Google cookies before falling back to the daemon.
- You can also provide credentials via environment variables:
NOTEBOOKLM_AUTH_TOKENNOTEBOOKLM_COOKIES- The daemon stops after 10 minutes of inactivity; any agent-browser command resets the timer.
- Set
AGENT_BROWSER_OWNER_PIDto stop the daemon when your agent process exits. scripts/run.pysetsAGENT_BROWSER_OWNER_PIDto its parent PID by default; override it if your agent runs differently.- Stopping the daemon ends the active session and requires re-authentication.
- The skill stores minimal metadata in
data/auth_info.jsonanddata/agent_browser/session_id.
Troubleshooting Authentication
1. Not authenticated
python scripts/run.py auth_manager.py setup2. Reset and re-authenticate
python scripts/run.py auth_manager.py clear
python scripts/run.py auth_manager.py setup3. Refresh NotebookLM API credentials
python scripts/run.py auth_manager.py setup4. Use API credentials without the browser daemon
export NOTEBOOKLM_AUTH_TOKEN="..."
export NOTEBOOKLM_COOKIES="SID=...; HSID=..."3. Missing browsers
npm install
npm run install-browsers4. Stop the daemon
python scripts/run.py auth_manager.py stop-daemon5. Check watchdog status
python scripts/run.py auth_manager.py watchdog-statusSecurity Notes
- All browser activity runs locally.
- The
data/directory contains sensitive auth metadata. Never commit it. - Use a dedicated Google account for automation if you prefer extra isolation.
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[2.1.0] - 2026-01-24
Changed
- BREAKING: Replaced notebooklm-kit (TypeScript) with notebooklm-py (Python) for all NotebookLM API operations
- Migrated source_manager.py and ask_question.py to async
- API-first approach: queries and uploads use notebooklm-py, browser fallback on failure
Added
NotebookLMWrapperclass - thin async wrapper over notebooklm-py- Token extraction during auth setup (csrf_token, session_id)
- Silent token refresh on auth errors
- Browser fallback for file uploads using agent-browser upload command
AGENTS.md- mirror of CLAUDE.md for all AI agents
Removed
notebooklm_kit_bridge.mjs- Node.js bridge no longer needednotebooklm_kit_client.py- replaced by notebooklm_wrapper.pynotebooklm-kitnpm dependency
Added
- NotebookLM kit bridge and client for NotebookLM uploads
- NotebookLM auth token/cookie persistence in
data/auth/google.json - Agent-browser helpers for cookie collection and page evaluation
- API fallback for
ask_question.pyvia notebooklm-kit when daemon is unavailable - Environment variable support for
NOTEBOOKLM_AUTH_TOKENandNOTEBOOKLM_COOKIES - HTTP-first refresh for NotebookLM auth tokens using stored cookies with a 10-day staleness policy
- Upload progress tracking that waits for NotebookLM source processing to complete
- Text upload fallback when browser automation is unavailable (set
NOTEBOOKLM_UPLOAD_MODE=text) - PDF text extraction fallback via
pypdf
Fixed
- Z-Library direct download (
/dl/) links wait for downloads before navigation - Z-Library auth detection recognizes login/logout indicators
- NotebookLM auth extraction restores saved browser state and persists tokens after setup
- File uploads now use browser automation so the actual file contents are uploaded
[2.0.0] - 2026-01-18
Added
- agent-browser CLI integration for faster, token-efficient automation
- Node.js dependency management via
npm install
Changed
- Authentication and query flow now use agent-browser sessions
- Python wrapper updated to drive CLI commands with
--session
Removed
- Patchright/Playwright-specific browser helpers and state files
[1.3.0] - 2025-11-21
Added
- Modular Architecture - Refactored codebase for better maintainability
- New
config.py- Centralized configuration (paths, selectors, timeouts) - New
browser_utils.py- BrowserFactory and StealthUtils classes - Cleaner separation of concerns across all scripts
Changed
- Timeout increased to 120 seconds - Long queries no longer timeout prematurely
ask_question.py: 30s → 120sbrowser_session.py: 30s → 120s- Resolves Issue #4
Fixed
- Thinking Message Detection - Fixed incomplete answers showing placeholder text
- Now waits for
div.thinking-messageelement to disappear before reading answer - Answers like "Reviewing the content..." or "Looking for answers..." no longer returned prematurely
- Works reliably across all languages and NotebookLM UI changes
- Correct CSS Selectors - Updated to match current NotebookLM UI
- Changed from
.response-content, .message-contentto.to-user-container .message-text-content - Consistent selectors across all scripts
- Stability Detection - Improved answer completeness check
- Now requires 3 consecutive stable polls instead of 1 second wait
- Prevents truncated responses during streaming
[1.2.0] - 2025-10-28
Added
- Initial public release
- NotebookLM integration via browser automation
- Session-based conversations with Gemini 2.5
- Notebook library management
- Knowledge base preparation tools
- Google authentication with persistent sessions
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
⚠️ IRON RULE - English Output
Always respond in English. All code, documentation, comments, commit messages, and written output must be in English, regardless of the language used in user input.
⚠️ IRON RULE - NotebookLM Usage
When working in this repository and needing to reference or query NotebookLM, you MUST use the skill provided by this repo itself. Do not use external NotebookLM tools or services - always use the scripts and tooling defined here.
This is a non-negotiable project law.
Project Overview
nblm - enables AI coding agents to query Google NotebookLM for source-grounded, citation-backed answers. Uses the agent-browser daemon (Node.js) and a Unix socket protocol for automation.
Session Model: Stateless per question; the daemon keeps browser state in memory until it is stopped.
Development Commands
Running Scripts (Always use run.py wrapper)
# CORRECT - Always use run.py:
python scripts/run.py auth_manager.py status
python scripts/run.py notebook_manager.py list
python scripts/run.py ask_question.py --question "..."
# WRONG - Will fail without venv:
python scripts/auth_manager.py statusThe run.py wrapper automatically creates .venv, installs Python deps, and installs Node.js deps if needed.
Manual Environment Setup (if automatic fails)
python -m venv .venv
source .venv/bin/activate # Linux/Mac
pip install -r requirements.txt
npm install
npm run install-browsersCommon Script Commands
# Authentication
python scripts/run.py auth_manager.py setup # Default: Google
python scripts/run.py auth_manager.py setup --service zlibrary
python scripts/run.py auth_manager.py status # Show all services
python scripts/run.py auth_manager.py status --service zlibrary
python scripts/run.py auth_manager.py clear --service zlibrary # Clear auth data
# Multi-Account Management (Google)
python scripts/run.py auth_manager.py accounts list # List all accounts
python scripts/run.py auth_manager.py accounts add # Add new account
python scripts/run.py auth_manager.py accounts switch 1 # Switch by index
python scripts/run.py auth_manager.py accounts switch user@gmail.com # Switch by email
python scripts/run.py auth_manager.py accounts remove 2 # Remove account
# Notebook Library (Smart Add auto-discovers metadata)
python scripts/run.py notebook_manager.py list
python scripts/run.py notebook_manager.py add <notebook-id-or-url> # Auto-discovers name, description, topics
python scripts/run.py notebook_manager.py add <id> --name "Override Name" --topics "custom,topics"
python scripts/run.py notebook_manager.py search --query KEYWORD
python scripts/run.py notebook_manager.py activate --id ID
python scripts/run.py notebook_manager.py remove --id ID
# Query
python scripts/run.py ask_question.py --question "..." [--notebook-id ID] [--notebook-url URL] [--show-browser]
# Source Manager
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..."
python scripts/run.py source_manager.py add --file "/path/to/book.pdf"
# Cleanup
python scripts/run.py cleanup_manager.py # Preview
python scripts/run.py cleanup_manager.py --confirm # Execute
python scripts/run.py cleanup_manager.py --preserve-library # Keep notebooks
# Artifacts (Audio/Podcast Generation)
python scripts/run.py artifact_manager.py list # List all artifacts
python scripts/run.py artifact_manager.py list --type audio # List audio only
python scripts/run.py artifact_manager.py get <artifact-id> # Get artifact details
python scripts/run.py artifact_manager.py delete <artifact-id> # Delete artifact
python scripts/run.py artifact_manager.py generate --wait --output podcast.mp3 # Generate & download
python scripts/run.py artifact_manager.py generate --format DEBATE --length SHORT
python scripts/run.py artifact_manager.py generate --instructions "Focus on key findings"
python scripts/run.py artifact_manager.py status --task-id <task-id> # Check generation status
python scripts/run.py artifact_manager.py download ./output.mp3 # Download latest audioArchitecture
scripts/
├── run.py # Entry point wrapper - handles venv and npm deps
├── ask_question.py # Core query logic - uses agent-browser client
├── auth_manager.py # Multi-service authentication and session persistence
├── notebook_manager.py # CRUD operations for notebook library (library.json)
├── source_manager.py # Source ingestion (file/Z-Library)
├── artifact_manager.py # Audio/podcast generation and artifact management
├── agent_browser_client.py # Unix socket client for agent-browser daemon
├── cleanup_manager.py # Data cleanup with preservation options
├── config.py # Configuration management
└── setup_environment.py # Automatic venv and dependency installation
scripts/zlibrary/
├── downloader.py # Z-Library download automation
└── epub_converter.py # EPUB to Markdown conversion
data/ # Git-ignored local storage
├── library.json # Notebook metadata (with account associations)
├── auth/ # Per-service auth state
│ ├── google/ # Multi-account Google auth
│ │ ├── index.json # Account index (active account, list)
│ │ └── *.json # Per-account credentials
│ └── zlibrary.json
└── agent_browser/ # Session metadata (session_id)
references/ # Extended documentation
├── api_reference.md
├── troubleshooting.md
└── usage_patterns.mdKey Flow: run.py → ensures Python/Node deps → scripts use NotebookLMWrapper (async) → notebooklm-py API → agent-browser fallback
Key Dependencies
Foundation Libraries (Project Decision)
This skill uses two foundation libraries for NotebookLM integration:
1. agent-browser (npm - vercel-labs/agent-browser)
- Headless browser automation CLI for AI agents
- Used for: Authentication, token refresh, browser fallback for uploads, Z-Library automation
- Key commands:
snapshot,click,fill,upload,navigate,evaluate
2. notebooklm-py (pip - teng-lin/notebooklm-py)
- Python async API client for Google NotebookLM
- Used for: All NotebookLM API operations (notebooks, sources, chat, artifacts)
- Key APIs:
client.notebooks.create(name)- Create notebooksclient.notebooks.list()- List notebooksclient.sources.add_file(notebook_id, Path(...))- Upload filesclient.sources.add_url(notebook_id, url)- Add URL sourcesclient.sources.add_youtube(notebook_id, url)- Add YouTubeclient.sources.add_text(notebook_id, title, content)- Add textclient.sources.list(notebook_id)- List sourcesclient.chat(notebook_id, message)- Query notebookclient.artifacts.generate_audio(notebook_id, ...)- Generate podcastclient.artifacts.list(notebook_id)- List artifactsclient.artifacts.download_audio(notebook_id, path)- Download audio
Architecture:
- API-first: All NotebookLM operations go through notebooklm-py
- Browser fallback: File uploads fall back to agent-browser on API failure
- Auth extraction: agent-browser extracts csrf_token/session_id for notebooklm-py
- Token refresh: Silent refresh on auth errors before retry
Other Dependencies
- python-dotenv==1.0.0: Environment configuration
- ebooklib / beautifulsoup4 / lxml: EPUB conversion
- Node.js: Required to run the agent-browser daemon
Testing
No automated test suite. Testing is manual/functional via the scripts.
# Auth (Google + Z-Library)
python scripts/run.py auth_manager.py setup --service zlibrary
python scripts/run.py auth_manager.py status
# Download + upload
python scripts/run.py source_manager.py add --url "https://zh.zlib.li/book/..."Important Notes
- Authentication requires a visible browser session (
--show-browser) - Free tier rate limit: 50 queries/day per Google account
- Multi-account support: Add multiple Google accounts to bypass rate limits
data/directory contains sensitive auth data - never commitdata/auth/google/stores per-account credentials with email in filenameNOTEBOOKLM_AUTH_TOKEN+NOTEBOOKLM_COOKIESallow API fallback if the daemon cannot start- Each question is independent (stateless model)
- Answers include follow-up prompt to encourage comprehensive research
- Notebooks are automatically associated with the account that added them
Manage multiple Google accounts for NotebookLM access.
Usage: /nblm accounts [action] [identifier]
Actions:
list(default) - List all configured Google accountsadd- Add a new Google account (opens browser for login)switch <index-or-email>- Switch to a different accountremove <index-or-email>- Remove an account
Examples:
/nblm accounts- List all accounts/nblm accounts add- Add new account/nblm accounts switch 2- Switch to account #2/nblm accounts switch user@gmail.com- Switch by email/nblm accounts remove 1- Remove account #1
Based on the user's request, run the appropriate command:
For listing accounts (default): !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py auth_manager.py accounts list
For adding a new account: !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py auth_manager.py accounts add
For switching accounts (replace IDENTIFIER with the index or email): !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py auth_manager.py accounts switch IDENTIFIER
For removing accounts (replace IDENTIFIER with the index or email): !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py auth_manager.py accounts remove IDENTIFIER
After the command completes, summarize the result for the user.
Set a notebook as the active default for queries.
$IF($1, Run: !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py notebook_manager.py activate --id "$1"
Confirm which notebook is now active.,
ERROR: Please provide a notebook ID. Usage: /nblm-activate <notebook-id>
To see available notebooks, use /nblm-list )
Add an existing NotebookLM notebook to your local library for easy access.
$IF($ARGUMENTS, Run: !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py notebook_manager.py add $ARGUMENTS
Confirm the notebook was added and show its ID.,
ERROR: Please provide notebook details.
Usage: /nblm-add --url <notebook-url> --name <name> --description <desc> --topics <topics>
Example: /nblm-add --url "https://notebooklm.google.com/notebook/abc123" --name "Research Notes" --description "My research project" --topics "research,notes" )
Ask a question to NotebookLM and get a source-grounded answer with citations.
$IF($ARGUMENTS, Run: !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py ask_question.py --question "$ARGUMENTS"
Present the answer clearly. If the answer suggests follow-up questions, offer to ask them.,
ERROR: Please provide a question. Usage: /nblm-ask <your question> [--notebook-id ID] )
List all notebooks in the NotebookLM library.
Run: !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py notebook_manager.py list
Display the results in a clean format showing notebook names, IDs, and which one is active.
Authenticate with Google for NotebookLM access.
Run: !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py auth_manager.py setup --service google
After authentication completes, confirm the status and inform the user they can now use other nblm commands.
Show the current authentication status and active notebook.
Run these commands: 1. !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py auth_manager.py status 2. !cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py notebook_manager.py list
Summarize:
- Authentication status (Google, Z-Library)
- Active notebook (if any)
- Total notebooks in library
Upload a source to NotebookLM. Accepts local files (PDF, TXT, MD) or Z-Library URLs.
$IF($1, Determine if "$1" is a file path or URL:
- If it's a local file path: Run: !
cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py source_manager.py add --file "$1" $ARGUMENTS - If it's a Z-Library URL: Run: !
cd ${CLAUDE_PLUGIN_ROOT} && python scripts/run.py source_manager.py add --url "$1" $ARGUMENTS
Report the upload result including the notebook ID and source title.,
ERROR: Please provide a file path or URL. Usage: /nblm-upload <file-or-url> [--notebook-id ID] )
Folder Sync Feature Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add folder sync capability to nblm that can scan a directory, detect new/modified/unchanged files, and sync them to a NotebookLM notebook with proper tracking.
Architecture: New SyncManager class in scripts/sync_manager.py that handles scanning, hashing, comparing, and syncing files. Sync state stored under data/sync/<hash>.sync.json records file hashes and source IDs. Command extends source_manager.py with sync subcommand.
Tech Stack: Python 3.10+, notebooklm-py for API operations, hashlib for SHA-256, json for tracking file, argparse for CLI.
---
Prerequisites
Before starting implementation, verify these files exist and understand their structure:
scripts/source_manager.py- Existing source upload logic (333 lines)scripts/notebook_manager.py- NotebookLibrary class for notebook operationsscripts/notebooklm_wrapper.py- NotebookLMWrapper for API operationsscripts/config.py- Configuration and paths
---
Task 1: Create SyncManager Class Skeleton
Files:
- Create:
scripts/sync_manager.py
Step 1: Create the empty file with class skeleton
#!/usr/bin/env python3
"""
Folder sync manager for NotebookLM.
Scans local folders, tracks file changes, and syncs to NotebookLM notebooks.
"""
import hashlib
import json
import os
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional
SUPPORTED_EXTENSIONS = {'.pdf', '.txt', '.md', '.docx', '.html', '.epub'}
class SyncAction(Enum):
"""Sync action types."""
ADD = "add"
UPDATE = "update"
SKIP = "skip"
DELETE = "delete"
@dataclass
class TrackedFile:
"""Represents a tracked file in the sync state."""
filename: str
hash: str
modified_at: str
source_id: Optional[str] = None
uploaded_at: Optional[str] = None
@dataclass
class SyncState:
"""Represents the sync tracking state."""
version: int = 1
folder_path: str = ""
notebook_id: Optional[str] = None
notebook_url: Optional[str] = None
account_index: Optional[int] = None
account_email: Optional[str] = None
last_sync_at: Optional[str] = None
files: dict[str, TrackedFile] = field(default_factory=dict)
class SyncManager:
"""Manages folder-to-notebook synchronization."""
TRACKING_FILENAME = ".nblm-sync.json"
def __init__(self, folder_path: str):
self.folder_path = Path(folder_path).resolve()
self.tracking_file = self.folder_path / self.TRACKING_FILENAME
self.state = SyncState(folder_path=str(self.folder_path))
def load_state(self) -> bool:
"""Load sync state from tracking file."""
pass
def save_state(self) -> bool:
"""Save sync state to tracking file."""
pass
def scan_folder(self) -> dict[str, dict]:
"""Scan folder for supported files."""
pass
def compute_file_hash(self, file_path: Path) -> str:
"""Compute SHA-256 hash of a file."""
pass
def get_sync_plan(self) -> list[dict]:
"""Generate sync plan comparing local files with tracking state."""
pass
async def execute_sync(self, notebook_id: str, dry_run: bool = False) -> dict:
"""Execute the sync plan."""
passStep 2: Commit
git add docs/plans/2025-01-30-folder-sync-design.md
git commit -m "docs: add folder sync implementation plan"---
Task 2: Implement State Management (load_state, save_state)
Files:
- Modify:
scripts/sync_manager.py
Step 1: Write failing tests
# In tests/test_sync_manager.py
import pytest
from pathlib import Path
from sync_manager import SyncManager, SyncState, TrackedFile
def test_load_state_creates_new_if_not_exists(tmp_path):
"""Test that load_state creates new state if tracking file doesn't exist."""
manager = SyncManager(str(tmp_path))
result = manager.load_state()
assert result is True
assert manager.state.folder_path == str(tmp_path)
assert manager.state.files == {}
def test_load_state_loads_existing(tmp_path):
"""Test that load_state loads existing tracking file."""
tracking = tmp_path / ".nblm-sync.json"
tracking.write_text(json.dumps({
"version": 1,
"folder_path": str(tmp_path),
"notebook_id": "test-123",
"files": {
"test.md": {
"filename": "test",
"hash": "abc123",
"modified_at": "2025-01-30T10:00:00Z"
}
}
}))
manager = SyncManager(str(tmp_path))
result = manager.load_state()
assert result is True
assert manager.state.notebook_id == "test-123"
assert "test.md" in manager.state.files
def test_save_state_writes_json(tmp_path):
"""Test that save_state writes tracking file."""
manager = SyncManager(str(tmp_path))
manager.state.notebook_id = "test-456"
manager.state.files["doc.md"] = TrackedFile(
filename="doc",
hash="def456",
modified_at="2025-01-30T11:00:00Z"
)
result = manager.save_state()
assert result is True
assert tracking.exists()
data = json.loads(tracking.read_text())
assert data["notebook_id"] == "test-456"Step 2: Run tests to verify they fail
cd /Users/troy.huang/workspace/AgentSkills/nblm
python -m pytest tests/test_sync_manager.py -v
# Expected: FAIL (module not found, functions not defined)Step 3: Implement load_state and save_state
def load_state(self) -> bool:
"""Load sync state from tracking file."""
if not self.tracking_file.exists():
# Create fresh state for new sync folder
self.state = SyncState(folder_path=str(self.folder_path))
return True
try:
data = json.loads(self.tracking_file.read_text())
# Validate version
if data.get("version") != 1:
raise ValueError(f"Unsupported tracking file version: {data.get('version')}")
# Reconstruct state
self.state = SyncState(
version=data.get("version", 1),
folder_path=data.get("folder_path", str(self.folder_path)),
notebook_id=data.get("notebook_id"),
notebook_url=data.get("notebook_url"),
account_index=data.get("account_index"),
account_email=data.get("account_email"),
last_sync_at=data.get("last_sync_at"),
)
# Reconstruct file entries
for path, file_data in data.get("files", {}).items():
self.state.files[path] = TrackedFile(
filename=file_data.get("filename", ""),
hash=file_data.get("hash", ""),
modified_at=file_data.get("modified_at", ""),
source_id=file_data.get("source_id"),
uploaded_at=file_data.get("uploaded_at"),
)
return True
except (json.JSONDecodeError, ValueError) as e:
print(f"⚠️ Error loading tracking file: {e}")
# Backup corrupted file
broken = self.tracking_file.with_suffix(".json.broken")
if not broken.exists():
self.tracking_file.rename(broken)
print(f" Backed up corrupted file to: {broken}")
# Create fresh state
self.state = SyncState(folder_path=str(self.folder_path))
return True
def save_state(self) -> bool:
"""Save sync state to tracking file."""
try:
data = {
"version": self.state.version,
"folder_path": self.state.folder_path,
"notebook_id": self.state.notebook_id,
"notebook_url": self.state.notebook_url,
"account_index": self.state.account_index,
"account_email": self.state.account_email,
"last_sync_at": self.state.last_sync_at,
"files": {}
}
for path, file_info in self.state.files.items():
data["files"][path] = {
"filename": file_info.filename,
"hash": file_info.hash,
"modified_at": file_info.modified_at,
"source_id": file_info.source_id,
"uploaded_at": file_info.uploaded_at,
}
# Atomic write via temp file
temp_file = self.tracking_file.with_suffix(".json.tmp")
temp_file.write_text(json.dumps(data, indent=2))
temp_file.rename(self.tracking_file)
return True
except Exception as e:
print(f"❌ Error saving tracking file: {e}")
return FalseStep 4: Run tests to verify they pass
cd /Users/troy.huang/workspace/AgentSkills/nblm
python -m pytest tests/test_sync_manager.py::test_load_state_creates_new_if_not_exists tests/test_sync_manager.py::test_load_state_loads_existing tests/test_sync_manager.py::test_save_state_writes_json -v
# Expected: PASSStep 5: Commit
git add scripts/sync_manager.py tests/test_sync_manager.py
git commit -m "feat(sync): add SyncManager state management (load/save)"---
Task 3: Implement Folder Scanning and Hashing
Files:
- Modify:
scripts/sync_manager.py
Step 1: Write failing tests
def test_scan_folder_finds_supported_files(tmp_path):
"""Test that scan_folder finds all supported files."""
(tmp_path / "doc1.md").write_text("# Doc 1")
(tmp_path / "doc2.pdf").write_bytes(b"%PDF-1.4")
(tmp_path / "ignored.txt").write_text("ignored") # .txt not supported
(tmp_path / "subdir").mkdir()
(tmp_path / "subdir" / "doc3.md").write_text("# Doc 3")
manager = SyncManager(str(tmp_path))
result = manager.scan_folder()
assert "doc1.md" in result
assert "doc2.pdf" in result
assert "doc3.md" in result
assert "ignored.txt" not in result
def test_compute_file_hash_is_deterministic(tmp_path):
"""Test that file hash is deterministic."""
content = b"test content"
(tmp_path / "test.md").write_bytes(content)
manager = SyncManager(str(tmp_path))
hash1 = manager.compute_file_hash(tmp_path / "test.md")
hash2 = manager.compute_file_hash(tmp_path / "test.md")
assert hash1 == hash2
assert hash1.startswith("sha256:")
def test_compute_file_hash_differs_for_different_content(tmp_path):
"""Test that different content produces different hashes."""
(tmp_path / "test1.md").write_text("content 1")
(tmp_path / "test2.md").write_text("content 2")
manager = SyncManager(str(tmp_path))
hash1 = manager.compute_file_hash(tmp_path / "test1.md")
hash2 = manager.compute_file_hash(tmp_path / "test2.md")
assert hash1 != hash2Step 2: Run tests to verify they fail
python -m pytest tests/test_sync_manager.py -v -k "scan_folder or compute_file_hash"
# Expected: FAIL (functions not defined)Step 3: Implement scan_folder and compute_file_hash
def scan_folder(self) -> dict[str, dict]:
"""Scan folder for supported files.
Returns:
Dict mapping relative path -> file info dict with:
- path: relative path from folder
- absolute_path: full path
- filename: file stem (without extension)
- extension: file extension
- modified_at: ISO timestamp
- size: file size in bytes
"""
files = {}
if not self.folder_path.exists():
print(f"⚠️ Folder does not exist: {self.folder_path}")
return files
for root, dirs, filenames in os.walk(self.folder_path):
# Skip the tracking file itself and dotfiles
dirs[:] = [d for d in dirs if not d.startswith('.')]
for filename in filenames:
# Skip tracking file
if filename == self.TRACKING_FILENAME:
continue
path = Path(root) / filename
relative_path = path.relative_to(self.folder_path)
# Check extension
ext = path.suffix.lower()
if ext not in SUPPORTED_EXTENSIONS:
continue
try:
stat = path.stat()
files[str(relative_path)] = {
"path": str(relative_path),
"absolute_path": str(path),
"filename": path.stem,
"extension": ext,
"modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"size": stat.st_size,
}
except OSError as e:
print(f"⚠️ Could not access {path}: {e}")
print(f"📁 Found {len(files)} supported files in {self.folder_path}")
return files
def compute_file_hash(self, file_path: Path) -> str:
"""Compute SHA-256 hash of a file.
Returns:
Hash string prefixed with algorithm name, e.g., "sha256:abc123..."
"""
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
sha256.update(chunk)
return f"sha256:{sha256.hexdigest()}"Step 4: Run tests to verify they pass
python -m pytest tests/test_sync_manager.py -v -k "scan_folder or compute_file_hash"
# Expected: PASSStep 5: Commit
git add scripts/sync_manager.py
git commit -m "feat(sync): add folder scanning and file hashing"---
Task 4: Implement Sync Plan Generation
Files:
- Modify:
scripts/sync_manager.py
Step 1: Write failing tests
def test_get_sync_plan_add_new_file(tmp_path):
"""Test that new files are marked for addition."""
# No tracking file - all files should be new
(tmp_path / "new.md").write_text("new content")
manager = SyncManager(str(tmp_path))
manager.load_state()
local_files = manager.scan_folder()
plan = manager.get_sync_plan(local_files)
assert len(plan) == 1
assert plan[0]["action"] == "add"
assert plan[0]["path"] == "new.md"
def test_get_sync_plan_skip_unchanged(tmp_path):
"""Test that unchanged files are skipped."""
# Create tracking file with existing file
tracking = tmp_path / ".nblm-sync.json"
tracking.write_text(json.dumps({
"version": 1,
"folder_path": str(tmp_path),
"files": {
"existing.md": {
"filename": "existing",
"hash": "sha256:abc123", # Pre-set hash
"modified_at": "2025-01-30T10:00:00Z"
}
}
}))
# Create file with same content (will hash to same value)
content = b"same content"
expected_hash = f"sha256:{hashlib.sha256(content).hexdigest()}"
(tmp_path / "existing.md").write_bytes(content)
manager = SyncManager(str(tmp_path))
manager.load_state()
local_files = manager.scan_folder()
plan = manager.get_sync_plan(local_files)
assert len(plan) == 1
assert plan[0]["action"] == "skip"
assert plan[0]["path"] == "existing.md"
def test_get_sync_plan_update_modified(tmp_path):
"""Test that modified files are marked for update."""
tracking = tmp_path / ".nblm-sync.json"
tracking.write_text(json.dumps({
"version": 1,
"folder_path": str(tmp_path),
"files": {
"modified.md": {
"filename": "modified",
"hash": "sha256:oldhash",
"modified_at": "2025-01-30T10:00:00Z",
"source_id": "source-123"
}
}
}))
# Create file with different content
(tmp_path / "modified.md").write_text("new content")
manager = SyncManager(str(tmp_path))
manager.load_state()
local_files = manager.scan_folder()
plan = manager.get_sync_plan(local_files)
assert len(plan) == 1
assert plan[0]["action"] == "update"
assert plan[0]["path"] == "modified.md"
assert plan[0]["source_id"] == "source-123" # Has existing source_idStep 2: Run tests to verify they fail
python -m pytest tests/test_sync_manager.py -v -k "get_sync_plan"
# Expected: FAIL (function not defined)Step 3: Implement get_sync_plan
def get_sync_plan(self, local_files: dict[str, dict]) -> list[dict]:
"""Generate sync plan comparing local files with tracking state.
Args:
local_files: Dict from scan_folder() with file info
Returns:
List of sync actions with:
- action: SyncAction value
- path: relative file path
- local_info: file info from local_files
- tracked_info: previous tracking info (if exists)
- source_id: existing source ID for update/delete (if exists)
"""
plan = []
# Check each local file
for path, local_info in local_files.items():
# Compute hash for comparison
abs_path = Path(local_info["absolute_path"])
current_hash = self.compute_file_hash(abs_path)
local_info["hash"] = current_hash
if path not in self.state.files:
# New file - needs addition
plan.append({
"action": SyncAction.ADD.value,
"path": path,
"local_info": local_info,
"tracked_info": None,
"source_id": None,
})
else:
tracked = self.state.files[path]
if tracked.hash != current_hash:
# Content changed - needs update
if tracked.source_id:
plan.append({
"action": SyncAction.UPDATE.value,
"path": path,
"local_info": local_info,
"tracked_info": tracked,
"source_id": tracked.source_id,
})
else:
# No existing source, treat as add
plan.append({
"action": SyncAction.ADD.value,
"path": path,
"local_info": local_info,
"tracked_info": tracked,
"source_id": None,
})
else:
# Unchanged - skip
plan.append({
"action": SyncAction.SKIP.value,
"path": path,
"local_info": local_info,
"tracked_info": tracked,
"source_id": tracked.source_id,
})
# Check for deleted files (in tracking but not in local)
for path in self.state.files:
if path not in local_files:
plan.append({
"action": SyncAction.DELETE.value,
"path": path,
"local_info": None,
"tracked_info": self.state.files[path],
"source_id": self.state.files[path].source_id,
})
return planStep 4: Run tests to verify they pass
python -m pytest tests/test_sync_manager.py -v -k "get_sync_plan"
# Expected: PASSStep 5: Commit
git add scripts/sync_manager.py
git commit -m "feat(sync): add sync plan generation"---
Task 5: Implement Sync Execution
Files:
- Modify:
scripts/sync_manager.py - Dependencies: notebooklm_wrapper.py, config.py
Step 1: Write failing test (mock-based)
@pytest.mark.asyncio
async def test_execute_sync_adds_new_file(tmp_path):
"""Test that sync execution adds new files."""
(tmp_path / "new.md").write_text("new content")
manager = SyncManager(str(tmp_path))
manager.load_state()
local_files = manager.scan_folder()
plan = manager.get_sync_plan(local_files)
# Mock the wrapper
mock_wrapper = AsyncMock()
mock_wrapper.add_file.return_value = {"source_id": "new-source-123"}
result = await manager._execute_plan(mock_wrapper, plan, "notebook-123", dry_run=True)
assert result["add"] == 1
assert mock_wrapper.add_file.called
@pytest.mark.asyncio
async def test_execute_sync_updates_modified(tmp_path):
"""Test that sync execution updates modified files."""
tracking = tmp_path / ".nblm-sync.json"
tracking.write_text(json.dumps({
"version": 1,
"folder_path": str(tmp_path),
"files": {
"modified.md": {
"filename": "modified",
"hash": "sha256:oldhash",
"modified_at": "2025-01-30T10:00:00Z",
"source_id": "old-source-123"
}
}
}))
(tmp_path / "modified.md").write_text("new content")
manager = SyncManager(str(tmp_path))
manager.load_state()
local_files = manager.scan_folder()
plan = manager.get_sync_plan(local_files)
mock_wrapper = AsyncMock()
mock_wrapper.delete_source.return_value = True
mock_wrapper.add_file.return_value = {"source_id": "new-source-456"}
result = await manager._execute_plan(mock_wrapper, plan, "notebook-123", dry_run=False)
assert result["update"] == 1
assert mock_wrapper.delete_source.called
assert mock_wrapper.add_file.calledStep 2: Run tests to verify they fail
python -m pytest tests/test_sync_manager.py -v -k "execute_sync"
# Expected: FAIL (function not defined, missing mock)Step 3: Implement execute_sync methods
async def execute_sync(
self,
notebook_id: str,
account_index: int,
account_email: str,
dry_run: bool = False,
) -> dict:
"""Execute full sync workflow.
Args:
notebook_id: Target NotebookLM notebook ID
account_index: Active Google account index
account_email: Active Google account email
dry_run: If True, only show plan without executing
Returns:
Dict with sync results: add, update, skip, delete, errors
"""
from notebooklm_wrapper import NotebookLMWrapper
# Load state
self.load_state()
# Validate account match
if self.state.account_index is not None and self.state.account_index != account_index:
print(f"⚠️ Tracking file was created with account [{self.state.account_index}] {self.state.account_email}")
print(f" Current active account: [{account_index}] {account_email}")
print(" Continuing with new account (tracking file will be updated)")
# Scan and plan
local_files = self.scan_folder()
plan = self.get_sync_plan(local_files)
# Show plan
self._print_sync_plan(plan, dry_run)
if dry_run:
return self._summarize_plan(plan)
# Execute
async with NotebookLMWrapper() as wrapper:
result = await self._execute_plan(wrapper, plan, notebook_id)
# Update state
self.state.notebook_id = notebook_id
self.state.account_index = account_index
self.state.account_email = account_email
self.state.last_sync_at = datetime.now(timezone.utc).isoformat()
self.save_state()
return result
async def _execute_plan(
self,
wrapper,
plan: list[dict],
notebook_id: str,
dry_run: bool = False,
) -> dict:
"""Execute sync plan using NotebookLMWrapper."""
result = {"add": 0, "update": 0, "skip": 0, "delete": 0, "errors": []}
for item in plan:
action = item["action"]
path = item["path"]
local_info = item["local_info"]
if action == SyncAction.SKIP.value:
result["skip"] += 1
continue
if dry_run:
print(f" [{'DRY-RUN' if dry_run else ''}] {action.upper()} {path}")
continue
try:
if action == SyncAction.ADD.value:
print(f" ➕ Adding: {path}")
file_path = Path(local_info["absolute_path"])
upload_result = await wrapper.add_file(notebook_id, file_path)
source_id = upload_result.get("source_id")
# Update tracking
self.state.files[path] = TrackedFile(
filename=local_info["filename"],
hash=local_info["hash"],
modified_at=local_info["modified_at"],
source_id=source_id,
uploaded_at=datetime.now(timezone.utc).isoformat(),
)
result["add"] += 1
elif action == SyncAction.UPDATE.value:
print(f" 🔄 Updating: {path}")
old_source_id = item["source_id"]
# Delete old
if old_source_id:
await wrapper.delete_source(notebook_id, old_source_id)
# Upload new
file_path = Path(local_info["absolute_path"])
upload_result = await wrapper.add_file(notebook_id, file_path)
source_id = upload_result.get("source_id")
# Update tracking
self.state.files[path] = TrackedFile(
filename=local_info["filename"],
hash=local_info["hash"],
modified_at=local_info["modified_at"],
source_id=source_id,
uploaded_at=datetime.now(timezone.utc).isoformat(),
)
result["update"] += 1
elif action == SyncAction.DELETE.value:
print(f" 🗑️ Deleting remote: {path}")
source_id = item["source_id"]
if source_id:
await wrapper.delete_source(notebook_id, source_id)
del self.state.files[path]
result["delete"] += 1
except Exception as e:
print(f" ❌ Error {action} {path}: {e}")
result["errors"].append({"path": path, "action": action, "error": str(e)})
return result
def _print_sync_plan(self, plan: list[dict], dry_run: bool = False):
"""Print formatted sync plan."""
prefix = "🔍 [DRY-RUN] " if dry_run else "📋 Sync Plan:"
print(f"\n{prefix}")
counts = {"add": 0, "update": 0, "skip": 0, "delete": 0}
for item in plan:
action = item["action"]
path = item["path"]
counts[action] += 1
symbol = {"add": "➕", "update": "🔄", "skip": "✓", "delete": "🗑️"}[action]
print(f" {symbol} {path:<30} [{action.upper()}]")
print(f"\n Total: {counts['add']} add, {counts['update']} update, {counts['skip']} skip, {counts['delete']} delete")
def _summarize_plan(self, plan: list[dict]) -> dict:
"""Summarize plan without executing."""
result = {"add": 0, "update": 0, "skip": 0, "delete": 0, "errors": []}
for item in plan:
result[item["action"]] += 1
return resultStep 4: Run tests to verify they pass
python -m pytest tests/test_sync_manager.py -v -k "execute_sync"
# Expected: PASSStep 5: Commit
git add scripts/sync_manager.py
git commit -m "feat(sync): add sync execution logic"---
Task 6: Integrate with source_manager.py CLI
Files:
- Modify:
scripts/source_manager.py
Step 1: Write failing test
def test_sync_command_parses_arguments():
"""Test that sync command parses folder and options."""
# This tests the CLI argument parsing
pass # CLI testing via integration testStep 2: Add sync command to source_manager.py
Add to async_main():
# In async_main() parser setup:
parser.add_argument("command", choices=["add", "sync"], help="Command to run")
# ... existing add arguments ...
# New sync arguments (add after existing add arguments)
sync_parser = subparsers.add_parser("sync", help="Sync a folder to NotebookLM")
sync_parser.add_argument("folder", help="Folder path to sync")
sync_parser.add_argument("--use-active", action="store_true",
help="Sync to currently active notebook")
sync_parser.add_argument("--notebook-id", help="Existing notebook ID")
sync_parser.add_argument("--create-new", action="store_true",
help="Create a new notebook named after the folder")
sync_parser.add_argument("--dry-run", action="store_true",
help="Show sync plan without executing")
sync_parser.add_argument("--rebuild", action="store_true",
help="Force rebuild tracking file (re-hash all files)")Add sync command handler:
elif args.command == "sync":
if not Path(args.folder).isdir():
print(f"❌ Folder not found: {args.folder}", file=sys.stderr)
raise SystemExit(1)
# Resolve notebook target
folder_name = Path(args.folder).stem
notebook_id, create_new = _resolve_notebook_target(args, folder_name)
# Get active account
account_mgr = AccountManager()
active = account_mgr.get_active_account()
if not active:
print("❌ No active Google account.", file=sys.stderr)
print(" Run: python scripts/run.py auth_manager.py accounts list", file=sys.stderr)
raise SystemExit(1)
# Create manager and run sync
manager = SyncManager(args.folder)
# Rebuild option - delete tracking file
if args.rebuild and manager.tracking_file.exists():
manager.tracking_file.unlink()
print(f"🗑️ Cleared tracking file for rebuild")
result = await manager.execute_sync(
notebook_id=notebook_id,
account_index=active.index,
account_email=active.email,
dry_run=args.dry_run,
)
print(json.dumps(result, indent=2))Step 3: Add import at top of source_manager.py
from sync_manager import SyncManagerStep 4: Test the CLI manually
# Create test folder
mkdir -p /tmp/test-sync-folder
echo "# Test Doc" > /tmp/test-sync-folder/test.md
# Dry-run (should show plan without executing)
python scripts/run.py source_manager.py sync /tmp/test-sync-folder --dry-run
# Expected: Shows add plan
# Cleanup
rm -rf /tmp/test-sync-folderStep 5: Commit
git add scripts/source_manager.py
git commit -m "feat(sync): add sync command to CLI"---
Task 7: Update SKILL.md Documentation
Files:
- Modify:
SKILL.md
Add to the Source Management section:
### Folder Sync
| Command | Description |
|---------|-------------|
| `upload <folder>` | Sync a folder of files to NotebookLM |
| `upload <folder> --dry-run` | Preview sync without executing |
| `upload <folder> --rebuild` | Force rebuild tracking file |
**Sync behavior:**
- New files → Uploaded
- Modified files → Old source deleted, new file uploaded
- Unchanged files → Skipped
- Files deleted locally → Remote source deleted
**Example:**/nblm upload ./docs --dry-run # Preview sync /nblm upload ./docs --use-active # Sync to active notebook /nblm upload ./docs --create-new # Create new notebook /nblm upload ./docs --rebuild # Rebuild tracking
Step 2: Commit
git add SKILL.md
git commit -m "docs: add folder sync documentation"---
Task 8: Integration Testing
Files:
- Create:
tests/test_folder_sync_integration.py(if project has integration tests)
Or run manual integration tests:
# Setup test notebook
python scripts/run.py notebook_manager.py create "Sync Test"
python scripts/run.py notebook_manager.py list # Note the notebook ID
# Create test folder with files
mkdir -p /tmp/nblm-test-sync
echo "# Document 1" > /tmp/nblm-test-sync/doc1.md
echo "# Document 2" > /tmp/nblm-test-sync/doc2.md
# Test 1: Dry-run
python scripts/run.py source_manager.py sync /tmp/nblm-test-sync --notebook-id <ID> --dry-run
# Expected: 2 add operations
# Test 2: Execute sync
python scripts/run.py source_manager.py sync /tmp/nblm-test-sync --notebook-id <ID>
# Expected: Files uploaded, tracking file created
# Test 3: Dry-run again (should show skip)
python scripts/run.py source_manager.py sync /tmp/nblm-test-sync --notebook-id <ID> --dry-run
# Expected: 0 add, 2 skip
# Test 4: Modify file and sync
echo "# Updated Document 1" > /tmp/nblm-test-sync/doc1.md
python scripts/run.py source_manager.py sync /tmp/nblm-test-sync --notebook-id <ID> --dry-run
# Expected: 1 update, 1 skip
# Cleanup
rm -rf /tmp/nblm-test-sync---
Summary
Files created/modified:
scripts/sync_manager.py- New (400+ lines)scripts/source_manager.py- Modified (add sync command)tests/test_sync_manager.py- New (unit tests)SKILL.md- Modified (documentation)
Commands:
# Sync a folder
python scripts/run.py source_manager.py sync ./docs --use-active
# Preview without executing
python scripts/run.py source_manager.py sync ./docs --dry-run
# Force rebuild tracking
python scripts/run.py source_manager.py sync ./docs --rebuildNext steps after completing tasks:
- Run full test suite
- Verify all commits are clean
- Test with real NotebookLM account
---
Plan complete and saved to docs/plans/2025-01-30-folder-sync-design.md.
Two execution options:
1. Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration
2. Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints
Which approach?
MIT License
Copyright (c) 2025 Please Prompto!
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "nblm",
"version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nblm",
"version": "2.1.0",
"dependencies": {
"agent-browser": "0.5.0"
}
},
"node_modules/agent-browser": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.5.0.tgz",
"integrity": "sha512-i0NGFBMwLMk7Q47vkyI8/DNxBmf3t5O+meq25rCJgxl4UbphwbnGamMmB/ZZBLLifXGYcU/joUnFEOxmV691zw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "^1.57.0",
"ws": "^8.19.0",
"zod": "^3.22.4"
},
"bin": {
"agent-browser": "bin/agent-browser"
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ws": {
"version": "8.19.0",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
{
"name": "nblm",
"version": "2.1.0",
"private": true,
"description": "Claude Code skill for NotebookLM integration using agent-browser",
"dependencies": {
"agent-browser": "0.5.0"
},
"scripts": {
"daemon": "node node_modules/agent-browser/dist/daemon.js",
"install-browsers": "npx playwright install chromium"
}
}
<div align="center">
English | 中文
nblm
Your AI Coding Agent's Gateway to NotebookLM
  
<br/>
🧠 Zero Hallucinations — Answers grounded exclusively in your documents <br/> ⚡ Zero Context Switching — Ask, upload, generate podcasts & slides from your editor <br/> 🔌 Infinite Sources — Z-Library today, arXiv / Notion / Confluence tomorrow
<br/>
<sub>Works with Claude Code · Cursor · Windsurf · Codex · and any Agent Skills compatible agent</sub>
<br/>
Installation · Quick Start · Commands · Architecture
</div>
---
Installation
Recommended: Using add-skill CLI
npx add-skill magicseek/nblmThis works with any supported agent. To install for a specific agent:
# Claude Code only
npx add-skill magicseek/nblm -a claude-code
# Global installation (available across all projects)
npx add-skill magicseek/nblm --global
# Multiple agents
npx add-skill magicseek/nblm -a claude-code -a cursor -a opencodeAlternative: Platform-specific initialization
If symlinks created by add-skill don't work well in your environment (e.g., Cursor, Windows), you can generate platform-specific files directly:
macOS / Linux:
# Clone the repo
git clone https://github.com/magicseek/nblm ~/.nblm
# Initialize for your AI assistant (run from your project directory)
python ~/.nblm/scripts/run.py init --ai cursor # Cursor
python ~/.nblm/scripts/run.py init --ai claude # Claude Code
python ~/.nblm/scripts/run.py init --ai codex # Codex CLI
python ~/.nblm/scripts/run.py init --ai antigravity # Antigravity
python ~/.nblm/scripts/run.py init --ai windsurf # Windsurf
python ~/.nblm/scripts/run.py init --ai copilot # GitHub Copilot
python ~/.nblm/scripts/run.py init --ai all # All platforms
# List available platforms
python ~/.nblm/scripts/run.py init --listWindows (PowerShell):
# Clone the repo
git clone https://github.com/magicseek/nblm $env:USERPROFILE\.nblm
# Initialize for your AI assistant (run from your project directory)
python $env:USERPROFILE\.nblm\scripts\run.py init --ai cursor # Cursor
python $env:USERPROFILE\.nblm\scripts\run.py init --ai claude # Claude Code
python $env:USERPROFILE\.nblm\scripts\run.py init --ai codex # Codex CLI
python $env:USERPROFILE\.nblm\scripts\run.py init --ai antigravity # Antigravity
python $env:USERPROFILE\.nblm\scripts\run.py init --ai windsurf # Windsurf
python $env:USERPROFILE\.nblm\scripts\run.py init --ai copilot # GitHub Copilot
python $env:USERPROFILE\.nblm\scripts\run.py init --ai all # All platforms
# List available platforms
python $env:USERPROFILE\.nblm\scripts\run.py init --listThis generates the appropriate skill/command files in your project directory (e.g., .cursor/commands/nblm.md).
First Run
On first use, nblm automatically:
- Creates an isolated Python environment (
.venv) - Installs Python and Node.js dependencies
- Starts the agent-browser daemon as needed
No manual setup required. If Playwright browsers are missing, run npm run install-browsers in the skill folder.
---
Quick Start
1. Authenticate with Google (one-time)
/nblm loginA browser window opens for Google login. This is required once.
2. Add a notebook to your library
Go to notebooklm.google.com → Create notebook → Upload your docs → Share with "Anyone with link"
/nblm add <notebook-url-or-id>nblm automatically queries the notebook to discover its content and metadata.
3. Ask questions
/nblm ask "What does the documentation say about authentication?"Answers are source-grounded with citations from your uploaded documents.
4. Manage your notebooks
/nblm local # List notebooks in your library
/nblm remote # List all notebooks from NotebookLM API
/nblm status # Show auth and library status5. Upload sources
/nblm upload ./document.pdf # Local file
/nblm upload-url https://example.com # Web URL
/nblm upload-zlib <z-library-url> # Z-Library book---
Commands
<details> <summary><strong>📚 Notebook Management</strong></summary>
| Command | Description |
|---|---|
/nblm login | Authenticate with Google |
/nblm accounts | List all Google accounts |
/nblm accounts add | Add a new Google account |
/nblm accounts switch <id> | Switch active account (by index or email) |
/nblm accounts remove <id> | Remove an account |
/nblm status | Show auth and library status |
/nblm local | List notebooks in local library |
/nblm remote | List all notebooks from NotebookLM API |
/nblm create <name> | Create a new notebook |
/nblm delete [--id ID] | Delete a notebook |
/nblm rename <name> [--id ID] | Rename a notebook |
/nblm summary [--id ID] | Get AI-generated summary |
/nblm describe [--id ID] | Get description and suggested topics |
/nblm add <url-or-id> | Add notebook to local library |
/nblm activate <id> | Set active notebook |
</details>
<details> <summary><strong>📄 Source Management</strong></summary>
| Command | Description |
|---|---|
/nblm sources [--id ID] | List sources in notebook |
/nblm upload <file> | Upload local file (PDF, TXT, MD, DOCX) |
/nblm upload-zlib <url> | Download from Z-Library and upload |
/nblm upload-url <url> | Add URL as source |
/nblm upload-youtube <url> | Add YouTube video as source |
/nblm upload-text <title> [--content TEXT] | Add text as source |
/nblm source-text <source-id> | Get full indexed text |
/nblm source-guide <source-id> | Get AI summary and keywords |
/nblm source-rename <source-id> <name> | Rename a source |
/nblm source-refresh <source-id> | Re-fetch URL content |
/nblm source-delete <source-id> | Delete a source |
</details>
<details> <summary><strong>💬 Chat & Query</strong></summary>
| Command | Description |
|---|---|
/nblm ask <question> | Query NotebookLM |
</details>
<details> <summary><strong>🎙️ Media Generation</strong></summary>
| Command | Description |
|---|---|
/nblm podcast [--instructions TEXT] | Generate audio podcast (deep-dive) |
/nblm podcast-status <task-id> | Check podcast generation status |
/nblm podcast-download [output-path] | Download latest podcast |
/nblm briefing [--instructions TEXT] | Generate brief audio summary |
/nblm debate [--instructions TEXT] | Generate debate-style audio |
/nblm slides [--instructions TEXT] | Generate slide deck |
/nblm slides-download [output-path] | Download slide deck as PDF |
/nblm infographic [--instructions TEXT] | Generate infographic |
/nblm infographic-download [output-path] | Download infographic |
/nblm media-list [--type TYPE] | List generated media |
/nblm media-delete <id> | Delete a generated media item |
Media generation options:
| Option | Values |
|---|---|
--length | SHORT, DEFAULT, LONG |
--instructions | Custom instructions for content |
--wait | Wait for generation to complete |
--output | Download path (requires --wait) |
</details>
---
Architecture
nblm uses a hybrid approach combining API-first operations with browser automation fallback:
┌─────────────────────────────────────────────────────────────┐
│ Your Agent │
│ (Claude Code / Cursor / OpenCode) │
└─────────────────────┬───────────────────────────────────────┘
│ /nblm commands
▼
┌─────────────────────────────────────────────────────────────┐
│ nblm │
├─────────────────────┬───────────────────────────────────────┤
│ notebooklm-py │ agent-browser │
│ (API operations) │ (browser automation) │
│ │ │
│ • Create notebooks │ • Google authentication │
│ • Add sources │ • File uploads (fallback) │
│ • Chat queries │ • Z-Library downloads │
│ • Generate media │ • Future non-API sources │
└─────────────────────┴───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Google NotebookLM │
│ (Gemini-powered document Q&A) │
└─────────────────────────────────────────────────────────────┘Key components:
| Component | Role |
|---|---|
| [notebooklm-py](https://github.com/teng-lin/notebooklm-py) | Async Python client for NotebookLM API operations |
| [agent-browser](https://github.com/vercel-labs/agent-browser) | Headless browser daemon for auth and non-API sources |
| scripts/run.py | Entry point that auto-manages venv and dependencies |
Data storage (in data/):
library.json— Your notebook metadata (with account associations)auth/google/— Multi-account Google authenticationindex.json— Account index and active account<n>-<email>.json— Per-account credentialsauth/zlibrary.json— Z-Library authentication state
---
Troubleshooting
| Problem | Solution |
|---|---|
| Skill not found | Verify installation: ls ~/.claude/skills/nblm/ |
ModuleNotFoundError | Always use /nblm commands — they auto-manage the environment |
| Authentication fails | Run /nblm login with a visible browser |
DAEMON_UNAVAILABLE | Ensure Node.js is installed, then run npm install in the skill folder |
| Rate limit (50/day) | Wait 24 hours or use a different Google account |
| Browser crashes | Run python scripts/run.py cleanup_manager.py --preserve-library |
For more details, see references/troubleshooting.md.
---
Acknowledgments
nblm builds upon the excellent work of these projects:
- [notebooklm-skill](https://github.com/PleasePrompto/notebooklm-skill) by PleasePrompto — The original Claude Code skill for NotebookLM integration with browser automation
- [zlibrary-to-notebooklm](https://github.com/zstmfhy/zlibrary-to-notebooklm) by zstmfhy — Z-Library to NotebookLM pipeline
- [notebooklm-py](https://github.com/teng-lin/notebooklm-py) by teng-lin — Async Python API client for NotebookLM
Additional dependencies:
- [agent-browser](https://github.com/vercel-labs/agent-browser) — Headless browser daemon for AI agents
- [add-skill](https://github.com/vercel-labs/add-skill) — Universal skill installer for AI coding agents
---
Limitations
- Rate limits — Free tier allows ~50 queries/day per Google account (use multiple accounts to increase limits)
- No session persistence — Each query is independent (no "previous answer" context)
- Manual notebook creation — You must create notebooks and upload docs via notebooklm.google.com
License
MIT
---
<div align="center">
nblm — Source-grounded answers from your documents, directly in your coding agent.
</div>
<div align="center">
English | 中文
nblm
AI 编程助手通往 NotebookLM 的桥梁
  
<br/>
🧠 零幻觉 — 答案完全基于你的文档 <br/> ⚡ 零切换 — 在编辑器中提问、上传、生成播客和幻灯片 <br/> 🔌 无限来源 — 今天是 Z-Library,明天是 arXiv / Notion / Confluence
<br/>
<sub>支持 Claude Code · Cursor · Windsurf · Codex · 以及任何兼容 Agent Skills 的 AI 助手</sub>
<br/>
</div>
---
安装
推荐:使用 add-skill CLI
npx add-skill magicseek/nblm适用于任何支持的 AI 助手。为特定助手安装:
# 仅 Claude Code
npx add-skill magicseek/nblm -a claude-code
# 全局安装(跨项目可用)
npx add-skill magicseek/nblm --global
# 多个助手
npx add-skill magicseek/nblm -a claude-code -a cursor -a opencode备选:平台特定初始化
如果 add-skill 创建的符号链接在你的环境中无法正常工作(如 Cursor、Windows),可以直接生成平台特定文件:
macOS / Linux:
# 克隆仓库
git clone https://github.com/magicseek/nblm ~/.nblm
# 为你的 AI 助手初始化(在项目目录中运行)
python ~/.nblm/scripts/run.py init --ai cursor # Cursor
python ~/.nblm/scripts/run.py init --ai claude # Claude Code
python ~/.nblm/scripts/run.py init --ai codex # Codex CLI
python ~/.nblm/scripts/run.py init --ai antigravity # Antigravity
python ~/.nblm/scripts/run.py init --ai windsurf # Windsurf
python ~/.nblm/scripts/run.py init --ai copilot # GitHub Copilot
python ~/.nblm/scripts/run.py init --ai all # 所有平台
# 列出可用平台
python ~/.nblm/scripts/run.py init --listWindows (PowerShell):
# 克隆仓库
git clone https://github.com/magicseek/nblm $env:USERPROFILE\.nblm
# 为你的 AI 助手初始化(在项目目录中运行)
python $env:USERPROFILE\.nblm\scripts\run.py init --ai cursor # Cursor
python $env:USERPROFILE\.nblm\scripts\run.py init --ai claude # Claude Code
python $env:USERPROFILE\.nblm\scripts\run.py init --ai codex # Codex CLI
python $env:USERPROFILE\.nblm\scripts\run.py init --ai antigravity # Antigravity
python $env:USERPROFILE\.nblm\scripts\run.py init --ai windsurf # Windsurf
python $env:USERPROFILE\.nblm\scripts\run.py init --ai copilot # GitHub Copilot
python $env:USERPROFILE\.nblm\scripts\run.py init --ai all # 所有平台
# 列出可用平台
python $env:USERPROFILE\.nblm\scripts\run.py init --list这会在你的项目目录中生成相应的技能/命令文件(如 .cursor/commands/nblm.md)。
首次运行
首次使用时,nblm 会自动:
- 创建隔离的 Python 环境(
.venv) - 安装 Python 和 Node.js 依赖
- 按需启动 agent-browser 守护进程
无需手动设置。如果缺少 Playwright 浏览器,在技能文件夹中运行 npm run install-browsers。
---
快速开始
1. Google 认证(仅需一次)
/nblm login浏览器窗口将打开进行 Google 登录。只需执行一次。
2. 添加笔记本到库
前往 notebooklm.google.com → 创建笔记本 → 上传文档 → 设置"任何有链接的人"可访问
/nblm add <笔记本-url-或-id>nblm 会自动查询笔记本以发现其内容和元数据。
3. 提问
/nblm ask "文档中关于认证的说明是什么?"答案基于来源,带有上传文档的引用。
4. 管理笔记本
/nblm local # 列出库中的笔记本
/nblm remote # 列出 NotebookLM API 中的所有笔记本
/nblm status # 显示认证和库状态5. 上传来源
/nblm upload ./document.pdf # 本地文件
/nblm upload-url https://example.com # 网页 URL
/nblm upload-zlib <z-library-url> # Z-Library 图书---
命令
<details> <summary><strong>📚 笔记本管理</strong></summary>
| 命令 | 说明 |
|---|---|
/nblm login | Google 认证 |
/nblm status | 显示认证和库状态 |
/nblm local | 列出本地库中的笔记本 |
/nblm remote | 列出 NotebookLM API 中的所有笔记本 |
/nblm create <名称> | 创建新笔记本 |
/nblm delete [--id ID] | 删除笔记本 |
/nblm rename <名称> [--id ID] | 重命名笔记本 |
/nblm summary [--id ID] | 获取 AI 生成的摘要 |
/nblm describe [--id ID] | 获取描述和建议主题 |
/nblm add <url-或-id> | 添加笔记本到本地库 |
/nblm activate <id> | 设置活动笔记本 |
</details>
<details> <summary><strong>📄 来源管理</strong></summary>
| 命令 | 说明 |
|---|---|
/nblm sources [--id ID] | 列出笔记本中的来源 |
/nblm upload <文件> | 上传本地文件(PDF、TXT、MD、DOCX) |
/nblm upload-zlib <url> | 从 Z-Library 下载并上传 |
/nblm upload-url <url> | 添加 URL 作为来源 |
/nblm upload-youtube <url> | 添加 YouTube 视频作为来源 |
/nblm upload-text <标题> [--content TEXT] | 添加文本作为来源 |
/nblm source-text <source-id> | 获取完整索引文本 |
/nblm source-guide <source-id> | 获取 AI 摘要和关键词 |
/nblm source-rename <source-id> <名称> | 重命名来源 |
/nblm source-refresh <source-id> | 重新获取 URL 内容 |
/nblm source-delete <source-id> | 删除来源 |
</details>
<details> <summary><strong>💬 聊天与查询</strong></summary>
| 命令 | 说明 |
|---|---|
/nblm ask <问题> | 查询 NotebookLM |
</details>
<details> <summary><strong>🎙️ 媒体生成</strong></summary>
| 命令 | 说明 |
|---|---|
/nblm podcast [--instructions TEXT] | 生成音频播客(深度对话) |
/nblm podcast-status <task-id> | 检查播客生成状态 |
/nblm podcast-download [输出路径] | 下载最新播客 |
/nblm briefing [--instructions TEXT] | 生成简短音频摘要 |
/nblm debate [--instructions TEXT] | 生成辩论式音频 |
/nblm slides [--instructions TEXT] | 生成幻灯片 |
/nblm slides-download [输出路径] | 下载幻灯片 PDF |
/nblm infographic [--instructions TEXT] | 生成信息图 |
/nblm infographic-download [输出路径] | 下载信息图 |
/nblm media-list [--type TYPE] | 列出生成的媒体 |
/nblm media-delete <id> | 删除生成的媒体项 |
媒体生成选项:
| 选项 | 值 |
|---|---|
--length | SHORT、DEFAULT、LONG |
--instructions | 内容的自定义指令 |
--wait | 等待生成完成 |
--output | 下载路径(需配合 --wait) |
</details>
---
架构
nblm 采用混合方式,优先使用 API 操作,浏览器自动化作为后备:
┌─────────────────────────────────────────────────────────────┐
│ 你的 AI 助手 │
│ (Claude Code / Cursor / OpenCode) │
└─────────────────────┬───────────────────────────────────────┘
│ /nblm 命令
▼
┌─────────────────────────────────────────────────────────────┐
│ nblm │
├─────────────────────┬───────────────────────────────────────┤
│ notebooklm-py │ agent-browser │
│ (API 操作) │ (浏览器自动化) │
│ │ │
│ • 创建笔记本 │ • Google 认证 │
│ • 添加来源 │ • 文件上传(后备) │
│ • 聊天查询 │ • Z-Library 下载 │
│ • 生成媒体 │ • 未来非 API 来源 │
└─────────────────────┴───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Google NotebookLM │
│ (Gemini 驱动的文档问答) │
└─────────────────────────────────────────────────────────────┘核心组件:
| 组件 | 作用 |
|---|---|
| [notebooklm-py](https://github.com/teng-lin/notebooklm-py) | NotebookLM API 操作的异步 Python 客户端 |
| [agent-browser](https://github.com/vercel-labs/agent-browser) | 用于认证和非 API 来源的无头浏览器守护进程 |
| scripts/run.py | 自动管理虚拟环境和依赖的入口点 |
数据存储(在 data/ 目录):
library.json— 你的笔记本元数据auth/google.json— Google 认证状态auth/zlibrary.json— Z-Library 认证状态
---
故障排除
| 问题 | 解决方案 |
|---|---|
| 找不到技能 | 验证安装:ls ~/.claude/skills/nblm/ |
ModuleNotFoundError | 始终使用 /nblm 命令 — 它们会自动管理环境 |
| 认证失败 | 使用可见浏览器运行 /nblm login |
DAEMON_UNAVAILABLE | 确保已安装 Node.js,然后在技能文件夹中运行 npm install |
| 速率限制(50/天) | 等待 24 小时或使用其他 Google 账号 |
| 浏览器崩溃 | 运行 python scripts/run.py cleanup_manager.py --preserve-library |
更多详情,请查看 references/troubleshooting.md。
---
致谢
nblm 基于这些优秀项目构建:
- [notebooklm-skill](https://github.com/PleasePrompto/notebooklm-skill) by PleasePrompto — 原始的 Claude Code NotebookLM 集成技能,使用浏览器自动化
- [zlibrary-to-notebooklm](https://github.com/zstmfhy/zlibrary-to-notebooklm) by zstmfhy — Z-Library 到 NotebookLM 管道
- [notebooklm-py](https://github.com/teng-lin/notebooklm-py) by teng-lin — NotebookLM 异步 Python API 客户端
其他依赖:
- [agent-browser](https://github.com/vercel-labs/agent-browser) — AI 助手的无头浏览器守护进程
- [add-skill](https://github.com/vercel-labs/add-skill) — AI 编程助手的通用技能安装器
---
限制
- 速率限制 — 免费层每个 Google 账号每天约 50 次查询
- 无会话持久化 — 每次查询独立(无"上一个答案"上下文)
- 手动创建笔记本 — 需要通过 notebooklm.google.com 创建笔记本和上传文档
许可证
MIT
---
<div align="center">
nblm — 从你的文档获取基于来源的答案,直接在你的编程助手中使用。
</div>
nblm API Reference
Complete API documentation for all NotebookLM skill modules.
Important: Always Use run.py Wrapper
All commands must use the `run.py` wrapper to ensure proper environment:
# ✅ CORRECT:
python scripts/run.py [script_name].py [arguments]
# ❌ WRONG:
python scripts/[script_name].py [arguments] # Will fail without venv!Core Scripts
ask_question.py
Query NotebookLM with automated browser interaction.
# Basic usage
python scripts/run.py ask_question.py --question "Your question"
# With specific notebook
python scripts/run.py ask_question.py --question "..." --notebook-id notebook-id
# With direct URL
python scripts/run.py ask_question.py --question "..." --notebook-url "https://..."
# Show browser (debugging)
python scripts/run.py ask_question.py --question "..." --show-browserParameters:
--question(required): Question to ask--notebook-id: Use notebook from library--notebook-url: Use URL directly--show-browser: Make browser visible
Returns: Answer text with follow-up prompt appended
notebook_manager.py
Manage notebook library with CRUD operations.
# Smart Add (discover content first)
python scripts/run.py ask_question.py --question "What is the content of this notebook? What topics are covered? Provide a complete overview briefly and concisely" --notebook-url "[URL]"
# Then add with discovered info
python scripts/run.py notebook_manager.py add \
--url "https://notebooklm.google.com/notebook/..." \
--name "Name" \
--description "Description" \
--topics "topic1,topic2"
# Direct add (when you know the content)
python scripts/run.py notebook_manager.py add \
--url "https://notebooklm.google.com/notebook/..." \
--name "Name" \
--description "What it contains" \
--topics "topic1,topic2"
# List notebooks
python scripts/run.py notebook_manager.py list
# Search notebooks
python scripts/run.py notebook_manager.py search --query "keyword"
# Activate notebook
python scripts/run.py notebook_manager.py activate --id notebook-id
# Remove notebook
python scripts/run.py notebook_manager.py remove --id notebook-id
# Show statistics
python scripts/run.py notebook_manager.py statsCommands:
add: Add notebook (requires --url, --name, --topics)list: Show all notebookssearch: Find notebooks by keywordactivate: Set default notebookremove: Delete from librarystats: Display library statistics
auth_manager.py
Handle Google authentication and browser state.
# Setup (browser visible for login)
python scripts/run.py auth_manager.py setup
# Check status
python scripts/run.py auth_manager.py status
# Clear authentication
python scripts/run.py auth_manager.py clear
# Multi-account management
python scripts/run.py auth_manager.py accounts list
python scripts/run.py auth_manager.py accounts add
python scripts/run.py auth_manager.py accounts switch 1Commands:
setup: Initial authentication (browser MUST be visible)status: Check if authenticatedclear: Remove all auth dataaccounts: Multi-account management (list, add, switch, remove)
Note: Expired credentials are automatically re-authenticated when running commands.
cleanup_manager.py
Clean skill data with preservation options.
# Preview cleanup
python scripts/run.py cleanup_manager.py
# Execute cleanup
python scripts/run.py cleanup_manager.py --confirm
# Keep library
python scripts/run.py cleanup_manager.py --confirm --preserve-library
# Force without prompt
python scripts/run.py cleanup_manager.py --confirm --forceOptions:
--confirm: Actually perform cleanup--preserve-library: Keep notebook library--force: Skip confirmation prompt
artifact_manager.py
Manage audio overviews, podcasts, and other generated artifacts.
# List all artifacts
python scripts/run.py artifact_manager.py list
# List only audio artifacts
python scripts/run.py artifact_manager.py list --type audio
# Get artifact details
python scripts/run.py artifact_manager.py get <artifact-id>
# Delete an artifact
python scripts/run.py artifact_manager.py delete <artifact-id>
# Generate audio podcast (async - returns immediately)
python scripts/run.py artifact_manager.py generate
# Generate with custom options
python scripts/run.py artifact_manager.py generate \
--format DEEP_DIVE \
--length DEFAULT \
--instructions "Focus on the key findings"
# Generate and wait for completion
python scripts/run.py artifact_manager.py generate --wait
# Generate, wait, and download
python scripts/run.py artifact_manager.py generate --wait --output ./podcast.mp3
# Check generation status
python scripts/run.py artifact_manager.py status --task-id <task-id>
# Download latest audio
python scripts/run.py artifact_manager.py download ./output.mp3
# Download specific artifact
python scripts/run.py artifact_manager.py download ./output.mp3 --artifact-id <id>Commands:
list: List all artifacts (filter with--type)get: Get artifact details by IDdelete: Delete an artifactgenerate: Generate audio podcaststatus: Check generation task statusdownload: Download artifact to local file
Generate Options:
--format:DEEP_DIVE(default),BRIEF,CRITIQUE,DEBATE--length:SHORT,DEFAULT,LONG--instructions: Custom instructions for the podcast--wait: Wait for generation to complete--output: Download path (requires--wait)--timeout: Timeout in seconds (default: 600)
run.py
Script wrapper that handles environment setup.
# Usage
python scripts/run.py [script_name].py [arguments]
# Examples
python scripts/run.py auth_manager.py status
python scripts/run.py ask_question.py --question "..."Automatic actions: 1. Creates .venv if missing 2. Installs dependencies 3. Activates environment 4. Executes target script
Python API Usage
Using subprocess with run.py
import subprocess
import json
# Always use run.py wrapper
result = subprocess.run([
"python", "scripts/run.py", "ask_question.py",
"--question", "Your question",
"--notebook-id", "notebook-id"
], capture_output=True, text=True)
answer = result.stdoutDirect imports (after venv exists)
# Only works if venv is already created and activated
from notebook_manager import NotebookLibrary
from auth_manager import AuthManager
library = NotebookLibrary()
notebooks = library.list_notebooks()
auth = AuthManager()
is_auth = auth.is_authenticated()Data Storage
Location: ~/.claude/skills/notebooklm/data/
data/
├── library.json # Notebook metadata
├── auth_info.json # Auth status
└── agent_browser/ # Session metadata
└── session_idSecurity: Protected by .gitignore, never commit.
Environment Variables
Optional .env file configuration is not required for normal usage.
Error Handling
Common patterns:
# Using run.py prevents most errors
result = subprocess.run([
"python", "scripts/run.py", "ask_question.py",
"--question", "Question"
], capture_output=True, text=True)
if result.returncode != 0:
error = result.stderr
if "rate limit" in error.lower():
# Wait or switch accounts
pass
elif "not authenticated" in error.lower():
# Run auth setup
subprocess.run(["python", "scripts/run.py", "auth_manager.py", "setup"])Rate Limits
Free Google accounts: 50 queries/day
Solutions: 1. Wait for reset (midnight PST) 2. Switch accounts with accounts switch 3. Use multiple Google accounts
Advanced Patterns
Parallel Queries
import concurrent.futures
import subprocess
def query(question, notebook_id):
result = subprocess.run([
"python", "scripts/run.py", "ask_question.py",
"--question", question,
"--notebook-id", notebook_id
], capture_output=True, text=True)
return result.stdout
# Run multiple queries simultaneously
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
futures = [
executor.submit(query, q, nb)
for q, nb in zip(questions, notebooks)
]
results = [f.result() for f in futures]Batch Processing
def batch_research(questions, notebook_id):
results = []
for question in questions:
result = subprocess.run([
"python", "scripts/run.py", "ask_question.py",
"--question", question,
"--notebook-id", notebook_id
], capture_output=True, text=True)
results.append(result.stdout)
time.sleep(2) # Avoid rate limits
return resultsModule Classes
NotebookLibrary
add_notebook(url, name, topics)list_notebooks()search_notebooks(query)get_notebook(notebook_id)activate_notebook(notebook_id)remove_notebook(notebook_id)
AuthManager
is_authenticated()setup_auth(headless=False)get_auth_info()clear_auth()validate_auth()
BrowserSession (internal)
- Handles browser automation
- Manages stealth behavior
- Not intended for direct use
Best Practices
1. Always use run.py - Ensures environment 2. Check auth first - Before operations 3. Handle rate limits - Implement retries 4. Include context - Questions are independent 5. Clean sessions - Use cleanup_manager
# nblm Dependencies
# Browser automation now handled by agent-browser (Node.js)
# Environment management
python-dotenv==1.0.0
# Z-Library conversion
ebooklib>=0.18
beautifulsoup4>=4.12.0
lxml>=4.9.0
# PDF text extraction fallback
pypdf>=4.0.0
# NotebookLM API client
notebooklm-py>=0.1.0
# Patchright for Google auth (anti-detection Playwright fork)
patchright>=1.50.0
#!/usr/bin/env python3
"""
nblm Scripts Package
Provides automatic environment management for all scripts
"""
import os
import sys
import subprocess
from pathlib import Path
def ensure_venv_and_run():
"""
Ensure virtual environment exists and run the requested script.
This is called when any script is imported or run directly.
"""
# Only do this if we're not already in the skill's venv
skill_dir = Path(__file__).parent.parent
venv_dir = skill_dir / ".venv"
# Check if we're in a venv
in_venv = hasattr(sys, 'real_prefix') or (
hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
)
# Check if it's OUR venv
if in_venv:
venv_path = Path(sys.prefix)
if venv_path == venv_dir:
# We're already in the correct venv
return
# 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...")
# Create venv
import venv
venv.create(venv_dir, with_pip=True)
# Install requirements
requirements_file = skill_dir / "requirements.txt"
if requirements_file.exists():
if os.name == 'nt': # Windows
pip_exe = venv_dir / "Scripts" / "pip.exe"
else:
pip_exe = venv_dir / "bin" / "pip"
print(" Installing dependencies in isolated environment...")
subprocess.run(
[str(pip_exe), "install", "-q", "-r", str(requirements_file)],
check=True
)
# Install Node.js dependencies if present
package_json = skill_dir / "package.json"
if package_json.exists():
print(" Installing Node.js dependencies...")
subprocess.run(
["npm", "install"],
check=True,
capture_output=True,
cwd=str(skill_dir)
)
subprocess.run(
["npm", "run", "install-browsers"],
check=True,
capture_output=True,
cwd=str(skill_dir)
)
print("✅ 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(" Recommended: Use scripts/run.py to ensure clean execution")
print(" Or activate: source .venv/bin/activate")
# Check environment when module is imported
ensure_venv_and_run()
"""
Configuration for nblm
Centralizes constants, selectors, and paths
"""
from pathlib import Path
from typing import Optional
import os
import re
import tempfile
# Paths
SKILL_DIR = Path(__file__).parent.parent
DATA_DIR = SKILL_DIR / "data"
AUTH_INFO_FILE = DATA_DIR / "auth_info.json"
AUTH_DIR = DATA_DIR / "auth"
GOOGLE_AUTH_FILE = AUTH_DIR / "google.json"
ZLIBRARY_AUTH_FILE = AUTH_DIR / "zlibrary.json"
LIBRARY_FILE = DATA_DIR / "library.json"
# Multi-account Google auth structure
GOOGLE_AUTH_DIR = AUTH_DIR / "google"
GOOGLE_AUTH_INDEX = GOOGLE_AUTH_DIR / "index.json"
# Legacy path (for migration detection)
GOOGLE_AUTH_FILE_LEGACY = AUTH_DIR / "google.json"
def _sanitize_agent_id(agent_id: str) -> str:
"""Sanitize agent ID for use as a filesystem path component.
Removes path traversal characters and other unsafe chars, limits length.
Example: "my/agent/../evil" -> "myagentevil"
"""
# Remove path separators and traversal sequences
sanitized = re.sub(r"[/\\.]", "-", agent_id)
# Remove any other characters that are unsafe in filenames
sanitized = re.sub(r"[^a-zA-Z0-9_\-]", "", sanitized)
# Collapse multiple dashes
sanitized = re.sub(r"-{2,}", "-", sanitized).strip("-")
# Limit length
return sanitized[:64] or "default"
def get_agent_id() -> Optional[str]:
"""Get the current agent ID from environment variables.
Priority: NBLM_AGENT_ID > OPENCLAW_AGENT > AGENT_NAME
Returns None if no agent ID is set.
"""
return (
os.environ.get("NBLM_AGENT_ID")
or os.environ.get("OPENCLAW_AGENT")
or os.environ.get("AGENT_NAME")
or None
)
def get_agent_config_dir() -> Path:
"""Get the per-agent config directory.
Returns DATA_DIR / "agents" / <agent_id> if an agent ID is set,
otherwise returns DATA_DIR.
"""
agent_id = get_agent_id()
if agent_id:
return DATA_DIR / "agents" / _sanitize_agent_id(agent_id)
return DATA_DIR
def get_agent_active_account_file() -> Path:
"""Get the path to the active account file for the current agent."""
return get_agent_config_dir() / "active_account.json"
# Set NOTEBOOKLM_HOME to use our auth directory for notebooklm-py
# This ensures download methods find our storage_state.json
os.environ.setdefault("NOTEBOOKLM_HOME", str(AUTH_DIR))
# Agent-browser configuration
AGENT_BROWSER_PROFILE_DIR = DATA_DIR / "agent_browser" / "profile"
AGENT_BROWSER_SESSION_FILE = DATA_DIR / "agent_browser" / "session_id"
AGENT_BROWSER_SOCKET_DIR = Path(tempfile.gettempdir())
DEFAULT_SESSION_ID = "notebooklm"
AGENT_BROWSER_ACTIVITY_FILE = DATA_DIR / "agent_browser" / "last_activity.json"
AGENT_BROWSER_WATCHDOG_PID_FILE = DATA_DIR / "agent_browser" / "watchdog.pid"
AGENT_BROWSER_STATE_FILE = DATA_DIR / "agent_browser" / "storage_state.json"
AGENT_BROWSER_IDLE_TIMEOUT_SECONDS = int(
os.environ.get("AGENT_BROWSER_IDLE_TIMEOUT_SECONDS", "600")
)
AGENT_BROWSER_WATCHDOG_INTERVAL_SECONDS = int(
os.environ.get("AGENT_BROWSER_WATCHDOG_INTERVAL_SECONDS", "30")
)
# NotebookLM token staleness threshold
NOTEBOOKLM_TOKEN_STALENESS_DAYS = 7
# NotebookLM Selectors
QUERY_INPUT_SELECTORS = [
"textarea.query-box-input", # Primary
'textarea[aria-label="Feld für Anfragen"]', # Fallback German
'textarea[aria-label="Input for queries"]', # Fallback English
]
RESPONSE_SELECTORS = [
".to-user-container .message-text-content", # Primary
"[data-message-author='bot']",
"[data-message-author='assistant']",
]
# Timeouts
LOGIN_TIMEOUT_MINUTES = 10
QUERY_TIMEOUT_SECONDS = 120
PAGE_LOAD_TIMEOUT = 30000
"""Z-Library helpers."""
import tempfile
import unittest
from pathlib import Path
import sys
repo_root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(repo_root / "scripts"))
sys.path.insert(0, str(repo_root))
from scripts.zlibrary import epub_converter
class ZlibraryEpubConverterTests(unittest.TestCase):
def test_count_words_handles_english_and_chinese(self):
text = "Hello 世界"
self.assertEqual(epub_converter.count_words(text), 3)
def test_split_markdown_file_splits_large_chunks(self):
with tempfile.TemporaryDirectory() as tmpdir:
markdown_path = Path(tmpdir) / "book.md"
markdown_path.write_text("# Title\n\nalpha beta gamma\n\ndelta epsilon zeta\n")
chunks = epub_converter.split_markdown_file(markdown_path, max_words=5)
self.assertEqual(len(chunks), 2)
self.assertTrue(chunks[0].exists())
self.assertTrue(chunks[1].exists())
self.assertIn("alpha beta gamma", chunks[0].read_text())
if __name__ == "__main__":
unittest.main()
Related skills
How it compares
Choose nblm when sources live in Google NotebookLM; choose generic RAG skills when documents are stored in local vector databases or Git repos.
FAQ
What does nblm do?
Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced ha
When should I invoke nblm?
Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced ha
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Nblm safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.