## File: README.md

# notebooklm-py

  


**A Comprehensive Google Gemini Notebook Skill & Unofficial Python API.** Full programmatic access to NotebookLM's features—including capabilities the web UI doesn't expose—via Python, CLI, and AI agents like Claude Code, Codex, and OpenClaw.

> **Note (July 2026):** Google rebranded **NotebookLM** to **[Gemini Notebook](https://blog.google/innovation-and-ai/products/gemini-notebook/notebooklm-gemini-notebook/)**. It remains the same standalone product (now also reachable inside the Gemini app), existing links redirect automatically, and this library drives the same underlying service and works unchanged. The package keeps the `notebooklm-py` name.

[](https://pypi.org/project/notebooklm-py/)
[](https://pypi.org/project/notebooklm-py/)
[](https://opensource.org/licenses/MIT)
[](https://github.com/teng-lin/notebooklm-py/actions/workflows/test.yml)

  [](https://trendshift.io/repositories/19116)


**Source & Development**: <https://github.com/teng-lin/notebooklm-py>

> **⚠️ Unofficial Library - Use at Your Own Risk**
>
> This library uses **undocumented Google APIs** that can change without notice.
>
> - **Not affiliated with Google** - This is a community project
> - **APIs may break** - Google can change internal endpoints anytime
> - **Rate limits apply** - Heavy usage may be throttled
>
> Best for prototypes, research, and personal projects. See [Troubleshooting](docs/troubleshooting.md) for debugging tips.

## What You Can Build

🤖 **AI Agent Tools** - Integrate NotebookLM into Claude Code, Codex, and other LLM agents. Ships with a root [NotebookLM skill](SKILL.md) for GitHub and `npx skills add` discovery, local `notebooklm skill install` support for Claude Code and `.agents` skill directories, and repo-level Codex guidance in [`AGENTS.md`](AGENTS.md).

📚 **Research Automation** - Bulk-import sources (URLs, PDFs, YouTube, Google Drive), run web/Drive research queries with auto-import, and extract insights programmatically. Build repeatable research pipelines.

🎙️ **Content Generation** - Generate Audio Overviews (podcasts), videos, slide decks, quizzes, flashcards, infographics, data tables, mind maps, and study guides. Full control over formats, styles, and output.

📥 **Downloads & Export** - Download all generated artifacts locally (MP3, MP4, PDF, PNG, CSV, JSON, Markdown). Export to Google Docs/Sheets. **Features the web UI doesn't offer**: batch downloads, quiz/flashcard export in multiple formats, mind map JSON extraction.

## Use Cases & Recipes

NotebookLM is a **grounded** engine: Gemini does the heavy reading and answers from *your* sources with citations. The winning pattern is to let it do the expensive analysis while your agent (Claude Code, Codex, …) orchestrates and handles the final mile — using NotebookLM as a **zero-token synthesis + memory layer an agent drives in a loop**, and pulling structured artifacts **out** in bulk and in richer, scriptable formats. Recipes people build on top of this library, grouped by what they use NotebookLM *as*:

**Spend fewer tokens** — let NotebookLM do the expensive thinking:

- **🪙 Zero-token research offload** — Throw 30 documents into a notebook, let Gemini do the heavy analysis, and have your agent spend tokens only on the final polish. The agent just orchestrates (`create` → `source add` → `ask`); the reasoning happens server-side. *In the wild: [a four-workflow guide to stop Claude Code burning tokens on NotebookLM](https://x.com/hooeem/status/2042293751805329445).*
- **🧠 Knowledge distillation → a permanent skill** — Run [Deep Research](docs/cli-reference.md#source-add-research) (`source add-research "your topic" --mode deep`) or load a doc corpus, let NotebookLM's Gemini condense it, and bake the result into a `SKILL.md` your agent loads at startup — **build once, reuse with zero runtime tokens or network calls**, git-versioned and immune to UI drift. A packaged domain expert without hand-curating sources. (Dumping raw docs into a skill flattens the hierarchy; NotebookLM condensing first is what makes it work.)
- **✅ Self-validating skills** — Have NotebookLM generate the *eval set* — a quiz straight from your sources — to grade an agent skill against ground truth instead of test questions you'd bias yourself. Build the skill, run it against the NotebookLM-authored evals, iterate to a pass. *In the wild: [a skill that scored 4/10 on the first pass and 10/10 after one iteration, graded by a NotebookLM-generated quiz](https://x.com/nurijanian/status/2037136490157986277).*

**Give your agent memory** — persistent, grounded recall:

- **💾 Persistent cross-session memory** — Keep a "Master Brain" notebook; a wrap-up step appends each session's decisions and fixes as notes (`note create` / `ask --save-as-note`), and a line in your `CLAUDE.md` queries it (`ask`) at the start of the next session. Storage and recall live on Google's infrastructure.
- **🧩 Grounded memory for coding agents** — Expose a notebook of your internal docs/RFCs/architecture over the [MCP server](docs/mcp-guide.md) (or plain `ask`) so an agent answers from *your* code with citations rather than plausible-sounding guesses — a zero-infra alternative to standing up your own vector DB and embedding pipeline. *In the wild: [turning a notebook into the source-grounded "project brain" a coding agent consults before it writes code](https://medium.com/@pradeep00271/every-software-project-needs-a-project-brain-5cbc33917160).*
- **🪞 Query your own notes / journal** — Load years of daily notes, meeting logs, or a journal and `ask` for **cited** answers *across your own history* — surfacing long-term patterns a keyword search can't (e.g. a weekly summary synthesized from 282 daily notes, every claim linked back to the entry it came from). *In the wild: [chatting with a year of daily notes as a cited knowledge base](https://artemxtech.substack.com/p/notebooklm-has-a-knowledge-graph).*

**Turn your sources into answers & artifacts** — cited responses, generated media, and exports:

- **📞 Grounded knowledge base / troubleshooting oracle (RAG)** — Load product docs, FAQs, RFCs, and past tickets, then `ask --json` for **source-grounded, cited** answers for support, on-call, or internal Q&A. Or have an agent point it at an entire fast-moving tool's docs — more than the agent can hold in context — as a **troubleshooting oracle** it queries the moment it hits an error. *In the wild: [OpenClaw drove the library to scrape all 524 pages of `docs.openclaw.ai`, dedupe the duplicate translations, and audit it down to 269 clean sources (missing/extra/duplicate = 0)](https://x.com/onenewbite/status/2024819940327379286).*
- **🔁 Multi-format content repurposing** — One source set, every format: `generate audio` (podcast), `generate video`, `generate slide-deck`, plus a `generate report` blog draft, `generate quiz`, and `generate flashcards` — fan a single notebook out across channels.
- **📤 Bulk, scriptable exports** — Pull mind maps as JSON, flashcards/quizzes as JSON/Markdown/HTML, data tables as CSV, and reports as Markdown — **in bulk, to local files, straight into Anki, your mind-mapping tool, or a repo** (`download <type>` / `download <type> --all`). The programmatic "get data *out*" half of the library, not just "put sources in."
- **🕸️ Obsidian / knowledge-graph sync** — Run the CLI from your vault root so downloaded artifacts (reports, mind-map JSON, transcripts) land as files in your knowledge graph; community skills built on this library even resolve NotebookLM's citation markers into Obsidian `[[wikilinks]]`. Pair with a podcast overview for an audio digest of your notes. *In the wild: ["Claude Code + NotebookLM + Obsidian = GOD MODE"](https://www.youtube.com/watch?v=kU3qYQ7ACMA).*

**Run it unattended, at scale, or on the go** — scheduled, headless, and remote:

- **🚨 Incident runbook generator** — On an alert, spin up a notebook of the relevant docs, ask targeted diagnostic questions, and generate a briefing-doc report (`generate report --format briefing-doc --wait`, then `download report`) as an automated runbook.
- **📚 Curriculum / study-set builder** — Scrape a syllabus or developer roadmap, create one notebook per topic (with deliberate pacing to dodge rate limits), and bulk-generate podcasts, quizzes, and flashcards for each.
- **📰 Scheduled audio briefings** — Pair `auth refresh --quiet` (cron/launchd/systemd) with `generate audio` to publish a fresh personalized briefing to a podcast feed on a schedule.
- **📱 NotebookLM from your phone, agent-driven** — Self-host the [remote MCP connector](docs/mcp-guide.md#remote-deployment-docker--a-tunnel) behind a Cloudflare/Tailscale tunnel and add it as a custom connector **on the web** (claude.ai Connectors, or ChatGPT with Developer Mode). Then drive the full toolset — deep research, source ingestion, studio generation, cited Q&A — from the **claude.ai mobile app** on the go (ChatGPT's MCP connectors are web-only), chained with your other MCP tools, instead of app-hopping.

These combine ordinary library primitives — see the [CLI Reference](docs/cli-reference.md) and [Python API](docs/python-api.md). The agent-side glue (skills, scheduling, vault layout) lives in your own setup, not this package. Per-notebook source counts depend on your Google account tier — split across notebooks if you hit a cap.

**New here?** Start with a walkthrough: [Claude Code + NotebookLM = CHEAT CODE (video)](https://www.youtube.com/watch?v=usTeU4Uh0iM) · [5 demos + 50 use cases, with prompts](https://aiblewmymind.substack.com/p/notebooklm-claude-code-use-cases).

## Ways to Use

| Method | Best For |
|--------|----------|
| **Python API** | Application integration, async workflows, custom pipelines |
| **CLI** | Shell scripts, quick tasks, CI/CD automation |
| **MCP Server** | Claude Desktop/Code, Codex, etc. — locally via stdio, or as a self-hosted remote connector (behind a Cloudflare/Tailscale tunnel) reachable from claude.ai and ChatGPT, mobile included. |
| **REST Server** | Local automation over guarded HTTP routes without spawning a CLI process per call |
| **Agent Integration** | Claude Code, Codex, LLM agents, natural language automation |

## Features

### Complete NotebookLM Coverage

| Category | Capabilities |
|----------|--------------|
| **Notebooks** | Create, list, rename, delete |
| **Sources** | URLs, YouTube, files (PDF, text, Markdown, Word, EPUB, audio, video, images), Google Drive, pasted text; refresh, get guide/fulltext |
| **Chat** | Questions, conversation history, custom personas, suggested starter prompts |
| **Notes** | Create, list, rename, delete, save chat answers, save conversation history |
| **Source Labels** | AI-generated or manual topic labels; add/remove source membership; filter sources by label |
| **Research** | Web and Drive research agents (fast/deep modes) with auto-import |
| **Sharing** | Public/private links, user permissions (viewer/editor), view level control |

### Content Generation (All Artifact Types)

| Type | Options | Download Format |
|------|---------|-----------------|
| **Audio Overview** | 4 formats (deep-dive, brief, critique, debate), 3 lengths, 50+ languages | MP3 |
| **Video Overview** | 4 formats (explainer, brief, cinematic, short), 8 visual styles (+ auto/custom), plus a dedicated `cinematic-video` CLI alias | MP4 |
| **Slide Deck** | Detailed or presenter format, adjustable length; individual slide revision | PDF, PPTX |
| **Infographic** | 3 orientations, 3 detail levels | PNG |
| **Quiz** | Configurable quantity and difficulty | JSON, Markdown, HTML |
| **Flashcards** | Configurable quantity and difficulty | JSON, Markdown, HTML |
| **Report** | Briefing doc, study guide, blog post, or custom prompt | Markdown |
| **Data Table** | Custom structure via natural language | CSV |
| **Mind Map** | Hierarchical node tree — **two kinds**: note-backed JSON or the newer interactive studio map (`--kind` / `MindMapKind`) | JSON |

### Beyond the Web UI

Programmatic, batch, and local-file capabilities the API/CLI make easy — several in richer formats, or at a scale, than clicking through the web app:

- **Batch downloads** - Download all artifacts of a type at once
- **Quiz/Flashcard export** - Get structured JSON, Markdown, or HTML files
- **Mind map data extraction** - Export hierarchical JSON for visualization tools
- **Data table CSV export** - Download structured tables as spreadsheets
- **Slide deck as PPTX or PDF** - Download editable PowerPoint or PDF files
- **Slide revision** - Modify individual slides with natural-language prompts
- **Report template customization** - Append extra instructions to built-in format templates
- **Save chat history to notes** - Persist a whole Q&A conversation (not just a single answer) as a notebook note
- **Source fulltext access** - Retrieve the indexed text content of any source
- **Programmatic sharing** - Manage permissions without the UI

## Installation

The full install guide — six personas (agent, end-user, library, headless, contributor, power-user), optional extras matrix, platform notes — lives in **[docs/installation.md](docs/installation.md)**.

**Quickest start** (CLI users and AI agents) — install the CLI with `uv tool` (recommended) or `pipx`:

```bash
uv tool install "notebooklm-py[browser]"   # or: pipx install "notebooklm-py[browser]"
notebooklm login                           # first run auto-downloads Chromium (~170 MB), then Google sign-in
notebooklm auth check --test --json        # verify: expect "status": "ok"
```

**Why `uv tool` / `pipx`?** They install the CLI into its own isolated environment and put `notebooklm` on your `PATH` — no dependency clashes with other tools, a one-line upgrade (`uv tool upgrade notebooklm-py`) or uninstall, and, crucially, they work on modern macOS (Homebrew Python) and Debian/Ubuntu where a system-wide `pip install` is blocked with `error: externally-managed-environment` ([PEP 668](https://peps.python.org/pep-0668/)). No `uv` yet? `curl -LsSf https://astral.sh/uv/install.sh | sh` (or `brew install uv` / `winget install astral-sh.uv`).

**Prefer plain `pip`?** It works the same **inside a virtualenv** (and directly on Windows, where Python isn't externally-managed):

```bash
python3 -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install "notebooklm-py[browser]"
```

**As a library** (embedded in your app — no Playwright, no Chromium):

```bash
uv add notebooklm-py                    # or, inside a virtualenv: pip install notebooklm-py
```

If `playwright install chromium` fails on Linux with `TypeError: onExit is not a function`, see the [Linux workaround](docs/troubleshooting.md#linux). **Contributors:** see [CONTRIBUTING.md](CONTRIBUTING.md).


### Authentication & Access

Flexible auth for local dev, headless servers, and multi-tenant setups:

- **Three ways to get cookies** - Interactive Playwright login (default), import from an already-signed-in browser (`login --browser-cookies chrome`, no Playwright), or a durable **master token**.
- **Master-token auth** - Mints fresh web cookies **on demand** with no per-session browser (`login --master-token --account you@example.com`), so it self-heals expired sessions unattended — the auth model for servers, CI, and the remote MCP connector (claude.ai / ChatGPT).
- **Multi-account profiles** - Switch between Google accounts without re-authenticating.

### Agent Setup

**Option 1 — CLI install**:

```bash
notebooklm skill install
```

Installs the skill into `~/.claude/skills/notebooklm` and `~/.agents/skills/notebooklm`.

**Option 2 — `npx` install** (via the open skills ecosystem):

```bash
npx skills add teng-lin/notebooklm-py
```

Fetches the canonical [SKILL.md](SKILL.md) directly from GitHub.

## Quick Start



### CLI

```
/* Detailed source-code truncated for AI context efficiency. */
```

Other useful CLI commands:

```bash
notebooklm auth check --test         # Diagnose auth/cookie issues
notebooklm auth refresh --quiet      # One-shot cookie keepalive (for cron / launchd / systemd)
notebooklm auth refresh --browser-cookies chrome  # Re-extract and repair account routing
notebooklm auth inspect --browser 'chrome::Profile 1'  # Preview one Chromium profile
notebooklm agent show codex          # Print bundled Codex instructions
notebooklm agent show claude         # Print bundled Claude Code skill template
notebooklm language list             # List supported output languages
notebooklm metadata --json           # Export notebook metadata and sources
notebooklm share status              # Inspect sharing state
notebooklm source add-research "AI" --import-all  # web research + import found sources
notebooklm skill status              # Check local agent skill installation
notebooklm profile list              # List all Google account profiles
notebooklm profile switch work       # Switch active account profile
```

Use `--prompt-file PATH` with `ask`, prompt-based `generate` commands, and `source add-research` when the text is too long for the shell command line. This reads prompt/query text from a file and is separate from `source add ./file.pdf`, which still uploads that file as a NotebookLM source.

### Python API

```python
import asyncio
from notebooklm import NotebookLMClient, MindMapKind


async def main():
    async with NotebookLMClient.from_storage() as client:
        # Create notebook and add sources
        nb = await client.notebooks.create("Research")
        await client.sources.add_url(nb.id, "https://example.com", wait=True)

        # Chat with your sources
        result = await client.chat.ask(nb.id, "Summarize this")
        print(result.answer)

        # Generate content (podcast, video, quiz, etc.)
        status = await client.artifacts.generate_audio(nb.id, instructions="make it fun")
        await client.artifacts.wait_for_completion(nb.id, status.task_id)
        await client.artifacts.download_audio(nb.id, "podcast.m4a")

        # Generate quiz and download as JSON
        status = await client.artifacts.generate_quiz(nb.id)
        await client.artifacts.wait_for_completion(nb.id, status.task_id)
        await client.artifacts.download_quiz(nb.id, "quiz.json", output_format="json")

        # Generate a mind map via the unified client.mind_maps API (issue #1256) —
        # two kinds: the newer MindMapKind.INTERACTIVE studio map (shown; polled to
        # completion by default) or MindMapKind.NOTE_BACKED JSON. Both export via:
        mm = await client.mind_maps.generate(nb.id, kind=MindMapKind.INTERACTIVE)
        await client.artifacts.download_mind_map(nb.id, "mindmap.json", mm.id)


asyncio.run(main())
```

## Documentation

- **[CLI Reference](docs/cli-reference.md)** - Complete command documentation
- **[Python API](docs/python-api.md)** - Full API reference
- **[MCP Guide](docs/mcp-guide.md)** - MCP server setup, transports, and tool reference
- **[REST API Server](docs/installation.md#rest-api-server)** - Experimental localhost FastAPI server
- **[Configuration](docs/configuration.md)** - Storage and settings
- **[Quota & Tier Limits](docs/quota-limits.md)** - Per-tier notebook/source/studio limits and how they map to `AccountLimits.tier`
- **[Release Guide](docs/releasing.md)** - Release checklist and packaging verification
- **[Troubleshooting](docs/troubleshooting.md)** - Common issues and solutions
- **[API Stability](docs/stability.md)** - Versioning policy and stability guarantees
- **[Upgrading to v0.8.0](docs/upgrading-to-0.8.0.md)** - Breaking-change migration guide for the v0.8.0 error-and-return contract

### For Contributors

- **[Architecture](docs/architecture.md)** - Architectural overview and design principles
- **[Development Guide](docs/development.md)** - Architecture, testing, and releasing
- **[RPC Development](docs/rpc-development.md)** - Protocol capture and debugging
- **[RPC Reference](docs/rpc-reference.md)** - Payload structures
- **[Changelog](CHANGELOG.md)** - Version history and release notes
- **[Security](SECURITY.md)** - Security policy and credential handling

## License

MIT License. See [LICENSE](LICENSE) for details.

---

## File: docs/cli-reference.md

# CLI Reference

**Status:** Active
**Last Updated:** 2026-08-12

Complete command reference for the `notebooklm` CLI—providing full programmatic access to all NotebookLM features, including capabilities not exposed in the web UI.

> **Exit codes:** every command follows the convention documented in [CLI Exit-Code Convention](cli-exit-codes.md) (`0` success, `1` user/app error, `2` system/unexpected, `130` SIGINT). Two commands intentionally deviate for shell control-flow use (`source stale --exit-on-stale` opts into an inverted predicate; `source wait` is three-way); see the doc for details.

## Command Structure

```bash
notebooklm [-p PROFILE] [--storage PATH] [--version] [-v|--quiet] <command> [OPTIONS] [ARGS]
```

### Global Options

- `-p, --profile NAME` - Use a named profile (overrides `NOTEBOOKLM_PROFILE` env var)
- `--storage PATH` - Override the default storage location
- `-v, --verbose` - Increase verbosity (`-v` for INFO, `-vv` for DEBUG)
- `--quiet` - Suppress status output and INFO/WARN log records (only errors survive). Structured `--json` payloads are still emitted. Mutually exclusive with `-v`/`-vv`; combining the two raises `UsageError` (exit `2`).
- `--version` - Show version and exit
- `--help` - Show help message

### Environment Variables

- `NOTEBOOKLM_HOME` - Base directory for all config files (default: `~/.notebooklm`)
- `NOTEBOOKLM_PROFILE` - Active profile name (default: `default`)
- `NOTEBOOKLM_NOTEBOOK` - Default notebook ID for any command exposing `-n/--notebook`. Resolution order: `-n/--notebook` flag > `NOTEBOOKLM_NOTEBOOK` env > active context (`notebooklm use`) > error. Empty/whitespace values are treated as unset.
- `NOTEBOOKLM_AUTH_JSON` - Inline authentication JSON (for CI/CD, no file writes needed)
- `NOTEBOOKLM_HL` - Default output language for artifact generation (overridden by `--language`)
- `NOTEBOOKLM_LOG_LEVEL` - Override the package logger level (e.g. `DEBUG`, `INFO`, `WARNING`, `ERROR`)
- `NOTEBOOKLM_DEBUG_RPC` - Enable RPC debug logging (`1` to enable)
- `NOTEBOOKLM_QUIET_DEPRECATIONS` - Suppress deprecation notices (`1` to enable)

See [Configuration](configuration.md) for full env-var precedence and CI/CD setup.

### Command Organization

- **Session commands** - Authentication and context management
- **Notebook commands** - CRUD operations on notebooks
- **Chat commands** - Querying and conversation management
- **Grouped commands** - `source`, `label`, `collection`, `artifact`, `agent`, `generate`, `download`, `note`, `share`, `research`, `language`, `skill`, `auth`, `profile`, `mcp`
- **Utility commands** - `metadata`, `doctor`

---

## Quick Reference

### Session Commands

| Command | Description | Example |
|---------|-------------|---------|
| `login` | Authenticate via browser | `notebooklm login` / `notebooklm login --browser msedge` |
| `login --master-token` | Headless auth: mint cookies from a durable master token (`[headless]`) | `notebooklm login --master-token --account you@gmail.com` |
| `use ` | Set active notebook | `notebooklm use abc123` |
| `status` | Show current context | `notebooklm status` |
| `status --paths` | Show configuration paths | `notebooklm status --paths` |
| `status --json` | Output status as JSON | `notebooklm status --json` |
| `clear` | Clear current context | `notebooklm clear` |
| `auth check` | Diagnose authentication issues | `notebooklm auth check` |
| `auth check --test` | Validate with network test | `notebooklm auth check --test` |
| `auth check --json` | Output as JSON | `notebooklm auth check --json` |
| `auth inspect` | List Google accounts visible to a browser cookie store (read-only) | `notebooklm auth inspect --browser chrome` |
| `auth import-cookies` | Import auth cookies from a JSON file (or stdin) into the active profile | `notebooklm auth import-cookies cookies.json` |
| `auth logout` | Clear saved cookies and cached browser profile | `notebooklm auth logout` |
| `auth refresh` | One-shot SIDTS rotation poke (for OS schedulers) | `notebooklm auth refresh` |
| `auth refresh --quiet` | Refresh; suppress success output | `notebooklm auth refresh --quiet` |
| `doctor` | Check environment health | `notebooklm doctor` |
| `doctor --fix` | Auto-fix detected issues | `notebooklm doctor --fix` |
| `doctor --json` | Output diagnostics as JSON | `notebooklm doctor --json` |
| `completion <shell>` | Print shell completion script (`bash`/`zsh`/`fish`) | `notebooklm completion zsh > ~/.zfunc/_notebooklm` |

### Profile Commands (`notebooklm profile <cmd>`)

| Command | Description | Example |
|---------|-------------|---------|
| `list` | List all profiles | `notebooklm profile list` |
| `create <name>` | Create a new profile | `notebooklm profile create work` |
| `switch <name>` | Set the active profile | `notebooklm profile switch work` |
| `delete <name>` | Delete a profile | `notebooklm profile delete old` |
| `rename <old> <new>` | Rename a profile | `notebooklm profile rename old new` |

### Language Commands (`notebooklm language <cmd>`)

| Command | Description | Example |
|---------|-------------|---------|
| `list` | List all supported languages | `notebooklm language list` |
| `get` | Show current language setting | `notebooklm language get` |
| `get --local` | Show local config only (skip server sync) | `notebooklm language get --local` |
| `set <code>` | Set language for artifact generation | `notebooklm language set zh_Hans` |
| `set <code> --local` | Set local config only (skip server sync) | `notebooklm language set ja --local` |

**Note:** Language is a **GLOBAL** setting that affects all notebooks in your account.

### Notebook Commands

| Command | Description | Example |
|---------|-------------|---------|
| `list` | List all notebooks | `notebooklm list` |
| `list --json` | Output as JSON | `notebooklm list --json` |
| `list --limit N` | Show at most N notebooks (default: unlimited) | `notebooklm list --limit 10` |
| `list --no-truncate` | Do not truncate the Title column | `notebooklm list --no-truncate` |
| `use ` | Set active notebook (verifies existence by default) | `notebooklm use abc123` |
| `use  --force` | Skip the existence check (offline / debugging) | `notebooklm use abc123 --force` |
| `use  --json` | Emit `{active_notebook_id, success, verified, notebook}` envelope | `notebooklm use abc --json` |
| `create <title>` | Create notebook (does not change active context) | `notebooklm create "Research"` |
| `create <title> --use` | Create notebook and make it the active context | `notebooklm create "Research" --use` |
| `create <title> --json` | JSON envelope; with `--use` includes `active_notebook_id` | `notebooklm create "X" --use --json` |
| `delete -n ` | Delete notebook (uses current notebook if `-n` omitted) | `notebooklm delete -n abc123` |
| `delete -n  -y` | Skip confirmation | `notebooklm delete -n abc123 -y` |
| `delete -n  --json` | Emit `{notebook_id, success}` envelope (plus `context_cleared: true` when deleting the active notebook); requires `-y` (refuses to prompt in JSON mode) | `notebooklm delete -n abc123 -y --json` |
| `rename <title>` | Rename current notebook | `notebooklm rename "New Title"` |
| `summary` | Get AI summary | `notebooklm summary` |
| `summary --topics` | Include suggested topics | `notebooklm summary --topics` |

### Chat Commands

| Command | Description | Example |
|---------|-------------|---------|
| `ask <question>` | Ask a question (auto-resumes the last conversation) | `notebooklm ask "What is this about?"` |
| `ask -` | Read question from stdin (Unix `-` convention) | `echo "what is X?" \| notebooklm ask -` |
| `ask --prompt-file PATH` | Read question from file (use `-` for stdin) | `notebooklm ask --prompt-file q.txt` |
| `ask -c ` | Continue a specific conversation | `notebooklm ask -c conv_abc "..."` |
| `ask --new` | **DESTRUCTIVE.** Permanently delete the notebook's current server-side conversation (turns are not recoverable) and start fresh on the next `ask`. Prompts for confirmation with the conversation's short id; pass `--yes`/`-y` to skip the prompt. `--json` implies `--yes` so scripted callers don't hang on stdin. Closes the workaround documented in [#659](https://github.com/teng-lin/notebooklm-py/issues/659). | `notebooklm ask --new -y "start fresh"` |
| `ask --new --yes` (alias `-y`) | Skip the `--new` destructive-delete confirmation prompt. | `notebooklm ask --new --yes "..."` |
| `ask -s ` | Limit to specific source IDs (repeatable) | `notebooklm ask "Summarize" -s src1 -s src2` |
| `ask --json` | Get answer with source references | `notebooklm ask "Explain X" --json` |
| `ask --request-timeout N` | Per-invocation HTTP request/read timeout in seconds for chat. Defaults to the library chat timeout. `--timeout` is a back-compat alias for the same flag. | `notebooklm ask "long prompt" --request-timeout 120` |
| `ask --save-as-note` | Save response as a note. When the answer contains `[N]` citations, the saved note preserves interactive hover-anchored citation links matching the NotebookLM web UI's "Save to note" behavior ([issue #660](https://github.com/teng-lin/notebooklm-py/issues/660)). Answers without citations fall back to a plain-text note. | `notebooklm ask "Explain X" --save-as-note` |
| `ask --save-as-note --note-title` | Save response with custom note title. The NotebookLM server may apply smart-title generation for citation-rich saves and override the requested title; the success message reflects what the server actually stored. | `notebooklm ask "Explain X" --save-as-note --note-title "Title"` |
| `suggest-prompts` | Get AI-suggested prompts for the notebook (each a title plus a ready-to-send instruction for `ask`) | `notebooklm suggest-prompts` |
| `suggest-prompts --mode N` | Select the suggestion surface (1-10, default 4): 1=audio deep-dive, 2=audio brief, 3=video explainer, 4=chat questions, 5=audio critique, 6=audio debate, 8=quiz, 9=flashcards, 10=video short. Out-of-range exits 1. | `notebooklm suggest-prompts --mode 8` |
| `suggest-prompts --query TEXT` | Free-text steer for the kind of prompts to suggest | `notebooklm suggest-prompts --query "key risks"` |
| `suggest-prompts -s ` | Limit to specific source IDs (repeatable; defaults to all sources) | `notebooklm suggest-prompts -s src1 -s src2` |
| `suggest-prompts --json` | Machine-readable output (`{notebook_id, suggestions, count}`) | `notebooklm suggest-prompts --json` |
| `configure --mode` | Set predefined chat mode (`default`, `learning-guide`, `concise`, `detailed`) | `notebooklm configure --mode learning-guide` |
| `configure --persona` | Set custom persona prompt (up to 10,000 chars) | `notebooklm configure --persona "Act as a tutor"` |
| `configure --response-length` | Response verbosity (`default`, `longer`, `shorter`) | `notebooklm configure --response-length longer` |
| `configure --json` | Machine-readable output | `notebooklm configure --mode concise --json` |
| `history` | View conversation history | `notebooklm history` |
| `history -l N` | Maximum number of Q&A turns to show | `notebooklm history -l 5` |
| `history --json` | Machine-readable output | `notebooklm history --json` |
| `history --clear` | Clear local conversation cache | `notebooklm history --clear` |
| `history --save` | Save history as a note | `notebooklm history --save` |
| `history --save -t "Title"` | Save history with custom title | `notebooklm history --save -t "Summary"` |
| `history --show-all` | Show full Q&A content (not preview) | `notebooklm history --show-all` |
| `history --no-truncate` | Disable the 50-char preview cap on the Question/Answer columns in the table view (the existing `-l/--limit` flag is unchanged: it caps the number of Q&A turns fetched server-side) | `notebooklm history --no-truncate` |

Setting just one of `configure --persona` / `--response-length` **merges** with the current settings — the field you omit is preserved, not reset. A bare `configure` with no flags resets all custom chat settings to their defaults. (A `--mode` preset cannot be combined with `--persona`/`--response-length`.)

### Source Commands (`notebooklm source <cmd>`)

Supported source types: URLs, YouTube videos, files (PDF, text, Markdown, Word, audio, video, images), Google Drive documents, and pasted text.

| Command | Arguments | Options | Example |
|---------|-----------|---------|---------|
| `list` | - | `--json`, `--limit N`, `--no-truncate`, `--label`, `--status` | `source list --limit 20 --no-truncate` |
| `add <content>` | URL/file/text (use `-` for stdin) | `--title`, `--type`, `--timeout`, `--follow-symlinks`, `--allow-internal` (URL sources only), `--json` (file-source `--mime-type` overrides extension inference — see [detailed section](#source-add-mime-type-file-sources)) | `source add "https://..." --timeout 90` |
| `add-drive  <title>` | Drive file ID, title | `--mime-type [google-doc\|google-slides\|google-sheets\|pdf]`, `--json` | `source add-drive abc123 "Doc" --mime-type google-slides` |
| `add-drive-file ` | Drive file ID or share URL | `--title`, `--wait`, `--json` | `source add-drive-file abc123 --title "Notes" --wait` |
| `add-research [query]` | Search query (or `--prompt-file -` for stdin) | `--mode [fast\|deep]`, `--from [web\|drive]`, `--import-all`, `--cited-only`, `--no-wait`, `--timeout`, `--prompt-file PATH` | `source add-research "AI" --mode deep --no-wait` |
| `get ` | Source ID | `--json` | `source get src123` |
| `fulltext ` | Source ID | `--json`, `-o FILE`, `--force`, `--no-clobber`, `-f [text\|markdown]` | `source fulltext src123 -f markdown -o out.md` (`-f markdown` requires the `markdown` extra: `pip install "notebooklm-py[markdown]"` — full extras matrix: [docs/installation.md#optional-extras-matrix](installation.md#optional-extras-matrix)) |
| `guide ` | Source ID | `--json` | `source guide src123` |
| `stale ` | Source ID | `--exit-on-stale`, `--json` | `source stale src123` (exit 0 on success; pass `--exit-on-stale` for the back-compat inverted predicate — see [exit codes](cli-exit-codes.md)) |
| `wait ` | Source ID | `--timeout`, `--interval`, `--json` | `source wait src123 --timeout 300 --interval 5` |
| `clean` | - | `--dry-run`, `-y/--yes`, `--json` | `source clean --dry-run` |
| `rename  <title>` | Source ID, new title | `--json` | `source rename src123 "New Name"` |
| `refresh ` | Source ID | `--json` | `source refresh src123` |
| `delete ` | Source ID | `-y/--yes`, `--json` | `source delete src123 -y` |
| `delete-by-title <title>` | Exact source title | `-y/--yes`, `--json` | `source delete-by-title "My Source"` |

All `source` subcommands also accept `-n/--notebook ID` (resolves via flag > `NOTEBOOKLM_NOTEBOOK` env > active context).

`source delete ` accepts only full source IDs or unique partial-ID prefixes. To delete by exact source title, use `source delete-by-title "<title>"`.

`source clean` automatically removes duplicate, error, and access-blocked sources; combine with `--dry-run` to preview the candidate set first.

`source fulltext -o FILE` auto-renames an existing output path by default (`FILE` -> `FILE (2)`, etc.) instead of overwriting it. Pass `--force` to overwrite intentionally, or `--no-clobber` to fail if the path exists.

`source stale` reports whether a URL/Drive source needs a refresh. By default it follows the standard CLI exit convention (`0` on success, `1` on error); branch on the JSON `stale`/`fresh` fields (or stdout text) for the freshness verdict. Pass `--exit-on-stale` to opt into the back-compat inverted predicate (`0` = stale, `1` = fresh) for shell idioms like `if notebooklm source stale --exit-on-stale ID; then refresh; fi`.

`source list` also accepts `--label ` to list only the sources in a given label (a saved selection). The selector resolves a label id (or partial prefix) **or** an exact label name; see [Label Commands](#label-commands-notebooklm-label-cmd).

`source list --status <state>` restricts the listing to one ingestion status — `ready`, `processing`, `error`, `preparing`, or `unknown`. The choices are derived from `SourceStatus`, so they cannot drift from the labels the Status column renders, and the filter is applied inside the fetch, so `count` in `--json` always matches the rows shown. It composes with `--label`.

#### Finding orphaned sources (`--status preparing`)

A file add that fails *after* its source row is registered leaves that row in place deliberately — it is the evidence of what happened, and it still counts against the notebook's source quota. The row sits at `preparing`, **not** `error`, so the status a caller reaches for first finds nothing ([#2138](https://github.com/teng-lin/notebooklm-py/issues/2138)):

```bash
notebooklm source list --status preparing        # the reconciliation query
notebooklm source delete                     # once you have confirmed it is stuck
```

Rows that are genuinely mid-upload also report `preparing`, so this filter cannot by itself tell "abandoned" from "in flight" — re-run it a minute apart and act only on rows that persist. Nothing is deleted automatically; that posture is deliberate (see [#2110](https://github.com/teng-lin/notebooklm-py/issues/2110)).

When the failing add raised in your own process, you do not need to search at all: the exception carries the retained row's id directly, via `getattr(exc, "source_id", None)`.

#### Drive-backed sources in `source list` / `source get`

Both commands emit the same `--json` source row:

```json
{
  "id": "ef72c03c-…", "title": "Rubisco Research", "type": "google_docs", "url": null,
  "status": "ready", "status_id": 2, "created_at": "2026-01-23T18:42:00",
  "drive_document_id": "1oAk_INJ…", "drive_status": "deleted", "is_drive_degraded": true
}
```

(`source list` prefixes each row with a 1-based `index`.)

- **`drive_document_id`** — the Google Drive file id. A Drive source carries no `url`, so this is the only field tying it back to the file it was created from; `source add-drive` matches on it to stay idempotent.
- **`drive_status`** — Drive-side health: `inaccessible` / `syncing` / `active` / `deleted` / `gen_ai_access_denied`, or `unknown` for a code this client cannot map. **`null` means the row made no Drive-health claim at all**, which is a different answer from `"unknown"`.
- **`is_drive_degraded`** — `true` only when the backend explicitly reported a non-healthy Drive state. `false` means "nothing degraded was reported" — equally true for a non-Drive source, for `active`, and for an unreadable code — not "the file is confirmed present".

All three keys are present on **every** row (null/false on non-Drive sources), so `jq '.sources[] | select(.drive_status == "deleted")'` needs no `// empty` guard.

**Which rows are Drive-backed?** `drive_document_id` and `drive_status` are decoded from structurally independent parts of the response, so **either one being non-null** means the row is Drive-backed — neither alone is the authoritative test. In particular, the common case is an id with **no** health claim (`{"drive_document_id": "1oAk…", "drive_status": null}`), so filtering on `drive_status != null` will miss most Drive sources.

Unlike the MCP/REST surfaces, the CLI ships **no raw Drive status code** beside the label. It would carry no extra information (an unmappable code is replaced by a client-side sentinel before it reaches output), and its code space collides adversarially with `status_id`'s — `2`/`3` mean `ready`/`error` for ingestion but `syncing`/`active` for Drive, so a consumer reasoning by analogy would select exactly the wrong rows.

### `status` and `drive_status` are different axes

`status` reports NotebookLM's own ingestion, which completes and **stays** complete after the Drive file is deleted or unshared. A source therefore reads `"status": "ready"` while `"drive_status": "deleted"` — answers grounded on it may be stale.

Human (non-`--json`) output reflects this without adding a column:

- **`source list`** appends the Drive verdict to the Status cell — `ready (drive: deleted)` — but only for a row the backend reports as degraded (`is_drive_degraded`), whatever that row's ingestion status is. An unreadable code (`unknown`) is not flagged here; it is not evidence of degradation, and the table should not cry wolf on protocol drift. Note that Rich sizes columns table-wide, so a single annotated row does re-flow the whole table; that cost is only paid when something is actually wrong.
- **`source get`** adds `Drive File ID:` and `Drive Status:` lines, each shown only when that field is present.

> **Cross-surface naming.** MCP and REST spell this axis `drive_status` (raw code) + `drive_status_label` (string); the CLI uses `drive_status` for the **string**, matching its own long-standing `status` (label) / `status_id` (code) pairing. So `select(.drive_status == "deleted")` is right for the CLI and wrong for MCP/REST, where that key holds an integer. Both surfaces resolve the label through the same mapping helper, so the vocabulary never diverges — only the key name.

### Label Commands (`notebooklm label <cmd>`)

Source labels group a notebook's sources into topic buckets. A `` argument accepts a label id (or partial prefix) **or** an exact label name; an ambiguous name lists the matching ids so you can disambiguate.

| Command | Arguments | Options | Example |
|---------|-----------|---------|---------|
| `list` | - | `--json` | `label list -n nb123` |
| `sources ` | Label id or name | `--json` | `label sources Papers` |
| `generate` | - | `--scope [all\|unlabeled]`, `-y/--yes`, `--json` | `label generate --scope all -y` |
| `create <name>` | Label name | `--emoji 📄`, `--json` | `label create "Papers" --emoji 📄` |
| `rename  <new_name>` | Label ref, new name | `--json` | `label rename Papers "Research Papers"` |
| `emoji  ` | Label ref, emoji | `--json` | `label emoji Papers 📚` |
| `add  ...` | Label ref, one+ source ids | `--json` | `label add Papers src123 src456` |
| `remove  ...` | Label ref, one+ source ids | `--json` | `label remove Papers src123` |
| `delete ...` | One+ label refs | `-y/--yes`, `--json` | `label delete Papers -y` |

All `label` subcommands accept `-n/--notebook ID` (resolves via flag > `NOTEBOOKLM_NOTEBOOK` env > active context).

`label generate --scope all` (wipes and regenerates every label with new ids) and `label delete` are destructive and require `-y/--yes` to confirm (or an interactive prompt). `label generate --scope unlabeled` (the default) only labels currently-unlabeled sources and needs no confirmation. `label add` appends sources (existing members survive; labels may overlap) and `label remove` un-assigns sources from the label only — the sources stay in the notebook (and in any other label). `label delete` removes the label only — its sources become unlabeled, not deleted. `source_id` arguments to `label add`/`label remove` accept partial-prefix matching like every other source-id command.

### Collection Commands (`notebooklm collection <cmd>`)

Collections group whole **notebooks** into named, account-level buckets (playlist-style) — the sibling of `label`, which groups sources *within* a notebook. A notebook can belong to multiple collections. A `` argument accepts a collection id (or partial prefix) **or** an exact collection name; an ambiguous name lists the matching ids so you can disambiguate.

| Command | Arguments | Options | Example |
|---------|-----------|---------|---------|
| `list` | - | `--json` | `collection list` |
| `notebooks ` | Collection id or name | `--json` | `collection notebooks "Research Q3"` |
| `create <name>` | Collection name | `--json` | `collection create "Research Q3"` |
| `rename  <new_name>` | Collection ref, new name | `--json` | `collection rename "Research Q3" "Research Q4"` |
| `add  <notebook_id>...` | Collection ref, one+ notebook ids | `--json` | `collection add "Research Q3" nb123 nb456` |
| `remove  <notebook_id>...` | Collection ref, one+ notebook ids | `--json` | `collection remove "Research Q3" nb123` |
| `delete ...` | One+ collection refs | `-y/--yes`, `--json` | `collection delete "Research Q3" -y` |

Collections are account-level, so — unlike `label` — the `collection` commands take **no** `-n/--notebook` option; their membership arguments *are* notebooks. `collection add` appends notebooks (existing members survive; a notebook may belong to multiple collections) and `collection remove` un-assigns a notebook from that collection only — the notebook is not deleted and stays in any other collection. `collection delete` (destructive, requires `-y/--yes`) removes the collection only, never its member notebooks. `notebook_id` arguments to `collection add`/`collection remove` accept partial-prefix matching like every other notebook-id command.

### Research Commands (`notebooklm research <cmd>`)

| Command | Arguments | Options | Example |
|---------|-----------|---------|---------|
| `status` | - | `-n/--notebook`, `--json` | `research status` |
| `wait` | - | `-n/--notebook`, `--timeout`, `--interval`, `--import-all`, `--cited-only`, `--json` | `research wait --import-all --cited-only` |
| `import` | - | `-n/--notebook`, `--run-id`, `--cited-only`, `--timeout`, `--max-sources`, `--allow-duplicate`, `--json` | `research import` |
| `cancel` | `RUN_ID` | `-n/--notebook`, `--json` | `research cancel <run_id>` |

### Generate Commands (`notebooklm generate <type>`)

All generate commands (except the synchronous `mind-map`) accept the same uniform polling-flag surface:
- `-n, --notebook ID` to target a specific notebook (also resolved from `NOTEBOOKLM_NOTEBOOK` / active context)
- `-s, --source ID` to select specific sources (repeatable)
- `--json` for machine-readable output (returns `task_id` and `status`)
- `--wait / --no-wait` to block until completion (default: `--no-wait` — returns immediately with a `task_id`)
- `--timeout SECONDS` to cap the `--wait` budget (defaults: 1200s for audio, 1800s for video, 3600s for cinematic-video, and 300s for quiz/flashcards/slide-deck/infographic/data-table/report/revise-slide). No-op without `--wait`.
- `--interval SECONDS` to tune polling cadence (default: 2). No-op without `--wait`.
- `--retry N` to automatically retry on rate limits with exponential backoff
- `--prompt-file PATH` to read the description/query from a file (or `-` for stdin) instead of the command line. Mutually exclusive with the positional argument; useful for long prompts that exceed shell length limits.

Language-aware generate commands (`audio`, `video`, `cinematic-video`, `report`, `infographic`, `slide-deck`, `data-table`, `mind-map`) also support:
- `--language LANG` to override output language (precedence: `--language` > `NOTEBOOKLM_HL` env > config > `'en'`)

`quiz`, `flashcards`, and `revise-slide` do not accept `--language`.

| Command | Arguments | Type-specific options | Example |
|---------|-----------|-----------------------|---------|
| `audio [description]` | Instructions | `--format [deep-dive\|brief\|critique\|debate]`, `--length [short\|default\|long]` | `generate audio "Focus on history"` |
| `video [description]` | Instructions | `--format [explainer\|brief\|cinematic\|short]`, `--style [auto\|custom\|classic\|whiteboard\|kawaii\|anime\|watercolor\|retro-print\|heritage\|paper-craft]` (not for `--format cinematic`/`short`), `--style-prompt TEXT` (required with `--style custom`; rejected with `--format cinematic`/`short`) | `generate video "Explainer for kids"` |
| `cinematic-video [description]` | Instructions | Alias for `video --format cinematic` | `generate cinematic-video "Documentary about quantum physics"` |
| `slide-deck [description]` | Instructions | `--format [detailed\|presenter]`, `--length [default\|short]` | `generate slide-deck` |
| `revise-slide <description>` | Revision instructions | `-a/--artifact ` (required), `--slide N` (required) | `generate revise-slide "Move title up" --artifact  --slide 0` |
| `quiz [description]` | Instructions | `--difficulty [easy\|medium\|hard]`, `--quantity [fewer\|standard\|more]` | `generate quiz --difficulty hard` |
| `flashcards [description]` | Instructions | `--difficulty [easy\|medium\|hard]`, `--quantity [fewer\|standard\|more]` | `generate flashcards` |
| `infographic [description]` | Instructions | `--orientation [landscape\|portrait\|square]`, `--detail [concise\|standard\|detailed]`, `--style [auto\|sketch-note\|professional\|bento-grid\|editorial\|instructional\|bricks\|clay\|anime\|kawaii\|scientific]` | `generate infographic` |
| `data-table <description>` | Instructions | (uniform options only) | `generate data-table "compare concepts"` |
| `mind-map` | - | `--kind [interactive\|note-backed]`, `--instructions TEXT` *(no `--wait` / `--timeout` / `--interval` / `--retry` / `--prompt-file`)* | `generate mind-map --kind interactive` |
| `report [description]` | Instructions | `--format [briefing-doc\|study-guide\|blog-post\|custom]`, `--append TEXT` (no effect with `--format custom`) | `generate report --format study-guide` |

> **Two kinds of mind map (issue #1256).** NotebookLM has two distinct mind-map objects. `generate mind-map --kind interactive` (the default) builds the **interactive** kind — a studio artifact (the one the web app now creates) that is polled to completion. `generate mind-map --kind note-backed` builds the **note-backed** kind — a JSON node tree stored as a note, generated synchronously. Both produce the same `{mind_map, note_id, kind}` output, appear in `artifact list --type mind-map`, and download via `download mind-map`. `--instructions` is a free-text prompt that steers generation: the interactive kind applies it reliably (it travels in the `CREATE_ARTIFACT` prompt slot the server honors and that `artifact get-prompt` reads back); the note-backed kind passes it through to `GENERATE_MIND_MAP`, but the server may not always act on it.

### Artifact Commands (`notebooklm artifact <cmd>`)

| Command | Arguments | Options | Example |
|---------|-----------|---------|---------|
| `list` | - | `--type [all\|audio\|video\|slide-deck\|quiz\|flashcard\|infographic\|data-table\|mind-map\|report\|fantasy-map\|file]`, `--limit N`, `--no-truncate`, `--json` | `artifact list --type audio --limit 5` |
| `get ` | Artifact ID | `--json` | `artifact get art123` |
| `get-prompt ` | Artifact ID | `--json` | `artifact get-prompt art123` |
| `rename  <title>` | Artifact ID, title | `--json` | `artifact rename art123 "Title"` |
| `delete ` | Artifact ID | `-y/--yes`, `--json` | `artifact delete art123 -y` |
| `export ` | Artifact ID | `--title TEXT` (required), `--type [docs\|sheets]`, `--json` | `artifact export art123 --title "My Doc" --type sheets` |
| `poll <task_id>` | Task ID (from `generate <type>`) | `--json` | `artifact poll task123` |
| `wait ` | Artifact ID (from `artifact list`) | `--timeout` (default: 300), `--interval` (default: 2), `--json` | `artifact wait art123 --timeout 600` |
| `retry ` | Artifact ID (from `artifact list`) | `--wait`, `--timeout` (default: 300), `--interval` (default: 2), `--json` | `artifact retry art123 --wait` |
| `suggestions` | - | `--json` | `artifact suggestions` |

All `artifact` subcommands also accept `-n/--notebook ID`.

> **Note:** `artifact delete` on a Mind Map clears its content rather than removing the artifact entry — Mind Maps are stored alongside notes and Google may garbage-collect cleared entries later. The CLI prints a `Cleared mind map:` message instead of `Deleted artifact:` when this happens.

> **Common confusion: `poll` vs `wait` ID kind.** Both commands accept the *same identifier* — the API returns one ID that serves as both the generation `task_id` (during creation) and the `artifact_id` (once the row appears in `artifact list`). The split is operational, not semantic:
>
> - **`artifact poll <task_id>`** — single non-blocking status check. Pass the raw `task_id` returned by `notebooklm generate <type>` straight through. `poll` does **not** prefix-match against `artifact list`, so it works *immediately* after a `generate` call returns, before the artifact has appeared in any list response.
> - **`artifact wait <artifact_id>`** — blocks (with exponential backoff) until the artifact is `completed`, `failed`, or `--timeout` elapses. Accepts a full UUID **or a unique prefix** that resolves against `artifact list` via the standard partial-ID resolver.
>
> Rule of thumb: **just generated something? use `poll`.** **Found it in `artifact list`? use `wait`.** You can pass the same string to both — the kind label is about lifecycle stage, not about a different identifier format.

> **`artifact retry <artifact_id>`** re-runs generation for a *failed* artifact in place (the web UI "Retry" button) — the artifact is not deleted first and the same ID is preserved. Accepts a full UUID or a unique prefix. By default it kicks off the retry and returns immediately (`Retry started: `); pass `--wait` to block until the retried generation reaches a terminal state. A synchronous refusal (rate limit / quota / not-retryable) exits non-zero with a typed error rather than reporting a started task. A retry may itself fail again provider-side, in which case you can run `artifact retry` again against the same ID.

### Download Commands (`notebooklm download <type>`)

Every `download` subcommand accepts the same selection / safety / output flag set: `-n/--notebook ID`, `-a/--artifact ID`, `--all`, `--latest` (default), `--earliest`, `--name TEXT` (fuzzy title match), `--dry-run`, `--force`, `--no-clobber` (don't overwrite existing files: a single download fails, `--all` skips them; default is auto-rename), and `--json`.

| Command | Arguments | Type-specific options | Example |
|---------|-----------|-----------------------|---------|
| `audio [path]` | Output path | (none) | `download audio --all` |
| `video [path]` | Output path | (none) | `download video --latest` |
| `cinematic-video [path]` | Output path | Alias for `download video`; cinematic and standard videos share the same artifact type | `download cinematic-video ./documentary.mp4` |
| `slide-deck [path]` | Output path | `--format [pdf\|pptx]` (default: pdf) | `download slide-deck ./slides.pdf` |
| `infographic [path]` | Output path | (none) | `download infographic ./info.png` |
| `report [path]` | Output path | (none) | `download report ./report.md` |
| `mind-map [path]` | Output path | (none) | `download mind-map ./map.json` |
| `data-table [path]` | Output path | (none) | `download data-table ./data.csv` |
| `quiz [path]` | Output path | `--format [json\|markdown\|html]` (default: json) | `download quiz --format markdown quiz.md` |
| `flashcards [path]` | Output path | `--format [json\|markdown\|html]` (default: json) | `download flashcards cards.json` |



### Note Commands (`notebooklm note <cmd>`)

| Command | Arguments | Options | Example |
|---------|-----------|---------|---------|
| `list` | - | `--json` | `note list --json` |
| `create [content]` | Note content (or `-` for stdin) | `--content TEXT` (or `-` for stdin; mutex with positional), `-t/--title TEXT`, `--json` | `cat notes.md \| note create -` |
| `get ` | Note ID | `--json` | `note get note123` |
| `save ` | Note ID | `--title`, `--content`, `--json` | `note save note123 --title "Updated title"` |
| `rename  <title>` | Note ID, title | `--json` | `note rename note123 "Title"` |
| `delete ` | Note ID | `-y/--yes`, `--json` | `note delete note123 -y` |

All `note` subcommands also accept `-n/--notebook ID`.

> **`source get` / `artifact get` / `note get` exit `1` on not-found (BREAKING).** All three `get` commands now exit `1` when the requested ID does not resolve to an existing item, matching the rest of the CLI's user-error convention. Under `--json` the failure body is the standard typed error envelope (`{"error": true, "code": "NOT_FOUND", "message": "...", "id": "...", "notebook_id": "..."}`); without `--json` the message is written to stderr. The previous behavior was exit `0` with a "not found" line on stdout. The pre-existing "no partial-ID match" branch (raised by `_resolve_partial_id` as a `ClickException`) was already exit `1` and is unchanged. See [CLI Exit-Code Convention](cli-exit-codes.md#get-on-not-found-exits-1-was-0-landed) for migration guidance.

### Metadata Command

Export notebook metadata and a simplified source list.

```bash
notebooklm metadata [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Specify notebook (uses current if not set)
- `--json` - Output as JSON for scripts

**Examples:**
```bash
notebooklm metadata
notebooklm metadata -n abc123 --json
```

### Skill Commands (`notebooklm skill <cmd>`)

Manage NotebookLM agent skill integration.

| Command | Description | Example |
|---------|-------------|---------|
| `install` | Install/update the skill for `claude`, `.agents`, or both | `skill install --target all` |
| `status` | Check installed targets and version info | `skill status --scope project` |
| `uninstall` | Remove one or more installed targets | `skill uninstall --target agents` |
| `show` | Display the packaged skill or an installed target | `skill show --target source` |
| `package` | Build a Claude-uploadable skill archive (chat/Cowork) | `skill package -o dist/` |

Defaults:

- `skill install` uses `--scope user --target all`
- `claude` maps to `.claude/skills/notebooklm/SKILL.md`
- `agents` maps to `.agents/skills/notebooklm/SKILL.md`
- `show --target source` prints the canonical packaged skill file
- Project-scope installs support `--dry-run`, `--no-clobber`, and `--force`; these flags are rejected for user-scope installs.

The packaged wheel includes the repo-root `SKILL.md`, so the same skill content powers `notebooklm skill install`, GitHub discovery, and `npx skills add teng-lin/notebooklm-py`.

Codex does not use the `skill` subcommand. In this repository it reads the root [`AGENTS.md`](../AGENTS.md) file and invokes the `notebooklm` CLI or Python API directly.

### MCP Commands (`notebooklm mcp <cmd>`)

Install the NotebookLM MCP server block into supported MCP client configs.

| Command | Description | Example |
|---------|-------------|---------|
| `install <client>` | Configure `claude-desktop`, `claude-code`, `cursor`, or `windsurf` | `mcp install claude-desktop` |
| `install <client> --config-path PATH` | Write a non-standard config path | `mcp install cursor --config-path ./mcp.json` |

The installer writes a `notebooklm` server entry that launches `notebooklm-mcp`
through `uvx`. Re-running is idempotent and preserves unrelated servers in the
same config file. See [MCP server guide](mcp-guide.md).

### Agent Commands (`notebooklm agent <cmd>`)

Show bundled instructions for supported agent environments.

| Command | Description | Example |
|---------|-------------|---------|
| `show codex` | Print the Codex repository guidance | `agent show codex` |
| `show claude` | Print the bundled Claude Code skill template | `agent show claude` |

`agent show codex` prefers the root [`AGENTS.md`](../AGENTS.md) file when running from a source checkout, so the CLI mirrors the same instructions Codex sees in the repository.

### Features Beyond the Web UI

These CLI capabilities are not available in NotebookLM's web interface:

| Feature | Command | Description |
|---------|---------|-------------|
| **Batch downloads** | `download <type> --all` | Download all artifacts of a type at once |
| **Quiz/Flashcard export** | `download quiz --format json` | Export as JSON, Markdown, or HTML |
| **Mind map extraction** | `download mind-map` | Export hierarchical JSON for visualization tools |
| **Data table export** | `download data-table` | Download structured tables as CSV |
| **Slide deck as PPTX** | `download slide-deck --format pptx` | Download as editable .pptx (web UI only offers PDF) |
| **Slide revision** | `generate revise-slide "prompt" --artifact  --slide N` | Modify individual slides with a natural-language prompt |
| **Report template append** | `generate report --format study-guide --append "..."` | Append instructions to built-in templates |
| **Source fulltext** | `source fulltext ` | Retrieve the indexed text content of any source |
| **Save chat to note** | `ask "..." --save-as-note` / `history --save` | Save Q&A answers or full conversation as notebook notes |
| **Programmatic sharing** | `share` commands | Manage permissions without the UI |

---

## Detailed Command Reference

### Authentication: `login`

Authenticate with Google NotebookLM via browser.

> **Python equivalent:** load saved credentials with [`NotebookLMClient.from_storage(...)`](python-api.md#authentication) and use it as an async context manager. The CLI's interactive browser-login flow has no Python counterpart — run `notebooklm login` once to seed `storage_state.json`, then drive the API from Python.

```bash
notebooklm login [OPTIONS]
```

By default, opens a Chromium browser with a persistent profile. Complete the Google login in the browser window — the CLI detects the redirect back to either personal host (`notebook.google.com` or `notebooklm.google.com`) and saves the session automatically (no terminal keystroke required). The wait window is 5 minutes by default; if login is not detected before then, the command exits with a retry hint. Use `--browser-timeout` to extend that human-interaction window, `--browser msedge` for Microsoft Edge, or `--browser-cookies ` to import cookies from an already-logged-in browser without launching Playwright.

**Options:**
- `--storage PATH` - Where to save storage_state.json (default: `$NOTEBOOKLM_HOME/profiles//storage_state.json`)
- `--browser [chromium|msedge|chrome]` - Browser to use for login (default: `chromium`). Use `chrome` for system Google Chrome (workaround when bundled Chromium crashes, e.g. macOS 15+); use `msedge` for Microsoft Edge. **Note:** only `chromium` is auto-installed by the CLI on first login (~170 MB Chromium download); `--browser msedge` and `--browser chrome` require the corresponding browser to be already installed on your system.
- `--browser-timeout SECONDS` - Human-interaction window for ordinary headed login and browser-assisted master-token bootstrap (default: `300`).
- `--browser-cookies <auto|chrome|edge|firefox|safari|brave|arc|...>` - Read cookies from an installed browser instead of launching Playwright. Pass an explicit browser name, or `auto` to let rookiepy auto-detect. For Chromium-family user profiles, use `chrome::` (for example `chrome::Profile 1` or `brave::Work`) to extract from one profile explicitly. For Firefox Multi-Account Containers, use `firefox::<container-name>` to extract from a single container, or `firefox::none` for the no-container default — unscoped `firefox` merges every container's cookies (and emits a warning when that's happening). Requires `pip install "notebooklm-py[cookies]"` (full extras matrix: [docs/installation.md#optional-extras-matrix](installation.md#optional-extras-matrix)).
- `--account EMAIL` - Pick a signed-in Google account by email when several are present in the browser. Saves to the active profile by default; use `--profile-name` for a separate named profile or `--storage` for an exact path. Only valid with `--browser-cookies`.
- `--all-accounts` - Extract every Google account signed in to the browser into separate profiles named from each account email. Only valid with `--browser-cookies`.
- `--update` - With `--all-accounts`: when an account's natural profile name (e.g. `alice` for `alice@gmail.com`) already exists but has no account metadata, update that profile in place instead of creating a suffixed `alice-2`. Profiles that already bind a different email still get a suffix to avoid clobbering. Only valid with `--all-accounts`.
- `--profile-name NAME` - Write a targeted `--account` import to this named profile instead of the active profile. Only valid with `--browser-cookies`.
- `--fresh` - Start with a clean browser session (deletes the cached browser profile). Use to switch Google accounts. With explicit `--storage`, a pre-existing browser sidecar is deleted only when it carries NotebookLM's ownership marker or is the canonical legacy/named-profile layout; arbitrary unowned directories are refused. Has no effect with `--browser-cookies`.
- `--include-domains LABEL[,LABEL...]` - Opt in to extracting sibling-product cookies (default: required Google auth/Drive cookies only). Supported labels: `youtube`, `docs`, `myaccount`, `mail`, `all`. Pass labels comma-separated or repeat the flag.

**Master-token (headless) options** — mint/refresh web cookies from a durable Google master token, no per-session browser. Requires `pip install "notebooklm-py[headless]"`. Full guide: [installation.md#d-headless-server-or-ci](installation.md#d-headless-server-or-ci).
- `--master-token` - Bootstrap headless auth. Requires `--account EMAIL`. A visible browser opens Google's EmbeddedSetup to capture the single-use `oauth_token` (needs `[browser]`), or pass it with `--oauth-token`. Exchanges it for a durable master token (saved `0600` at `master_token.json`), mints cookies into `storage_state.json`, and verifies by listing notebooks.
- `--master-token-refresh` - Legacy forced re-mint; prefer `notebooklm auth refresh`. It still unconditionally replaces `storage_state.json` from the stored master token and preserves CLI context.
- `--oauth-token VALUE` - Provide the single-use EmbeddedSetup `oauth_token` manually (headless boxes without `[browser]`).
- `--cdp-url URL` - Capture `oauth_token` by attaching to a running Chrome over CDP (e.g. `http://localhost:9222`) instead of launching a browser.
- `--android-id HEX` - Override the per-install Android id (default: generated and persisted with the token). ⚠️ The master token is a **full-account, durable** credential — use a dedicated/throwaway account only.
- `--force` - With `--master-token`, overwrite even if the target profile already holds a session for a **different** account. Without it, a mismatched `--account` is refused (use a dedicated `-p ` instead) so account B's mint can't silently clobber account A's profile.

**Examples:**
```bash
# Default (Chromium)
notebooklm login

# Use Microsoft Edge (for orgs that require Edge for SSO)
notebooklm login --browser msedge

# Reuse cookies from your already-logged-in Chrome session
notebooklm login --browser-cookies chrome
notebooklm login --browser-cookies 'chrome::Profile 1'  # one Chromium profile

# Auto-detect any supported browser via rookiepy
notebooklm login --browser-cookies auto

# Firefox Multi-Account Containers: target one container
notebooklm login --browser-cookies 'firefox::Work'
notebooklm login --browser-cookies 'firefox::none'  # no-container default

# Populate a named profile via cookie import
notebooklm --profile work login --browser-cookies chrome

# Pick a specific browser account by email
notebooklm login --browser-cookies chrome --account alice@example.com

# Extract every signed-in browser account into separate profiles
notebooklm login --browser-cookies chrome --all-accounts

# Force a clean browser session before logging in
notebooklm login --fresh

# Headless master-token auth (one browser sign-in, then no per-session browser)
notebooklm login --master-token --account you@gmail.com
notebooklm login --master-token --account you@gmail.com --oauth-token "$OAUTH_TOKEN"  # headless box
notebooklm login --master-token-refresh   # legacy: force a re-mint unconditionally
```

**Notes on `--browser-cookies`:**
- Honors `--profile` / `NOTEBOOKLM_PROFILE` and writes to that profile's `storage_state.json`.
- Without `--account` or `--all-accounts`, imports the selected browser/profile's default Google account into the target profile.
- For Chromium-family browsers, unscoped `chrome`, `brave`, `edge`, etc. fan out across populated user profiles when account selection is needed. Use `chrome::` to read exactly one profile; directory names such as `Default` and `Profile 1` are stable across UI renames.
- Use `notebooklm auth inspect --browser ` to see available account emails before a targeted import; pass `-v` to show the Chromium profile directory each account came from.

### Authentication: `auth import-cookies`

Import authentication cookies from a JSON file (or stdin) and persist them to the active profile's `storage_state.json` — a file-backed, persistent alternative to the env-var-based `NOTEBOOKLM_AUTH_JSON` for users who can obtain cookies as JSON but find the browser/Playwright login flow difficult.

> **⚠️ These are full-account credentials.** A JSON cookie export grants the same access as being logged in. Only import a file you exported yourself, keep it private, and delete it after import. Be especially cautious with third-party browser cookie-export extensions.

```bash
notebooklm auth import-cookies JSON_PATH [OPTIONS]
```

Accepts either a Playwright `storage_state` object (`{"cookies": [...]}`) or a bare JSON list of cookie objects (the shape most browser cookie-export tools produce). Use `-` to read JSON from stdin. Common export fields are normalized (e.g. `expirationDate` → `expires`), `__Secure-`/`__Host-` cookies are forced `Secure`, and any `storage_state` `origins` (localStorage/sessionStorage) are dropped.

Imported cookies are filtered through the **same domain allowlist** used by browser login and validated locally for the Tier 1 NotebookLM-required cookies. The required `__Secure-1PSIDTS` entry must also be live and RFC 6265-routable to the `accounts.google.com/RotateCookies` endpoint. If any secondary-binding cookie is present, the supplied set must form a usable binding — `OSID`, or `APISID`+`SAPISID` together with bare `LSID`; a completely absent Tier 2 set is preserved after a warning for compatibility with unablated account flows. The result is written atomically with private (`0o600`) permissions. Invalid input never overwrites an existing session, and an existing `storage_state.json` is first copied to `storage_state.json.bak` so a stale import can be rolled back (one step — each run overwrites the previous `.bak`).

Incompatible with `NOTEBOOKLM_AUTH_JSON`: unset that env var first, or the command exits with an error (it does not silently fall back to the env auth).

**Options:**
- `--include-domains LABEL` (repeatable) - Opt in to persisting sibling-product cookies (default: required Google auth/Drive/NotebookLM domains only). Pass labels comma-separated or repeat the flag (e.g. `--include-domains youtube,docs` or `--include-domains youtube --include-domains docs`). Same labels as `login`: `youtube`, `docs`, `myaccount`, `mail`, `all`.
- `--include-optional` - Persist all optional sibling-product cookie domains.
- `--json` - Emit a JSON result (`storage_path`, `cookie_count`, `backup_path`).
- `--quiet` - Suppress success output.

**Examples:**
```bash
# Import a bare cookie list exported by a browser tool
notebooklm auth import-cookies cookies.json

# Import a Playwright storage_state into a named profile
notebooklm -p work auth import-cookies playwright-storage-state.json

# Pipe JSON from stdin
cat cookies.json | notebooklm auth import-cookies -
```

### Session: `use`

Set the active notebook for subsequent commands.

```bash
notebooklm use [OPTIONS] <notebook_id>
```

By default, the notebook must exist on the server; a typo or unreachable backend results in a non-zero exit and the saved context is left untouched. Pass `--force` to bypass verification (offline / debugging).

Supports partial ID matching:
```bash
notebooklm use abc  # Matches abc123def456...
```

**Options:**
- `--force` - Skip the existence check and persist the notebook ID even if verification fails.
- `--json` - Emit `{"active_notebook_id": "", "success": true, "verified": true|false, "notebook": {...}}`. `verified: false` covers the `--force` path.

When credentials are expired, `use` routes through the canonical "Not logged in" handler instead of a generic "Could not verify" message; under `--json` you get the standard `{"error": true, "code": "AUTH_REQUIRED", ...}` envelope.

### Session: `status`

Show current context (active notebook and conversation).

```bash
notebooklm status [OPTIONS]
```

**Options:**
- `--paths` - Show resolved configuration file paths
- `--json` - Output as JSON (useful for scripts)

**Examples:**
```bash
# Basic status
notebooklm status

# Show where config files are located
notebooklm status --paths
# Output shows home_dir, storage_path, context_path, browser_profile_dir

# JSON output for scripts
notebooklm status --json
```

**With `--paths`:**
```
                Configuration Paths
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ File            ┃ Path                         ┃ Source          ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ Home Directory  │ /home/user/.notebooklm      │ default         │
│ Storage State   │ .../storage_state.json      │                 │
│ Context         │ .../context.json            │                 │
│ Browser Profile │ .../browser_profile         │                 │
└─────────────────┴──────────────────────────────┴─────────────────┘
```

### Language: `list`, `get`, `set`

Manage the output language for artifact generation (audio, video, etc.).

**Important:** Language is a **GLOBAL** setting that affects all notebooks in your account.

```bash
# List all supported languages with native names
notebooklm language list

# Show current language setting (syncs from server)
notebooklm language get

# Set language to Simplified Chinese
notebooklm language set zh_Hans

# Set language to Japanese
notebooklm language set ja
```

**Options for `get`:**
- `--local` - Skip server sync, show local config only
- `--json` - Output as JSON

**Options for `set`:**
- `--local` - Save to local config only, skip server sync
- `--json` - Output as JSON

**Common language codes:**
| Code | Language |
|------|----------|
| `en` | English |
| `zh_Hans` | 中文（简体） - Simplified Chinese |
| `zh_Hant` | 中文（繁體） - Traditional Chinese |
| `ja` | 日本語 - Japanese |
| `ko` | 한국어 - Korean |
| `es` | Español - Spanish |
| `fr` | Français - French |
| `de` | Deutsch - German |
| `pt_BR` | Português (Brasil) - Brazilian Portuguese |

Run `notebooklm language list` for all 80+ supported languages.

### Share: `status`, `public`, `view-level`, `add`, `update`, `remove`

Manage notebook sharing settings and user permissions.

```bash
# Show current sharing status and shared users
notebooklm share status

# Enable public link sharing (anyone with link can view)
notebooklm share public --enable

# Disable public sharing
notebooklm share public --disable

# Set what viewers can access
notebooklm share view-level full   # Full notebook (chat, sources, notes)
notebooklm share view-level chat   # Chat interface only

# Share with specific users
notebooklm share add user@example.com                        # Add as viewer (default)
notebooklm share add user@example.com --permission editor    # Add as editor
notebooklm share add user@example.com -m "Check this out!"   # With message
notebooklm share add user@example.com --no-notify            # Skip email notification

# Update user permission
notebooklm share update user@example.com --permission editor

# Remove user access
notebooklm share remove user@example.com
notebooklm share remove user@example.com -y   # Skip confirmation
```

**Options (all commands):**
- `-n, --notebook ID` - Specify notebook (uses current if not set, supports partial IDs)
- `--json` - Output as JSON

**Permission levels:**
| Level | Access |
|-------|--------|
| `viewer` | Read-only access (default) |
| `editor` | Can edit notebook content |

**View levels:**
| Level | Viewers can see |
|-------|-----------------|
| `full` | Chat, sources, and notes |
| `chat` | Chat interface only |

### Label: `list`, `sources`, `generate`, `create`, `rename`, `emoji`, `add`, `remove`, `delete`

Manage source labels — AI-generated (or manually named) topic groupings of a notebook's sources.

```bash
# List labels (with member ids + resolved source titles)
notebooklm label list

# Expand a label to its source objects
notebooklm label sources Papers

# AI-group sources into topic labels (the UI's "Reorganize")
notebooklm label generate                       # --scope unlabeled (default, safe)
notebooklm label generate --scope all -y        # destructive: wipe + regenerate every label

# Create an empty, manually-named label
notebooklm label create "Papers" --emoji 📄

# Rename a label (preserves its emoji) / set its emoji
notebooklm label rename Papers "Research Papers"
notebooklm label emoji Papers 📚

# Add source(s) to a label (append; existing members preserved)
notebooklm label add Papers src123 src456

# Remove source(s) from a label (un-assign only; source stays in the notebook)
notebooklm label remove Papers src123

# Delete one or more labels (the label only, not its sources)
notebooklm label delete Papers -y
```

**Options (all commands):**
- `-n, --notebook ID` - Specify notebook (uses current if not set, supports partial IDs)
- `--json` - Output as JSON

**Confirmation gates:**
- `label generate --scope all` wipes and regenerates **every** label with new ids — requires `-y/--yes` (or an interactive prompt).
- `label delete` requires `-y/--yes` (or an interactive prompt).

**Name-or-ID resolution:** `` arguments accept a label id (or partial prefix) or an exact label name. An ambiguous name lists the matching ids (with emoji + source count) so you can re-run with the id.

**Selecting sources by label:** `notebooklm source list --label ` lists only the sources in a label — a read-only saved-selection filter.

### Authentication: `auth check`

Diagnose authentication issues by validating storage file, cookies, and optionally testing token fetch.

```bash
notebooklm auth check [OPTIONS]
```

**Options:**
- `--test` - Also test token fetch from NotebookLM (makes network request)
- `--passive` - With `--test`, validate read-only: never run `NOTEBOOKLM_REFRESH_CMD`, rotate cookies, or write to disk. Use for unattended readiness/health checks that must not mutate state or race real work. No effect without `--test`.
- `--json` - Output as JSON (useful for scripts)

**Examples:**
```bash
# Quick local validation
notebooklm auth check

# Full validation with network test
notebooklm auth check --test

# Read-only readiness probe (no refresh cmd, no cookie write) for a health check
notebooklm auth check --test --passive

# JSON output for automation
notebooklm auth check --json
```

**Checks performed:**
1. Storage file exists and is readable
2. JSON structure is valid
3. Required cookies (`SID` + `__Secure-1PSIDTS`) are present (the Tier 1 `MINIMUM_REQUIRED_COOKIES` set). The loader also evaluates the warning-only secondary-binding check — `OSID`, or `APISID`+`SAPISID` together with bare `LSID` — but `auth check` does not mark a Tier 2 warning as failure; see [auth-cookie-lifecycle.md](auth-cookie-lifecycle.md#33-empirical-cookie-requirements)
4. Cookie domains are correct (.google.com vs regional)
5. (With `--test`) Token fetch succeeds

**Output shows:**
- Authentication source (file path or environment variable)
- Which cookies were found and from which domains
- Detailed cookie breakdown by domain (highlighting key auth cookies)
- Token lengths when using `--test`

**Use cases:**
- Debug "Not logged in" errors
- Verify auth setup in CI/CD environments
- Check if cookies are from correct domain (regional vs .google.com)
- Diagnose NOTEBOOKLM_AUTH_JSON environment variable issues

**Exit codes:** `auth check` exits `0` only when every *executed* check passes
and non-zero (`1`) when any executed check fails — in **both** text and `--json`
modes. A skipped check does not count as a failure: without `--test`, the token
fetch is skipped, so the exit status reflects only the local cookie checks.
Unattended monitors should therefore rely on the exit code (not on parsing the
table). To gate readiness on a real token fetch, run `notebooklm auth check
--test` and check the exit status. See [CLI Exit-Code Convention](cli-exit-codes.md).

### Authentication: `auth refresh`

One-shot keepalive and recovery: open a file-backed session, trigger the layer-1 SIDTS rotation poke against `accounts.google.com`, persist rotated cookies, and exit. If storage is absent but the exact sibling `master_token.json` exists, it mints the initial file and passively validates it once; `--verify` reuses that result. Existing cookies that redirect to Google sign-in automatically try a sibling master-token re-mint; `--allow-headless` permits layer-3 browser recovery first. Healthy storage stays on the cheap path. When a Playwright storage state lacks in-band `notebooklm.account` metadata, `auth refresh` also repairs it if account discovery is unambiguous. It does not replace existing metadata; use `login --browser-cookies  --account EMAIL` to re-bind a profile that already points at the wrong account. Designed for direct use or OS scheduling (launchd / systemd / cron / Task Scheduler / k8s CronJob).

```bash
notebooklm auth refresh [OPTIONS]
```

**Options:**
- `--browser-cookies `, `--browser-cookie ` - Re-extract cookies from an installed browser and match the current profile's account (the unified in-band `storage_state.json` record — a pre-v0.5.0 profile's account, if it's still only in the legacy sibling `context.json`, is promoted in-band automatically on read). This repairs account routing when browser account order changes after another account logs out. Accepts the same scoped syntax as `login`: `chrome::` for one Chromium profile, and `firefox::<container-name>` or `firefox::none` for one Firefox container.
- `--include-domains LABEL[,LABEL...]` - Forward to the browser-cookie reader (only meaningful with `--browser-cookies`). Same syntax as `notebooklm login --include-domains`.
- `--quiet`, `-q` - Suppress success output; print only on error (cron-friendly)
- `--verify` - After refreshing, run a read-only passive token fetch to confirm the resulting cookies actually authenticate; exit non-zero if they still fail. A missing-storage master-token bootstrap always performs this validation and `--verify` reuses its result. Especially valuable with `--browser-cookies`, which rewrites the cookie jar but does not otherwise verify it.
- `--allow-headless` - Permit layer-3 browser recovery for this invocation when stored cookies are fully expired. Uses the storage-bound persisted browser profile or loopback `NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL`, and does not launch or attach unless the ordinary refresh fails. Incompatible with `--browser-cookies`.
- `--json` - Emit one structured success or error document. On success the keys are `status`, `storage_path`, and `verified`.

**Cadence:** 15-20 minutes is the recommended interval. Tighter is wasteful (the 60 s mtime guard would skip it anyway); significantly looser may cross the `__Secure-1PSIDTS` server-side validity window for your account/region.

**Requires file-backed authentication.** `auth refresh` refuses `NOTEBOOKLM_AUTH_JSON` because inline JSON has no writable backing store. A root `--storage PATH` is an explicit file-backed override and wins over that environment variable for the invocation.

**Exit codes:**
- `0` - the auth path completed without raising. The rotation POST is **best-effort**: exit 0 also covers (a) the 60 s mtime guard skipping the POST, (b) `NOTEBOOKLM_DISABLE_KEEPALIVE_POKE=1` being set, (c) another process holding the cross-process rotate lock, and (d) a transient `httpx` error during the POST being caught and logged at DEBUG. Treat exit 0 as "no error" rather than "rotation occurred." For verification, enable `NOTEBOOKLM_LOG_LEVEL=DEBUG` and check for the `RotateCookies` log line.
- `1` - a typed command failure occurred (for example, an incompatible option, a failed master-token mint, or a failed post-refresh passive token fetch). The OS scheduler's next firing is the retry mechanism; this command does not retry in-process.
- `2` - an unexpected error occurred. Missing storage without a sibling token and malformed existing storage retain this existing contract; neither bootstraps.

**Examples:**
```bash
# One-shot refresh against the default profile
notebooklm auth refresh

# Refresh a named profile (works with --profile / NOTEBOOKLM_PROFILE)
notebooklm --profile work auth refresh

# Permit browser-backed L3 only if ordinary recovery reaches a login redirect
notebooklm --profile work auth refresh --allow-headless

# Re-extract from Chrome and repair account routing if browser account order changed
notebooklm --profile work auth refresh --browser-cookies chrome
notebooklm --profile work auth refresh --browser-cookies 'chrome::Profile 1'

# Quiet variant for cron / systemd
notebooklm --profile work auth refresh --quiet
```

**Pairs with:**
- `NOTEBOOKLM_REFRESH_CMD` for in-process auth-expiry recovery (covers the case where `auth refresh` itself fails because cookies have already expired beyond rotation)
- `keepalive=<seconds>` on `NotebookLMClient` for in-process long-lived workers (no OS scheduler needed)

See [Troubleshooting](troubleshooting.md) for full per-OS scheduler recipes (launchd plist, systemd user timer, cron, Task Scheduler, k8s CronJob).

### Authentication: `auth inspect`

List Google accounts visible to a browser's cookie store. **Read-only — never writes to disk.** Use this before `notebooklm login --browser-cookies  --account ` to see which account emails are available. Chromium-family browsers fan out across populated user profiles by default; use `chrome::` to inspect only one profile.

```bash
notebooklm auth inspect [OPTIONS]
```

**Options:**
- `--browser TEXT` - Browser to read cookies from (`chrome`, `firefox`, `brave`, `edge`, `safari`, `arc`, ...). `auto` picks the first one rookiepy can read. Use `chrome::` for one Chromium profile, or `firefox::<container-name>` / `firefox::none` for one Firefox container. Requires `pip install "notebooklm-py[cookies]"`.
- `--include-domains LABEL[,LABEL...]` - Opt in to enumerating accounts via sibling-product cookies (same syntax as `notebooklm login --include-domains`). By default this command consults only required Google auth cookies, which is sufficient for account discovery on every tested path.
- `--json` - Output as JSON

**Examples:**
```bash
notebooklm auth inspect --browser chrome
notebooklm auth inspect --browser 'chrome::Profile 1' --json
notebooklm auth inspect --browser firefox --json
```

### Authentication: `auth logout`

Log out by clearing saved authentication. Removes both the saved cookie file (`storage_state.json`) and the cached browser profile. With explicit `--storage`, an arbitrary unowned browser sidecar is left intact; canonical managed profile layouts are still removed. After logout, run `notebooklm login` to authenticate with a different Google account.

```bash
notebooklm auth logout
```

**Examples:**
```bash
notebooklm auth logout                        # Clear auth for active profile
notebooklm -p work auth logout                # Clear auth for 'work' profile
notebooklm --storage A.json auth logout       # Clear the override auth file
```

### Session: `completion`

Print the shell completion script for `bash`, `zsh`, or `fish`. Pipe the output into a file your shell sources at startup; once the script is loaded, Click handles the `_NOTEBOOKLM_COMPLETE` env-var protocol automatically.

```bash
notebooklm completion 
```

**Install (one-time):**

```bash
# zsh — drop the script anywhere on $fpath; the file MUST be named _notebooklm
notebooklm completion zsh > ~/.zfunc/_notebooklm
# (ensure ~/.zfunc is on $fpath in ~/.zshrc, then `autoload -Uz compinit && compinit`)

# bash — source from ~/.bashrc
notebooklm completion bash > ~/.notebooklm-complete.bash
echo 'source ~/.notebooklm-complete.bash' >> ~/.bashrc

# fish — drop into the system completions directory
notebooklm completion fish > ~/.config/fish/completions/notebooklm.fish
```

**ID-aware tab completion:** Once the script is installed, `-n/--notebook`, `-s/--source`, and `-a/--artifact` complete from the active profile's live IDs:

```bash
notebooklm ask -n <TAB>            # lists notebook IDs (filtered by what you've typed)
notebooklm ask -s <TAB>            # lists sources in the active notebook
notebooklm download audio -a <TAB> # lists artifacts in the active notebook
```

Note: commands that take a positional source / artifact / notebook ID (e.g. `source delete `, `artifact get `, `use `) currently complete only after the option / argument is fully typed — Click's `shell_complete=` is attached to the flag-style `-n/--notebook`, `-s/--source`, `-a/--artifact` declarations enumerated below.

For `-s` and `-a` the active notebook is resolved with the same precedence the command body uses: `-n/--notebook` flag value already on the line > `NOTEBOOKLM_NOTEBOOK` env var > the persisted `notebooklm use` context. With no resolvable notebook (and on any auth / network failure), the completer returns no suggestions silently — it never prints a traceback into your terminal.

**Print-only by design:** the command never writes to your shell config; you decide where the script lands. This keeps the install path discoverable and avoids surprising shutdowns of existing completion setups.

### Source: `add` — how an argument is classified

With no `--type`, `source add` decides in this order:

1. **URL-shaped** (contains `://`) → a `url` or `youtube` source.
2. **The path exists on disk** → a `file` source, uploaded. This is an existence
   check, not an extension check, so a real file is uploaded whatever it is
   named — `deck.pptx`, `./deck.pptx` and `notes` all work.
3. **Otherwise** → a `text` source, ingesting the argument itself as content.

Step 3 is where a typo bites: `source add dekc.pptx` adds a source whose entire
content is the string `dekc.pptx`. To make that visible, an argument that *looks*
like a file but does not exist is added with a warning:

```console
$ notebooklm source add dekc.pptx
warning: 'dekc.pptx' looks like a path but does not exist; ingesting as inline
text. Pass --type text to suppress this warning, or check the path for typos.
```

An argument looks like a file when it contains a slash, or when its extension is
one the upload accepts — `.pdf` `.txt` `.md` `.markdown` `.doc` `.docx` `.pptx`
`.rtf` `.odt` `.csv` `.tsv` `.epub` — or is HTML-family (`.html` `.htm` `.xhtml`
`.xht`, which the upload endpoint rejects with convert-first guidance), or is
`.ppt` (file-shaped, but legacy PowerPoint has never been proven uploadable, so
it earns the warning without being routed to the uploader). That list is derived
from the single upload-support declaration the Drive download-and-upload router
also reads, so a newly supported file type earns its warning at the same time it
becomes uploadable, rather than drifting behind it (#2202).

Pass `--type text` to opt out of detection entirely, or `--type file` to require
the upload path (which then fails loudly on a missing file instead of falling
back to text).

### Source: `add` — `--follow-symlinks` security gate

File-source uploads reject symlinks by default. If the path you pass (or any ancestor directory) is a symbolic link, `source add` refuses the upload rather than silently following it — a workspace symlink could otherwise exfiltrate the file it points at (e.g. `~/Downloads/foo.pdf -> /etc/passwd`). Pass `--follow-symlinks` to opt in explicitly.

URL sources reject internal hosts (`localhost`, loopback, private IP ranges,
and link-local addresses) by default so the CLI cannot be used as an SSRF
trampoline. Pass `--allow-internal` only for a deliberate local NotebookLM test;
non-HTTP(S) schemes remain rejected even with the flag.

> **Python equivalent:** [`client.sources.add_file(nb_id, path, title=...)`](python-api.md#sourcesapi-clientsources). The symlink gate is a CLI-only safeguard; callers using the Python API are responsible for resolving symbolic links before passing the path.

```bash
# Default — symlink rejected with: "Path is a symlink; pass --follow-symlinks to follow it explicitly."
notebooklm source add ./link-to-doc.pdf --type file

# Opt-in: resolve the symlink, then upload the resolved target
notebooklm source add ./link-to-doc.pdf --type file --follow-symlinks
```

The same gate applies on the explicit `--type file` path (no auto-detect), so typing the source type as `file` does not bypass the check.

<a id="source-add-mime-type-file-sources"></a>

### Source: `add` `--mime-type` (file sources)

The `--mime-type` flag on `notebooklm source add` (file-source path) is a
**supported** parameter. It overrides the filename-extension inference and
sets the content-type header used for the resumable upload handshake. Omit it
to let the MIME type be inferred from the file extension.

```bash
# Inferred from the extension (typical case — no flag needed)
notebooklm source add ./report.pdf --type file

# Override inference (e.g. an extensionless export, or a misnamed file)
notebooklm source add ./report.bin --type file --mime-type application/pdf
```

> **Note:** The same `--mime-type` flag on `notebooklm source add-drive`
> (Google Drive sources) selects between `google-doc` / `google-slides` /
> `google-sheets` / `pdf` Drive document types.
>
> The `<title>` you pass to `source add-drive` is **ignored** by NotebookLM for
> native Drive imports — the backend re-derives the display title from live Drive
> metadata, so the source keeps its Drive name regardless. Run
> `notebooklm source rename  "<title>"` after the add if you need a specific
> title.
>
> Historical: an earlier release treated the file-source `--mime-type` as a
> deprecated no-op. It was re-wired to set the upload content-type and is no
> longer deprecated; the `NOTEBOOKLM_QUIET_DEPRECATIONS` notice it used to
> emit is gone.

### Source: `add-research`

Perform AI-powered research and add discovered sources to the notebook.

> **Python equivalent:** [`client.research.start(nb_id, query, source="web", mode="deep")`](python-api.md#researchapi-clientresearch) followed by `client.research.wait_for_completion(...)` / `client.research.import_sources(...)`.

```bash
notebooklm source add-research [query] [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--mode [fast|deep]` - Research depth (default: fast)
- `--from [web|drive]` - Search source (default: web)
- `--import-all` - Automatically import all found sources (works with blocking mode)
- `--cited-only` - With `--import-all`, import only cited sources
- `--no-wait` - Start research and return immediately (non-blocking)
- `--timeout SECONDS` - Per-phase seconds budget for (a) the research-completion poll loop and (b) the `--import-all` retry loop (default: 1800). Each phase gets the full budget independently, so worst-case total wall time is up to 2× this value. Matches `research wait --timeout` semantics. Before 0.4.2 the in-line poll was hardcoded to 5 minutes, so deep research that ran longer was silently abandoned and left an "Add sources?" modal hanging in the NotebookLM web UI — bump `--timeout` for long deep-research runs.
- `--prompt-file PATH` - Read query from a file (or `-` for stdin) instead of the positional argument

> **Note:** `--mode deep` is only supported with `--from web` (the default). Combining `--mode deep --from drive` is rejected by the backend with `ValidationError("Deep Research only supports Web sources.")` — for Drive, stick with `--mode fast`.

**Examples:**
```bash
# Fast web research (blocking)
notebooklm source add-research "Quantum computing basics"

# Drive research (fast mode only — Drive does not support --mode deep)
notebooklm source add-research "Project Alpha" --from drive

# Non-blocking deep research for agent workflows (web only — see note above)
notebooklm source add-research "AI safety papers" --mode deep --no-wait

# Import only cited sources for tighter relevance
notebooklm source add-research "topic" --import-all --cited-only

# Bounded import-retry budget for large result sets
notebooklm source add-research "AI papers" --mode deep --import-all --timeout 3600

# Read long query from file (or stdin via '-')
notebooklm source add-research --prompt-file research_query.txt --mode deep
echo "very long query..." | notebooklm source add-research --prompt-file -
```

### Research: `status`

Check research status for the current notebook (non-blocking).

> **Python equivalent:** [`client.research.poll(nb_id)`](python-api.md#researchapi-clientresearch).

```bash
notebooklm research status [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--json` - Output as JSON

**Output states:**
- **No research running** - No active research session
- **Research in progress** - Deep research is still running
- **Research completed** - Shows query, found sources, and summary

**Examples:**
```bash
# Check status
notebooklm research status

# JSON output for scripts/agents
notebooklm research status --json
```

### Research: `wait`

Wait for research to complete (blocking).

> **Python equivalent:** [`client.research.wait_for_completion(nb_id)`](python-api.md#researchapi-clientresearch), then optionally call `client.research.import_sources(...)` (matches the CLI's `--import-all` / `--cited-only` behavior).

```bash
notebooklm research wait [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--timeout SECONDS` - Per-phase budget (default: 1800, matching `source add-research`). Deep runs regularly exceed the former 300s default — 374s live, 358s in the `research_deep_poll_long` cassette; fast runs settle in seconds, so the cap only ever binds on deep. With `--import-all` the poll loop and the import-retry loop each get the full budget independently, so worst-case wall time is up to 2× this value.
- `--interval SECONDS` - Seconds between status checks (default: 5)
- `--import-all` - Import all found sources when done
- `--cited-only` - With `--import-all`, import only cited sources
- `--json` - Output as JSON

**Examples:**
```bash
# Basic wait
notebooklm research wait

# Wait longer for deep research
notebooklm research wait --timeout 600

# Wait and auto-import sources
notebooklm research wait --import-all

# Import only cited sources
notebooklm research wait --import-all --cited-only

# JSON output for agent workflows
notebooklm research wait --json --import-all
```

**Use case:** Primarily for LLM agents that need to wait for non-blocking deep research started with `source add-research --no-wait`.

### Research: `import`

Import a completed research run's sources — without blocking.

> **Python equivalent:** [`client.research.import_sources_with_verification(nb_id, run_id, sources)`](python-api.md#researchapi-clientresearch), after polling the run to `completed` yourself.

```bash
notebooklm research import [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--run-id ID` - Run to import. Omit it and the notebook's single research run is used; when a
  notebook has **more than one** run this errors rather than guessing which you meant, so pass
  the id (`research status` shows it)
- `--cited-only` - Import only report-cited sources (all of them, if no citation resolves — `cited_only_fallback` says which happened)
- `--timeout SECONDS` - Seconds budget for the import retry loop (default: 1800)
- `--max-sources N` - Import at most N sources (applied *after* `--cited-only` narrows)
- `--allow-duplicate` - Re-add sources whose URL is already in the notebook
- `--json` - Output as JSON

**Never waits for the run.** This is the counterpart to `research wait --import-all`: if the run is still in progress (or failed, or found nothing), the command exits 1 with an explanation instead of polling. The import RPC itself is not instant — `IMPORT_RESEARCH` commonly outlives a single client timeout on deep payloads and is retried with reconciliation, bounded by `--timeout` (default 1800s, same vocabulary as `research wait --timeout`). That makes the fully composable flow expressible for the first time — you own the cadence:

```bash
notebooklm source add-research "AI safety" --mode deep --no-wait   # returns immediately
notebooklm research status                                         # your loop, your interval
notebooklm research import                                         # imports, returns
```

**Idempotent — with two documented limits.** A source whose URL is already in the notebook is reported as already-present rather than duplicated, so a repeat import reads as "0 new, N already present" instead of looking like a no-op. Under `--json` that split is `imported` / `imported_sources` versus `already_present` / `already_present_sources`, and `status` is `already_imported` when nothing new landed.

The dedupe is by URL against a snapshot taken just before the import, so: a deep run's **report row has no URL** and is re-imported on every run, and if the snapshot call itself fails the filter is **skipped entirely** (the import still proceeds). Both are properties of `import_sources_with_verification`, shared with `research wait --import-all` and the MCP tool. If you interrupt an import, check `source list` before re-running it rather than assuming the re-run is a no-op.

**Examples:**
```bash
# Import the notebook's current completed run
notebooklm research import

# Pin a specific run and take only the cited sources
notebooklm research import --run-id <run_id> --cited-only

# Cap the import, JSON output for agent workflows
notebooklm research import --max-sources 10 --json
```

### Research: `cancel`

Cancel an in-flight research run.

> **Python equivalent:** [`client.research.cancel(nb_id, run_id)`](python-api.md#researchapi-clientresearch).

```bash
notebooklm research cancel RUN_ID [OPTIONS]
```

**Arguments:**
- `RUN_ID` - The run's poll-level id — the `task_id` shown by `research status`. For **deep** research this is the `report_id` returned by `source add-research`, **not** the deep start `task_id` (which is a sessionId and will not cancel anything).

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--json` - Output as JSON

**Examples:**
```bash
# Cancel a run (find the run id with `research status`)
notebooklm research cancel <run_id>

# JSON output for agent workflows
notebooklm research cancel <run_id> --json
```

> **Fire-and-forget:** the server reports neither success nor failure for a cancel and does not validate the run id, so this command cannot confirm the cancel took effect. Run `research status` afterward — a cancelled in-progress run shows as `failed`.

### Generate: `audio`

Generate an audio overview (podcast).

> **Python equivalent:** [`client.artifacts.generate_audio(nb_id, ...)`](python-api.md#generation-methods).

```bash
notebooklm generate audio [description] [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--format [deep-dive|brief|critique|debate]` - Podcast format (default: deep-dive)
- `--length [short|default|long]` - Duration (default: default)
- `--language LANG` - Output language (precedence: `--language` > `NOTEBOOKLM_HL` env > config > `'en'`)
- `-s, --source ID` - Limit to specific source IDs (repeatable, uses all if not specified)
- `--wait / --no-wait` - Wait for completion (default: `--no-wait`)
- `--timeout SECONDS` - Maximum seconds to wait (default: 1200; no-op without `--wait`)
- `--interval SECONDS` - Seconds between status checks (default: 2; no-op without `--wait`)
- `--retry N` - Retry N times with exponential backoff on rate limit
- `--json` - Output as JSON (returns `task_id` and `status`)
- `--prompt-file PATH` - Read description from a file (or `-` for stdin); mutually exclusive with positional argument

**Examples:**
```bash
# Basic podcast (starts async, returns immediately)
notebooklm generate audio

# Debate format with custom instructions
notebooklm generate audio "Compare the two main viewpoints" --format debate

# Generate and wait for completion
notebooklm generate audio "Focus on key points" --wait

# Generate using only specific sources
notebooklm generate audio -s src_abc -s src_def

# JSON output for scripting/automation
notebooklm generate audio --json
# Output: {"task_id": "abc123...", "status": "pending"}

# Read long instructions from a file
notebooklm generate audio --prompt-file instructions.txt --format debate
```

### Generate: `video`

Generate a video overview. Use `--format cinematic` for AI-generated documentary footage (Veo 3); cinematic videos ignore `--style` and take ~30-40 min (requires AI Ultra). For non-cinematic formats, see `generate cinematic-video` for the alias subcommand.

> **Python equivalent:** [`client.artifacts.generate_video(nb_id, ...)`](python-api.md#generation-methods).

```bash
notebooklm generate video [description] [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--format [explainer|brief|cinematic|short]` - Video format (`short` is a vertical short-form video with a fixed style)
- `--style [auto|custom|classic|whiteboard|kawaii|anime|watercolor|retro-print|heritage|paper-craft]` - Visual style (not supported with `--format cinematic` or `short`)
- `--style-prompt TEXT` - Custom visual style prompt (required when `--style custom`; rejected with `--format cinematic` or `short`)
- `--language LANG` - Output language (precedence: `--language` > `NOTEBOOKLM_HL` env > config > `'en'`)
- `-s, --source ID` - Limit to specific source IDs (repeatable, uses all if not specified)
- `--wait / --no-wait` - Wait for completion (default: `--no-wait`)
- `--timeout SECONDS` - Maximum seconds to wait (default: 1800; `--format cinematic` defaults to 3600; no-op without `--wait`)
- `--interval SECONDS` - Seconds between status checks (default: 2; no-op without `--wait`)
- `--retry N` - Retry N times with exponential backoff on rate limit
- `--json` - Output as JSON (returns `task_id` and `status`)
- `--prompt-file PATH` - Read description from a file (or `-` for stdin); mutually exclusive with positional argument

**Examples:**
```bash
# Kid-friendly explainer
notebooklm generate video "Explain for 5 year olds" --style kawaii

# Custom visual style
notebooklm generate video --style custom --style-prompt "hand-drawn diagrams"

# Cinematic format (Veo 3)
notebooklm generate video --format cinematic "documentary overview"

# Professional style
notebooklm generate video --style classic --wait

# Generate from specific sources only
notebooklm generate video -s src_123 -s src_456

# JSON output for scripting/automation
notebooklm generate video --json
```

**Note:** Passing a non-cinematic `--format` to the `cinematic-video` alias exits `2` with a `UsageError`. Use `generate video --format <other>` for non-cinematic formats.

### Generate: `revise-slide`

Revise an individual slide in an existing slide deck using a natural-language prompt.

> **Python equivalent:** [`client.artifacts.revise_slide(nb_id, artifact_id, slide_index, instructions)`](python-api.md#generation-methods).

```bash
notebooklm generate revise-slide [description] --artifact  --slide N [OPTIONS]
```

**Required Options:**
- `-a, --artifact ID` - The slide deck artifact ID to revise
- `--slide N` - Zero-based index of the slide to revise (0 = first slide)

**Optional:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--wait / --no-wait` - Wait for completion (default: `--no-wait`)
- `--timeout SECONDS` - Maximum seconds to wait (default: 300; no-op without `--wait`)
- `--interval SECONDS` - Seconds between status checks (default: 2; no-op without `--wait`)
- `--retry N` - Retry N times with exponential backoff on rate limit
- `--json` - Machine-readable output
- `--prompt-file PATH` - Read description from a file (or `-` for stdin); mutually exclusive with positional argument

**Examples:**
```bash
# Revise the first slide
notebooklm generate revise-slide "Move the title up" --artifact art123 --slide 0

# Revise the fourth slide and wait for completion
notebooklm generate revise-slide "Remove taxonomy table" --artifact art123 --slide 3 --wait
```

**Note:** The slide deck must already be fully generated before using `revise-slide`. Use `artifact list` to find the artifact ID.

---

### Generate: `report`

Generate a text report (briefing doc, study guide, blog post, or custom).

> **Python equivalent:** [`client.artifacts.generate_report(nb_id, report_format=..., ...)`](python-api.md#generation-methods).

```bash
notebooklm generate report [description] [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set)
- `--format [briefing-doc|study-guide|blog-post|custom]` - Report format (default: briefing-doc)
- `--append TEXT` - Append extra instructions to the built-in prompt (no effect with `--format custom`)
- `--language LANG` - Output language (precedence: `--language` > `NOTEBOOKLM_HL` env > config > `'en'`)
- `-s, --source ID` - Limit to specific source IDs (repeatable, uses all if not specified)
- `--wait / --no-wait` - Wait for completion (default: `--no-wait`)
- `--timeout SECONDS` - Maximum seconds to wait (default: 300; no-op without `--wait`)
- `--interval SECONDS` - Seconds between status checks (default: 2; no-op without `--wait`)
- `--retry N` - Retry N times with exponential backoff on rate limit
- `--json` - Output as JSON
- `--prompt-file PATH` - Read description from a file (or `-` for stdin); mutually exclusive with positional argument

**Examples:**
```bash
notebooklm generate report --format study-guide
notebooklm generate report "Executive summary for stakeholders" --format briefing-doc

# Generate report from specific sources
notebooklm generate report --format study-guide -s src_001 -s src_002

# Custom report with description (auto-selects custom format)
notebooklm generate report "Create a white paper analyzing the key trends"

# Append instructions to a built-in format
notebooklm generate report --format study-guide --append "Target audience: beginners"
notebooklm generate report --format briefing-doc --append "Focus on AI trends, keep it under 2 pages"

# Read a long custom prompt from file (auto-selects custom format)
notebooklm generate report --prompt-file custom_report.txt
```

### Artifact: `list`, `get`, `get-prompt`, `rename`, `delete`, `export`, `poll`, `wait`, `retry`, `suggestions`

Manage existing artifacts (audio, video, slide decks, quizzes, reports, etc.). Every subcommand resolves the notebook via the standard precedence (`-n/--notebook` flag > `NOTEBOOKLM_NOTEBOOK` env > active context).

> **Python equivalent:** [`client.artifacts.list/get/get_prompt/rename/delete/poll_status/wait_for_completion/retry_failed/suggest_reports(...)`](python-api.md#artifactsapi-clientartifacts) for management; [`export_report` / `export_data_table` / `export(...)`](python-api.md#export-methods) for export.

```bash
notebooklm artifact <list|get|get-prompt|rename|delete|export|poll|wait|retry|suggestions> [OPTIONS]
```

**Common options (all subcommands):**
- `-n, --notebook ID` - Notebook ID (uses current if not set; supports partial IDs)

**Per-subcommand options:**

| Subcommand | Required arguments | Options |
|---|---|---|
| `list` | (none) | `--type [all\|audio\|video\|slide-deck\|quiz\|flashcard\|infographic\|data-table\|mind-map\|report\|fantasy-map\|file]`, `--limit N` (default: unlimited), `--no-truncate`, `--json` |
| `get` | `ARTIFACT_ID` | `--json` |
| `get-prompt` | `ARTIFACT_ID` | `--json` |
| `rename` | `ARTIFACT_ID NEW_TITLE` | `--json` |
| `delete` | `ARTIFACT_ID` | `-y/--yes` (skip confirmation), `--json` |
| `export` | `ARTIFACT_ID` | `--title TEXT` (**required**), `--type [docs\|sheets]` (default: docs), `--json` |
| `poll` | `TASK_ID` (from `generate <type>`) | `--json` |
| `wait` | `ARTIFACT_ID` | `--timeout SECONDS` (default: 300), `--interval SECONDS` (default: 2), `--json` |
| `retry` | `ARTIFACT_ID` | `--wait` (block until terminal), `--timeout SECONDS` (default: 300), `--interval SECONDS` (default: 2), `--json` |
| `suggestions` | (none) | `--json` |

**Examples:**
```bash
# Filter artifact list to one notebook and one type, JSON for scripting
notebooklm artifact list --notebook nb_abc --type audio --json

# Inspect a single artifact (partial ID OK)
notebooklm artifact get art123 --json

# Show the prompt an artifact was generated from
notebooklm artifact get-prompt art123 --json

# Rename an artifact
notebooklm artifact rename art123 "Final cut"

# Delete without prompting
notebooklm artifact delete art123 -y --json

# Export a report to Google Docs (title is required)
notebooklm artifact export art123 --title "Climate briefing" --type docs

# Poll a task immediately after a generate call returns
notebooklm generate audio --json   # -> { "task_id": "task_abc...", "status": "..." }
notebooklm artifact poll task_abc

# Block until the artifact finishes (or 600s elapses)
notebooklm artifact wait art123 --timeout 600 --json

# Retry a failed artifact in place (UI "Retry"); --wait blocks until terminal
notebooklm artifact retry art123 --wait

# Get AI-suggested report topics for the active notebook
notebooklm artifact suggestions --json
```

> **`poll` vs `wait`:** The API returns one identifier that doubles as both `task_id` (right after `generate`) and `artifact_id` (once it appears in `artifact list`). `poll` is a one-shot status check that does not prefix-match against `artifact list` (use it immediately after `generate`); `wait` blocks until terminal status and accepts partial IDs that resolve against `artifact list`. See the quick-reference note above for full details.

### Download: `audio`, `video`, `slide-deck`, `infographic`, `report`, `mind-map`, `data-table`

Download generated artifacts to your local machine.

> **Python equivalent:** the per-type [`client.artifacts.download_audio/video/slide_deck/infographic/report/mind_map/data_table(nb_id, path, ...)`](python-api.md#downloading-artifacts) methods.

```bash
notebooklm download <type> [OUTPUT_PATH] [OPTIONS]
```

**Artifact Types and Output Formats:**

| Type | Default Extension | Description |
|------|-------------------|-------------|
| `audio` | `.m4a` | Audio overview (podcast) — AAC audio in an MP4 container |
| `video` | `.mp4` | Video overview |
| `slide-deck` | `.pdf` or `.pptx` | Slide deck as PDF (default) or PowerPoint |
| `infographic` | `.png` | Infographic image |
| `report` | `.md` | Report as Markdown (Briefing Doc, Study Guide, etc.) |
| `mind-map` | `.json` | Mind map as JSON tree structure |
| `data-table` | `.csv` | Data table as CSV (UTF-8 with BOM for Excel) |

**Options:**
- `--all` - Download all artifacts of this type
- `--latest` - Download only the most recent artifact (default if no ID/name provided)
- `--earliest` - Download only the oldest artifact
- `--name NAME` - Download artifact with matching title (supports partial matches)
- `-a, --artifact ID` - Select specific artifact by ID (supports partial IDs)
- `--dry-run` - Show what would be downloaded without actually downloading
- `--force` - Overwrite existing files
- `--no-clobber` - Don't overwrite existing files (a single download fails; `--all` skips them; default is auto-rename)
- `--format [pdf|pptx]` - Slide deck format (slide-deck command only, default: pdf)
- `--json` - Output result in JSON format

**Examples:**
```bash
# Download the latest podcast
notebooklm download audio ./podcast.m4a

# Download all infographics
notebooklm download infographic --all

# Download a specific slide deck by name
notebooklm download slide-deck --name "Final Presentation"

# Download slide deck as PPTX (editable PowerPoint)
notebooklm download slide-deck --format pptx

# Preview a batch download
notebooklm download audio --all --dry-run

# Download a report as markdown
notebooklm download report ./study-guide.md

# Download mind map as JSON
notebooklm download mind-map ./concept-map.json

# Download data table as CSV (opens in Excel)
notebooklm download data-table ./research-data.csv
```

### Download: `quiz`, `flashcards`

Download quiz questions or flashcard decks in various formats.

> **Python equivalent:** [`client.artifacts.download_quiz(nb_id, path, output_format="json|markdown|html")`](python-api.md#downloading-artifacts) and `download_flashcards(...)` with the same signature.

```bash
notebooklm download quiz [OUTPUT_PATH] [OPTIONS]
notebooklm download flashcards [OUTPUT_PATH] [OPTIONS]
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current context if not set)
- `--format FORMAT` - Output format: `json` (default), `markdown`, or `html`
- `-a, --artifact ID` - Select specific artifact by ID

**Output Formats:**
- **JSON** - Structured data preserving full API fields (answerOptions, rationale, isCorrect, hint)
- **Markdown** - Human-readable format with checkboxes for correct answers
- **HTML** - Raw HTML as returned from NotebookLM

**Examples:**
```bash
# Download quiz as JSON
notebooklm download quiz quiz.json

# Download quiz as markdown
notebooklm download quiz --format markdown quiz.md

# Download flashcards as JSON (normalizes f/b keys to front/back)
notebooklm download flashcards cards.json

# Download flashcards as markdown
notebooklm download flashcards --format markdown cards.md

# Download flashcards as raw HTML
notebooklm download flashcards --format html cards.html
```

### Note: `list`, `create`, `get`, `save`, `rename`, `delete`

Read, create, and update notebook notes (the "Notes" panel in the web UI). Mind-map artifacts surface as notes in the underlying API but are filtered out of `note list` — use `artifact list --type mind-map` for those.

> **Python equivalent:** [`client.notes.list/create/get/update/delete(...)`](python-api.md#notesapi-clientnotes). The CLI's `note save` and `note rename` both map to `client.notes.update(...)` (title and/or content). Mind-map helpers (`list_mind_maps`, `delete_mind_map`) live on the same API.

```bash
notebooklm note <list|create|get|save|rename|delete> [OPTIONS]
```

**Common options (all subcommands):**
- `-n, --notebook ID` - Notebook ID (uses current if not set; supports partial IDs)
- `--json` - Machine-readable output

**Per-subcommand options:**

| Subcommand | Required arguments | Type-specific options |
|---|---|---|
| `list` | (none) | — |
| `create` | `[CONTENT]` (positional or stdin via `-`) | `--content TEXT` (or `-` for stdin; mutually exclusive with positional), `-t/--title TEXT` (default: `"New Note"`) |
| `get` | `NOTE_ID` | — |
| `save` | `NOTE_ID` | `--title TEXT`, `--content TEXT` |
| `rename` | `NOTE_ID NEW_TITLE` | — |
| `delete` | `NOTE_ID` | `-y/--yes` (skip confirmation) |

**Examples:**
```bash
# List notes in the active notebook
notebooklm note list --json

# Create a titled note from a positional string
notebooklm note create "Quick thought" -t "Idea: research follow-up"

# Pipe markdown from a file or another command (use `-` to read stdin)
cat draft.md | notebooklm note create -

# Equivalent --content form
cat draft.md | notebooklm note create --content -

# Update an existing note (partial ID OK; only the fields you pass change)
notebooklm note save note_abc --title "Updated title"
notebooklm note save note_abc --content "New body"

# Rename without touching content
notebooklm note rename note_abc "Renamed"

# Delete without prompting
notebooklm note delete note_abc -y --json
```

### Profile: `list`, `create`, `switch`, `delete`, `rename`

Manage named profiles under `$NOTEBOOKLM_HOME/profiles/<name>/`. Each profile owns its own `storage_state.json`, `context.json`, and browser profile, so multiple Google accounts can coexist. The active profile is selected by `-p/--profile`, `NOTEBOOKLM_PROFILE`, or the `default_profile` field in `$NOTEBOOKLM_HOME/config.json` (in that precedence). See [configuration.md](configuration.md) for the full profile model.

```bash
notebooklm profile <list|create|switch|delete|rename> [OPTIONS]
```

**Per-subcommand options:**

| Subcommand | Required arguments | Options |
|---|---|---|
| `list` | (none) | `--json` |
| `create` | `NAME` | `--json` |
| `switch` | `NAME` | `--json` |
| `delete` | `NAME` | `--yes`/`-y` (skip prompt; `--confirm` is a deprecated alias; the active default profile cannot be deleted), `--json` |
| `rename` | `OLD_NAME NEW_NAME` | `--json` |

**Examples:**
```bash
# Enumerate profiles (active flag included in JSON)
notebooklm profile list --json

# Create a profile shell, then authenticate into it
notebooklm profile create work
notebooklm -p work login

# Make 'work' the default for subsequent commands
notebooklm profile switch work

# Rename a profile (does not change `storage_state.json` contents)
notebooklm profile rename work work-old

# Delete a non-active profile without prompting
notebooklm profile delete old-account --yes
```

> **Note:** `profile delete` refuses to remove the currently active default profile. Switch to a different profile first (`notebooklm profile switch <other>`) and then delete.

### Skill: `install`, `status`, `uninstall`, `show`, `package`

Manage the bundled NotebookLM agent-skill template. The skill lives in `SKILL.md` at the repository root; installing it materializes a copy under one of:
- `.claude/skills/notebooklm/SKILL.md` (Claude Code, `--target claude`)
- `.agents/skills/notebooklm/SKILL.md` (universal agent skill directory, `--target agents`)

`--scope` selects whether to write into the **user's** home (`~/.claude/skills/...`, `~/.agents/skills/...`) or the **current project** (`./.claude/skills/...`, `./.agents/skills/...`).

`skill package` takes neither `--scope` nor `--target`: it builds a ZIP archive (zip root: `notebooklm/SKILL.md`, version-stamped) for upload via **Claude Settings → Capabilities** — the supported hand-off for sandboxed agent environments such as Claude Cowork, which cannot run `skill install` against a local directory.

```bash
notebooklm skill  [OPTIONS]
```

**Options matrix** (defaults: `--scope user --target all`):

| Subcommand | `--scope` choices | `--target` choices | Default `--target` | Other options |
|---|---|---|---|---|
| `install` | `user`, `project` | `all`, `claude`, `agents` | `all` | `--dry-run`, `--no-clobber`, `--force` (project scope) |
| `status` | `user`, `project` | `all`, `claude`, `agents` | `all` | `--json` |
| `uninstall` | `user`, `project` | `all`, `claude`, `agents` | `all` | — |
| `show` | `user`, `project` | `source`, `claude`, `agents` | `source` | — |
| `package` | — | — | — | `-o/--output PATH` (default `./notebooklm-skill.zip`; an existing directory — or a PATH ending in a separator — gets the default filename inside it), `--force`, `--json` |

`skill show --target source` prints the packaged `SKILL.md` straight out of the wheel (the canonical content); the other `show` targets read the materialized copy from disk.

Project-scope install hardening:

- `--dry-run` prints the target files and actions without writing.
- `--no-clobber` creates missing targets but skips differing existing files.
- `--force` overwrites differing project targets.
- These flags are project-scope only; `--scope user` preserves the historical always-overwrite behavior.

**Examples:**
```bash
# Install both targets for the current user (default scope+target)
notebooklm skill install

# Install only the Claude Code target into the current project
notebooklm skill install --scope project --target claude --dry-run
notebooklm skill install --scope project --target claude --force

# Inspect what's installed in the user-scope agents directory
notebooklm skill status --scope user --target agents

# Print the packaged skill source (e.g. for piping into another agent's loader)
notebooklm skill show --target source

# Print the installed Claude target verbatim
notebooklm skill show --scope project --target claude

# Remove all installed targets from the current project
notebooklm skill uninstall --scope project --target all

# Build the uploadable archive in the current directory (./notebooklm-skill.zip)
notebooklm skill package

# Write to an explicit file, or drop the default filename into a directory
notebooklm skill package --output dist/notebooklm-v0.8.zip
notebooklm skill package --output dist/

# Overwrite an existing archive, or emit a machine-readable result
notebooklm skill package --output dist/ --force
notebooklm skill package --json
```

`skill package` refuses to overwrite an existing archive unless `--force` is passed. With `--json` it prints `{"path", "version", "entries", "size_bytes"}` on success and the standard error envelope on failure.

Codex does not consume the `skill` subcommand. In this repository it reads the root [`AGENTS.md`](../AGENTS.md) file and invokes the `notebooklm` CLI or Python API directly.

### Source: `add-drive`

Add a Google Drive document, slide deck, sheet, or PDF as a source. The Drive `--mime-type` selects which Drive document type to import (Google Doc / Slides / Sheets / PDF). This is distinct from the file-source `--mime-type` documented above, which sets the resumable-upload content-type for a locally-uploaded file.

> **By-reference vs upload-only:** NotebookLM's Drive import only ingests Google-native Docs/Slides/Sheets + PDF by reference — the four `--mime-type` choices above. An upload-only file that merely *lives* in Drive (e.g. `epub`/`docx`/`txt`/`md`/`rtf`/`odt`/`csv`/`tsv`) cannot be imported this way; use `source add-drive-file ` instead, which downloads it server-side (using your session) and uploads it — no local download step needed.
>
> **Python equivalent:** [`client.sources.add_drive(nb_id, file_id, title, mime_type=...)`](python-api.md#sourcesapi-clientsources).

```bash
notebooklm source add-drive [OPTIONS] FILE_ID TITLE
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set; supports partial IDs)
- `--mime-type [google-doc|google-slides|google-sheets|pdf]` - Drive document type (default: `google-doc`)
- `--json` - Output as JSON

**Examples:**
```bash
# Default — treat the Drive file as a Google Doc
notebooklm source add-drive 1AbcD...XyZ "Project Brief"

# Import as Google Slides
notebooklm source add-drive 1AbcD...XyZ "Quarterly Deck" --mime-type google-slides

# Import a Drive-hosted PDF
notebooklm source add-drive 1AbcD...XyZ "Whitepaper" --mime-type pdf --json
```

### Source: `add-drive-file`

Add an upload-only Google Drive file (`epub`/`docx`/`pptx`/`txt`/`md`/`rtf`/`odt`/`csv`/`tsv`/`pdf`) by id or share URL. NotebookLM's native Drive import (`source add-drive`) only ingests Google-native Docs/Slides/Sheets + PDF by reference; for every other Drive-hosted file type, this command downloads the file server-side (using your session) and uploads it through the resumable-upload path — a Drive PDF can go either way.

```bash
notebooklm source add-drive-file [OPTIONS] DOCUMENT_ID
```

**Options:**
- `-n, --notebook ID` - Notebook ID (uses current if not set; supports partial IDs)
- `--title TEXT` - Custom title (default: the file's Drive name)
- `--wait` - Wait for processing to finish
- `--json` - Output as JSON

**Examples:**
```bash
# Download + upload an upload-only Drive file (e.g. a .docx)
notebooklm source add-drive-file 1AbcD...XyZ

# Custom title, wait for processing to complete
notebooklm source add-drive-file 1AbcD...XyZ --title "Meeting Notes" --wait
```

### Source: `stale`, `clean`

`source stale` is a shell-friendly predicate that reports whether a URL/Drive source needs a refresh; `source clean` removes duplicate, error, or access-blocked sources in bulk.

> **Python equivalent:** [`client.sources.check_freshness(nb_id, source_id)`](python-api.md#sourcesapi-clientsources) returns the same staleness verdict. There is no single bulk "clean" call — combine `client.sources.list(...)` with a filter on duplicate/error/blocked status and call `client.sources.delete(...)` per match.

```bash
notebooklm source stale [OPTIONS] SOURCE_ID
notebooklm source clean [OPTIONS]
```

**`stale` exit codes (standard by default; opt-in inverted predicate via `--exit-on-stale`):**
- Default: `0` on success regardless of freshness, `1` on error. Branch on the JSON `stale`/`fresh` fields (or stdout text) for the verdict.
- With `--exit-on-stale`: `0` if stale (needs refresh), `1` if fresh — for back-compat with the `if … ; then refresh; fi` shell idiom.
- See [CLI Exit-Code Convention](cli-exit-codes.md) for details.

**`stale` options:** `-n/--notebook ID`, `--exit-on-stale`, `--json`.

**`clean` options:** `-n/--notebook ID`, `--dry-run` (preview the candidate set), `-y/--yes` (skip the confirmation prompt), `--json`.

**Examples:**
```bash
# Refresh a single stale URL source if needed (back-compat shell idiom)
if notebooklm source stale --exit-on-stale src_abc; then
  notebooklm source refresh src_abc
fi

# Default semantics: branch on JSON output instead
if [[ "$(notebooklm source stale src_abc --json | jq -r .stale)" == "true" ]]; then
  notebooklm source refresh src_abc
fi

# Preview what `clean` would remove
notebooklm source clean --dry-run

# Remove duplicates/errors/blocked sources without prompting
notebooklm source clean -y --json
```

---

## Common Workflows

### Research → Podcast

Find information on a topic and create a podcast about it.

```bash
# 1. Create a notebook for this research
notebooklm create "Climate Change Research" --use --json
# Output includes: {"active_notebook_id": "...", "notebook": {...}}

# 2. Add a starting source
notebooklm source add "https://en.wikipedia.org/wiki/Climate_change"

# 3. Research more sources automatically (blocking; --import-all retry budget defaults to 1800s)
notebooklm source add-research "climate change policy 2024" --mode deep --import-all

# 4. Generate a podcast
notebooklm generate audio "Focus on policy solutions and future outlook" --format debate --wait

# 5. Download the result
notebooklm download audio ./climate-podcast.m4a
```

### Research → Podcast (Non-blocking with Subagent)

For LLM agents, use non-blocking mode to avoid timeout:

```bash
# 1. Create notebook and set it active
notebooklm create "Climate Change Research" --use

# 2. Add initial source
notebooklm source add "https://en.wikipedia.org/wiki/Climate_change"

# 3. Start deep research (non-blocking)
notebooklm source add-research "climate change policy 2024" --mode deep --no-wait
# Returns immediately

# 4. In a subagent, wait for research and import
notebooklm research wait --import-all
# Blocks until complete, then imports sources

# 5. Continue with podcast generation...
```

**Research commands:**
- `research status` - Check if research is in progress, completed, or not running
- `research wait --import-all` - Block until research completes, then import sources

### Document Analysis → Study Materials

Upload documents and create study materials.

```bash
# 1. Create notebook
notebooklm create "Exam Prep"
notebooklm use 

# 2. Add your documents
notebooklm source add "./textbook-chapter.pdf"
notebooklm source add "./lecture-notes.pdf"

# 3. Get a summary
notebooklm summary

# 4. Generate study materials
notebooklm generate quiz --difficulty hard --wait
notebooklm generate flashcards --wait
notebooklm generate report --format study-guide --wait

# 5. Ask specific questions
notebooklm ask "Explain the key concepts in chapter 3"
notebooklm ask "What are the most likely exam topics?"
```

### YouTube → Quick Summary

Turn a YouTube video into notes.

```bash
# 1. Create notebook and add video
notebooklm create "Video Notes"
notebooklm use 
notebooklm source add "https://www.youtube.com/watch?v=VIDEO_ID"

# 2. Get summary
notebooklm summary

# 3. Ask questions
notebooklm ask "What are the main points?"
notebooklm ask "Create bullet point notes"

# 4. Generate a quick briefing doc
notebooklm generate report --format briefing-doc --wait
```

### Bulk Import

Add multiple sources at once.

```bash
# Set active notebook
notebooklm use 

# Add multiple URLs
notebooklm source add "https://example.com/article1"
notebooklm source add "https://example.com/article2"
notebooklm source add "https://example.com/article3"

# Add multiple local files (use a loop)
for f in ./papers/*.pdf; do
  notebooklm source add "$f"
done
```

---

### Doctor: `doctor`

Check profile setup, auth status, and migration.

Diagnoses common issues with profiles, authentication, and directory structure. Use `--fix` to automatically repair detected problems.

```bash
notebooklm doctor [OPTIONS]
```

**Options:**
- `--fix` - Attempt to fix detected issues (e.g. missing directories, broken configurations)
- `--json` - Output diagnostic results as a JSON structure for scripting/automation

**Examples:**
```bash
# Check profile and authentication health
notebooklm doctor

# Auto-repair environment issues
notebooklm doctor --fix

# Print diagnostics in machine-readable format
notebooklm doctor --json
```

---

### Agent: `agent show`

Show bundled instructions for supported agent environments.

This command displays tailored instructions for different LLM agents (Codex or Claude Code) to help them understand how to use this CLI programmatically.

```bash
notebooklm agent show [OPTIONS] {codex|claude}
```

**Examples:**
```bash
# Show instructions for Codex
notebooklm agent show codex

# Show instructions for Claude Code
notebooklm agent show claude
```

> **Note:** `agent show codex` prefers the root `AGENTS.md` file when running from a source checkout, so the CLI mirrors the same instructions Codex sees in the repository.

---

## Tips for LLM Agents

When using this CLI programmatically:

1. **Three ways to specify notebooks**: (a) `notebooklm use ` to set persistent context, (b) `NOTEBOOKLM_NOTEBOOK=` env var for the calling shell, or (c) `-n ` directly on each command. Resolution precedence: `-n/--notebook` flag > `NOTEBOOKLM_NOTEBOOK` env > active context > error.

2. **Generation commands are async by default** (except mind-map):
   - `mind-map`: Synchronous, completes instantly (no `--wait` option)
   - All others: Return immediately with task ID (default: `--no-wait`)

   Avoid `--wait` for LLM agents—all async operations can take minutes to 30+ minutes. Use `artifact wait ` in a background task or inform the user to check back later. Long-waits (`generate <kind> --wait`, `artifact wait`, `source wait`) honor SIGINT cleanly: Ctrl-C exits `130` with a `"Resume with: notebooklm artifact poll <task_id>"` hint (or `notebooklm source wait ` on the source path) instead of a Python traceback. Under `--json` the cancellation surfaces as `{"error": true, "code": "CANCELLED", "resume_hint": "..."}`.

3. **Partial IDs work**: `notebooklm use abc` matches any notebook ID starting with "abc". The same prefix-match resolves `-n/--notebook`, `-s/--source`, and `-a/--artifact` arguments across the CLI.

4. **Check status**: Use `notebooklm status` to see the current active notebook and conversation.

5. **Auto-detection**: `source add` auto-detects content type:
   - URLs starting with `http` → web source
   - YouTube URLs → video transcript extraction
   - File paths → file upload (PDF, text, Markdown, Word, audio, video, images)
   - Path-shaped strings that don't exist on disk (e.g. `./missin.md`) still ingest as inline text but emit a stderr warning so a typo doesn't silently masquerade as a successful upload. Pass `--type text` to suppress the warning.

6. **Stdin pipelines**: Four surfaces accept the canonical Unix `-` placeholder for "read from stdin": `notebooklm ask -`, `notebooklm ask --prompt-file -`, `notebooklm note create -` (or `--content -`), and `notebooklm source add -` (forces text-source path; bypasses path-shaped detection). Same convention applies to the various `generate <kind> --prompt-file -` flags.

7. **Quiet output for CI/cron**: Pass `--quiet` (root-level) to suppress status output and INFO/WARN log records; errors still surface and structured `--json` payloads are still emitted. `--quiet` is mutually exclusive with `-v/-vv` (combining the two raises `UsageError` with exit `2`). For long-running keepalive loops, `notebooklm auth refresh --quiet` is the subcommand-scoped equivalent.

8. **Error handling**: Commands exit with non-zero status on failure (`1` for user/library errors, `2` for system/unexpected errors per [CLI Exit-Code Convention](cli-exit-codes.md)). With `--json`, failures surface as a typed envelope `{"error": true, "code": "<TYPED_CODE>", "message": "..."}` on stdout; without `--json`, error messages go to stderr.

9. **Deep research**: Use `--no-wait` with `source add-research --mode deep` to avoid blocking. Then use `research wait --import-all` in a subagent to wait for completion.

10. **Shell completion**: `notebooklm completion ` prints a completion script that enables ID-aware tab completion for `-n/--notebook`, `-s/--source`, and `-a/--artifact` from your active profile's live IDs. See the [`completion`](#session-completion) section for install snippets.

---

## File: docs/configuration.md

# Configuration

**Status:** Active
**Last Updated:** 2026-08-05

This guide covers storage locations, environment settings, and configuration options for `notebooklm-py`.

## File Locations

All data is stored under `~/.notebooklm/` by default, organized by profile:

```
~/.notebooklm/
├── config.json           # Global config: default_profile, language
├── profiles/
│   ├── default/          # Default profile (auto-created)
│   │   ├── storage_state.json    # Authentication cookies and session
│   │   ├── context.json          # CLI context (active notebook, conversation)
│   │   └── browser_profile/      # Persistent Chromium profile
│   ├── work/             # Named profile example
│   │   ├── storage_state.json
│   │   ├── context.json
│   │   └── browser_profile/
│   └── personal/
│       └── ...
```

`config.json` stores process-wide settings — the persisted default profile
name (under the `default_profile` key) and the configured interface language
(`language`). It is **global, not per-profile** (see
`src/notebooklm/paths.py` and `src/notebooklm/cli/language_cmd.py`).

**Legacy layout:** If upgrading from a pre-profile version, the first run auto-migrates flat files into `profiles/default/`. The migration runs under a single-writer `filelock` rooted at `~/.notebooklm/.migration.lock`, so concurrent CLI invocations (e.g., container start-up races) cannot interleave copies — the loser of the lock re-checks the completion marker and no-ops (see `src/notebooklm/migration.py`). The legacy flat layout continues to work as a fallback.

You can relocate all files by setting `NOTEBOOKLM_HOME`:

```bash
export NOTEBOOKLM_HOME=/custom/path
# All files now go to /custom/path/profiles//
```

### Storage State (`storage_state.json`)

Contains the authentication data extracted from your browser session:

```json
{
  "cookies": [
    {
      "name": "SID",
      "value": "...",
      "domain": ".google.com",
      "path": "/",
      "expires": 1234567890,
      "httpOnly": true,
      "secure": true,
      "sameSite": "Lax"
    },
    ...
  ],
  "origins": [],
  "notebooklm": {
    "version": 1,
    "account": {
      "authuser": 0,
      "email": "you@example.com"
    }
  }
}
```

**Cookie requirements** (empirically validated via single-, pair-, and three-way ablation; see [auth-cookie-lifecycle.md](auth-cookie-lifecycle.md#33-empirical-cookie-requirements); enforced by `_validate_required_cookies()` in `_auth/cookie_policy.py`):

- **Tier 1 — strictly required (raises on absence):** `SID` AND `__Secure-1PSIDTS`. `SID` is the only individually-required cookie (`__Secure-1PSIDTS` is removable on its own because Google can re-mint it via `RotateCookies`), but the pair-wise check uncovered that as soon as `__Secure-1PSIDTS` and any one other auth cookie are both missing, Google rejects with `Authentication expired or invalid`. The library therefore enforces both up-front. Authoritative value: `MINIMUM_REQUIRED_COOKIES` in `_auth/cookie_policy.py`.
- **Tier 2 — secondary binding (logs a warning if absent):** either `OSID` is present, or `APISID` and `SAPISID` are present **together with bare `LSID`** (the `LSID` conjunct is required — the pair alone fails, per the three-way ablation in #1977). Without this, even valid Tier 1 cookies can't authenticate the homepage GET. Logged rather than raised so unverified edge-case flows (e.g. Workspace SSO) aren't broken by a too-strict client check.

In practice: extract the full cookie set via `notebooklm login` and don't try to subset it. Partial extractions (a known failure mode of browser-cookies tooling under Chrome 127+ App-Bound Encryption) are the leading suspect for "auth expires immediately" reports — see [#371](https://github.com/teng-lin/notebooklm-py/issues/371).

The optional `notebooklm.account` block records the Google account route for
multi-account sessions. New `notebooklm login` runs write it when the active
account can be discovered safely. Older Playwright-created files can be repaired
with `notebooklm auth refresh` when the storage state maps to one visible
account; if several accounts are visible, use
`notebooklm login --browser-cookies  --account EMAIL` to bind the
intended account explicitly. `auth refresh` only repairs absent metadata; if
metadata exists but points at the wrong account, re-bind explicitly with the
browser-cookie login path.

**Override location:**
```bash
notebooklm --storage /path/to/storage_state.json list
```

### Context File (`context.json`)

Stores the current CLI context, such as the active notebook:

```json
{
  "notebook_id": "abc123def456",
  "title": "Quarterly review notes",
  "is_owner": true,
  "role": "owner",
  "created_at": "2026-05-01T17:43:21Z"
}
```

Field summary:

- `notebook_id` — currently selected notebook, written by `notebooklm use` and read by every command that takes `-n/--notebook`.
- `title`, `is_owner`, `role`, `created_at` — optional notebook metadata captured at selection time so `status` / display commands don't need an extra round-trip. Omitted when the CLI didn't have the values to write (see `set_current_notebook` in `src/notebooklm/cli/context.py`). `role` is `"owner"` / `"editor"` / `"viewer"`; `is_owner` is the `role == "owner"` shorthand and is retained for backward compatibility. Contexts written before `role` existed carry only `is_owner`, and `status` falls back to it.

This file is managed automatically by `notebooklm use`, `notebooklm clear`, and the `auth` commands.

### Browser Profile (`browser_profile/`)

A persistent Chromium user data directory used during `notebooklm login`.

**Why persistent?** Google blocks automated login attempts. A persistent profile makes the browser appear as a regular user installation, avoiding bot detection.

**To reset:** Delete the `browser_profile/` directory and run `notebooklm login` again.

With `--storage `, the browser profile is isolated with that storage file.
The conventional `storage_state.json` uses a `browser_profile/` directory beside
it; any normal-length custom filename uses `.browser_profile`. For example,
`A.json` and `B.json` use `A.json.browser_profile/` and
`B.json.browser_profile/` respectively. If that filename would exceed the
255-byte component limit, the canonical storage path maps to a short stable
`storage-<hash>.browser_profile/` name; relative and absolute aliases map to the
same directory.

Browser directories newly created for explicit storage contain a
`.notebooklm-owned` marker. `login --fresh` refuses to recursively delete an
unowned sidecar, and `auth logout` skips it and reports the preserved path.
Canonical named-profile and legacy browser directories remain managed even when
their `storage_state.json` is selected explicitly with `--storage`; an arbitrary
same-named directory outside `NOTEBOOKLM_HOME` remains unowned. A marker-only
directory does not count as a reusable L3 browser session.

> **Upgrading:** Existing custom-storage browser sessions are not migrated. You
> may need one interactive sign-in per storage file. Do not copy or delete the
> old shared browser profile unless its account/profile ownership is known.

### Master Token (`master_token.json`)

Written only by `notebooklm login --master-token` (the `[headless]` extra). Holds
a durable Google master token (mode `0600`) that mints/refreshes the profile's
`storage_state.json` cookies with no per-session browser. When present beside a
profile's `storage_state.json`, an expired session re-mints from it
automatically. If storage is absent, `notebooklm auth refresh` mints it from the
exact sibling token and passively validates once. The legacy login refresh flag
remains an unconditional forced re-mint.

```json
{"version": 1, "email": "...", "android_id": "<hex>", "master_token": "aas_et/..."}
```

> ⚠️ **Full-account, durable credential** — larger blast radius than
> `storage_state.json`; dedicated/throwaway account only. See
> [installation.md#alternative-master-token-auth-no-cookie-file-to-ship-survives-expiry](installation.md#d-headless-server-or-ci).

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `NOTEBOOKLM_HOME` | Base directory for all files | `~/.notebooklm` |
| `NOTEBOOKLM_PROFILE` | Active profile name | `default` |
| `NOTEBOOKLM_AUTH_JSON` | Inline authentication JSON (for CI/CD) | - |
| `NOTEBOOKLM_NOTEBOOK` | Default notebook ID for commands without `-n/--notebook` | - |
| `NOTEBOOKLM_HL` | Default interface/output language code (e.g. `en`, `ja`, `zh_Hans`) | `en` |
| `NOTEBOOKLM_BASE_URL` | Gemini Notebook base URL. Constrained to `https://notebook.google.com` (default) or `https://notebooklm.google.com` (pre-rebrand personal, still served) or `https://notebooklm.cloud.google.com` (enterprise) | `https://notebook.google.com` |
| `NOTEBOOKLM_BL` | `bl` (build label) URL parameter for the chat streaming endpoint; override when chasing a regression tied to a specific frontend build snapshot | built-in default in `_env.DEFAULT_BL` (drift-monitored nightly) |
| `NOTEBOOKLM_TRANSPORT` | HTTP transport backend: `httpx` (default) or `curl_cffi` (opt-in browser-TLS impersonation; requires the `curl_cffi` package). Use `curl_cffi` where the default transport is TLS-fingerprint-blocked. | `httpx` |
| `NOTEBOOKLM_LOG_LEVEL` | Logging level: `DEBUG`, `INFO`, `WARNING`, `ERROR` | `WARNING` |
| `NOTEBOOKLM_DEBUG_RPC` | Legacy: Enable RPC debug logging (use `LOG_LEVEL=DEBUG` instead) | `false` |
| `NOTEBOOKLM_DEBUG` | Show untruncated RPC response bodies in error messages instead of the default 80-char preview (verbose; intended for deep debugging) | `0` |
| `NOTEBOOKLM_STRICT_DECODE` | **Retired (ignored since v0.7.0).** Strict decoding is now the only mode: `safe_index` always raises `UnknownRPCMethodError` on schema drift. The former `0` warn-and-fallback opt-out was removed. | (ignored) |
| `NOTEBOOKLM_RPC_OVERRIDES` | JSON object mapping `RPCMethod` enum names to RPC ID strings (community self-patch when Google rotates a method ID; e.g. `{"LIST_NOTEBOOKS":"AbC123"}`) | - |
| `NOTEBOOKLM_REFRESH_CMD` | Optional command (argv list, or shell string with `_USE_SHELL=1`) invoked when auth refresh is required. Must exit `0` after writing a refreshed `storage_state.json`; the parent reloads from disk | - |
| `NOTEBOOKLM_REFRESH_CMD_USE_SHELL` | Opt the `NOTEBOOKLM_REFRESH_CMD` subprocess back into `shell=True` execution. Default `shell=False` (argv list) — set to the literal `1` (only `"1"` is honored — not `true`/`yes`/`on`) when the refresh command requires shell metacharacters | `0` |
| `NOTEBOOKLM_REFRESH_CMD_MIDSESSION` | Opt in (literal `1`) to running `NOTEBOOKLM_REFRESH_CMD` **mid-session** (the L2.5 rung), not only at cold start. Off by default for one release so operators whose commands assume cold-start-only invocation are not surprised inside long-lived servers; flips to default-on a later release ([ADR-0030](adr/0030-one-recovery-ladder.md)) | `0` |
| `NOTEBOOKLM_REFRESH_CMD_LOG_OUTPUT` | Opt in (literal `1`) to routing the refresh command's captured `stdout`/`stderr` to the redacting **DEBUG** logger. Off by default — the default DEBUG line carries only basename + exit code + byte counts, so promoting the rung into long-lived servers does not widen exposure of whatever the command prints | `0` |
| `NOTEBOOKLM_REFRESH_PROFILE` | Child-process hint set for `NOTEBOOKLM_REFRESH_CMD`; names the resolved profile being refreshed | resolved profile |
| `NOTEBOOKLM_REFRESH_STORAGE_PATH` | Child-process hint set for `NOTEBOOKLM_REFRESH_CMD`; path to the `storage_state.json` file the command must rewrite | resolved storage path |
| `NOTEBOOKLM_DISABLE_KEEPALIVE_POKE` | Disable the proactive `accounts.google.com/RotateCookies` poke that refreshes `__Secure-1PSIDTS` ahead of expiry | `0` |
| `NOTEBOOKLM_PROMOTION_EXIT_TIMEOUT` | Seconds the process waits at exit for an in-flight one-time migration of a pre-`v0.x` `context.json` account into `storage_state.json`. It is a ceiling, not a delay: a finished migration exits immediately. Raise it on a machine where the write is slow (antivirus, network storage); set `0` to never wait. An incomplete wait is always reported at `WARNING`, so note that `--quiet` (which raises the floor to `ERROR`) suppresses that signal. Non-numeric, negative, and non-finite values are refused with a warning and the default is used; a finite value above the platform join limit is clamped. | `30` |
| `NOTEBOOKLM_HEADLESS_REAUTH` | Opt in to layer-3 headless re-auth during cold construction and automatic refresh paths. Explicit `from_storage(allow_headless=True)`, `client.refresh_auth(allow_headless=True)`, or `auth refresh --allow-headless` does not require this env var. | `0` |
| `NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL` | Optional loopback Chrome DevTools endpoint for layer-3 headless re-auth, e.g. `http://127.0.0.1:9222`. Non-loopback endpoints are ignored for credential safety. | - |
| `NOTEBOOKLM_MCP_TRANSPORT` | MCP server transport for `notebooklm-mcp`: `stdio` or `http` | `stdio` |
| `NOTEBOOKLM_MCP_HOST` | MCP HTTP transport bind host; non-loopback refused unless `NOTEBOOKLM_MCP_ALLOW_EXTERNAL_BIND=1` | `127.0.0.1` |
| `NOTEBOOKLM_MCP_PORT` | MCP HTTP transport bind port | `9420` |
| `NOTEBOOKLM_MCP_ALLOW_EXTERNAL_BIND` | Allow MCP HTTP transport to bind a non-loopback host. Use only behind a trusted proxy. | `0` |
| `NOTEBOOKLM_MCP_OAUTH_PASSWORD` | Password gating the self-hosted OAuth authorization server that lets claude.ai connect to the remote MCP server (≥16 chars). Set together with `NOTEBOOKLM_MCP_OAUTH_BASE_URL`; both unset → bearer-only. | - |
| `NOTEBOOKLM_MCP_OAUTH_BASE_URL` | Bare public HTTPS origin (no path) the self-hosted OAuth endpoints (`/authorize`, `/token`, `/.well-known/*`) mount under. Required with `NOTEBOOKLM_MCP_OAUTH_PASSWORD`; partial/weak/non-HTTPS config refuses to start. | - |
| `NOTEBOOKLM_MCP_PUBLIC_URL` | Public base URL for the remote MCP file upload/download signed-URL side-channel (falls back to `NOTEBOOKLM_MCP_OAUTH_BASE_URL`). Unset → `source_add type=file` / `artifact_download` return a "not configured" error. | - |
| `NOTEBOOKLM_MCP_TRUST_PROXY` | Trust the proxy-set `CF-Connecting-IP` header as the self-hosted-OAuth login-throttle key. Only enable behind a trusted proxy (e.g. the Cloudflare tunnel); default off keys on the socket peer. | `0` |
| `NOTEBOOKLM_MCP_STRICT_IDS` | Strict IDs-only mode for MCP tools: require a full canonical id for every `notebook`/`source`/`note`/`artifact` reference and reject names, titles, and short id prefixes (fail-fast, deterministic automation). | `0` |
| `NOTEBOOKLM_SERVER_TOKEN` | Bearer token required by every REST `/v1` request. The REST server refuses to start without it. | - |
| `NOTEBOOKLM_SERVER_HOST` | REST server bind host; non-loopback refused unless `NOTEBOOKLM_SERVER_ALLOW_EXTERNAL_BIND=1` | `127.0.0.1` |
| `NOTEBOOKLM_SERVER_PORT` | REST server bind port | `8000` |
| `NOTEBOOKLM_SERVER_ALLOW_EXTERNAL_BIND` | Allow REST server to bind a non-loopback host. Use only behind a trusted proxy. | `0` |
| `NOTEBOOKLM_SERVER_SOURCE_MUTATION_CONCURRENCY` | Max concurrent REST source create/rename/delete/batch handlers. | `4` |
| `NOTEBOOKLM_SERVER_SOURCE_WAIT_CONCURRENCY` | Max concurrent REST source wait handlers. | `4` |
| `NOTEBOOKLM_SERVER_GENERATION_CONCURRENCY` | Max concurrent REST artifact generation/retry handlers. | `2` |
| `NOTEBOOKLM_SERVER_DOWNLOAD_CONCURRENCY` | Max concurrent REST artifact download handlers. | `2` |
| `NOTEBOOKLM_SERVER_RESEARCH_CONCURRENCY` | Max concurrent REST research start/cancel/import handlers. | `2` |
| `NOTEBOOKLM_SERVER_CHAT_CONCURRENCY` | Max concurrent REST blocking chat ask handlers. | `4` |
| `NOTEBOOKLM_QUIET_DEPRECATIONS` | Suppress the project's public-API `DeprecationWarning`s (the one-off warnings routed through `warn_deprecated`, e.g. awaiting `from_storage(...)`). Set to a truthy value (`1` / `true` / `yes` / `on`, case-insensitive) to silence them; see `docs/deprecations.md`. | (warnings emitted) |
| `NOTEBOOKLM_FUTURE_ERRORS` | **Retired (removed in v0.8.0; ignored).** It was the v0.7.0 forward-compat preview gate for the v0.8.0 error contract; now that every break it staged is the default, the flag is a no-op — setting it has no effect. See `docs/deprecations.md`. | (ignored) |
| `NOTEBOOKLM_VCR_RECORD_ERRORS` | Synthetic-error injection mode for VCR test cassettes (`429`, `5xx`, `expired_csrf`) | - |

### Public config API vs internal resolvers

`src/notebooklm/_env.py` owns internal environment/default resolution for
runtime behavior. It reads process environment variables and contains internal
defaults such as `DEFAULT_BL` for the chat streaming build label.

`notebooklm.config` is the stable public import surface. It intentionally
re-exports only the supported endpoint/language helpers:
`DEFAULT_BASE_URL`, `ENTERPRISE_BASE_HOST`, `get_base_host`, `get_base_url`,
`get_default_language`, and `PERSONAL_BASE_HOST`. Existing imports
from `notebooklm.config` remain supported; internal-only `_env` names should
not be imported by downstream code.

### Env vars and precedence

Every `NOTEBOOKLM_*` variable read by the library and CLI, in one place. CLI
flags always win over env vars; env vars win over persisted profile config /
context; built-in defaults are the last fallback. The "Resolved by" column
points at the canonical resolver so the precedence rule for each variable can
be audited from one location.

| Variable | Purpose | Resolution order (highest → lowest) | Resolved by |
|----------|---------|-------------------------------------|-------------|
| `NOTEBOOKLM_PROFILE` | Active profile name. Selects which `~/.notebooklm/profiles/<name>/` directory backs storage and context. | `-p/--profile` flag → `NOTEBOOKLM_PROFILE` → `default_profile` from `~/.notebooklm/config.json` → `default` | `paths.resolve_profile` |
| `NOTEBOOKLM_AUTH_JSON` | Inline `storage_state.json` payload for CI/CD; bypasses on-disk profile storage entirely. | `--storage` flag → `NOTEBOOKLM_AUTH_JSON` → profile-aware `storage_state.json` → legacy fallback | `auth.load_auth_from_storage` |
| `NOTEBOOKLM_HOME` | Base directory for all per-profile files. | `NOTEBOOKLM_HOME` → `~/.notebooklm` | `paths.get_home_dir` |
| `NOTEBOOKLM_HL` | Default interface/output language for `generate <kind>` and the `hl` query parameter on every batchexecute RPC. | `--language` flag → `NOTEBOOKLM_HL` → `language` value from **global** `~/.notebooklm/config.json` (NOT per-profile) → `en` | `language.resolve_hl` |
| `NOTEBOOKLM_LOG_LEVEL` | `DEBUG`/`INFO`/`WARNING`/`ERROR` floor for the `notebooklm` package logger. | `--quiet` flag (forces `ERROR`) → `-v/-vv` flags (force `INFO`/`DEBUG`) → `NOTEBOOKLM_DEBUG_RPC=1` (forces `DEBUG`) → `NOTEBOOKLM_LOG_LEVEL` → `WARNING` | `_logging.configure_logging` + `notebooklm_cli.cli` |
| `NOTEBOOKLM_DEBUG_RPC` | Legacy alias that sets the package logger to `DEBUG`. Prefer `NOTEBOOKLM_LOG_LEVEL=DEBUG` for new code. | (See `NOTEBOOKLM_LOG_LEVEL`.) | `_logging.configure_logging` |
| `NOTEBOOKLM_NOTEBOOK` | Default notebook ID when no `-n/--notebook` flag is passed. Composes with `notebooklm use ` so per-shell overrides do not clobber the persisted active-notebook context. | `-n/--notebook` flag → `NOTEBOOKLM_NOTEBOOK` → active context (from `notebooklm use`) → error | `cli.helpers.require_notebook` (Click also reads it natively via `cli/options.py:notebook_option`'s `envvar=`) |
| `NOTEBOOKLM_RPC_OVERRIDES` | **JSON object** mapping `RPCMethod` enum names to RPC ID strings (e.g. `{"LIST_NOTEBOOKS": "AbC123"}`). Overrides runtime RPC IDs — community self-patch when Google rotates a method ID. Empty string / unset disables the mechanism; invalid JSON or non-object payloads emit a `WARNING` and are ignored. | Process env, evaluated per RPC resolve (cached on the raw env string). | `notebooklm.rpc.overrides._parse_rpc_overrides` |
| `NOTEBOOKLM_QUIET_DEPRECATIONS` | Suppress the project's public-API `DeprecationWarning`s — the one-off warnings routed through `src/notebooklm/_deprecation.py::warn_deprecated` (e.g. awaiting `from_storage(...)`). Set to a truthy value (`1` / `true` / `yes` / `on`) to silence them. See `docs/deprecations.md`. | (warnings emitted) | `_deprecation._deprecations_quiet` / `deprecations_quiet` |
| `NOTEBOOKLM_FUTURE_ERRORS` | **Retired (removed in v0.8.0; ignored).** It was the v0.7.0 forward-compat preview gate for the v0.8.0 error contract (ADR-0019 / umbrella [#1346](https://github.com/teng-lin/notebooklm-py/issues/1346)). Now that every break it staged — `get()` raising `*NotFoundError`, the attribute-only typed returns, the removed `interval=` alias, the bool→`None` returns, the refusal-raises, and the mutate-existing fail-loud — is the default, the flag is a **no-op**: setting it has no effect. See `docs/deprecations.md`. | (ignored) | — |
| `NOTEBOOKLM_STRICT_DECODE` | **Retired (ignored since v0.7.0).** Strict decoding is the only mode — `safe_index` always raises `UnknownRPCMethodError` on schema drift. The former `0` warn-and-fallback opt-out was removed; setting the variable has no effect. | (ignored) | — |
| `NOTEBOOKLM_BASE_URL` | Gemini Notebook base URL. Constrained to `https://notebook.google.com` (default) or `https://notebooklm.google.com` (pre-rebrand personal host, still served — the documented rollback lever; if auth fails after switching, re-run `notebooklm login --fresh`) or `https://notebooklm.cloud.google.com` (enterprise); other schemes/hosts/paths raise `ValueError`. | Process env on every base-URL lookup. | `_env.get_base_url` |
| `NOTEBOOKLM_BL` | `bl` (build label) URL parameter sent on the chat streaming endpoint (`ChatAPI.ask`). Pins the frontend build the request is attributed to. The built-in `_env.DEFAULT_BL` is watched by the nightly canary's [build-label lane](rpc-development.md#build-label-lane-bl--_envdefault_bl), which compares it against the label Google actually serves; an override here does not change that verdict. | Process env on every chat stream call; whitespace-only falls back to `_env.DEFAULT_BL`. | `_env.get_default_bl` |
| `NOTEBOOKLM_DEBUG` | When `1`, RPC error messages include the **full** untruncated response body instead of the default 80-char preview. Verbose; intended for deep debugging only. | Process env on each error formatting call. | `exceptions._truncate_response_preview` |
| `NOTEBOOKLM_REFRESH_CMD` | Optional command invoked when auth refresh is required. Must exit `0` after writing a refreshed `storage_state.json`; the parent reloads cookies from disk. Stdout/stderr are not parsed (only surfaced in the non-zero-exit error message). Parsing honors `NOTEBOOKLM_REFRESH_CMD_USE_SHELL`. | Process env on each refresh subprocess spawn. | `auth` refresh-spawn helper (constant `NOTEBOOKLM_REFRESH_CMD_ENV` in `notebooklm.auth`) |
| `NOTEBOOKLM_REFRESH_CMD_USE_SHELL` | Opt the optional `NOTEBOOKLM_REFRESH_CMD` subprocess back into `shell=True`. Default `shell=False` parses the command with `shlex.split` and invokes it as an argv list (safer; resists shell-injection footguns when the env var is sourced from CI configs or container env files). | Process env on each refresh subprocess spawn. | `auth` refresh-spawn helper (constant `NOTEBOOKLM_REFRESH_CMD_USE_SHELL_ENV` in `notebooklm.auth`) |
| `NOTEBOOKLM_REFRESH_CMD_MIDSESSION` | Opt in (literal `1`) to firing `NOTEBOOKLM_REFRESH_CMD` mid-session (the L2.5 rung of the unified ladder), not just at cold start. Default off **for one release** — the rung was cold-start-only before, and enabling it inside a long-lived server changes when the operator's command runs; flips to default-on a later release. See [ADR-0030](adr/0030-one-recovery-ladder.md). | Read on the mid-session recovery path. | `auth._run_refresh_cmd` (constant `NOTEBOOKLM_REFRESH_CMD_MIDSESSION_ENV` in `notebooklm.auth`) |
| `NOTEBOOKLM_REFRESH_CMD_LOG_OUTPUT` | Opt in (literal `1`) to routing the refresh command's captured `stdout`/`stderr` into the redacting DEBUG logger. Default off: because the mid-session promotion widens exposure of whatever the command prints inside long-lived servers, the default DEBUG line carries only basename + exit code + byte counts. | Process env / logging path on each refresh subprocess spawn. | `auth._run_refresh_cmd` (constant `NOTEBOOKLM_REFRESH_CMD_LOG_OUTPUT_ENV` in `notebooklm.auth`) |
| `NOTEBOOKLM_REFRESH_PROFILE` | Child env var injected into `NOTEBOOKLM_REFRESH_CMD`; names the resolved NotebookLM profile that is being refreshed. Refresh scripts may read it, but setting it in the parent shell does not select the profile. | Set by `auth` refresh-spawn helper from the resolved profile. | `auth._run_refresh_cmd` |
| `NOTEBOOKLM_REFRESH_STORAGE_PATH` | Child env var injected into `NOTEBOOKLM_REFRESH_CMD`; points to the `storage_state.json` file the command must rewrite before exiting `0`. Refresh scripts may read it, but setting it in the parent shell does not select storage. | Set by `auth` refresh-spawn helper from the explicit storage path or profile-aware storage path. | `auth._run_refresh_cmd` |
| `NOTEBOOKLM_DISABLE_KEEPALIVE_POKE` | When `1`, disable the proactive `accounts.google.com/RotateCookies` poke that refreshes `__Secure-1PSIDTS` ahead of expiry. Useful when running behind a proxy that rejects the extra request, or in offline test fixtures. | Process env on every keepalive check. | `auth` keepalive guards (constant `NOTEBOOKLM_DISABLE_KEEPALIVE_POKE_ENV` in `notebooklm.auth`) |
| `NOTEBOOKLM_PROMOTION_EXIT_TIMEOUT` | Seconds the process waits at exit for an in-flight one-time migration of a pre-`v0.x` `context.json` account into `storage_state.json`. A ceiling shared by all outstanding writers, not a delay — a finished migration exits immediately. `0` never waits. Unset, empty, or whitespace-only falls back to the default; non-numeric, negative, and non-finite (`inf`/`nan`) values are refused with a `WARNING` and the default is used; a finite value above `threading.TIMEOUT_MAX` is clamped. An incomplete wait is always reported at `WARNING` (so `--quiet`, which forces `ERROR`, suppresses it). | Process env, read once per process at exit → `30.0` | `_auth.profile_migration._promotion_exit_timeout` |
| `NOTEBOOKLM_HEADLESS_REAUTH` | Opt in to layer-3 headless re-auth for cold construction and automatic refresh paths. Explicit Python/CLI `allow_headless` flags do not require the env var. | Literal `1` enables; all other values disabled. | `_auth.headless_reauth.headless_reauth_env_enabled` |
| `NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL` | Optional Chrome DevTools Protocol endpoint for layer-3 headless re-auth. Must be loopback (`127.0.0.1`, `::1`, or `localhost`); remote endpoints are ignored because CDP is account-equivalent. | Explicit function argument → env var → no CDP arm. | `_auth.headless_reauth.resolve_cdp_url` |
| `NOTEBOOKLM_MCP_TRANSPORT` | Default transport for `notebooklm-mcp`: `stdio` or `http`. CLI `--transport` wins. | `--transport` flag → env var → `stdio` | `mcp.__main__._build_parser` |
| `NOTEBOOKLM_MCP_HOST` | HTTP bind host for `notebooklm-mcp --transport http`. Non-loopback refused unless `NOTEBOOKLM_MCP_ALLOW_EXTERNAL_BIND=1`. | `--host` flag → env var → `127.0.0.1` | `mcp.__main__._build_parser` / `_serving.check_bind_allowed` |
| `NOTEBOOKLM_MCP_PORT` | HTTP bind port for `notebooklm-mcp --transport http`. | `--port` flag → env var → `9420` | `mcp.__main__._build_parser` / `_resolve_port` |
| `NOTEBOOKLM_MCP_ALLOW_EXTERNAL_BIND` | Allow MCP HTTP transport to bind a non-loopback host. Use only behind a trusted proxy. | Literal `1` enables; all other values disabled. | `mcp.__main__._check_http_bind_allowed` → `_serving.check_bind_allowed` |
| `NOTEBOOKLM_MCP_TRUST_PROXY` | Trust the proxy-set `CF-Connecting-IP` header as the self-hosted-OAuth login-throttle key. Enable only behind a trusted proxy (e.g. the Cloudflare tunnel); default off keys the throttle on the socket peer. | Literal `1` enables; all other values disabled. | `mcp._oauth.get_oauth_config` / `_client_ip` |
| `NOTEBOOKLM_MCP_STRICT_IDS` | Strict IDs-only mode: MCP `notebook`/`source`/`note`/`artifact` references must be a full canonical id; names, titles, and short id prefixes are rejected before any list call (deterministic automation). Off by default → default name/prefix/title resolution is unchanged. | Literal `1` enables; all other values disabled. | `mcp._resolve._strict_ids_enabled` |
| `NOTEBOOKLM_SERVER_TOKEN` | Bearer token required by every REST `/v1` request. The server refuses to start when unset/empty. | `--token` flag → env var → startup failure | `server.__main__._check_token_configured` / `server._auth.require_auth` |
| `NOTEBOOKLM_SERVER_HOST` | REST server bind host. Non-loopback refused unless `NOTEBOOKLM_SERVER_ALLOW_EXTERNAL_BIND=1`. | `--host` flag → env var → `127.0.0.1` | `server.__main__._build_parser` / `_serving.check_bind_allowed` |
| `NOTEBOOKLM_SERVER_PORT` | REST server bind port. | `--port` flag → env var → `8000` | `server.__main__._build_parser` / `_resolve_port` |
| `NOTEBOOKLM_SERVER_ALLOW_EXTERNAL_BIND` | Allow REST server to bind a non-loopback host. Use only behind a trusted proxy. | Literal `1` enables; all other values disabled. | `server.__main__._check_bind_allowed` → `_serving.check_bind_allowed` |
| `NOTEBOOKLM_SERVER_SOURCE_MUTATION_CONCURRENCY` | Max concurrent REST source create/rename/delete/batch handlers. | Env var → `4`; blank uses default; must be integer `>=1`. | `server._limits.ServerLimiters.from_env` |
| `NOTEBOOKLM_SERVER_SOURCE_WAIT_CONCURRENCY` | Max concurrent REST source wait handlers. | Env var → `4`; blank uses default; must be integer `>=1`. | `server._limits.ServerLimiters.from_env` |
| `NOTEBOOKLM_SERVER_GENERATION_CONCURRENCY` | Max concurrent REST artifact generation/retry handlers. | Env var → `2`; blank uses default; must be integer `>=1`. | `server._limits.ServerLimiters.from_env` |
| `NOTEBOOKLM_SERVER_DOWNLOAD_CONCURRENCY` | Max concurrent REST artifact download handlers. | Env var → `2`; blank uses default; must be integer `>=1`. | `server._limits.ServerLimiters.from_env` |
| `NOTEBOOKLM_SERVER_RESEARCH_CONCURRENCY` | Max concurrent REST research start/cancel/import handlers. | Env var → `2`; blank uses default; must be integer `>=1`. | `server._limits.ServerLimiters.from_env` |
| `NOTEBOOKLM_SERVER_CHAT_CONCURRENCY` | Max concurrent REST blocking chat ask handlers. | Env var → `4`; blank uses default; must be integer `>=1`. | `server._limits.ServerLimiters.from_env` |
| `NOTEBOOKLM_VCR_RECORD_ERRORS` | Synthetic-error injection mode for VCR test cassettes. Lowercase-normalized; valid values are `429` (rate limit), `5xx` (server error), or `expired_csrf` (CSRF token expiration). Used to record synthetic error cassettes under VCR. | Process env on each request, evaluated by `ErrorInjectionMiddleware` to intercept and synthesize failures. | `_error_injection._get_error_injection_mode` |

**Boolean handling.** `NOTEBOOKLM_DEBUG_RPC` treats `1` / `true` / `yes`
(case-insensitive) as truthy; everything else is falsy.
`NOTEBOOKLM_STRICT_DECODE` is ignored as of v0.7.0 (strict decoding is the
only mode); no value changes decoder behavior.
`NOTEBOOKLM_QUIET_DEPRECATIONS` treats `1` / `true` / `yes` / `on`
(case-insensitive) as truthy; any other value (including unset) leaves
deprecation warnings enabled. It silences the project's public-API
`DeprecationWarning`s — the `get()`-returns-`None` warning and deprecated
keyword aliases (e.g. `ResearchAPI.wait_for_completion`'s legacy `interval=`) —
without changing their behavior.
`NOTEBOOKLM_NOTEBOOK` is treated as unset when empty or whitespace-only so a
bare `export NOTEBOOKLM_NOTEBOOK=` does not block `notebooklm use` /
`-n/--notebook` from resolving.

**The `--quiet` global flag.** `notebooklm --quiet <subcommand>` suppresses
status prose and raises the `notebooklm` package logger floor to `ERROR` for
the duration of one invocation, so cron and CI logs stay clean while real
failures still surface. Structured `--json` payloads are still emitted. It is
mutually exclusive with `-v/-vv` — combining the two raises a `UsageError`
(exit `2`) since the resolved log levels conflict (`ERROR` vs `INFO`/`DEBUG`).
For shell-wide / always-on log suppression, use `NOTEBOOKLM_LOG_LEVEL`.

### NOTEBOOKLM_HOME

Relocates all configuration files to a custom directory:

```bash
export NOTEBOOKLM_HOME=/custom/path

# All files now go here:
# /custom/path/profiles//storage_state.json
# /custom/path/profiles//context.json
# /custom/path/profiles//browser_profile/
```

**Use cases:**
- Per-project isolation
- Custom storage locations

### NOTEBOOKLM_PROFILE

Selects the active profile without changing the persisted default:

```bash
export NOTEBOOKLM_PROFILE=work
notebooklm list   # Uses ~/.notebooklm/profiles/work/
```

Equivalent to passing `-p work` on every command. The CLI flag takes precedence over the env var.

The **persisted default** is read from the `default_profile` key of
`~/.notebooklm/config.json` (set via `notebooklm profile switch <name>`). When
neither a `-p/--profile` flag nor `NOTEBOOKLM_PROFILE` is set, `paths.resolve_profile`
falls back to this value (and finally to `"default"` if `config.json` doesn't
exist or has no `default_profile` key).

### NOTEBOOKLM_AUTH_JSON

Provides authentication inline without writing files. Ideal for CI/CD:

```bash
export NOTEBOOKLM_AUTH_JSON='{"cookies":[...]}'
notebooklm list  # Works without any file on disk
```

**Precedence:**
1. `--storage` CLI flag (highest)
2. `NOTEBOOKLM_AUTH_JSON` environment variable
3. Profile-aware path: `$NOTEBOOKLM_HOME/profiles//storage_state.json`
4. `~/.notebooklm/profiles/default/storage_state.json` (default)
5. `~/.notebooklm/storage_state.json` (legacy fallback)

**Note:** Cannot run `notebooklm login` when `NOTEBOOKLM_AUTH_JSON` is set.

### `NOTEBOOKLM_REFRESH_CMD`

Optional. If set, this command is invoked when an auth refresh is required —
replacing the default browser-cookie path. The contract is **exit-code based**:

1. The command must exit `0`.
2. On exit, it must have written a refreshed `storage_state.json` at the
   path in `NOTEBOOKLM_REFRESH_STORAGE_PATH`. The parent sets this child
   env var to the explicit storage path or the resolved profile-aware
   storage path before spawning the command.

The parent then reloads cookies from disk and retries the token fetch. Stdout
and stderr are **not** parsed — they are only captured for inclusion in the
error message when the command exits non-zero. Read by the
`NOTEBOOKLM_REFRESH_CMD_ENV` constant in `notebooklm.auth`.
The child process also receives `NOTEBOOKLM_REFRESH_PROFILE` with the resolved
profile name.

See also `NOTEBOOKLM_REFRESH_CMD_USE_SHELL` to opt back into `shell=True`
parsing.

By default the command fires only at **active cold start** — client construction
/ `AuthTokens.from_storage`, which run the recovery ladder when the stored
cookies are dead. **Passive readiness probes do NOT invoke it**: `auth check
--test --passive` (and other passive token fetches) use the strict, no-recovery
loader and never enter the refresh path, so a passive check reports expiry
without spawning the command. Set `NOTEBOOKLM_REFRESH_CMD_MIDSESSION=1` to also
fire it **mid-session** (the L2.5 rung of the unified recovery ladder), e.g.
inside a long-lived server that has been running past cookie expiry. This is **opt-in for
one release** and flips to default-on afterward; enable it only once you have
confirmed your command is safe to invoke while the client is live. When you need
to see what the command printed, set `NOTEBOOKLM_REFRESH_CMD_LOG_OUTPUT=1` to
route its captured `stdout`/`stderr` into the redacting DEBUG logger — the
default DEBUG line records only the command basename, exit code, and byte
counts. See [ADR-0030](adr/0030-one-recovery-ladder.md) for the ladder design.

### NOTEBOOKLM_HL

Sets the default interface/output language used by the client. The value is
passed as the `hl` query parameter on every batchexecute RPC call and is used by
the `generate audio|video|slide-deck|infographic|data-table|mind-map|report`
commands. In the Python API, omitted `language` on `ArtifactsAPI.generate_*`
keeps the historical `"en"` artifact default; pass `language=None` to opt in to
this `NOTEBOOKLM_HL` resolver.

```bash
export NOTEBOOKLM_HL=ja
notebooklm generate audio "deep dive"   # Japanese audio overview
```

Surrounding whitespace is stripped; an empty or whitespace-only value falls
back to `en`. For the generate commands, the resolution order is:

1. `--language` CLI flag
2. `NOTEBOOKLM_HL` environment variable
3. `language` value from the **global** `~/.notebooklm/config.json` (set via
   `notebooklm language set <code>`). The language is stored once per
   `NOTEBOOKLM_HOME`, **not** per profile — switching `notebooklm -p work`
   does not switch the configured language. See
   `src/notebooklm/cli/language_cmd.py` for the resolver and
   `src/notebooklm/paths.py` for the storage location.
4. `en` (built-in default)

### NOTEBOOKLM_QUIET_DEPRECATIONS

Suppresses the project's public-API `DeprecationWarning`s while you migrate. Set
it to a truthy value (`1`, `true`, `yes`, or `on`, case-insensitive) to silence
them; any other value (including unset) leaves them enabled.

It gates the one-off deprecation warnings routed through
`src/notebooklm/_deprecation.py::warn_deprecated` — e.g. awaiting
`NotebookLMClient.from_storage(...)` instead of using the `async with` form. (The
v0.7.0 error-contract runways it also gated — the `get()`-returns-`None` warning,
the `wait_for_completion(interval=...)` alias, and the dict-subscript bridge — all
**completed their removal in v0.8.0**, so those warnings no longer exist; see
[`deprecations.md`](deprecations.md).)

The helper that reads this variable is
`notebooklm._deprecation._deprecations_quiet` (public alias
`deprecations_quiet`).

```bash
# Silence the project's public-API deprecation warnings while migrating
export NOTEBOOKLM_QUIET_DEPRECATIONS=1
```

> Note: this variable does **not** affect `source add --mime-type` /
> `client.sources.add_file(mime_type=...)` — `mime_type` is a supported
> parameter and emits no warning.

### NOTEBOOKLM_FUTURE_ERRORS (removed in v0.8.0)

**Removed.** This was the v0.7.0 forward-compat preview gate for the v0.8.0 error
contract (ADR-0019 / umbrella
[#1346](https://github.com/teng-lin/notebooklm-py/issues/1346)): setting it made
the v0.7.0 warn-runways adopt their v0.8.0 raise-target early so you could test
forward-compatibility before the breaking flips shipped. v0.8.0 makes every one
of those flips the default — `get()` raising `*NotFoundError` on a miss, the
attribute-only typed returns, the removed `interval=` alias, the bool→`None`
returns, the refusal-raises, and the mutate-existing fail-loud — so the flag and
its resolver were deleted. **Setting `NOTEBOOKLM_FUTURE_ERRORS` now has no
effect.** Remove it from your environment / CI config. See
[`deprecations.md`](deprecations.md) for the full Removed-in-v0.8.0 table.

### Timeouts

Most batchexecute RPCs issued by the client (whether through `NotebookLMClient`
or any of the CLI commands) use a **30-second** HTTP request timeout by default,
with a tighter **10-second** connection-establishment timeout. The shorter
connect timeout helps surface network-level issues quickly while the read
timeout accommodates slow server responses. The timeout is exposed as a
constructor argument on `NotebookLMClient` (`timeout=`) for callers that need to
tune it per-workload — see the `DEFAULT_TIMEOUT` / `DEFAULT_CONNECT_TIMEOUT`
constants in `src/notebooklm/_runtime/config.py`.

The chat streaming endpoint (`ChatAPI.ask`) also exposes a separate per-read
silence window (`chat_timeout=`). Left unset it is **`max(180 s, timeout=)`** —
180 seconds because shared notebooks can be slow to send the first streamed chat
byte, floored at `timeout=` so a larger configured budget still reaches chat (see
[below](#how-the-per-rpc-windows-compose-with-timeout)); fast metadata RPCs stay
on the normal **30-second** timeout. A chat read timeout means the server
sent no stream bytes for that window, either before the first byte or between
chunks; it does not mean total generation time exceeded 30 seconds. Pass
`chat_timeout=None` to inherit the normal client timeout for chat. The CLI
`ask --request-timeout N` flag overrides both values for that invocation.

`IMPORT_RESEARCH` (`research.import_sources`) has a third window: the server
ingests every requested entry before answering one RPC, so the window scales
with batch size — **60 s + 3 s per requested source, capped at 240 s** — and is
tunable outright via `import_research_timeout=`.

#### How the per-RPC windows compose with `timeout=`

`timeout=` is the *base* read budget for every RPC. The two built-in per-RPC
windows above are **defaults, not caps**: they only ever lengthen that base,
never shorten it. So `NotebookLMClient(auth, timeout=600)` really does buy 600
seconds everywhere, including chat and IMPORT_RESEARCH (before this rule landed
they silently clamped such a client back to 180 s / 240 s — issue #2205).

| Construction | chat window | IMPORT_RESEARCH window (1 source) |
| --- | --- | --- |
| `NotebookLMClient(auth)` | 180 s | 63 s |
| `NotebookLMClient(auth, timeout=600)` | 600 s | 600 s |
| `NotebookLMClient(auth, timeout=600, chat_timeout=10)` | 10 s | 600 s |
| `NotebookLMClient(auth, import_research_timeout=900)` | 180 s | 900 s |

An explicitly passed `chat_timeout=` / `import_research_timeout=` is the
caller's final word and is used as given — including a value *below* `timeout=`,
so deliberately fast failure stays expressible. Only the untouched defaults
compose. Both kwargs read identically:

| value | meaning |
| --- | --- |
| unset | the built-in window, floored at `timeout=` |
| a number | exactly that window, replacing the built-in and the floor |
| `None` | inherit `timeout=` verbatim, with no per-RPC window at all |

A non-positive or non-finite value for either raises `ValueError` at
construction rather than silently producing a window that times out instantly.

Independently, an IMPORT_RESEARCH attempt made by
`import_sources_with_verification` is clamped to whatever is left of that call's
own `max_elapsed` retry budget, so a late retry cannot be *granted* a window
larger than the budget it has left. Note what that does and does not promise:
every timeout here is an `httpx` slot, and the read slot is an inactivity limit
between socket reads — so `max_elapsed` bounds when a new attempt may *start*,
not the wall-clock duration of one already in flight. Enforcing the latter would
mean cancelling an in-flight non-idempotent POST, which risks duplicated sources
the client can no longer see. When less than 10 seconds of that budget remains — too little for an
attempt to outlast connection establishment — the loop stops instead of sending
one: `IMPORT_RESEARCH` is non-idempotent, so an attempt whose result the client
cannot observe can still commit sources server-side and duplicate them. The
first attempt is exempt, so `max_elapsed=0` still means "try once".

### Decoder strictness

NotebookLM's batchexecute responses are obfuscated, undocumented, and reshaped
by Google without notice. The decoder uses a shared `safe_index` helper to walk
nested response payloads. When it can't descend (an index is out of range, or
the value at a step isn't indexable), it **raises**
`UnknownRPCMethodError` (a subclass of `DecodingError` / `RPCError`) with
structured `method_id`, `path`, `source`, and `data_at_failure` attributes.

Strict decoding is the only mode. The legacy `NOTEBOOKLM_STRICT_DECODE=0`
warn-and-return-`None` opt-out (which emitted a `DeprecationWarning` through
v0.5.0/v0.6.0) was **retired in v0.7.0**; the env var is now ignored. The
strict contract surfaces real schema drift (Google rotating a response shape)
as a typed exception instead of a silent `None` return, so callers that
previously treated `None` as a sentinel must handle `UnknownRPCMethodError`.

The same `UnknownRPCMethodError` is also raised by `decode_response()` when the
batchexecute response contains RPC IDs but not the one the call requested
(typically a sign that Google rotated the method ID).

> Background and rationale for the flip: see
> [`docs/adr/0011-schema-validation-policy.md`](adr/0011-schema-validation-policy.md).

## CLI Options

### Global Options

| Option | Description | Default |
|--------|-------------|---------|
| `--storage PATH` | Path to storage_state.json | `$NOTEBOOKLM_HOME/profiles//storage_state.json` |
| `-p, --profile NAME` | Use a named profile | Active profile or `default` |
| `-v, --verbose` | Enable verbose output (`-v` for INFO, `-vv` for DEBUG) | - |
| `--quiet` | Suppress status output and INFO/WARN logs (only errors survive). Mutually exclusive with `-v`. | - |
| `--version` | Show version | - |
| `--help` | Show help | - |

### Viewing Configuration

See where your configuration files are located:

```bash
notebooklm status --paths
```

Output:
```
                 Configuration Paths
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ File             ┃ Path                                     ┃ Source    ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Profile          │ default                                  │ default   │
│ Home Directory   │ /home/user/.notebooklm                   │ default   │
│ Profile Directory│ /home/user/.notebooklm/profiles/default  │           │
│ Storage State    │ .../profiles/default/storage_state.json  │           │
│ Context          │ .../profiles/default/context.json        │           │
│ Browser Profile  │ .../profiles/default/browser_profile     │           │
└──────────────────┴──────────────────────────────────────────┴───────────┘
```

## Session Management

### Session Lifetime

Authentication sessions are tied to Google's cookie expiration:
- Sessions typically last several days to weeks
- Google may invalidate sessions for security reasons
- Rate limiting or suspicious activity can trigger earlier expiration

### Refreshing Sessions

**Automatic Refresh:** CSRF tokens and session IDs are automatically refreshed when authentication errors are detected. This handles most "session expired" errors transparently.

**Manual Re-authentication:** If your session cookies have fully expired (automatic refresh won't help), re-authenticate:

```bash
notebooklm login
```

### Multiple Accounts

**Profiles (recommended):** Use named profiles to manage multiple Google accounts under a single home directory:

```bash
# Create and authenticate profiles
notebooklm profile create work
notebooklm -p work login
notebooklm -p work list

notebooklm profile create personal
notebooklm -p personal login
notebooklm -p personal list

# Switch the active profile
notebooklm profile switch work
notebooklm list   # Uses work profile

# List all profiles
notebooklm profile list

# Use env var for session-wide override
export NOTEBOOKLM_PROFILE=personal
notebooklm list   # Uses personal profile
```

Each profile stores its own `storage_state.json`, `context.json`, and `browser_profile/` under `~/.notebooklm/profiles/<name>/`.

**Alternative: `NOTEBOOKLM_HOME`** still works for full directory-level isolation:

```bash
export NOTEBOOKLM_HOME=~/.notebooklm-work
notebooklm login
```

**One-off override with `--storage`:**

```bash
notebooklm --storage /path/to/storage_state.json list
```

When `--storage ` is set, **two different context files** are used —
they are NOT the same file:

- **Notebook / conversation context** lives at a *suffixed* file
  `.context.json` (`storage_path.with_suffix(storage_path.suffix + ".context.json")`,
  see `paths.get_context_path`). Two `--storage` invocations against different
  files cannot see each other's selected notebook, and neither pollutes the
  default profile context.
- **Account-routing metadata** (the `notebooklm.account` object — `authuser`
  index and optional `email`) lives in-band inside the selected
  `storage_state.json`. This keeps copied files and `NOTEBOOKLM_AUTH_JSON`
  secrets bound to the same Google account route as the original profile.
- **Persistent browser state** lives beside the selected storage file. A path
  named `storage_state.json` uses `browser_profile/`; other filenames append
  `.browser_profile` to the full filename when it fits, or use a stable
  canonical-path hash when it does not. Login, headless re-auth, doctor's L3
  readiness row, `status --paths`, and logout all resolve the same
  storage-specific directory. Recursive deletion by `login --fresh` or logout
  requires either the ownership marker or a canonical managed profile layout.

Run `notebooklm --storage  status --paths` to see exactly which
storage, context, and browser-profile paths are in use.

## CI/CD Configuration

### GitHub Actions (Recommended)

Use `NOTEBOOKLM_AUTH_JSON` for secure, file-free authentication:

```yaml
jobs:
  notebook-task:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install notebooklm-py
        run: pip install notebooklm-py

      # Pre-flight: fail fast and loud on missing/expired auth.
      # `auth check --json` returns exit 0 even when status is "error"; --test makes the network
      # call needed to detect expired cookies, and the `jq -e` flag converts a non-"ok" status
      # into a non-zero exit code so the runner step actually fails.
      - name: Verify auth (fail-fast on expired cookies)
        env:
          NOTEBOOKLM_AUTH_JSON: ${{ secrets.NOTEBOOKLM_AUTH_JSON }}
        run: notebooklm auth check --test --json | jq -e '.status == "ok"'

      - name: List notebooks
        env:
          NOTEBOOKLM_AUTH_JSON: ${{ secrets.NOTEBOOKLM_AUTH_JSON }}
        run: notebooklm list
```

**Benefits:**
- No file writes needed
- Secret stays in memory only
- Clean, simple workflow

### Obtaining the Secret Value

1. Run `notebooklm login` locally
2. If the file was created by an older Playwright login and lacks `notebooklm.account`, run `notebooklm auth refresh` to repair single-account states or re-login with `notebooklm login --browser-cookies  --account EMAIL` for multi-account states
3. Copy the contents of `~/.notebooklm/profiles/default/storage_state.json` (the canonical write location; the legacy `~/.notebooklm/storage_state.json` is only read as a fallback)
4. Add as a GitHub repository secret named `NOTEBOOKLM_AUTH_JSON` (see [installation.md#d-headless-server-or-ci](installation.md#d-headless-server-or-ci) for trailing-newline + ephemeral-runner refresh notes)

### Alternative: File-Based Auth

If you prefer file-based authentication:

```yaml
- name: Setup NotebookLM auth
  run: |
    mkdir -p ~/.notebooklm/profiles/default
    echo "${{ secrets.NOTEBOOKLM_AUTH_JSON }}" > ~/.notebooklm/profiles/default/storage_state.json
    chmod 600 ~/.notebooklm/profiles/default/storage_state.json

- name: List notebooks
  run: notebooklm list
```

For profile-specific CI auth:

```yaml
- name: Setup work profile auth
  run: |
    mkdir -p ~/.notebooklm/profiles/work
    echo "${{ secrets.WORK_AUTH_JSON }}" > ~/.notebooklm/profiles/work/storage_state.json
    chmod 600 ~/.notebooklm/profiles/work/storage_state.json

- name: List notebooks (work)
  run: notebooklm -p work list
```

### Session Expiration

CSRF tokens are automatically refreshed during API calls. However, the underlying session cookies still expire. For long-running CI pipelines:
- Update the `NOTEBOOKLM_AUTH_JSON` secret every 1-2 weeks
- Monitor for persistent auth failures (these indicate cookie expiration)

## Debugging

### Enable Verbose Output

Some commands support verbose output via Rich console:

```bash
# Most errors are printed to stderr with details
notebooklm list 2>&1 | cat
```

### Enable RPC Debug Logging

```bash
NOTEBOOKLM_DEBUG_RPC=1 notebooklm list
```

### Logger namespace compatibility

Runtime transport and middleware logs still use the historical
`notebooklm._core` logger key via `CORE_LOGGER_NAME`. That name is a
compatibility logging contract for existing filters and tests; it does not
mean the deleted `_core.py` module or a concrete `Session` owner still exists.

### Check Authentication

Verify your session is working:

```bash
# Should list notebooks or show empty list
notebooklm list

# If you see "Unauthorized" or redirect errors, re-login
notebooklm login
```

### Check Configuration Paths

```bash
# See where files are being read from
notebooklm status --paths
```

### Network Issues

The CLI uses `httpx` for HTTP requests. Common issues:

- **Timeout**: Google's API can be slow; large operations may time out
- **SSL errors**: Ensure your system certificates are up to date
- **Proxy**: Set standard environment variables (`HTTP_PROXY`, `HTTPS_PROXY`) if needed

## Platform Notes

### macOS

Works out of the box. Chromium is downloaded automatically by Playwright.

### Linux

For Playwright system dependencies and the Chromium install on Debian/Ubuntu, see [docs/installation.md#platform-notes](installation.md#platform-notes) (and [troubleshooting.md#linux](troubleshooting.md#linux) if you hit `TypeError: onExit is not a function`).

### Windows

Works with PowerShell or CMD. Use backslashes for paths:

```powershell
notebooklm --storage C:\Users\Name\.notebooklm\storage_state.json list
```

Or set environment variable:

```powershell
$env:NOTEBOOKLM_HOME = "C:\Users\Name\custom-notebooklm"
notebooklm list
```

### WSL

Browser login opens in the Windows host browser. The storage file is saved in the WSL filesystem.

### Headless Servers & Containers

**Playwright is only required for the `notebooklm login` command.** All other operations use standard HTTP requests via `httpx`.

For the install + auth-bootstrap recipe (run `notebooklm login` on a workstation, copy `storage_state.json` to the server, set `NOTEBOOKLM_AUTH_JSON`), see the canonical Persona D guide: [docs/installation.md#d-headless-server-or-ci](installation.md#d-headless-server-or-ci).