## File: README.md

# Gemini Notebook (formerly Google NotebookLM) CLI & MCP Server



[](https://pypi.org/project/notebooklm-mcp-cli/)
[](https://pypistats.org/packages/notebooklm-mcp-cli)
[](https://pepy.tech/projects/notebooklm-mcp-cli)
[](https://pypi.org/project/notebooklm-mcp-cli/)
[](https://github.com/jacob-bd/gemini-notebook-mcp-cli/blob/main/LICENSE)

**Programmatic access to Gemini Notebook** — via command-line interface (CLI) or Model Context Protocol (MCP) server.

> 🚨 **New: Protected login storage (recommended)**
> Your saved Google login can now be encrypted, with its key kept in your computer's keychain instead of a plain file. We highly recommend everyone switch on a personal computer:
>
>     nlm auth storage set protected
>
> 💡 You can also use the 'nlm setup' wizard and select the 'Credentials Protection' option in the wizard
> 
> Optional: nothing changes unless you turn it on. Servers, cron, Docker and SSH setups can keep the plain file.
> 📖 [How it works](docs/AUTHENTICATION.md#protected-storage) · 📺 [Watch this short video](https://www.youtube.com/shorts/7ZmF_lwJh3M)
>
> 🤖 Fun fact: this video was created by an agent using the gemini-notebook-mcp.

> **Note:** Personal/consumer accounts are tested regularly. Gemini Notebook Enterprise support is experimental. The documented `notebook.cloud.google.com` host has been live-verified with a project-qualified `global` deployment; other Enterprise host variants may require additional validation.

> ☕ **If you find notebooklm-mcp-cli useful, consider [buying me a coffee](https://buymeacoffee.com/jacobbd).**
> It's free and built in my spare time — but testing every Gemini Notebook feature takes real time and resources. A coffee helps me cover it and keep shipping. Thank you! 🙏
>
> [](https://buymeacoffee.com/jacobbd)

📺 **Watch the Demos**

### Latest

| **Claude Cowork & Interactive Reports** | **Codex Setup + Cinematic Video & Slides** |
| :---: | :---: |
| [](https://www.youtube.com/watch?v=UXxqTpNNGt0) | [](https://www.youtube.com/watch?v=KrgLCrvU1dw) |

### MCP Demos

|                                              **General Overview**                                               |                                               **Claude Desktop**                                               |                                               **Perplexity Desktop**                                               |                                                  **MCP Super Assistant**                                                   |
| :-------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------: |
| [](https://www.youtube.com/watch?v=d-PZDQlO4m4) | [](https://www.youtube.com/watch?v=PU8JhgLPxes) | [](https://www.youtube.com/watch?v=BCKlDNg-qxs) | [](https://www.youtube.com/watch?v=7aHDbkr-l_E) |

### CLI Demos

|                                                   **CLI Overview**                                                   |                                                   **CLI, MCP & Skills**                                                   |                                                   **Setup, Doctor & mcpb**                                                   |                                               **Infographics Support**                                               |
| :------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------: |
| [](https://www.youtube.com/watch?v=XyXVuALWZkE) | [](https://www.youtube.com/watch?v=ZQBQigFK-E8) | [](https://www.youtube.com/watch?v=5tOUilBTJ3Q) | [](https://www.youtube.com/watch?v=Uc6iH5NuQ9A) |

## ⚡ Quick Start

Three commands take you from nothing to a connected AI assistant:

1. Install (gives you `nlm` and the MCP server):

```bash
uv tool install notebooklm-mcp-cli
```

2. Sign in to your Google account:

```bash
nlm login
```

3. Connect your AI tools:

```bash
nlm setup
```

### The setup wizard (`nlm setup`)

`nlm setup` finds the AI tools installed on your machine and connects them to
Gemini Notebook — no config files to edit. Pick an option with the arrow keys:

| Menu option | What it does |
|---|---|
| **Show my tools' status** | One table for every detected tool: MCP connection (✓ set up · ✗ not yet · ⚠ old name) and skill version (⬆ when an upgrade is available). |
| **Add the MCP to my tools/agents** | Tick the tools to connect (Space), then Enter. Nothing is pre-selected. Connections still using the old `notebooklm-mcp` name appear under **Needs a fix** and are renamed to `gemini-notebook-mcp`, keeping their settings. Then it offers the skill. |
| **Add the skill to my tools/agents** | Installs the skill that teaches your AI how to use Gemini Notebook well — for **All my projects** (default) or **Just this folder**. Upgrades are pre-ticked; older versions are only replaced after you confirm. |
| **Remove an MCP or skill** | Lists what's installed, grouped into MCP connections and skills. Nothing is pre-selected, and each group needs its own confirmation. |
| **Credential protection** | Protect your saved logins in your OS keystore, or restore them to plain files. It only offers what applies: *Protect* when a login is plain, *Restore* when one is protected. With several logins you get a checkbox picker. |
| **Copy MCP setup for a tool not listed** | Copies a ready-to-paste JSON snippet to your clipboard. **Advanced options** switch to uvx, the bare command, or entry-only JSON. |

- **Esc** goes back from any screen (and quits from the main menu).
- Every file is **backed up** to `~/.notebooklm-mcp-cli/backups/` before it is changed.
- **Connects to:** Claude Code, Claude Desktop (regular and Relay AI / 3P), Codex CLI / ChatGPT desktop app, Gemini CLI, Cursor, Windsurf, Cline, Antigravity, GitHub Copilot (VS Code), and OpenCode. The skill also installs for Hermes and OpenClaw.
- **Claude Desktop Chat & Cowork, and claude.ai:** these only use skills uploaded to your Claude account. In **Add the skill**, tick **Claude Desktop / claude.ai** — the wizard saves `~/Downloads/nlm-skill.zip` and shows where to upload it (**Customize → Skills → Add**). Or run `nlm skill package`.
- **Claude Desktop:** fully quit it (Cmd+Q) before connecting — it can overwrite its settings while open, so the wizard refuses to write until it's closed.
- **Scripts and CI:** every option has a direct command — `nlm setup add <tool>`, `nlm setup remove <tool>`, `nlm setup list`, `nlm skill install <tool>`. See [MCP Configuration](#mcp-configuration).

## Two Ways to Use

### 🖥️ Command-Line Interface (CLI)

Use `nlm` directly in your terminal for scripting, automation, or interactive use:

```bash
nlm notebook list                              # List all notebooks
nlm notebook create "Research Project"         # Create a notebook
nlm source add <notebook> --url "https://..."  # Add sources
nlm audio create <notebook> --confirm          # Generate podcast
nlm download audio <notebook> --id <artifact-id>    # Download audio file
nlm download all <notebook> -d ./exports       # Download every artifact
nlm share public <notebook>                    # Enable public link
```

Run `nlm --ai` for comprehensive AI-assistant documentation.

### 🤖 MCP Server (for AI Agents)

Connect AI assistants (Claude, Gemini, Cursor, etc.) to Gemini Notebook:

```bash
nlm setup                       # Guided wizard — see Quick Start above
nlm setup add codex             # Or connect one client directly
```

Then use natural language: _"Create a notebook about quantum computing and generate a podcast"_

## Features

| Capability                                    | CLI Command                     | MCP Tool                             |
| --------------------------------------------- | ------------------------------- | ------------------------------------ |
| List notebooks                                | `nlm notebook list`             | `notebook_list`                      |
| Create notebook                               | `nlm notebook create`           | `notebook_create`                    |
| Add Sources (URL, Text, Drive, File)          | `nlm source add`                | `source_add`                         |
| Query notebook (persists to web UI)           | `nlm notebook query`            | `notebook_query`                     |
| List/view/export chat sessions                | `nlm chats list/get/export`     | `chat_list`/`chat_get`/`chat_export` |
| Create Studio Content (Audio, Video, etc.)    | `nlm studio create`             | `studio_create`                      |
| Interactive reports (read + embedded elements)| `nlm report get/elements/element` | `report` (action=get/elements/generate) |
| Revise slide decks                            | `nlm slides revise`             | `studio_revise`                      |
| Download artifacts                            | `nlm download <type>`           | `download_artifact`                  |
| Download all artifacts (one or all notebooks) | `nlm download all`              | `download_all_artifacts`             |
| Web/Drive research                            | `nlm research start`            | `research_start`                     |
| Share notebook                                | `nlm share public/invite`       | `notebook_share_*`                   |
| Sync Drive sources                            | `nlm source sync`               | `source_sync_drive`                  |
| Batch operations                              | `nlm batch query/create/delete` | `batch`                              |
| Cross-notebook query                          | `nlm cross query`               | `cross_notebook_query`               |
| Pipelines (multi-step workflows)              | `nlm pipeline run/list`         | `pipeline`                           |
| Tag & smart select                            | `nlm tag add/list/select`       | `tag`                                |
| Configure AI tools                            | `nlm setup` (wizard), `nlm setup add/remove/list` | —                          |
| Install AI Skills                             | `nlm skill install/update`      | —                                    |
| Diagnose issues                               | `nlm doctor`                    | —                                    |
| Check remaining plan usage                    | `nlm usage [--profile <name>]`  | `usage_get`                          |
| Manage login encryption (Protected mode)      | `nlm auth storage status/set`   | —                                    |

📚 **More Documentation:**

- **[Getting Started](docs/GETTING_STARTED.md)** — Install, login, agent setup, and migration from another Gemini Notebook MCP
- **[CLI Guide](docs/CLI_GUIDE.md)** — Complete command reference
- **[MCP Guide](docs/MCP_GUIDE.md)** — All 53 MCP tools with examples
- **[Authentication](docs/AUTHENTICATION.md)** — Setup and troubleshooting
- **[Remote MCP](docs/REMOTE_MCP.md)** — Web/mobile connector feasibility, security, and authentication limitations
- **[API Reference](docs/API_REFERENCE.md)** — Internal API docs for contributors

## Important Disclaimer

This MCP and CLI use **internal APIs** that:

- Are undocumented and may change without notice
- Require cookie extraction from your browser (I have a tool for that!)
- May not be aligned with Google's Terms of Service

Use at your own risk for personal/experimental purposes.

## Installation

> 🆕 **Claude Desktop users:** [Download the extension](https://github.com/jacob-bd/gemini-notebook-mcp-cli/releases/latest) (`.mcpb` file) → double-click → done! One-click install, no config needed.

Install from PyPI. This single package includes **both the CLI and MCP server**:

### Using uv (Recommended)

```bash
uv tool install notebooklm-mcp-cli
```

### Using uvx (Run Without Install)

```bash
uvx --from notebooklm-mcp-cli nlm --help
uvx --from notebooklm-mcp-cli notebooklm-mcp
```

### Using pip

```bash
pip install notebooklm-mcp-cli
```

### Using pipx

```bash
pipx install notebooklm-mcp-cli
```

**After installation, you get:**

- `nlm` — Command-line interface
- `notebooklm-mcp` — Gemini Notebook MCP server for AI assistants


Alternative: Install from Source

```bash
# Clone the repository
git clone https://github.com/jacob-bd/gemini-notebook-mcp-cli.git
cd notebooklm-mcp

# Install with uv
uv tool install .
```



## Upgrading

```bash
# Using uv
uv tool upgrade notebooklm-mcp-cli

# Using pip
pip install --upgrade notebooklm-mcp-cli

# Using pipx
pipx upgrade notebooklm-mcp-cli
```

After upgrading, restart your AI tool to reconnect to the updated MCP server:

- **Claude Code:** Restart the application, or use `/mcp` to reconnect
- **Cursor:** Restart the application
- **Gemini CLI:** Restart the CLI session

## Upgrading from Legacy Versions

If you previously installed the **separate** CLI and MCP packages, you need to migrate to the unified package.

### Step 1: Check What You Have Installed

```bash
uv tool list | grep notebooklm
```

**Legacy packages to remove:**
| Package | What it was |
|---------|-------------|
| `notebooklm-cli` | Old CLI-only package |
| `notebooklm-mcp-server` | Old MCP-only package |

### Step 2: Uninstall Legacy Packages

```bash
# Remove old CLI package (if installed)
uv tool uninstall notebooklm-cli

# Remove old MCP package (if installed)
uv tool uninstall notebooklm-mcp-server
```

### Step 3: Reinstall the Unified Package

After removing legacy packages, reinstall to fix symlinks:

```bash
uv tool install --force notebooklm-mcp-cli
```

> **Why `--force`?** When multiple packages provide the same executable, `uv` can leave broken symlinks after uninstalling. The `--force` flag ensures clean symlinks.

### Step 4: Verify Installation

```bash
uv tool list | grep notebooklm
```

You should see only:

```
notebooklm-mcp-cli v0.2.0
- nlm
- notebooklm-mcp
```

### Step 5: Re-authenticate

Your existing cookies should still work, but if you encounter auth issues:

```bash
nlm login
```

> **Note:** The configured MCP server name is now `gemini-notebook-mcp`. The executable remains `notebooklm-mcp` for compatibility with existing installations.

## Getting Started

If you are setting up the tool for the first time — or migrating from a
browser-based Gemini Notebook MCP — see the
[Getting Started Guide](docs/GETTING_STARTED.md). It covers install,
login, agent registration, and a step-by-step migration path that avoids
the "two Gemini Notebook servers registered" trap.

## Uninstalling

To completely remove the MCP:

```bash
# Using uv
uv tool uninstall notebooklm-mcp-cli

# Using pip
pip uninstall notebooklm-mcp-cli

# Using pipx
pipx uninstall notebooklm-mcp-cli

# Remove cached auth tokens and data (optional)
rm -rf ~/.notebooklm-mcp-cli
```

Also remove from your AI tools:

```bash
nlm setup remove claude-code
nlm setup remove cursor
# ... or any configured tool
```

## Authentication

Before using the CLI or MCP, you need to authenticate with Gemini Notebook:

### CLI Authentication (Recommended)

```bash
# Auto mode: launches your browser, you log in, cookies extracted automatically
nlm login

# Check if already authenticated
nlm login --check

# Use a named profile (for multiple Google accounts)
nlm login --profile work
nlm login --profile personal

# Manual mode: import cookies from a file
nlm login --manual --file cookies.txt

# External CDP provider (e.g., OpenClaw-managed browser)
nlm login --provider openclaw --cdp-url http://127.0.0.1:18800
```

New profiles ask where to keep the saved login **before** the browser opens: **Protected** (encrypted, key in your OS keystore, recommended) or a plain file. Scripts can skip the question with `nlm login --storage protected` (or `--storage file`). Existing profiles keep their mode; change it any time in `nlm setup` → **Credential protection**.

**Profile management:**

```bash
nlm login --check                    # Show current auth status
nlm login switch            # Switch the default profile
nlm login profile list               # List all profiles with email addresses
nlm login profile delete    # Delete a profile
nlm login profile rename <old> <new> # Rename a profile
```

**From an MCP-only app (for example Claude CoWork):** ask the AI to use the `profile` tool. It can list your accounts, switch for the session, and, only if you ask, make one your saved default.

**Unattended / scheduled refresh:**

A live session self-heals — when Google's short-lived cookies age out, the
client automatically runs a headless refresh. For unattended machines you can
also refresh proactively from a scheduler so a session never lapses:

```bash
nlm auth refresh                     # Headless, no interaction; exits non-zero on failure
nlm auth refresh --profile work      # Refresh a named profile
```

Run it on a timer (cron/launchd). It needs a saved Chrome profile from a prior
`nlm login`, and does not apply when `NOTEBOOKLM_COOKIES` is set as an
environment variable (that value overrides saved credentials).

### Enterprise Authentication (Gemini Notebook Enterprise)

For organizations using **Gemini Notebook Enterprise** via Google Cloud, ask your Enterprise administrator for the project ID or number, the deployment location/multi-region, and confirmation that your account has access. The current documented host is `notebook.cloud.google.com`; older deployments may use `notebooklm.cloud.google.com` or `vertexaisearch.cloud.google.com`.

1. **Set Base URL, GCP Project ID & Location:**
   ```bash
   export NOTEBOOKLM_BASE_URL="https://notebook.cloud.google.com"
   # Required: your GCP project ID or project number from your administrator
   export NOTEBOOKLM_PROJECT_ID="your-gcp-project-id-or-number"
   # Optional: GCP location/multi-region from your administrator: global (default), us, or eu
   export NOTEBOOKLM_LOCATION="global"
   ```

2. **Save an Enterprise Auth Profile:**
   This opens the configured Enterprise URL in an isolated browser profile. Sign in with your organization account:
   ```bash
   nlm login --profile enterprise
   nlm login switch enterprise
   ```

   You can switch back to a personal profile at any time:
   ```bash
   unset NOTEBOOKLM_BASE_URL NOTEBOOKLM_PROJECT_ID NOTEBOOKLM_LOCATION
   nlm login switch personal
   ```

3. **Configure MCP Server with Enterprise Environment Variables:**
   When adding the MCP server to AI assistants (Claude, Cursor, etc.), pass `NOTEBOOKLM_BASE_URL`, `NOTEBOOKLM_PROJECT_ID`, and optionally `NOTEBOOKLM_LOCATION`:
   ```json
   {
     "mcpServers": {
       "gemini-notebook-mcp": {
         "command": "notebooklm-mcp",
         "env": {
           "NOTEBOOKLM_BASE_URL": "https://notebook.cloud.google.com",
           "NOTEBOOKLM_PROJECT_ID": "your-gcp-project-id-or-number",
           "NOTEBOOKLM_LOCATION": "global"
         }
       }
     }
   }
   ```

Each profile gets its own isolated browser session, so you can be logged into multiple Google accounts simultaneously. Enterprise environment variables apply to the current process, so keep them in an Enterprise-only shell or MCP configuration when you also use a personal account.

### Standalone Auth Tool

If you only need the MCP server (not the CLI):

```bash
nlm login              # Auto mode (launches browser)
nlm login --manual     # Manual file mode
```

**How it works:** Auto mode launches a dedicated browser profile (supports Chrome, Arc, Dia, Comet, Brave, Edge, Chromium, Firefox, and more), you log in to Google, and cookies are extracted automatically. Your login persists for future auth refreshes.

**Prefer a specific browser?** Set it with `nlm config set auth.browser chromium` (or `comet`, `dia`, `brave`, `arc`, `edge`, `chrome`, `firefox`, etc.). Falls back to auto-detection if the preferred named browser is not found. For any other Chromium-compatible browser, set its executable with `nlm config set auth.browser_path /path/to/browser` or `NLM_BROWSER_PATH`; an invalid explicit path fails closed instead of launching a different browser. When Chromium/CDP is unavailable, `auto` can fall back to Firefox.

For detailed instructions and troubleshooting, see **[docs/AUTHENTICATION.md](docs/AUTHENTICATION.md)**.

## MCP Configuration

> **⚠️ Context Window Warning:** This MCP provides **53 tools**. Disable it when not using Gemini Notebook to preserve context. In Claude Code: `@gemini-notebook-mcp` to toggle. To keep it on but expose only a subset, see [Selective tool exposure](docs/MCP_GUIDE.md#selective-tool-exposure).

### Automatic Setup (Recommended)

The setup wizard is the recommended way to connect tools — see
[Quick Start](#-quick-start) for what each menu option does:

```bash
nlm setup
```

MCP configuration goes to each app's user-level settings. Codex CLI and the
ChatGPT desktop app share one MCP configuration on the same host. GitHub
Copilot uses the VS Code user profile in the wizard; the direct command without
`--scope user` targets the current workspace.

For direct setup or scripts, keep using the existing commands:

```bash
nlm setup add codex                         # Codex CLI / ChatGPT desktop
nlm setup add github-copilot --scope user   # VS Code user profile
nlm setup add github-copilot                # Current workspace
nlm setup add claude-desktop --profile 3p   # Relay AI / Claude 3P
nlm setup list                              # Show MCP configuration status
```

Claude Desktop setup only writes to profiles that are detected as present. If
both regular and Relay AI/3P profiles exist, the CLI asks whether to configure
regular, 3P, or both. For scripts, use `--profile regular|3p|both`. If no
Claude Desktop profile is detected, nothing is created or changed.

Removal uses the same profile selection, for example
`nlm setup remove claude-desktop --profile regular`.
Removal only offers detected profiles containing this MCP or a recognized
legacy entry; unrelated MCP servers are left untouched.

Before adding or removing the MCP, fully quit the selected Claude Desktop
profile. The CLI detects running regular and Relay AI/3P instances and refuses
to write while they are open, because Claude may rewrite the config and discard
the change. Reopen Claude Desktop after setup completes.

### Install AI Skills (Optional)

The wizard can install the prompting and workflow skill after MCP setup. It
defaults to **All projects (user level)** and can instead install into the
current project. Existing current or newer versions are preserved; older or
unversioned skills require confirmation before replacement, with a backup made
first.

You can also manage skills directly. Supported local skill targets include
**Cline**, **Antigravity**, **OpenClaw**, **Hermes**, **Codex**, **OpenCode**,
**Claude Code**, and **Gemini CLI**.

```bash
# Install skill files
nlm skill install cline
nlm skill install openclaw
nlm skill install codex
nlm skill install antigravity

# Update skills
nlm skill update
```

User-level skill installation requires the target tool to be detected first;
the CLI will not create a missing tool directory or install anyway. Use
`--level project` when you intentionally want a project-local skill.

#### Claude Desktop (Chat and Cowork) and claude.ai

Claude Desktop's **Code** tab is Claude Code, so it uses the skill installed in
`~/.claude/skills/`. The **Chat** and **Cowork** tabs, and claude.ai, only use
skills uploaded to your Claude account. Create the upload file with the wizard
(tick **Claude Desktop / claude.ai** in **Add the skill**) or directly:

```bash
nlm skill package                  # saves ~/Downloads/nlm-skill.zip
nlm skill package --output ~/Desktop
```

Then in Claude Desktop (or claude.ai) open **Customize → Skills → Add**, pick
`nlm-skill.zip`, and turn the skill on. Re-upload after updating `nlm` to get
the latest skill. The MCP for Chat and Cowork comes from the normal Claude
Desktop connection (`nlm setup` → **Add the MCP**).

### Remove from a tool

```bash
nlm setup remove claude-code
```

### Using uvx (No Install Required)

If you don't want to install the package, you can use `uvx` to run on-the-fly:

```bash
# Run CLI commands directly
uvx --from notebooklm-mcp-cli nlm setup add cursor
uvx --from notebooklm-mcp-cli nlm login
```

For tools that use JSON config, point them to uvx:

```json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "uvx",
      "args": ["--from", "notebooklm-mcp-cli", "notebooklm-mcp"]
    }
  }
}
```


Manual Setup (if you prefer)

> **Tip:** Run `nlm setup add json` for an interactive wizard that generates the right JSON snippet for your tool.

**Claude Code / Gemini CLI** support adding MCP servers via their own CLI:

```bash
claude mcp add --scope user gemini-notebook-mcp notebooklm-mcp
gemini mcp add --scope user gemini-notebook-mcp notebooklm-mcp
```

**Cursor / Windsurf** resolve commands from your `PATH`, so the command name is enough:

```json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "notebooklm-mcp"
    }
  }
}
```

| Tool     | Config Location                       |
| -------- | ------------------------------------- |
| Cursor   | `~/.cursor/mcp.json`                  |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |

**GitHub Copilot (VS Code workspace)** uses `.vscode/mcp.json` with a top-level `servers` key:

```json
{
  "servers": {
    "gemini-notebook-mcp": {
      "command": "notebooklm-mcp",
      "args": []
    }
  }
}
```

**Claude Desktop** may not resolve `PATH` — use the full path to the binary:

```json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "/full/path/to/notebooklm-mcp"
    }
  }
}
```

Find your path with: `which notebooklm-mcp`

| Tool                              | Config Location                                                                                    |
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
| Claude Desktop (macOS current/3P) | `~/Library/Application Support/Claude-3p/claude_desktop_config.json`                               |
| Claude Desktop (macOS legacy)     | `~/Library/Application Support/Claude/claude_desktop_config.json`                                  |
| Claude Desktop (Windows)          | `%APPDATA%\Claude\claude_desktop_config.json` (an unambiguous MSIX path is detected automatically) |
| Claude Desktop (Windows 3P)       | `%LOCALAPPDATA%\Claude-3p\claude_desktop_config.json`                                              |
| Claude Desktop (Linux)            | `~/.config/Claude/claude_desktop_config.json`                                                      |
| Claude Desktop (Linux 3P)         | `${XDG_CONFIG_HOME:-~/.config}/Claude-3p/claude_desktop_config.json`                               |
| GitHub Copilot                    | `.vscode/mcp.json`                                                                                 |



📚 **Full configuration details:** [MCP Guide](docs/MCP_GUIDE.md) — Server options, environment variables, HTTP transport, and context window management. For Claude web/mobile and public deployment, read [Remote MCP Deployment](docs/REMOTE_MCP.md) first.

## What You Can Do

Simply chat with your AI tool (Claude Code, Cursor, Gemini CLI) using natural language. Here are some examples:

### Research & Discovery

- "List all my Gemini Notebook notebooks"
- "Create a new notebook called 'AI Strategy Research'"
- "Start web research on 'enterprise AI ROI metrics' and show me what sources it finds"
- "Do a deep research on 'cloud marketplace trends' and import the top 10 sources"
- "Search my Google Drive for documents about 'product roadmap' and create a notebook"

### Adding Content

- "Add this URL to my notebook: https://example.com/article"
- "Add this YouTube video about Kubernetes to the notebook"
- "Add my meeting notes as a text source to this notebook"
- "Import this Google Doc into my research notebook"

### AI-Powered Analysis

- "What are the key findings in this notebook?"
- "Summarize the main arguments across all these sources"
- "What does this source say about security best practices?"
- "Get an AI summary of what this notebook is about"
- "Configure the chat to use a learning guide style with longer responses"

_(All queries sent from CLI or MCP automatically persist in your Gemini Notebook web UI chat history!)_

### Content Generation

- "Create an audio podcast overview of this notebook in deep dive format"
- "Generate a video explainer with classic visual style"
- "Make a short vertical video overview of the key idea"
- "Make a briefing doc from these sources"
- "Create flashcards for studying, medium difficulty"
- "Generate an infographic in landscape orientation with professional style"
- "Build a mind map from my research sources"
- "Create a slide deck presentation from this notebook"

### Smart Management

- "Check which Google Drive sources are out of date and sync them"
- "Show me all the sources in this notebook with their freshness status"
- "Delete this source from the notebook"
- "Check the status of my audio overview generation"
- "Check this specific artifact without listing every Studio item"
- "List only the generated videos in this notebook"

### Sharing & Collaboration

- "Show me the sharing settings for this notebook"
- "Make this notebook public so anyone with the link can view it"
- "Disable public access to this notebook"
- "Invite user@example.com as an editor to this notebook"
- "Add a viewer to my research notebook"

**Pro tip:** After creating studio content (audio, video, reports, etc.), poll the status to get download URLs when generation completes.

## Authentication Lifecycle

| Component  | Duration        | Refresh                                              |
| ---------- | --------------- | ---------------------------------------------------- |
| Cookies    | ~2-4 weeks      | Auto-refresh via headless browser (if profile saved) |
| CSRF Token | ~minutes        | Auto-refreshed on every request failure              |
| Session ID | Per MCP session | Auto-extracted on MCP start                          |

**v0.1.9+**: The server now automatically handles token expiration:

1. Refreshes CSRF tokens immediately when expired
2. Reloads cookies from disk if updated externally
3. Runs headless browser auth if profile has saved login

You can also call `refresh_auth()` to explicitly reload tokens.

If automatic refresh fails (Google login fully expired), run `nlm login` again.

For suspected browser-bound auth replay failures, run `nlm doctor auth-replay`.
If the `cdp_in_page` probe succeeds while normal replay fails, see
[Experimental browser-backed RPC transport](docs/AUTHENTICATION.md#experimental-browser-backed-rpc-transport).

## Troubleshooting

### `uv tool upgrade` Not Installing Latest Version

**Symptoms:**

- Running `uv tool upgrade notebooklm-mcp-cli` installs an older version (e.g., 0.1.5 instead of 0.1.9)
- `uv cache clean` doesn't fix the issue

**Why this happens:** `uv tool upgrade` respects version constraints from your original installation. If you initially installed an older version or with a constraint, `upgrade` stays within those bounds by design.

**Fix — Force reinstall:**

```bash
uv tool install --force notebooklm-mcp-cli
```

This bypasses any cached constraints and installs the absolute latest version from PyPI.

**Verify:**

```bash
uv tool list | grep notebooklm
# Should show: notebooklm-mcp-cli v0.1.9 (or latest)
```

## Limitations

- **Rate limits**: Chat and Studio usage is metered as compute against a rolling (~5h) window and a weekly cap, scaled by plan tier. Run `nlm usage` to see what is left and when it resets
- **No official support**: API may change without notice
- **Cookie expiration**: Need to re-extract cookies every few weeks

## Contributing

See [CLAUDE.md](CLAUDE.md) for detailed API documentation and how to add new features.

## Vibe Coding Alert

Full transparency: this project was built by a non-developer using AI coding assistants. If you're an experienced Python developer, you might look at this codebase and wince. That's okay.

The goal here was to scratch an itch - programmatic access to Gemini Notebook - and learn along the way. The code works, but it's likely missing patterns, optimizations, or elegance that only years of experience can provide.

**This is where you come in.** If you see something that makes you cringe, please consider contributing rather than just closing the tab. This is open source specifically because human expertise is irreplaceable. Whether it's refactoring, better error handling, type hints, or architectural guidance - PRs and issues are welcome.

Think of it as a chance to mentor an AI-assisted developer through code review. We all benefit when experienced developers share their knowledge.

## Credits

Special thanks to:

- **Le Anh Tuan** ([@latuannetnam](https://github.com/latuannetnam)) for contributing the HTTP transport, debug logging system, and performance optimizations.
- **David Szabo-Pele** ([@davidszp](https://github.com/davidszp)) for the `source_get_content` tool and Linux auth fixes.
- **saitrogen** ([@saitrogen](https://github.com/saitrogen)) for the research polling query fallback fix.
- **devnull03** ([@devnull03](https://github.com/devnull03)) for multi-browser CDP authentication support (Arc, Brave, Edge, Chromium, Vivaldi, Opera).
- **VooDisss** ([@VooDisss](https://github.com/VooDisss)) for multi-browser authentication improvements.
- **codepiano** ([@codepiano](https://github.com/codepiano)) for the configurable DevTools timeout for the auth CLI.
- **Tony Hansmann** ([@997unix](https://github.com/997unix)) for contributing the `nlm setup` and `nlm doctor` commands and CLI Guide documentation.
- **Fabiana Furtado** ([@fabianafurtadoff](https://github.com/fabianafurtadoff)) for batch operations, cross-notebook query, pipelines, and smart select/tagging (PR #90).
- **Amy-Ra-lph** ([@Amy-Ra-lph](https://github.com/Amy-Ra-lph)) for security hardening: TOCTOU-safe credential storage, sensitive cookie redaction from debug logs, and pinning all CI actions to full commit SHAs (PRs #205–207).
- **Kyle Brodeur** ([@kylebrodeur](https://github.com/kylebrodeur)) for WSL2 authentication support with Windows Chrome integration (PR #138).
- **Robiton** ([@Robiton](https://github.com/Robiton)) for enterprise Gemini Notebook support via configurable base URL (PR #114).
- **pjeby** ([@pjeby](https://github.com/pjeby)) for connection pooling and fast startup improvements (PR #54).
- **beausea** ([@beausea](https://github.com/beausea)) for making the interface language configurable via the `NOTEBOOKLM_HL` environment variable (PR #59).
- **JumpLao** ([@JumpLao](https://github.com/JumpLao)) for extended audio, video, and image format support (PR #82).
- **cbruyndoncx** ([@cbruyndoncx](https://github.com/cbruyndoncx)) for including `cited_text` passages in query output (PR #81).
- **zxyasfas** ([@zxyasfas](https://github.com/zxyasfas)) for cited-only research import (PR #188).
- **Serdar Akın** ([@SERDAR-AKIN](https://github.com/SERDAR-AKIN)) for the multi-probe `AuthHealthChecker` that fixes false `"stale"` reports for semi-stale cookies (PR #219).

## Star History

[](https://star-history.dera.page/#jacob-bd/gemini-notebook-mcp-cli&Date)

## License

[MIT License](LICENSE)

---

## File: docs/releases/v0.8.2.md

# v0.8.2 Maintenance Release

This maintenance release adds diagnostics and a low-risk recovery path for emerging Gemini Notebook (formerly Google NotebookLM) auth replay failures reported in issue #248.

## Added

- Added `nlm doctor auth-replay` to compare:
  - saved-cookie replay through normal `httpx`
  - `httpx` replay after a forced Google `RotateCookies` call
  - optional in-page CDP fetch from the saved Chrome profile
- Added verdicts that distinguish ordinary cookie freshness failures from likely browser-bound replay behavior.
- Added best-effort Google `RotateCookies` support during auth token refresh.

## Fixed

- Matched the auth replay diagnostic HTTP probes to the normal Gemini Notebook RPC headers to avoid false `browser_bound_replay` verdicts from bare `httpx` requests.
- Confirmed `nlm login --check` homepage login redirects with a real Gemini Notebook RPC before reporting cookies as expired. Thanks to `laofun` for reporting and contributing this fix in PR #250.
- Preserved raw Chrome cookie-list handling across auth checks instead of flattening too early.
- Preferred exact `.google.com` cookie values when flattening duplicate browser cookies. Thanks to `laofun` for reporting and contributing this fix in PR #249.
- Persisted the Gemini Notebook build label from profile metadata so diagnostics and replay checks use the current saved value.

## Verification

- `uv run ruff check .`
- `uv run pytest tests/services/test_auth_service.py tests/core/test_cookie_rotation.py tests/services/test_auth_replay_diagnostic.py -q`
- `uv run pytest -q`
- `uv cache clean && uv tool install --force .`
- Live smoke test:
  - `nlm login --check`
  - `nlm notebook list --title`
  - `nlm query notebook <notebook-id> "In one sentence, what is this notebook about?" --json`

## Notes

This release does not add a full CDP transport. The new doctor command is
intended to collect evidence first, so we can confirm whether a user is seeing
stale cookies, normal auth expiration, or the stronger browser-bound behavior
described in issue #248.

---

## File: docs/releases/v0.8.3.md

# v0.8.3 Maintenance Release

This maintenance release adds an experimental opt-in browser-backed transport
for Gemini Notebook (formerly Google NotebookLM) auth replay failures reported in issue #248.

## Added

- Added `NOTEBOOKLM_RPC_TRANSPORT=cdp` to route normal batchexecute RPCs through
  an in-page `fetch` inside the saved Gemini Notebook browser profile.
- Added CDP transport support for notebook chat via `GenerateFreeFormStreamed`,
  including longer CDP response waits for long answers.
- Added focused tests for CDP transport routing, streamed query routing, long
  CDP timeouts, and cleanup of browser instances launched by the client.
- Documented when and how to use the experimental transport.

## Changed

- Reused the new CDP fetch helper inside `nlm doctor auth-replay`, so the
  diagnostic path matches the real experimental transport more closely.
- Kept the default `httpx` transport unchanged. The CDP path is off by default.

## Notes

Use `nlm doctor auth-replay` first. Only try
`NOTEBOOKLM_RPC_TRANSPORT=cdp` when normal replay fails but `cdp_in_page`
passes. Uploads, downloads, and artifact file transfers still use the existing
HTTP paths.

## Verification

- `uv run ruff check .`
- `uv run ruff format --check .`
- `uv run --dev --with pytest-github-actions-annotate-failures --with pytest-asyncio pytest -v --tb=short -m "not e2e"`
- `uv run pytest -q`
- `uv cache clean && uv tool install --force .`
- Live smoke test:
  - `nlm login --check`
  - `nlm notebook list --title`
  - `nlm doctor auth-replay --timeout 20`
  - `NOTEBOOKLM_RPC_TRANSPORT=cdp nlm notebook list --title`
  - `NOTEBOOKLM_RPC_TRANSPORT=cdp nlm query notebook <notebook-id> "Answer in one short sentence: what is this notebook about?" --json`
  - MCP stdio `notebook_list` with `NOTEBOOKLM_RPC_TRANSPORT=cdp`

---

## File: docs/API_REFERENCE.md

# Gemini Notebook (formerly Google NotebookLM) MCP API Reference

This document contains detailed API documentation for the internal Gemini Notebook APIs. Only read this file when debugging API issues or adding new features.

**For general project info, see [CLAUDE.md](../CLAUDE.md)**

---

## Python Usage Examples

These examples show how to use the MCP tools programmatically via Python (for developers building with the API). **For end users**: see the main README for natural language examples.

### List Notebooks
```python
notebooks = notebook_list()
```

### Create and Query
```python
# Create a notebook
notebook = notebook_create(title="Research Project")

# Add sources
notebook_add_url(notebook_id, url="https://example.com/article")
notebook_add_text(notebook_id, text="My research notes...", title="Notes")

# Ask questions
result = notebook_query(notebook_id, query="What are the key points?")
print(result["answer"])
```

Queries use a 120-second wall-clock timeout by default. Source-heavy notebooks
may need a larger budget, for example `notebook_query(..., timeout=180)`. If a
query may take longer, use `notebook_query_start(..., timeout=180)` and poll
`notebook_query_status` until it completes. A deadline error includes a retry
hint and the `query_deadline_exceeded` debug code.

### Configure Chat Settings
```python
# Set a custom chat persona with longer responses
chat_configure(
    notebook_id=notebook_id,
    goal="custom",
    custom_prompt="You are an expert data analyst. Provide detailed statistical insights.",
    response_length="longer"
)

# Use learning guide mode with default length
chat_configure(
    notebook_id=notebook_id,
    goal="learning_guide",
    response_length="default"
)

# Reset to defaults with concise responses
chat_configure(
    notebook_id=notebook_id,
    goal="default",
    response_length="shorter"
)
```

**Goal Options:** default, custom (requires custom_prompt), learning_guide
**Response Lengths:** default, longer, shorter

### Get AI Summaries

```python
# Get AI-generated summary of what a notebook is about
summary = notebook_describe(notebook_id)
print(summary["summary"])  # Markdown with **bold** keywords
print(summary["suggested_topics"])  # Suggested report topics

# Get AI-generated summary of a specific source
source_info = source_describe(source_id)
print(source_info["summary"])  # AI summary with **bold** keywords
print(source_info["keywords"])  # Topic chips: ["Medical education", "AI tools", ...]
```

### Get Raw Source Content

```python
# Get raw text content from a source (no AI processing)
# Much faster than notebook_query for bulk content export
content = source_get_content(source_id)
print(content["title"])        # Source title
print(content["source_type"])  # pdf, web_page, youtube, pasted_text, google_docs, etc.
print(content["url"])          # Source URL (if available)
print(content["char_count"])   # Character count
print(content["content"])      # Full raw text

# Example: Export all sources to markdown files
sources = notebook_get(notebook_id)["sources"]
for source in sources:
    content = source_get_content(source["id"])
    with open(f"{content['title']}.md", 'w') as f:
        f.write(content["content"])
```

**Supported source types:** google_docs, google_slides_sheets, pdf, pasted_text, web_page, youtube

### Batch, Cross-Notebook, Pipelines & Tags
```python
# Tag notebooks for organization
tag(action="add", notebook_id="...", tags="ai,research")

# Batch query across tagged notebooks
batch(action="query", query="What are the key findings?", tags="ai")

# Cross-notebook query with aggregated answers
cross_notebook_query(query="Compare approaches", notebook_names="Project A, Project B")

# Batch generate podcasts for all tagged notebooks
batch(action="studio", artifact_type="audio", tags="research", confirm=True)

# Run a multi-step pipeline
pipeline(action="run", notebook_id="...", pipeline_name="ingest-and-podcast", input_url="https://...")

# Find relevant notebooks by tag match
tag(action="select", query="ai research")
```

**Built-in pipelines:** `ingest-and-podcast`, `research-and-report`, `multi-format`

### Sync Stale Drive Sources
```python
# Check which sources need syncing
sources = source_list_drive(notebook_id)

# For very large notebooks, skip freshness checks when you only need the source list
sources = source_list_drive(notebook_id, skip_freshness=True)

# Sync stale sources (after user confirmation)
source_sync_drive(source_ids=["id1", "id2"], confirm=True)
```

### Delete Sources
```python
# Delete a source from notebook (after user confirmation)
source_delete(source_id="source-uuid", confirm=True)
```

### Research and Import Sources
```python
# Start web research (fast mode, ~30 seconds)
result = research_start(
    query="value of ISVs on cloud marketplaces",
    source="web",   # or "drive" for Google Drive
    mode="fast",    # or "deep" for extended research (web only)
    title="ISV Research"
)
notebook_id = result["notebook_id"]

# Poll until complete (built-in wait, polls every 30s for up to 5 min)
# By default, report is truncated to 500 chars to save tokens
# Use compact=False to get full 10,000+ char report and all sources
status = research_status(notebook_id)

# Import all discovered sources
research_import(
    notebook_id=notebook_id,
    task_id=status["research"]["task_id"],
    timeout=600  # Optional: increase for large notebooks (default: 300s)
)

# Or import specific sources by index
research_import(
    notebook_id=notebook_id,
    task_id=status["research"]["task_id"],
    source_indices=[0, 2, 5],  # Import only sources at indices 0, 2, and 5
    timeout=600  # Optional: increase for large notebooks (default: 300s)
)

# Or import only sources cited by the deep research report
research_import(
    notebook_id=notebook_id,
    task_id=status["research"]["task_id"],
    cited_only=True,  # Overrides source_indices when enabled
    timeout=600
)
```

**Research Modes:**
- `fast` + `web`: Quick web search, ~10 sources in ~30 seconds
- `deep` + `web`: Extended research with AI report, ~40 sources in 3-5 minutes
- `fast` + `drive`: Quick Google Drive search, ~10 sources in ~30 seconds

### Generate Audio/Video Overviews
```python
# Create an audio overview (podcast)
result = audio_overview_create(
    notebook_id=notebook_id,
    format="deep_dive",  # deep_dive, brief, critique, debate
    length="default",    # short, default, long
    language="en",
    confirm=True         # Required - show settings first, then confirm
)

# Create a video overview
result = video_overview_create(
    notebook_id=notebook_id,
    format="explainer",      # explainer, brief, cinematic, short
    visual_style="classic",  # auto_select, custom, classic, whiteboard, kawaii, anime, etc.
    focus_prompt="",         # Optional host/content focus text
    visual_style_prompt="",  # Optional custom style text when visual_style="custom"
    language="en",
    confirm=True
)

# Check generation status (takes several minutes)
status = studio_status(notebook_id, artifact_id=result["artifact_id"])
for artifact in status["artifacts"]:
    print(f"{artifact['title']}: {artifact['status']}")

# Rich fields are opt-in and responses are paginated.
detailed = studio_status(notebook_id, include_details=True, limit=20, offset=0)

# Delete an artifact (after user confirmation)
studio_delete(
    notebook_id=notebook_id,
    artifact_id="artifact-uuid",
    confirm=True
)
```

**Audio Formats:** deep_dive (conversation), brief, critique, debate
**Audio Lengths:** short, default, long
**Video Formats:** explainer, brief, cinematic, short (vertical, ~60s, no visual style)
**Video Styles:** auto_select, custom, classic, whiteboard, kawaii, anime, watercolor, retro_print, heritage, paper_craft

For Short videos, language selection is best-effort. The captured RPC uses a
null language slot, so non-English requests are reinforced through the focus
prompt rather than an undocumented payload change.

---

## Base Endpoint

```
POST https://notebook.google.com/_/LabsTailwindUi/data/batchexecute
```

### Gemini Notebook Enterprise

Enterprise uses a project- and location-scoped Cloud NotebookLM endpoint. The
configured base host may be `notebook.cloud.google.com` (current documented
host), `notebooklm.cloud.google.com`, or `vertexaisearch.cloud.google.com` for
older deployments. Set `NOTEBOOKLM_PROJECT_ID` and `NOTEBOOKLM_LOCATION` before
starting the client. `NOTEBOOKLM_PROJECT_ID` is required for Enterprise; the
location defaults to `global`.

For the documented `notebook.cloud.google.com` host, the routes are:

```
POST https://notebook.cloud.google.com/{location}/_/CloudNotebookLmUi/data/batchexecute
POST https://notebook.cloud.google.com/{location}/_/CloudNotebookLmUi/data/google.cloud.notebooklm.v1main.NotebookService/GenerateFreeFormStreamed
POST https://notebook.cloud.google.com/{location}/upload/_/
```

The Enterprise list-notebooks RPC is `rG2vCb` with a project-qualified parent:

```json
["projects/{project}/locations/{location}", null, null, 1]
```

Enterprise streamed-query requests identify the notebook with the resource
name `projects/{project}/locations/{location}/notebooks/{notebook_id}`:

```json
[
  [[["source-id"]]],
  "question",
  {"70000": "projects/{project}/locations/{location}/notebooks/{notebook_id}"}
]
```

These structures are based on a contributor-provided Enterprise web-client
capture and are covered by the Enterprise routing tests. Live validation still
requires access to an Enterprise deployment; record a redacted network capture
when updating them because Google may rotate the internal RPC IDs and paths.

## Request Format

```
Content-Type: application/x-www-form-urlencoded

f.req=&at=<CSRF token>
```

The `f.req` structure:
```json
[[["<RPC_ID>", "", null, "generic"]]]
```

The web client additionally sends browser-style headers on every call
(`X-Same-Domain: 1`, Chrome UA + `sec-ch-ua*` hints, `Referer`, and — on
studio calls such as `rc3d8d`, `Rytqqe` and `gArtLc` — an
`x-goog-ext-525006520-jspb` header whose value is the request's config block,
e.g. `[2,null,[1],[1,null,...,[1,3]]]`). None of these headers are required:
the CLI omits them for every RPC, and adding them from a non-browser client
does not change the `Rytqqe` outcome (see the Interactive Report section).

## URL Query Parameters

| Param | Description |
|-------|-------------|
| `rpcids` | The RPC ID being called |
| `source-path` | Current page path (e.g., `/notebook/`) |
| `bl` | Build label, auto-extracted from page HTML (`cfb2h` key). Override via `NOTEBOOKLM_BL` env var. |
| `f.sid` | Session ID |
| `hl` | Language code (e.g., `en`) |
| `_reqid` | Request counter |
| `rt` | Response type (`c`) |

## Response Format

```
)]}'

<json_array>
```

- Starts with `)]}'` (anti-XSSI prefix) - MUST be stripped
- Followed by byte count, then JSON
- Multiple chunks may be present

---

## Known RPC IDs

| RPC ID | Purpose | Params Structure |
|--------|---------|------------------|
| `wXbhsf` | List notebooks | `[null, 1, null, [2]]` |
| `rLM1Ne` | Get notebook details | `[notebook_id, null, [2], null, 0]` |
| `CCqFvf` | Create notebook | `[title, null, null, [2], [1,null,null,null,null,null,null,null,null,null,[1]]]` |
| `s0tc2d` | Rename notebook / Configure chat | See s0tc2d section below |
| `WWINqb` | Delete notebook | `[[notebook_id], [2]]` |
| `izAoDd` | Add source (unified) | See source types below |
| `hizoJc` | Get source details | `[["source_id"], [2], [2]]` |
| `yR9Yof` | Check source freshness | `[null, ["source_id"], [2]]` - returns `false` if stale |
| `FLmJqe` | Sync Drive source | `[null, ["source_id"], [2]]` |
| `b7Wfje` | Rename source | `[null, ["source_id"], [[["new_title"]]]]` - path: `/notebook/<notebook_id>` |
| `tGMBJ` | Delete source | `[[["source_id"]], [2]]` - deletion is IRREVERSIBLE |
| `hPTbtc` | Get conversation IDs | `[notebook_id]` |
| `khqZz` | Get conversation turns (full Q&A history) | See "Conversation Turns (khqZz)" below |
| `hT54vc` | User preferences | - |
| `ZwVcOc` | Settings | - |
| `ozz5Z` | Add source v2 (Unified) | See source types below |
| `Ljjv0c` | Start Fast Research | `[["query", source_type], null, 1, "notebook_id"]` |
| `QA9ei` | Start Deep Research | `[null, [1], ["query", source_type], 5, "notebook_id"]` |
| `e3bVqc` | Poll Research Results | `[null, null, "notebook_id"]` |
| `LBwxtb` | Import Research Sources | `[null, [1], "task_id", "notebook_id", [sources]]` |
| `R7cb6c` | Create Studio Content | See Studio RPCs section |
| `gArtLc` | Poll Studio Status | `[[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"']` |
| `V5N4be` | Delete Studio Content | `[[2], "artifact_id"]` |
| `rc3d8d` | Rename Studio Artifact / Update artifact fields (field mask) | `[["artifact_id", "new_title"], [["title"]]]` or `[sparse_artifact, [["app.generation_options.free_text_steering_prompt", ...]], null, config]` |
| `v9rmvd` | Get one artifact by id (metadata, quiz HTML, report elements) | `[artifact_id, [2, null, null, [1, null*9, [1]], [[1, 4, 8, 10, 14, 2, 3, 6, 7]]]]` (simple `[artifact_id]` also works) |
| `Rytqqe` | Kick off generation of a (suggested) artifact | `[[2, null, null, [1, null*9, [1]], [[1, 4, 8, 10, 14, 2, 3, 6]]], "<artifact_id>"]` (plain-string id) |
| `Fxmvse` | Set artifact read state | `[config, artifact_id, [null, null, null, null, 0], [["is_unread"]]]` |
| `HpN0Ub` | Mark artifact viewed / session bookkeeping | `[config, [artifact_id]]` (returns `[]`) |
| `KmcKPe` | Revise Slide Deck | `[[2], artifact_id, [[[0-based_index, "instruction"], ...]]]` |
| `yyryJe` | Generate Mind Map | See Mind Map RPCs section |
| `CYK0Xb` | Save Mind Map | See Mind Map RPCs section |
| `cFji9` | List Mind Maps | `[notebook_id]` |
| `ciyUvf` | Get Suggested Report Formats | `[[2], notebook_id, [[source_id1], ...]]` |
| `VfAZjd` | Get Report Suggestions | `[notebook_id, [2]]` |
| `tr032e` | Get Source Guide | `[[[["source_id"]]]]` |
| `JFMDGd` | Get current share status / access state (fires on notebook page load; used by `notebook share status`) | `[notebook_id, [2, null, [1], [1, null x9, [1, 3]]]]` |
| `ub2Bae` | Unidentified notebook page-load call (observed 2026-09-24) | `[[2, null, [1], [1, null x9, [1, 3]]]]` |
| `sqTeoe` | Unidentified notebook page-load call (observed 2026-09-24) | `[[2, null, null, [1, null x9, [1]], [[1, 4, 8, 10, 14, 2, 3, 6]]], null, 1]` |
| `I3xc3c` | Unidentified notebook page-load call (observed 2026-09-24) | `[[2, null, [1], [1, null x9, [1, 3]]], "<notebook_id>"]` |
| `sODAg` | Unidentified dialog-open call; fires alongside `EylDcb` when generation dialogs open | `[]` -> response `[true]` |

---

## `s0tc2d` - Notebook Update RPC

This RPC handles multiple notebook update operations based on which array position is populated.

### Rename Notebook

Updates the notebook title.

```python
# Request params
[notebook_id, [[null, null, null, [null, "New Title"]]]]

# Example
["549e31df-1234-5678-90ab-cdef01234567", [[null, null, null, [null, "My New Notebook Name"]]]]

# Response
# Returns updated notebook info
```

### Configure Chat Settings

Configures the notebook's chat behavior - goal/style and response length.

```python
# Request params
[notebook_id, [[null, null, null, null, null, null, null, [[goal_code, custom_prompt?], [response_length_code]]]]]

# chat_settings is at position 7 in the nested array
# Format: [[goal_code, custom_prompt_if_custom], [response_length_code]]

# Example - Default goal + Longer response:
["549e31df-...", [[null, null, null, null, null, null, null, [[1], [4]]]]]

# Example - Custom goal + Default response:
["549e31df-...", [[null, null, null, null, null, null, null, [[2, "You are an expert..."], [1]]]]]

# Example - Learning Guide + Shorter response:
["549e31df-...", [[null, null, null, null, null, null, null, [[3], [5]]]]]
```

### Goal/Style Codes

| Code | Goal | Description |
|------|------|-------------|
| 1 | Default | General purpose research and brainstorming |
| 2 | Custom | Custom prompt (up to 10,000 characters) |
| 3 | Learning Guide | Educational focus with learning-oriented responses |

### Response Length Codes

| Code | Length | Description |
|------|--------|-------------|
| 1 | Default | Standard response length |
| 4 | Longer | Verbose, detailed responses |
| 5 | Shorter | Concise, brief responses |

---

## Source Types (via `izAoDd` RPC)

All source types use the same RPC but with different param structures:

### URL/YouTube Source

**IMPORTANT:** YouTube and regular web URLs use **different positions** in the source_data array!

#### Regular Website URL
```python
source_data = [
    None,
    None,
    [url],  # URL at position 2 for regular websites
    None, None, None, None, None, None, None,
    1
]
params = [[[source_data]], notebook_id, [2], settings]
```

#### YouTube URL
```python
source_data = [
    None,
    None,
    None,  # Position 2 must be None for YouTube
    None, None, None, None,
    [url],  # URL at position 7 for YouTube
    None, None,
    1
]
params = [[[source_data]], notebook_id, [2], settings]
```

**Detection:** Check if URL contains `youtube.com` or `youtu.be` to determine which format to use.

### Pasted Text Source
```python
source_data = [
    None,
    [title, text_content],  # Title and content at position 1
    None,
    2,  # Type indicator at position 3
    None, None, None, None, None, None,
    1
]
params = [[[source_data]], notebook_id, [2], settings]
```

### Google Drive Source
```python
source_data = [
    [document_id, mime_type, 1, title],  # Drive doc at position 0
    None, None, None, None, None, None, None, None, None,
    1
]
params = [[[source_data]], notebook_id, [2], settings]
```

**MIME Types:**
- `application/vnd.google-apps.document` - Google Docs
- `application/vnd.google-apps.presentation` - Google Slides
- `application/vnd.google-apps.spreadsheet` - Google Sheets
- `application/pdf` - PDF files

---

## Query Endpoint (Streaming)

Queries use a **different endpoint** - NOT batchexecute!

```
POST /_/LabsTailwindUi/data/google.internal.labs.tailwind.orchestration.v1.LabsTailwindOrchestrationService/GenerateFreeFormStreamed
```

### Query Request Structure
```python
params = [
    [  # Source IDs - each in nested array
        [[["source_id_1"]]],
        [[["source_id_2"]]],
    ],
    "Your question here",  # Query text
    None,
    [2, None, [1]],  # Config
    "conversation-uuid"  # For follow-up questions
]

f_req = [None, json.dumps(params)]
```

### Query Response
Streaming JSON with multiple chunks:
1. **Thinking steps** - "Understanding...", "Exploring...", etc.
2. **Final answer** - Markdown formatted with citations
3. **Source references** - Links to specific passages in sources

---

## Conversation Turns (`khqZz`)

Discovered via Chrome DevTools capture of the web UI loading a notebook's chat panel
(2026-07-22). This is a `batchexecute` RPC (unlike the streaming query endpoint above)
that fetches the **full Q&A history for a past conversation from the server** — it's
what lets the same chat re-appear after closing and reopening a notebook, and it's what
`nlm chats get` / `chat_get` / `chat_export` use to show real transcripts even on a
fresh CLI invocation or MCP server session (`get_conversation_turns()` in
`core/conversation.py`, wrapping `RPC_GET_CONVERSATION_TURNS`).

Companion to `hPTbtc` (`get_conversation_id`), which only returns the conversation
UUID — `khqZz` is the RPC that returns the actual turn content for that UUID.

### Request
```python
params = [
    [2, None, [1], [1, None, None, None, None, None, None, None, None, None, [1, 3]]],  # boilerplate client-capability descriptor, same shape used by hPTbtc
    None,
    None,
    "conversation-uuid",  # from get_conversation_id()
    20,                   # max turns to return (page size; no pagination implemented yet)
]
```

### Response
```
[[turn, turn, ...], "continuation_token"]
```
Turns are returned **newest-first**, alternating **answer then query** per turn pair
(the same `[answer, None, 2]` / `[query, None, 1]` shape the client already builds
locally in `_build_conversation_history()` for follow-up queries):

- **Answer turn**: `[turn_id, [unix_sec, nanos], 2, None, content]` where
  `content[0] = [answer_text, None, [conv_id, conv_id, num]]` — the answer text is
  nested **one level inside `content[0]`** (`content[0][0]`), not `content[0]` directly.
  The remaining `content` entries (`content[1]`, `content[3]`, ...) carry rich
  formatting/citation spans.
- **Query turn**: `[turn_id, [unix_sec, nanos], 1, "query_text"]`

`get_conversation_turns()` pairs consecutive `(answer, query)` entries, reverses them
to chronological order, and returns `[{"turn": 1, "query": ..., "answer": ...}, ...]` —
the same shape as the local-cache-based `get_conversation_history()`. Only plain
answer/query text is extracted; the rich formatting and citation spans are not yet
parsed. The `continuation_token` for fetching turns beyond the requested `limit` is
not yet used (no pagination).

---

## Research RPCs (Source Discovery)

Gemini Notebook's "Research" feature discovers and suggests sources based on a query. It supports two source types (Web and Google Drive) and two research modes (Fast and Deep).

### Source Types
| Type | Value | Description |
|------|-------|-------------|
| Web | `1` | Searches the public web for relevant sources |
| Google Drive | `2` | Searches user's Google Drive for relevant documents |

### Research Modes
| Mode | Description | Duration | Can Leave Page |
|------|-------------|----------|----------------|
| Fast Research | Quick search, ~10 sources | ~10-30 seconds | No |
| Deep Research | Extended research with AI report, ~40+ sources | 3-5 minutes | Yes |

### `Ljjv0c` - Start Fast Research

Initiates a Fast Research session for either Web or Drive sources.

```python
# Request params
[["query", source_type], null, 1, "notebook_id"]

# source_type: 1 = Web, 2 = Google Drive
# Example (Web):  [["What is OpenShift", 1], null, 1, "549e31df-..."]
# Example (Drive): [["sales strategy documents", 2], null, 1, "549e31df-..."]

# Response
["task_id"]
# Example: ["6837228d-d832-4e5c-89d3-b9aa33ff7815"]
```

### `QA9ei` - Start Deep Research (Web Only)

Initiates a Deep Research session with extended web crawling and AI-generated report.

```python
# Request params
[null, [1], ["query", source_type], 5, "notebook_id"]

# The `5` indicates Deep Research mode
# source_type: 1 = Web (Drive not supported for Deep Research)
# Example: [null, [1], ["enterprise kubernetes trends 2025", 1], 5, "549e31df-..."]

# Response
["task_id", "report_id"]
# Example: ["a02dd39b-94c0-443e-b9e4-9c15ab9016c5", null]
```

### `e3bVqc` - Poll Research Results

Polls for research completion and retrieves results. Call repeatedly until status = 2.

```python
# Request params
[null, null, "notebook_id"]

# Response structure (when completed)
[[[
  "task_id",
  [
    "notebook_id",
    ["query", source_type],
    research_mode,  # 1 = Fast, 5 = Deep
    [
      # Array of discovered sources
      [
        "url",           # Web URL or Drive URL
        "title",         # Source title
        "description",   # AI-generated description
        result_type      # 1 = Web, 2 = Google Doc, 3 = Slides, 8 = Sheets
      ],
      # ... more sources
    ],
    "summary"  # AI-generated summary of sources
  ],
  status  # 1 = in progress, 2 = completed
],
[end_timestamp, nanos],
[start_timestamp, nanos]
]]

# Deep Research also includes a report in the results (long markdown document)
```

**Result Types (in poll response):**
| Type | Meaning |
|------|---------|
| 1 | Web URL |
| 2 | Google Doc |
| 3 | Google Slides |
| 5 | Deep Research Report |
| 8 | Google Sheets |

### `LBwxtb` - Import Research Sources

Imports selected sources from research results into the notebook.

```python
# Request params
[null, [1], "task_id", "notebook_id", [source1, source2, ...]]

# Each source structure:
# Web source:
[null, null, ["url", "title"], null, null, null, null, null, null, null, 2]

# Drive source:
[["document_id", "mime_type", null, "title"], null, null, null, null, null, null, null, null, null, 1]

# Response
# Array of created source objects with source_id, title, metadata
[[source_id, title, metadata, [null, 2]], ...]
```

### Research Flow Summary

```
1. Start Research
   ├── Fast: Ljjv0c with source_type (1=Web, 2=Drive)
   └── Deep: QA9ei with mode=5 (Web only)

2. Poll Results
   └── e3bVqc → repeat until status=2

3. Import Sources
   └── LBwxtb with selected sources

4. Sources appear in notebook → can query them
```

### Important Notes

- **Only one active research per notebook**: Starting a new research cancels any pending results
- **Deep Research runs in background**: User can navigate away after initiation
- **Fast Research blocks navigation**: Must stay on page until complete
- **Drive URLs format**: `https://drive.google.com/a/redhat.com/open?id=<document_id>`
- **Web URLs**: Standard HTTP/HTTPS URLs

---

## Studio RPCs (Audio/Video Overviews)

Gemini Notebook's "Studio" feature generates audio podcasts and video overviews from notebook sources.

### `R7cb6c` - Create Studio Content

Creates both Audio and Video Overviews using the same RPC, distinguished by type code.

#### Audio Overview Request
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        1,                         # STUDIO_TYPE_AUDIO
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None,
        [
            None,
            [
                focus_prompt,      # Focus text (what AI should focus on)
                length_code,       # 1=Short, 2=Default, 3=Long
                None,
                [[source_id1], [source_id2], ...],  # Source IDs (simpler format)
                language_code,     # "en", "es", etc.
                None,
                format_code        # 1=Deep Dive, 2=Brief, 3=Critique, 4=Debate
            ]
        ]
    ]
]
```

#### Video Overview Request
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        3,                         # STUDIO_TYPE_VIDEO
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None, None,
        [
            None, None,
            [
                [[source_id1], [source_id2], ...],  # Source IDs
                language_code,     # "en", "es", etc.
                focus_prompt,      # Host/content focus text
                None,
                format_code,       # 1=Explainer, 2=Brief, 3=Cinematic, 4=Short
                visual_style_code,  # 1=Auto, 3=Classic, etc. (null when using custom style text)
                visual_style_prompt  # Present when visual_style="custom"
            ]
        ]
    ]
]
```

**Short format (code 4) — verified via live capture, 2026-06-30:** Short Video
Overviews have no visual style picker, so the inner options list omits
`visual_style_code`/`visual_style_prompt` entirely (matching Cinematic), and
additionally sends `language_code` as `null` plus a trailing flag `1` whose
meaning is undocumented by Google but required for the request to succeed.
Current service behavior adds a best-effort language requirement to
`focus_prompt` for non-English Short requests:
```python
[
    [[source_id1], [source_id2], ...],  # Source IDs
    None,                                # language — always null for Short
    focus_prompt,
    None,
    4,                                   # format_code = Short
    None, None,                          # no visual style
    1,                                   # required trailing flag (unexplained)
]
```

#### Response Structure
```python
# Returns: [[artifact_id, title, type, sources, status, ...]]
# status: 1 = in_progress, 3 = completed
```

### `gArtLc` - Poll Studio Status

Polls for audio/video generation status.

```python
# Request
params = [[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"']

# Response Structure
[
    [
        # Artifact Data Array
        [
            artifact_id,       # [0] UUID
            "Title",           # [1] Title
            type_code,         # [2] Type (1=Audio, 3=Video, etc)
            [source_ids],      # [3] Source IDs used
            wait_time_sec,     # [4] Est wait time
            None,              # [5] (Previously thought to be prompt)
            [                  # [6] Custom Prompt / Focus Data
                 null,
                 [
                     "Custom Prompt Text",  # [1][0] The actual prompt text
                     audience_level,        # [1][1] (e.g. 2)
                     ...
                 ]
            ],
            None,              # [7]
            [url_data],        # [8] URL for audio/video (nested)
            ...
        ],
        ... # More artifacts
    ],
    ...
]
```

Artifacts with type code `4` use the nested subtype at `[9][1][0]`:

- `1` = flashcards
- `2` = quiz
- `4` = mind map

Mind map subtype `4` was confirmed from a live `gArtLc` response on 2026-07-14.
Older clients that only distinguish flashcards and quizzes will mislabel these
saved mind maps as flashcards.

### `V5N4be` - Delete Studio Content

Deletes an audio or video overview artifact permanently.

```python
# Request
params = [[2], "artifact_id"]

# Response
[]  # Empty array on success
```

**WARNING:** This action is IRREVERSIBLE. The artifact is permanently deleted.

### `rc3d8d` - Rename Studio Artifact

Renames a studio artifact (audio, video, report, etc.).

```python
# Request
params = [["artifact_id", "New Title"], [["title"]]]

# Response
# Returns the updated artifact data on success
```

**Note:** This RPC was discovered in v0.2.8. The second array `[["title"]]` specifies which field(s) to update.

### `KmcKPe` - Revise Slide Deck

Revises individual slides in an existing slide deck. Creates a **new** artifact — the original is not modified.

#### Request
```python
params = [
    [2],                           # Version/mode indicator (always [2])
    artifact_id,                   # UUID of the existing slide deck
    [
        [                          # Array of slide revision instructions
            [0, "Make the title larger"],   # [0-based slide index, instruction text]
            [2, "Remove the image"],        # Multiple instructions supported
        ]
    ]
]
```

#### Response
Returns the same structure as `R7cb6c` (Create Studio Content):
- `result[0][0]` — New artifact UUID
- `result[0][2]` — Title (original title + " (2)")
- `result[0][4]` — Status code (1 = in_progress, 3 = completed)
- `result[0][20]` — Original artifact UUID

#### Notes
- Slide index is **0-based** (slide 1 = index 0)
- Multiple slides can be revised in one call
- Original deck settings (focus prompt, language, format, length) are preserved
- Poll with `gArtLc` (studio_status) for completion
- Only slide decks support revision (no other artifact types)

### Audio Options

| Option | Values |
|--------|--------|
| **Formats** | 1=Deep Dive (conversation), 2=Brief, 3=Critique, 4=Debate |
| **Lengths** | 1=Short, 2=Default, 3=Long |
| **Languages** | BCP-47 codes, including regional values such as `"es-ES"`, `"es-US"`, and `"es-419"` |

For Audio Overviews, Gemini Notebook has been observed using the region subtag to
select the voice accent. `es` and `es-ES` produce Spain Spanish, while `es-US`
and `es-419` produce Latin-American Spanish. Prompt text does not reliably
override the accent. This is observed behavior and may change upstream.

### Video Options

| Option | Values |
|--------|--------|
| **Formats** | 1=Explainer (comprehensive), 2=Brief, 3=Cinematic, 4=Short (vertical, ~60s) |
| **Visual Styles** | 1=Auto-select, 2=Custom, 3=Classic, 4=Whiteboard, 5=Kawaii, 6=Anime, 7=Watercolor, 8=Retro print, 9=Heritage, 10=Paper-craft (not applicable to Cinematic or Short) |
| **Languages** | BCP-47 codes: "en", "es", "fr", "de", "ja", etc. Short uses best-effort prompt steering because its RPC language slot is null. |

#### Infographic Request
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        7,                         # STUDIO_TYPE_INFOGRAPHIC
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None, None, None, None, None, None, None, None,  # 10 nulls
        [[focus_prompt, language, None, orientation_code, detail_level_code, visual_style_code]]  # Options at position 14
    ]
]
```

### Infographic Options

| Option | Values |
|--------|--------|
| **Orientations** | 1=Landscape (16:9), 2=Portrait (9:16), 3=Square (1:1) |
| **Detail Levels** | 1=Concise, 2=Standard, 3=Detailed (BETA) |
| **Visual Styles** | 1=Auto-select, 2=Sketch Note, 3=Professional, 4=Bento Grid, 5=Editorial, 6=Instructional, 7=Bricks, 8=Clay, 9=Anime, 10=Kawaii, 11=Scientific |
| **Languages** | BCP-47 codes: "en", "es", "fr", "de", "ja", etc. |

#### Slide Deck Request
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        8,                         # STUDIO_TYPE_SLIDE_DECK
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None, None, None, None, None, None, None, None, None, None,  # 12 nulls
        [[focus_prompt, language, format_code, length_code]]  # Options at position 16
    ]
]
```

### Slide Deck Options

| Option | Values |
|--------|--------|
| **Formats** | 1=Detailed Deck (comprehensive), 2=Presenter Slides (key points) |
| **Lengths** | 1=Short, 3=Default |
| **Languages** | BCP-47 codes: "en", "es", "fr", "de", "ja", etc. |

### Studio Flow Summary

```
1. Create Studio Content
   ├── Audio: R7cb6c with type=1 and audio options
   ├── Video: R7cb6c with type=3 and video options
   ├── Infographic: R7cb6c with type=7 and infographic options
   └── Slide Deck: R7cb6c with type=8 and slide deck options

2. Returns immediately with artifact_id (status=in_progress)

3. Poll Status
   └── gArtLc → repeat until status=3 (completed)

4. When complete, response includes download URLs

5. Delete (optional)
   └── V5N4be with artifact_id → permanently removes content
```

---

## Report RPCs

Reports use the same `R7cb6c` RPC with **type code 2** (STUDIO_TYPE_REPORT).

### Report Request Structure
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        2,                         # STUDIO_TYPE_REPORT
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None,
        [
            None,
            [
                "Briefing Doc",           # Report title/format
                "Key insights and quotes", # Short description
                None,
                [[source_id1], [source_id2], ...],  # Source IDs (simpler format)
                "en",                      # Language code
                "Create a comprehensive...",  # Full prompt/instructions
                None,
                True                       # Unknown flag
            ]
        ]
    ]
]
```

### Standard Report Formats

| Format | Description | Prompt Style |
|--------|-------------|--------------|
| **Briefing Doc** | Key insights and important quotes | Comprehensive briefing with Executive Summary |
| **Study Guide** | Short-answer quiz, essay questions, glossary | Educational focus with test prep materials |
| **Blog Post** | Insightful takeaways in readable article format | Engaging, accessible writing style |
| **Create Your Own** | Custom format with user-defined structure | User provides custom prompt |

### `ciyUvf` - Get Suggested Report Formats

Returns AI-generated suggested report topics based on notebook sources.

```python
# Request params
params = [[2], notebook_id, [[source_id1], [source_id2], ...]]

# Response: Array of suggested reports with full prompts
[
    [
        "Strategy Briefing",           # Title
        "An analysis of...",           # Description
        None,
        [[source_ids]],                # Sources
        "Synthesize the provided...",  # Full AI prompt
        2                              # Audience level (1=beginner, 2=advanced)
    ],
    # ... more suggestions
]
```

### `VfAZjd` - Get Notebook Summary and Report Suggestions

Returns an AI-generated summary of the notebook and suggested report topics.

```python
# Request params
[notebook_id, [2]]

# Response structure
[
    [
        "The provided documents explore...",  # AI-generated summary (markdown formatted)
    ],
    [
        [
            [
                "How do generative AI tools...",  # Suggested topic question
                "Create a detailed briefing..."   # Full prompt for report
            ],
            # ... more suggested topics
        ]
    ]
]
```

**Summary format:** Markdown text with **bold** keywords highlighting key themes.

**Use case:** This RPC provides the notebook description shown in the Chat panel when you first open a notebook. Perfect for a `notebook_describe` tool to give users a high-level overview of what a notebook contains.

### `tr032e` - Get Source Guide

Generates an AI summary and keyword chips for a specific source. This is the "Source Guide" feature shown when clicking on a source in the Gemini Notebook UI.

```python
# Request params
params = [[[["source_id"]]]]
# Source ID in deeply nested arrays

# Example
params = [[[["5d318300-1b66-4bf6-ad3a-072c76f8a8eb"]]]]

# Response structure
[
    [
        null,
        [
            "This facilitator's guide outlines a specialized workshop designed to help **medical residents and fellows** leverage **generative artificial intelligence**..."
            # AI-generated summary with **bold** markdown for keywords
        ],
        [
            ["Medical education", "Generative AI tools", "Resident teaching skills", "Educational content creation", "Ethics and risks"]
            # Array of keyword chips
        ],
        []
    ]
]
```

**Response fields:**
- `[0][1][0]`: AI-generated summary (markdown formatted with **bold** keywords)
- `[0][2][0]`: Array of keyword chip strings

**Use case:** Perfect for a `source_describe` tool that provides an AI-generated overview of individual sources, similar to `notebook_describe` for notebooks.

---

## Interactive Report RPCs (type 11)

Interactive Reports (`STUDIO_TYPE_INTERACTIVE_REPORT = 11`) are long-form reports
that embed Studio elements - audio, video, mind map, infographic, flashcards,
slide deck and quiz - as *suggested* placeholder cards the reader can generate
in place ("Add" -> "Generate", or "Add" -> "Customize"). They are created with the standard `R7cb6c` create
RPC, read back with `v9rmvd`, and their elements are generated with an `rc3d8d`
field update followed by a `Rytqqe` kickoff.

### Create request (`R7cb6c`, type 11)

```python
params = [
    [2, null, null,
     [1, null, null, null, null, null, null, null, null, null, [1]],  # template block ([1] = Learning Overview)
     [[1, 4, 8, 10, 14, 2, 3, 6]]],                                   # embeddable element type codes
    notebook_id,
    [
        null, null,
        11,                                   # STUDIO_TYPE_INTERACTIVE_REPORT
        [[[source_id1]], [[source_id2]], ...],
        null, null, null, null, null, null, null, null, null, null,
        null, null, null, null, null, null, null, null, null, null,
        null, null, null, null, null, null, null, null, null, null,      # indices 4..33 are padding
        [null, [custom_prompt, language]],                               # options block at index 34 (not 7!)
    ],
]
```

| Difference from classic (type 2) reports | Detail |
|------------------------------------------|--------|
| Config block | Gains the template selector and the element-type allowlist |
| Options block | Moves from index 7 to index 34 |
| Response `[2]` | `11` instead of `2` |

### Artifact layout

| Index | Contents |
|-------|----------|
| `[0]` | artifact id |
| `[1]` | title (auto-generated by the model) |
| `[2]` | `11` |
| `[3]` | sources |
| `[4]` | status code (see below) |
| `[10]` | modified timestamp `[seconds, nanos]` |
| `[15]` | created timestamp `[seconds, nanos]` |
| `[21]` | base64 task id |
| `[34]` | `[document_or_null, [prompt, language, generated?]]` |

Suggested *element* artifacts (status 5) are 36 long and carry
`[title, card_description]` at index `[35]`. The description is the text shown
on the card; a plain "Generate" click sends it as the steering prompt.

Sources: the report stores its sources at `[3]` as `[[[""], null, 5], ...]`.
Suggested infographic / slide-deck elements carry the report's sources, but
suggested type-4 elements (mind map, quiz, flashcards) were observed with
`[3] = null`. The client therefore always sends the **report's** source ids in
the `rc3d8d` update and refuses to generate when they cannot be resolved; it
never widens to every notebook source.

Status codes observed at index `[4]` (extends the standard set):

| Code | Meaning |
|------|---------|
| 1 | in_progress (document rendering) |
| 2 | queued/generating (observed for interactive reports, mind-map, quiz, flashcard and slide-deck elements; audio keeps its completed-with-media special case) |
| 3 | completed |
| 4 | failed |
| 5 | suggested (element placeholders; excluded by the default `gArtLc` filter) |

### Document blocks (`artifact[34][0][0][0]` once generated)

A flat list of blocks:

| Block | Shape |
|-------|-------|
| Heading | `[null, null, [[[null, null, ["<title>"]]], [null, 5]]]` |
| Paragraph | `[start, end, [[[start, end, ["<text>", attrs?]], ...]]]` - `attrs == [true]` renders bold; offsets are character positions |
| Bullet | `[start, end, [[span], [null, 1], null, [null, null, 0, meta]]]` with `meta = {"101": bullet_char, "102": 1, "103": item_index, "104": running_counter}` |
| Embed | `[null x12, ["<element id>", 1]]` - placeholder for a suggested Studio element (the trailing flag was observed as `1` both before and after the element was generated, so treat it as informational) |

### Generating an embedded element

Two calls, mirroring the web UI:

1. `rc3d8d` - update artifact fields via field mask:

```python
[
    [element_id, null, null, [[[source_id]], ...], null, null, null, null, null,
     [null, [subtype, null, steering_prompt, language]]],      # subtype e.g. 4 = mind map
    [["app.generation_options.free_text_steering_prompt",
      "app.generation_options.language_code",
      "sources"]],
    null,
    [2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]],
     [[1, 4, 8, 10, 14, 2, 3, 6, 7]]],
]
```

2. `Rytqqe` - kick off generation (response is the refreshed artifact). The
   element id is a **plain string**, not wrapped in a list, and the config is
   the same for every element kind:

```python
[[2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]],
  [[1, 4, 8, 10, 14, 2, 3, 6]]],
 "<element_id>"]
```

Masks and option blocks are **kind-specific** (all captured live 2026-09-24):

### Settings — payload and field mask pairs

"Generate" = plain Generate click. "Customize" = Customize panel. Codes match
the existing `constants.py` mappers unless noted.

| Kind | Mode | Options block | Field mask (besides `sources`) |
|------|------|---------------|--------------------------------|
| Quiz | both | `[9] = [null, [2, null, prompt, lang, null, null, null, [count, difficulty]]]` | `app.generation_options.free_text_steering_prompt`, `.language_code`, `.quiz_generation_options.question_quantity`, `.quiz_difficulty` |
| Flashcards | both | `[9] = [null, [1, null, prompt, lang, null, null, [count, difficulty]]]` | `app…free_text_steering_prompt`, `.language_code`, `.flashcards_generation_options.card_quantity`, `.flashcards_difficulty` |
| Mind map | both | `[9] = [null, [4, null, prompt, lang]]` | `app…free_text_steering_prompt`, `.language_code` |
| Infographic | both | `[14] = [[prompt, lang, null, orientation, detail, style]]` | `infographic.generation_options.user_steering_prompt`, `.language_code`, `.aspect_ratio`, `.information_density`, `.style` |
| Slide deck | both | `[16] = [[prompt, lang, format, length]]` | `slides.generation_options.user_steering_prompt`, `.language_code`, `.deck_type`, `.length` |
| Audio | Generate | `[6] = [null, [prompt, null, null, null, lang, null, format]]` | `audio_overview.generation_options.episode_focus`, `.language_code`, `.show_format` |
| Audio | Customize | `[6] = [null, [prompt, 2, null, null, lang, null, format]]` | adds `.episode_length` |
| Video | Generate | `[8] = [null, null, [null, lang, prompt, null, format]]` | `explainer_video.generation_options.language_code`, `.video_focus`, `.template_format` |
| Video | Customize | `[8] = [null, null, [null, lang, prompt, null, format, 1]]` | adds `.video_overview_style` |
| Video (Short) | Customize | `[8] = [null, null, [null, lang, prompt, null, 4]]` | same as Generate: **no** `.video_overview_style` |

Value codes:

| Setting | Values (code) | Plain-Generate default |
|---------|---------------|------------------------|
| quiz `question_amount` | fewer 1, standard 2, more 3 | standard |
| flashcards `card_amount` | fewer 1, standard 2, more 3 | **fewer** (page pre-selects it though "Standard" is labelled default) |
| `difficulty` (quiz, flashcards) | easy 1, medium 2, hard 3 | medium |
| infographic `orientation` | landscape 1, portrait 2, square 3 | landscape |
| infographic `detail_level` | concise 1, standard 2, detailed 3 | standard |
| infographic `infographic_style` | existing 11-value mapper (e.g. kawaii 10) | auto_select |
| slide `slide_format` | detailed_deck 1, presenter_slides 2 | detailed_deck |
| slide `slide_length` | short **2**, default 3 (report elements; regular Studio slide decks use short 1) | default |
| audio `audio_format` | deep_dive 1, brief 2, critique 3, debate 4 | brief |
| video `video_format` | explainer 1, cinematic 3, short 4 | cinematic |

Report elements expose **no** audio length and **no** video style in the UI;
the tool uses the Customize mask with the fixed values the page sends
(length 2, style 1) and does not let agents set them. Short videos are sent
without the style field, exactly as the page does.

Sources: a plain "Generate" in the web UI sends every source currently
selected in the notebook's source panel, even when the report was built from
fewer. The client deliberately sends the **report's** sources instead, so
elements stay scoped to what the report covers (verified 2026-09-24).

> **Correction (2026-09-24).** An earlier version of this section said the
> `Rytqqe` kickoff only works from the web page. That was wrong: the kickoff was
> being sent with the element id wrapped in a list (`[element_id]`), which the
> server rejects with `INVALID_ARGUMENT`. The `[2, null, [1], [..., [1, 3]]]`
> value previously listed as the mind-map kickoff config is the page's
> `x-goog-ext-525006520-jspb` request **header**, not part of the body. With the
> plain-string id, the kickoff succeeds from the plain HTTP client.

After generation the element also appears in the notebook Studio panel while
remaining referenced by the report's embed block.

### Related observed RPCs

| RPC | Purpose | Notes |
|-----|---------|-------|
| `Fxmvse` | Set artifact read state | `[config, artifact_id, [null, null, null, null, 0], [["is_unread"]]]`; response echoes the flag |
| `HpN0Ub` | Mark artifact viewed / session bookkeeping | `[config, [artifact_id]]`, empty response |
| `EylDcb` | AI usage meter / pre-generation estimate | Called when the create dialog or element dialog opens |
| `sODAg` | Fired alongside `EylDcb` when generation dialogs open | Empty params, response `[true]`; purpose unidentified |

## Flashcard RPCs

Flashcards use the same `R7cb6c` RPC with **type code 4** (STUDIO_TYPE_FLASHCARDS).

### Flashcard Request Structure
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        4,                         # STUDIO_TYPE_FLASHCARDS
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None, None, None,  # 5 nulls (positions 4-8)
        [
            None,
            [
                1,                     # Unknown (possibly default count)
                None, None, None, None, None,
                [difficulty, card_count]  # [difficulty_code, card_count_code]
            ]
        ]
    ]
]
```

### Flashcard Options

| Option | Values |
|--------|--------|
| **Difficulty** | `easy` (1), `medium` (2), `hard` (3) - MCP tools accept string names |
| **Card Count** | Default count generated by AI |

**Note:** MCP tools (`flashcards_create`, `quiz_create`) accept string difficulty names which are mapped to internal codes via `constants.CodeMapper`.

---

## Quiz RPCs

Quizzes use the same `R7cb6c` RPC with **type code 4** (shared with Flashcards) but with different options structure.

### Quiz Request Structure
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        4,                         # STUDIO_TYPE_FLASHCARDS (shared with Quiz)
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None, None, None,  # 5 nulls (positions 4-8)
        [
            None,
            [
                2,                     # Format/variant code (distinguishes Quiz from Flashcards)
                None, None, None, None, None, None,
                [question_count, difficulty]  # [questions, difficulty_level]
            ]
        ]
    ]
]
```

### Quiz Options

| Option | Values |
|--------|--------|
| **Question Count** | Integer (default: 2) |
| **Difficulty** | `easy` (1), `medium` (2), `hard` (3) - MCP tools accept string names |

**Key Difference from Flashcards:** Quiz uses format code `2` at the first position of the options array, while Flashcards use `1`.

**Mind maps share type code 4 too:** newer mind maps are stored as type-4 studio artifacts with format code `4` at `artifact[9][1][0]` (older mind maps live in the notes store via the `cFji9` RPC). The status parser classifies format code 4 as `mind_map`, and downloading one fetches the artifact's interactive HTML (`v9rmvd`) and extracts the mind map JSON (`{"name": ..., "children": [...]}`) from the `data-app-data` attribute — the same mechanism quiz/flashcards use.

---

## Data Table RPCs

Data Tables use the `R7cb6c` RPC with **type code 9** (STUDIO_TYPE_DATA_TABLE).

Excel exports of data tables are returned by the same status RPC with **type
code 10** (`STUDIO_TYPE_DATA_TABLE_XLSX`). Their file metadata is at
`artifact[24]` as `[filename, mime_type, viewer_url, download_url]`. The
download path streams `download_url` as binary XLSX without CSV parsing.

### Data Table Request Structure
```python
params = [
    [2],                           # Config
    notebook_id,                   # Notebook UUID
    [
        None, None,
        9,                         # STUDIO_TYPE_DATA_TABLE
        [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
        None, None, None, None, None, None, None, None, None, None,  # 10 nulls (positions 4-13)
        None, None, None, None,    # 4 more nulls (positions 14-17)
        [
            None,
            [description, language]  # ["Description of table", "en"]
        ]
    ]
]
```

### Data Table Options

| Option | Description |
|--------|-------------|
| **Description** | String describing what data to extract (required) |
| **Language** | Language code (default: "en") |

**Note:** Data table options appear at position 18 in the content array, requiring 14 nulls after the sources.

---

## Mind Map RPCs

Mind Maps use a **two-step process** with separate Generate and Save RPCs.

### Step 1: `yyryJe` - Generate Mind Map

Generates the mind map JSON from sources.

```python
# Request params
params = [
    [[[source_id1]], [[source_id2]], ...],  # Source IDs (nested arrays)
    None, None, None, None,
    ["interactive_mindmap", [["[CONTEXT]", ""]], ""],  # Type identifier
    None,
    [2, None, [1]]  # Config
]

# Response
[
    json_mind_map_string,  # Hierarchical JSON with name/children structure
    None,
    [generation_id1, generation_id2, generation_number]
]
```

### Step 2: `CYK0Xb` - Save Mind Map

Saves the generated mind map to the notebook.

```python
# Request params
params = [
    notebook_id,
    json_mind_map_string,  # The full JSON structure from step 1
    [2, None, None, 5, [[source_id1], [source_id2], ...]],  # Metadata with sources
    None,
    "Mind Map Title"  # Display title
]

# Response
[
    mind_map_id,           # UUID for the saved mind map
    json_mind_map_string,  # The saved JSON structure
    [2, version_id, [timestamp, nanos], 5, [[source_ids]]],  # Metadata
    None,
    "Generated Title"      # AI-generated title
]
```

### `cFji9` - List Mind Maps

Retrieves all existing mind maps for a notebook.

```python
# Request params
[notebook_id]

# Response
[
    [
        [mind_map_id, [
            mind_map_id,
            json_mind_map_string,
            [2, version_id, [timestamp, nanos], 5, [[source_ids]]],
            None,
            "Mind Map Title"
        ]],
        # ... more mind maps
    ],
    [timestamp, nanos]  # Last updated
]
```

### Mind Map JSON Structure

```json
{
  "name": "Root Topic",
  "children": [
    {
      "name": "Category 1",
      "children": [
        { "name": "Subcategory 1.1" },
        { "name": "Subcategory 1.2" }
      ]
    },
    {
      "name": "Category 2",
      "children": [
        { "name": "Subcategory 2.1" },
        {
          "name": "Subcategory 2.2",
          "children": [
            { "name": "Leaf Node" }
          ]
        }
      ]
    }
  ]
}
```

### Mind Map Flow Summary

```
1. Generate Mind Map
   └── yyryJe with source IDs → returns JSON structure

2. Save Mind Map
   └── CYK0Xb with notebook_id, JSON, title → returns saved mind map with ID

3. List Mind Maps (optional)
   └── cFji9 with notebook_id → returns all mind maps
```

---

## Notes RPCs

Notes are saved AI chat responses that appear in the notebook's left panel. They share the same storage structure as mind maps (both use `cFji9` for listing) but are distinguished by their content format.

**Key Differences from Mind Maps:**
- Notes: Plain text content
- Mind Maps: JSON structure with `"children"` or `"nodes"` keys

### `CYK0Xb` - Create Note

Creates a new note in a notebook. Same RPC as Save Mind Map, differs by parameters.

```python
# Request params
params = [
    notebook_id,
    "",           # Empty content (updated separately via UPDATE_NOTE)
    [1],          # Note type identifier
    None,
    "Note Title"  # Display title
]

# Response
[
    [note_id, "Note Title"],  # Note ID and title
]
```

**Note:** After creation, use `cYAfTb` (UPDATE_NOTE) to set the content.

### `cFji9` - List Notes and Mind Maps

Retrieves all notes and mind maps for a notebook. Filter by content type to distinguish.

```python
# Request params
[notebook_id]

# Response
[
    [
        # Regular note
        [note_id, [
            note_id,
            "Note content text",
            [metadata],
            None,
            "Note Title"
        ], status],

        # Mind map (has JSON content)
        [mind_map_id, [
            mind_map_id,
            '{"children": [...]}',  # JSON structure
            [metadata],
            None,
            "Mind Map Title"
        ], status],

        # Deleted item (status = 2 or data is None)
        [deleted_id, None, 2],
    ],
    [timestamp, nanos]  # Last updated
]
```

**Item Structure:**
- `[0]`: Item ID
- `[1]`: Item data (or None if deleted)
  - `[0]`: Item ID (duplicate)
  - `[1]`: Content (text for notes, JSON for mind maps)
  - `[2]`: Metadata
  - `[3]`: Always None
  - `[4]`: Title
- `[2]`: Status (2 = deleted)

**Filtering Notes from Mind Maps:**
```python
for item in items:
    if item[1] is None or (len(item) > 2 and item[2] == 2):
        continue  # Skip deleted items

    content = item[1][1]
    try:
        parsed = json.loads(content)
        if "children" in parsed or "nodes" in parsed:
            # It's a mind map
        else:
            # It's a note
    except (json.JSONDecodeError, TypeError):
        # It's a note (plain text)
```

### `cYAfTb` - Update Note

Updates a note's content and/or title.

```python
# Request params
params = [
    notebook_id,
    note_id,
    [[[
        "Updated note content",
        "Updated Title",
        [],  # Unknown field
        0    # Unknown field
    ]]],
]

# Response
null  # Null on success
```

**Important:** Both notebook_id and note_id are required.

### `AH0mwd` - Delete Note

Soft-deletes a note (clears content, keeps ID). Same RPC as Delete Mind Map.

```python
# Request params
params = [
    notebook_id,
    None,
    [note_id]  # Can delete multiple: [note_id1, note_id2, ...]
]

# Response
null  # Null on success
```

**Note:** This is a soft-delete - the item appears in listings with `status = 2` or `data = None`.

### Notes Workflow

```
1. Create Note
   └── CYK0Xb with notebook_id, title → returns note_id

2. Update Content
   └── cYAfTb with notebook_id, note_id, content, title → returns null

3. List Notes
   └── cFji9 with notebook_id → filter by content type (text vs JSON)

4. Delete Note
   └── AH0mwd with notebook_id, note_id → returns null
```

---

## Studio Type Codes Summary

| Type Code | Feature | RPC |
|-----------|---------|-----|
| 1 | Audio Overview | `R7cb6c` |
| 2 | Report | `R7cb6c` |
| 3 | Video Overview | `R7cb6c` |
| 4 | Flashcards / Quiz / Mind Map (subtype at `[9][1][0]`: 1=cards, 2=quiz, 4=mind map) | `R7cb6c` |
| 7 | Infographic | `R7cb6c` |
| 8 | Slide Deck | `R7cb6c` |
| 9 | Data Table | `R7cb6c` |
| 10 | Data Table file export (XLSX/CSV/...) | `R7cb6c` |
| 11 | Interactive Report (embeds Studio elements; options at index 34) | `R7cb6c` + `v9rmvd` + `rc3d8d`/`Rytqqe` |
| N/A | Mind Map | `yyryJe` + `CYK0Xb` (separate RPCs) |

---

## Key Findings

1. **Filtering is client-side**: The `wXbhsf` RPC returns ALL notebooks. "My notebooks" vs "Shared with me" filtering happens in the browser.

2. **Unified source RPC**: All source types (URL, text, Drive) use the same `izAoDd` RPC with different param structures.

3. **Query is streaming**: The query endpoint streams the AI's thinking process before the final answer.

4. **Conversation support**: Pass a `conversation_id` for multi-turn conversations (follow-up questions).

5. **Rate limits**: Free tier has ~50 queries/day limit.

6. **Research uses same RPC for Web and Drive**: The `Ljjv0c` RPC handles both Web (source_type=1) and Drive (source_type=2) Fast Research. Only the source_type parameter differs.

7. **Deep Research is Web-only**: The `QA9ei` RPC only supports Web sources (source_type=1). Google Drive does not have a Deep Research equivalent.

---

## Drive Source Sync

### Problem
Gemini Notebook doesn't auto-update Google Drive sources when the underlying document changes. Users must manually click each source > "Check freshness" > "Click to sync with Google Drive".

### Solution
The `source_list_drive` and `source_sync_drive` tools automate this process.
For very large notebooks, `source_list_drive(notebook_id, skip_freshness=True)` skips
the per-source freshness RPCs and returns the source list faster, with stale status
reported as unknown.

### Source Metadata Structure (from `rLM1Ne` response)

Each source in the notebook response has this structure:
```python
[
  [source_id],           # UUID for the source
  "Source Title",        # Display title
  [                      # Metadata array
    drive_doc_info,      # [0] [doc_id, version_hash] for Docs/Slides/Sheets
    byte_count,          # [1] content size (0 for Drive, actual size for pasted text)
    [timestamp, nanos],  # [2] creation timestamp
    [version_uuid, [timestamp, nanos]],  # [3] last sync info
    source_type,         # [4] KEY FIELD: 1=Google Docs, 2=Slides/Sheets, 4=Pasted Text
    null,                # [5]
    null,                # [6]
    null,                # [7]
    content_bytes,       # [8] actual byte count (for Drive sources after sync)
    drive_file_info      # [9] [drive_id, version, mime_type, ""] for type-14 Drive-picker files
  ],
  [null, 2]              # Footer constant
]
```

### Source Types (metadata position 4)

The metadata example above shows a Drive source. For URL-bearing responses,
web sources (type 5) carry `[url]` at `metadata[7]`; YouTube sources (type 9)
carry `[url, video_id, channel]` at `metadata[5]` and normally leave
`metadata[7]` empty. The shared URL extractor checks position 7 first, then
falls back to position 5 only for YouTube. These are response metadata slots,
not the positions used in the add-source request payload.

| Type | Meaning | Drive Doc Info | Can Sync |
|------|---------|----------------|----------|
| 1 | **Google Docs** (Documents, including Gemini Notes) | `[doc_id, version_hash]` | **Yes** |
| 2 | **Google Slides/Sheets** (Presentations & Spreadsheets) | `[doc_id, version_hash]` | **Yes** |
| 4 | Pasted text | `null` | No |
| 14 | Drive-picker file (PDF, text, Markdown, Word, PowerPoint, etc.) | `[drive_id, version, mime_type, ""]` at `metadata[9]` | **Yes, when a Drive ID is present** |

Type 14 is also used for directly uploaded files. Those have no Drive metadata at
`metadata[9]` and are not syncable. The Drive ID must be present before a type-14
source is listed by `source_list_drive` or marked `can_sync: true`. This flag means
the source is eligible for a manual sync attempt; an individual RPC can still fail
(one stale type-14 PDF returned Google RPC code 13, while a separate PDF sync
succeeded). Google announced automatic Drive syncing for Docs, Sheets, and Slides,
but did not specify type-14 files in that update.

---

## How We Discovered This

### Method: Network Traffic Analysis

1. Used Chrome DevTools MCP to automate browser interactions
2. Captured network requests during each action
3. Decoded `f.req` body (URL-encoded JSON)
4. Analyzed response structures
5. Tested parameter variations

### Discovery Session Examples

**Creating a notebook:**
1. Clicked "Create notebook" button via Chrome DevTools
2. Captured POST to batchexecute with `rpcids=CCqFvf`
3. Decoded params: `["", null, null, [2], [1,null,...,[1]]]`
4. Response contained new notebook UUID at index 2

**Adding Drive source:**
1. Opened Add source > Drive picker
2. Double-clicked on a document
3. Captured POST with `rpcids=izAoDd`
4. Decoded: `[[[[doc_id, mime_type, 1, title], null,...,1]]]`
5. Different from URL/text which use different array positions

**Querying:**
1. Typed question in query box, clicked Submit
2. Found NEW endpoint: `GenerateFreeFormStreamed` (not batchexecute!)
3. Streaming response with thinking steps + final answer
4. Includes citations with source passage references

---

## Essential Cookies

The MCP needs these cookies (automatically filtered from the full cookie header):

| Cookie | Purpose |
|--------|---------|
| `SID`, `HSID`, `SSID`, `APISID`, `SAPISID` | Core auth (required) |
| `__Secure-1PSID`, `__Secure-3PSID` | Secure session variants |
| `__Secure-1PAPISID`, `__Secure-3PAPISID` | Secure API variants |
| `OSID`, `__Secure-OSID` | Origin-bound session |
| `__Secure-1PSIDTS`, `__Secure-3PSIDTS` | Timestamp tokens |
| `SIDCC`, `__Secure-1PSIDCC`, `__Secure-3PSIDCC` | Session cookies |

**Important:** Some cookies (PSIDTS, SIDCC, PSIDCC) rotate frequently. Always get fresh cookies from an active Chrome session.

### Token Extraction

**Three ways to get CSRF token and session ID:**

1. **From network request (fastest):**
   - Extracted directly from `get_network_request()` data
   - No page fetch required
   - Saves to cache for reuse

2. **From page fetch (slower first time):**
   - Client fetches `notebook.google.com` using cookies
   - Extracts `SNlM0e` (CSRF) and `FdrFJe` (session ID) from HTML
   - Saves to cache for reuse
   - ~1-2 seconds one-time delay

3. **From cache (instant):**
   - Subsequent requests reuse cached tokens
   - No fetching needed
   - Cache updates automatically when tokens are refreshed

`SNlM0e` is the CSRF field. `FdrFJe` is a session ID and `cfb2h` is a build
label; neither proves sign-in or substitutes for a CSRF token. Unrelated
`at=` text in page markup is not a token either. Interactive CDP login requires
the actual CSRF field together with the required signed-in cookies. Live
validation of a homepage without CSRF falls back to a notebook API check.

---

## Label Management RPCs

Labels allow users to organize sources into thematic categories. Available when a notebook has 5+ sources. Sources can belong to multiple labels.

**UI operations available:**
- Auto-label (AI generates categories from sources)
- Create a new empty label
- Rename a label
- Set/change an emoji on a label
- Remove a label (sources are preserved, not deleted)
- Move a source to a different label (multi-label assignment via checkboxes)
- Return to flat list view

### `agX4Bc` — Auto-Label / Reorganize / Create Label / List Labels

This RPC handles auto-labeling, force-reorganization, and manual label creation.
The 5th parameter (mode) controls the labeling behavior:

| Mode | Meaning |
|------|---------|
| `[]` | Return existing labels, or auto-generate if none exist |
| `[0]` | Reorganize: only label sources not yet in any label (no confirmation in UI) |
| `[1]` | Reorganize: force-regenerate ALL labels from scratch (UI shows confirmation dialog) |
| `null` + 6th param | Create a new empty label with the given name/emoji |

#### Auto-label all sources (returns existing if already labeled)

```python
# Request params
[[2], notebook_id, null, null, []]

# Example
[[2], "180cfc20-9d9f-4ebf-a5d3-1b50d5593b8b", null, null, []]

# Response: [null, [[label_name, [[src_id], ...], label_id, emoji], ...]]
[null, [
    ["User Tutorials",
     [["0baf49b7-..."], ["86132bc5-..."], ["acd49c62-..."]],
     "0245af0d-2663-40df-96b9-567f7ed1ce6f",
     ""],  # emoji (empty string = no emoji)
    ["Enterprise Use Cases",
     [["845deae5-..."], ["86132bc5-..."], ["98881af4-..."]],
     "286c7cc0-39f3-4349-940f-ea50e34169eb",
     ""],
    # ...
]]
```

#### Reorganize — all sources (replaces existing labels)

```python
# Request params — [1] = force full regeneration
[[2], notebook_id, null, null, [1]]

# Example
[[2], "180cfc20-9d9f-4ebf-a5d3-1b50d5593b8b", null, null, [1]]

# Response: same structure as auto-label — returns the new label set
# NOTE: UI shows a confirmation dialog before sending this request
```

#### Reorganize — unlabeled sources only

```python
# Request params — [0] = only label sources with no existing label assignment
[[2], notebook_id, null, null, [0]]

# Example
[[2], "180cfc20-9d9f-4ebf-a5d3-1b50d5593b8b", null, null, [0]]

# Response: same structure as auto-label — returns the full updated label set
# NOTE: UI fires this immediately without a confirmation dialog
```

#### Create a new empty label

```python
# Request params — note position 5 contains [[label_name, emoji]]
[[2], notebook_id, null, null, null, [[label_name, ""]]]

# Example: create "My New Label" with no emoji
[[2], "180cfc20-...", null, null, null, [["My New Label", ""]]]

# Response: same as auto-label — returns full updated label list with new label
# The new label has null sources and a freshly generated label_id:
[null, [
    # ... existing labels ...
    ["My New Label", null, "a0e1a3d0-bcc4-4619-bcd2-f152c64fe7d8", ""]
]]
```

### `le8sX` — Label Mutation (Rename / Set Emoji / Move Source)

This single RPC handles all label content mutations. The 4th parameter determines the operation type.

#### Rename a label

```python
# Request params
[[2], notebook_id, label_id, [[[new_name]]]]

# Example: rename to "Enterprise Use Cases"
[[2], "180cfc20-...", "286c7cc0-...", [[["Enterprise Use Cases"]]]]

# Response: [] (empty = success)
```

#### Set (or change) an emoji on a label

```python
# Request params — null name + emoji string at position 1
[[2], notebook_id, label_id, [[[null, "🏷️"]]]]

# Example
[[2], "180cfc20-...", "6d7564e6-...", [[[null, "📊"]]]]

# Response: [] (empty = success)
```

#### Move / assign a source to a label

Adds the source to the target label (multi-label — sources can be in multiple labels).

```python
# Request params — null at position 0, source_id list at position 1
[[2], notebook_id, target_label_id, [[null, [[source_id]]]]]

# Example: move source to "Marketing Applications"
[[2], "180cfc20-...", "6d7564e6-...", [[null, [["845deae5-df8a-4c9a-9d11-53b0761823af"]]]]]

# Response: [] (empty = success)
```

### `GyzE7e` — Delete Label(s)

Deletes one or more labels. Sources belonging to the deleted labels are NOT deleted.
If removing a label that contains sources exclusively, reassign them first via `le8sX`.

```python
# Request params — array of label_ids to delete
[[2], notebook_id, [label_id, ...]]

# Example: delete one label
[[2], "180cfc20-...", ["286c7cc0-39f3-4349-940f-ea50e34169eb"]]

# Example: delete multiple labels at once
[[2], "180cfc20-...", ["label-id-1", "label-id-2"]]

# Response: [] (empty = success)
```

### `LQhfEb` — Toggle Source Panel View

Saves the user's preference for label view vs flat list view. Not needed for label management itself, but fired when switching between views.

```python
# Label view (view=1)
[null, notebook_id, [null, [null, 1]], [["notebook_lm_state.saved_source_panel_view"]]]

# List view (view=2)
[null, notebook_id, [null, [null, 2]], [["notebook_lm_state.saved_source_panel_view"]]]

# Response: [] (empty = success)
```

### Remove Label — Two-step Process

The "Remove" operation in the UI (which preserves sources) is a two-step sequence:
1. `le8sX` — Move any sources that would become orphaned to another label
2. `GyzE7e` — Delete the now-empty label

```python
# Step 1: reassign orphaned sources to a different label
le8sX([[2], notebook_id, target_label_id, [[null, [[orphaned_source_id]]]]])

# Step 2: delete the label
GyzE7e([[2], notebook_id, [label_id_to_remove]])
```

---

## User Tier and Usage Limits

### `ozz5Z` — Get User Subscription Tier

Returns the user's current Gemini Notebook subscription tier. Fires on the homepage (`source-path=/`) during page load.

**Captured request params (2026-04-27, `source-path=/`):**
```python
# Inner JSON params sent to the RPC:
[[[[null, "1", 627], [null,null,null,null,null,null,null,null,null,[null,null,2]], 1]]]

# 627 and "1" appear to be hardcoded NotebookLM product/SKU constants.
# The same values appear in the support URL: ?ms=pt:1613;s:627
# path used: source-path=/ (homepage, no notebook context needed)
```

**Captured response (decoded, 2026-04-27):**
```python
# Tier string is at response[0][0][1][0][1][...]["NOTEBOOKLM_TIER_PRO_DASHER_END_USER"]
# Full decoded structure (abbreviated):
[[[[null,"1",627],[[1613,[..., "NOTEBOOKLM_TIER_PRO_DASHER_END_USER", ...]], 0]]]]

# Also contains:
# - "Manage subscription" link
# - Support URL: https://support.google.com/notebooklm/answer/16213268?ms=pt:1613;s:627;vp:9
# - Encoded tokens (session context): "CM0MEO4KICso..." and "CAI="
```

**Known tier strings:**
| Tier String | Plan |
|-------------|------|
| `NOTEBOOKLM_TIER_STANDARD` | Free / Standard |
| `NOTEBOOKLM_TIER_PLUS` | Google AI Plus |
| `NOTEBOOKLM_TIER_PRO` | Google AI Pro |
| `NOTEBOOKLM_TIER_PRO_DASHER_END_USER` | Google Workspace Pro user |
| `NOTEBOOKLM_TIER_ULTRA` | Google AI Ultra |

**Implementation note:** This RPC is dual-mapped in `core/utils.py` as `add_source_v2`. When called
with the homepage params above it returns tier info. When called in a notebook context with different
params it handles the newer source-add flow. Use `source-path=/` and the params above for tier detection.

### `ZwVcOc` — Settings (also fires on homepage)

Also fires on every page load. Returns app settings including what appear to be account-level limits.

**Captured request params (2026-04-27):**
```python
[null, [1, null, null, null, null, null, null, null, null, null, [1]]]
```

**Captured response (decoded, 2026-04-27):**
```python
[[null, [6, 500, 300, 500000, 2], [true, null, null, true, ["en", ...], ...], [[1]], [true, 2, 3, 2]]]

# [6, 500, 300, 500000, 2] — second element — suspected to be account limits:
#   500 = max notebooks (matches Pro tier: 500/user)
#   300 = max sources per notebook (matches Pro tier: 300/notebook)
#   6, 500000, 2 — unknown; could be tier code, storage, or other config
# Not confirmed — needs testing against Standard/Plus/Ultra accounts to validate.
```

### Usage Limits by Tier (from official docs, subject to change)

| Feature | Standard | Plus | Pro | Ultra |
|---------|----------|------|-----|-------|
| Notebooks | 100/user | 200/user | 500/user | 500/user |
| Sources | 50/notebook | 100/notebook | 300/notebook | 600/notebook |
| Chats | 50/day | 200/day | 500/day | 5,000/day |
| Audio Overviews | 3/day | 6/day | 20/day | 200/day |
| Video Overviews | 3/day | 6/day | 20/day | 200/day |
| Cinematic Videos | — | — | 2/day | 20/day |
| Reports | 10/day | 20/day | 100/day | 1,000/day |
| Flashcards | 10/day | 20/day | 100/day | 1,000/day |
| Quizzes | 10/day | 20/day | 100/day | 1,000/day |
| Mind Maps | Unlimited | Unlimited | Unlimited | Unlimited |
| Deep Research | 10/month | 3/day | 20/day | 200/day |
| Data Tables | Limited | More | Higher | Highest |
| Infographics | Limited | More | Higher | Highest |
| Slide Decks | Limited | More | Higher | Highest |

**Notes:**
- Auto-generated artifacts (on first source add) do NOT count toward limits
- **Superseded for chat and Studio as of 2026-09-02** — see the compute-based
  windows below. The per-notebook and per-source structural caps still apply.

### `EylDcb` — Get Remaining Usage (compute windows)

On 2026-09-02 Gemini Notebook replaced fixed daily caps for chat and Studio with
a compute-based allowance. Consumption depends on prompt complexity and the
features used, not on a count of requests, so the client cannot derive what is
left by counting its own calls. This RPC reports it directly. Fires when the
usage dialog is opened from the Settings menu.

**Captured request params (2026-09-12):**
```python
# The standard RPC header is the only parameter:
[[2, null, [1], [1, null, null, null, null, null, null, null, null, null, [1, 3]]]]
```

**Captured response (decoded, 2026-09-12):**
```python
[1,
 [[null, null, null, null, 2, [1789697670, 16986000], 8.703406947222222, 91.29659305277778],
  [null, null, null, null, 1, [1789200870, 16877000], null, 100]],
 null,
 [[1, true, 6, 3, 1, 11.177083333333334], ...]]   # 24 entries, appears to be recent activity
```

Each entry in `response[1]` describes one window:

| Index | Meaning |
|-------|---------|
| 4 | Window type: `1` = short rolling window (~5h), `2` = weekly window |
| 5 | Reset time as `[epoch_seconds, nanoseconds]` |
| 6 | Percent of the allowance **used**; `null` when nothing has been consumed |
| 7 | Percent of the allowance **remaining** |

**Index 6 is "used" and index 7 is "remaining"**, confirmed two ways: the web
dialog read "0% used" while the payload carried `100`, and a partly consumed
account returned `8.703406947222222` beside `91.29659305277778`, which sum to
exactly `100`.

**⚠️ The two entries are NOT returned in a stable order.** Observed as `[2, 1]`
in one call and `[1, 2]` in the next, for the same account. Always select a
window by its type code at index 4; reading by position silently swaps the
5-hour budget with the weekly one.

**⚠️ An expired session is not an exhausted quota.** When the saved session has
aged out, this RPC returns RPC error code 16 (`UNAUTHENTICATED`) and `ozz5Z`
returns a well-formed but empty entitlement. Neither may be read as "out of
quota" or as a free-tier account. Run `nlm auth refresh` and retry.

**The `authuser` query parameter has no effect here.** Omitting it and passing
`0`, `1` or `2` all returned byte-identical payloads for the same account.

**Implementation:** `core/usage.py` (`UsageMixin`), `services/usage.py`,
`nlm usage`, MCP tool `usage_get`.

---

## File: docs/AUTHENTICATION.md

# Authentication Guide

This guide explains how to authenticate with Gemini Notebook (formerly Google NotebookLM) MCP and CLI.

> For public HTTP deployment or Claude web/mobile connectors, see
> [Remote MCP Deployment](REMOTE_MCP.md). Remote use introduces a separate MCP
> endpoint authentication requirement in addition to the Google browser session
> described here.

## Overview

Gemini Notebook uses browser cookies for authentication (there is no official API). The CLI/MCP extracts these cookies automatically from a managed browser session:

- Chromium-family browsers use Chrome DevTools Protocol (CDP)
- Firefox uses an isolated profile and reads its cookie store directly (no CDP or WebDriver required)

**Supported browsers**: Google Chrome, Arc (macOS), Dia (macOS) Brave, Microsoft Edge, Microsoft Edge Beta, Chromium, Firefox, Vivaldi, Opera.

On Windows, standalone Chromium is discovered in the standard machine-wide
locations under `C:\Program Files` and `C:\Program Files (x86)`, plus the
per-user `%LOCALAPPDATA%\Chromium\Application\chrome.exe` location. You can
select it explicitly with `nlm config set auth.browser chromium`.

**Two authentication methods are available:**

| Method                   | Best For                        | Requires                        |
| ------------------------ | ------------------------------- | ------------------------------- |
| **Auto Mode** (default)  | Most users                      | Any supported browser installed |
| **File Mode** (`--file`) | Complex setups, troubleshooting | Manual cookie extraction        |

---

## Method 1: Auto Mode (Recommended)

This method launches your browser automatically and extracts cookies after you log in.

### Prerequisites

- A supported browser installed (Chrome, Arc, Dia, Brave, Edge, Edge Beta, Chromium, Firefox, Vivaldi, or Opera)
- Chromium-family browsers should be **completely closed** before running

### Steps

```bash
# 1. Close your browser completely (Cmd+Q on Mac, or quit from taskbar)

# 2. Run the auth command (CLI or standalone)
nlm login                      # Recommended
#    New profile? It first asks: Protected (OS keystore, recommended) or Plain file.
#    See "Choosing at first login" under Protected Storage below.

# 3. Log in to your Google account in the browser window that opens
#    (Ctrl+C cancels cleanly: nothing is saved and the browser window is closed)

# 4. Wait for "Successfully authenticated!"
```

Chromium/CDP login waits up to 300 seconds for sign-in. A page at
`notebook.google.com`, an HTTP 200 response, or an anonymous `NID` cookie does
not prove authentication. Login waits for signed-in Google cookies and the
app's CSRF token before saving credentials or reporting success. A timeout
does not save anonymous credentials or replace an existing saved login.

### What Happens Behind the Scenes

1. The first available supported browser is detected (or your preferred browser if configured)
2. A dedicated browser profile is created for authentication
3. The browser launches with the appropriate automation backend
4. You log in to Gemini Notebook via the browser
5. Signed-in cookies and the CSRF token are extracted and cached; session fields are refreshed automatically when needed
6. Builtin login closes the browser it launched after successful extraction. Externally managed CDP browsers remain open.

### Recovering a Saved Session

If authentication has expired, retry login with the same profile, then check it:

```bash
nlm login --profile work
nlm login --check --profile work
```

In v0.15.3 and later, an anonymous session saved by an older version no longer
passes validation and blocks browser login. Upgrade first if an older version
reports success with only `NID` and no CSRF token. Profile deletion is not the
first recovery step. If a check is inconclusive because of a timeout or network
failure, check connectivity or try a read-only API call before re-authenticating.

When Firefox is selected, the profile is isolated under the NLM storage directory and its cookie database is read directly. Because cookie extraction cannot prove which Google account is active, re-login against an existing saved profile requires explicit `nlm login --force` after you confirm the account.

### Browser Preference

By default, `nlm login` uses the first available browser. To use a specific browser:

```bash
# Set preferred browser
nlm config set auth.browser brave

# Or use an environment variable
export NLM_BROWSER=arc

# Valid values: auto, chrome, arc, brave, edge, edge-beta, chromium, firefox, vivaldi, opera
# If the preferred browser is not installed, falls back to auto-detection.
```

### Persistent Login

The dedicated browser profile persists your Google login:

- **First run:** You must log in to Google
- **Future runs:** Already logged in, just extracts fresh cookies

This profile is separate from your regular browser profile. Chromium profiles disable extensions.

---

## Multi-Profile Support

Use multiple Google accounts by creating named profiles:

```bash
# Create profiles for different accounts
nlm login --profile work       # Opens browser - log in with work account
nlm login --profile personal   # Opens browser - log in with personal account

# List all profiles
nlm login profile list
# Output:
#   work: jsmith@company.com
#   personal: jsmith@gmail.com

# Switch default profile (no --profile flag needed)
nlm login switch personal
# Output: ✓ Switched default profile to personal

# Use profiles
nlm notebook list                    # Uses default (personal)
nlm notebook list --profile work     # Uses work account

# Manage profiles
nlm login profile rename work company
nlm login profile delete old-profile
```

### Switching accounts from an MCP app (no CLI needed)

AI apps that only have the MCP (for example Claude CoWork) can use the `profile` tool: list your saved accounts, switch for the current session, and optionally make a profile your saved default (`make_default=true`). A session switch lasts until the MCP server restarts. Apps like Claude Desktop share one MCP server across chats, so a switch can show up in other open chats until you quit and reopen the app. While a switch is active, every tool result names the account in use.

### How Multi-Profile Works

Each profile gets:

- **Separate credentials**: Stored in `~/.notebooklm-mcp-cli/profiles/<name>/`
- **Separate browser profile**: Isolated browser session in `~/.notebooklm-mcp-cli/chrome-profiles/<name>/`
- **Captured email**: Automatically extracted during login for easy identification

This means you can stay logged into multiple Google accounts simultaneously without conflicts.

---

## Unattended / Scheduled Refresh

A logged-in session normally self-heals: when the short-lived cookies age out,
the client automatically runs a headless-browser pass to make Google reissue
them. For unattended machines (a Mac mini or server running scheduled jobs),
you can also refresh proactively so a session never lapses between jobs:

```bash
nlm auth refresh                 # Refresh the default profile
nlm auth refresh --profile work  # Refresh a named profile
```

`nlm auth refresh` runs a headless browser against the saved profile — no
interactive login, no window to click. It exits non-zero when the refresh
fails, so a scheduler can react. Example launchd/cron keep-alive (every 30 min):

```bash
*/30 * * * * /path/to/nlm auth refresh >/dev/null 2>&1
```

> **Note:** This needs a saved Chrome profile (from a prior `nlm login`). It
> does not help when `NOTEBOOKLM_COOKIES` is set as an environment variable —
> that value overrides saved credentials, so update it directly instead.
>
> In v0.15.4 and later, `nlm auth refresh` provides descriptive, actionable diagnostics
> indicating whether the saved profile was missing, signed out, encountered a DevTools websocket
> error, timed out loading NotebookLM, or had its extracted credentials rejected by the API.
> In addition, headless authentication automatically falls back to an OS-assigned ephemeral
> port if default DevTools ports (e.g. 9222/9223) are occupied or conflict.

> **Disabling the refresh:** Some Google Workspace accounts have their session
> revoked server-side whenever the saved browser profile is relaunched (issue
> #330). Set `NOTEBOOKLM_DISABLE_HEADLESS_REFRESH=1` to turn off both the
> automatic self-heal and `nlm auth refresh` on those accounts.

---

## Enterprise / Google Workspace

If your organization uses **Gemini Notebook Enterprise**, ask your Enterprise administrator for the project ID or number, the deployment location/multi-region, and confirmation that your account has access. Use the project- and location-specific host configured by your administrator (normally `notebook.cloud.google.com`). Set the base URL, project, and location before authenticating:

```bash
# Set the Enterprise URL and required Cloud resource context
export NOTEBOOKLM_BASE_URL=https://notebook.cloud.google.com
export NOTEBOOKLM_PROJECT_ID=your-gcp-project-id-or-number
export NOTEBOOKLM_LOCATION=global   # or us / eu, as provided by your administrator

# Then authenticate as usual
nlm login --profile enterprise
nlm login switch enterprise     # MCP uses the default profile
```

All CLI commands, MCP tools, and internal API calls will use this URL automatically. Enterprise requests require `NOTEBOOKLM_PROJECT_ID`; if the base URL is not set, the default personal URL (`https://notebooklm.google.com`) is used. The Enterprise variables apply to the current process, so use an Enterprise-only shell or MCP configuration when you also use a personal account.

> **Tip:** Add the export to your shell profile (`~/.zshrc`, `~/.bashrc`) so it persists across sessions.

For MCP server configuration, pass the variable in your client config:

```json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "notebooklm-mcp",
        "env": {
          "NOTEBOOKLM_BASE_URL": "https://notebook.cloud.google.com",
          "NOTEBOOKLM_PROJECT_ID": "your-gcp-project-id-or-number",
          "NOTEBOOKLM_LOCATION": "global"
        }
    }
  }
}
```

---

## The "Gemini Notebook" rebrand (`notebook.google.com`)

Google is rolling out a rebrand of Gemini Notebook that redirects some signed-in accounts to `notebook.google.com` instead of `notebooklm.google.com`. This is handled automatically: `nlm login` records whichever host accepts your account (per-profile, in `metadata.json`), and every CLI/MCP request is routed to that host afterward. No configuration is needed for personal accounts.

Resolution order, if you need to override it manually:

1. `NOTEBOOKLM_BASE_URL` env var, if set (see Enterprise section above).
2. The host your account last signed in on (auto-detected).
3. The default `https://notebooklm.google.com`.

---

## Method 2: File Mode

This method lets you manually extract and provide cookies. Use this if:

- Auto mode doesn't work on your system
- You have browser extensions that interfere (e.g., Google Antigravity IDE)
- You prefer manual control

### Steps

```bash
# Option A: Interactive mode (shows instructions, prompts for file path)
nlm login --manual

# Option B: Direct file path
nlm login --manual --file /path/to/cookies.txt
```

File mode verifies the imported cookies before saving them. For personal accounts, it checks both `notebooklm.google.com` and the rebranded `notebook.google.com`, then stores the host that accepts the session. Managed Workspace accounts should set `NOTEBOOKLM_BASE_URL` as described above.

To force the rebranded personal host explicitly:

```bash
NOTEBOOKLM_BASE_URL=https://notebook.google.com nlm login --manual --file /path/to/cookies.txt
```

### How to Extract Cookies Manually

1. Open Chrome and go to https://notebook.google.com
2. Make sure you're logged in
3. Press **F12** (or **Cmd+Option+I** on Mac) to open DevTools
4. Click the **Network** tab
5. In the filter box, type: `batchexecute`
6. Click on any notebook to trigger a request
7. Click on a `batchexecute` request in the list
8. In the right panel, scroll to **Request Headers**
9. Find the line starting with `cookie:`
10. Right-click the cookie **value** and select **Copy value**
11. Paste into a text file and save

### Cookie File Format

The cookie file should contain the raw cookie string from Chrome DevTools:

```
SID=abc123...; HSID=xyz789...; SSID=...; APISID=...; SAPISID=...; __Secure-1PSID=...; ...
```

**Notes:**

- Lines starting with `#` are treated as comments and ignored
- The file can contain the cookie string on one or multiple lines
- A template file `cookies.txt` is included in the repository
- Cookie files are static credentials. Re-export them when the live verification reports that they were rejected.

---

---

<a id="protected-storage"></a>
## Protected Storage (Protected Mode)

> 🔒 **New: Protected login storage (recommended)**
> Your saved Google login can now be encrypted, with its key kept in your computer's keychain instead of a plain file. We highly recommend everyone switch on a personal computer:
>
>     nlm auth storage set protected
>
> Optional: nothing changes unless you turn it on. Servers, cron, Docker and SSH setups can keep the plain file.
> [How it works](#protected-storage)

### Overview

By default, `notebooklm-mcp-cli` stores login cookies in readable JSON files (`file` mode) with `0600` permissions.

In **Protected mode**, your cookies and session tokens are encrypted on disk with AES-256-GCM (`credentials.enc`). The encryption key is generated uniquely for your computer and stored in your operating system's native keystore:
- **macOS:** macOS Keychain
- **Windows:** Windows Credential Manager
- **Linux:** SecretService API (GNOME Keyring, KWallet)

Protected mode is **optional**. File mode remains the default, and upgrading `notebooklm-mcp-cli` changes nothing until you explicitly choose to enable it.

### Choosing at first login

When you log in with a **new** profile in a terminal on a desktop computer, `nlm login` asks before the browser opens:

```
Where should your saved login live?
  1) Protected - encrypted, key kept in your OS keystore (recommended)
  2) Plain file - simple, readable by anything on this computer
```

Protected mode only accepts names made of letters, numbers, `-`, `_` and `.`. If you type something else (for example `my work` or `john@work.com`), the question offers a working name such as `my-work` and also lets you keep your name as a plain file. Choosing protected means the login is encrypted from the start and you will not be asked or reminded again. Choosing plain is respected too. Skip the question in scripts with `nlm login --storage protected` (or `--storage file`). Existing profiles keep their current mode; change it later with `nlm setup` (the "Credential protection" menu item) or `nlm auth storage set`.

### Commands

Manage credential storage using the `nlm auth storage` subcommands:

```bash
# Check current storage status for the default profile
nlm auth storage status

# Check a named profile
nlm auth storage status --profile work

# Switch to Protected mode (encrypts credentials, removes plain files)
nlm auth storage set protected
nlm auth storage set protected --profile work

# Switch back to File mode (decrypts credentials, restores readable JSON)
nlm auth storage set file
nlm auth storage set file --profile work

# Output status or results as JSON
nlm auth storage status --json
nlm auth storage set protected --json
```

### Conflict Resolution

If a profile has both plain-file credentials and encrypted credentials that differ (for example, if you logged in separately in both modes), `nlm` flags a conflict and asks you to resolve it:

```bash
# Keep the plain-file login and delete the encrypted leftover (stays in file mode)
nlm auth storage resolve file --profile work

# Keep the encrypted login and delete the plain files (switches to Protected mode)
nlm auth storage resolve protected --profile work

# Discard damaged or inaccessible credentials when the key is lost
nlm auth storage resolve file --profile work --discard-inaccessible --yes

# Clear a stuck or corrupt progress marker from an interrupted operation
nlm auth storage resolve --profile work --clear-marker --yes
```

- `resolve file`: Keeps your current plain-file login, deletes the encrypted leftover, and stays in file mode.
- `resolve protected`: Keeps the encrypted login, deletes the plain files, and switches to Protected mode.
- `--discard-inaccessible`: Use only when the encryption key was deleted from the OS keystore or the ciphertext file is corrupted beyond recovery. It removes the inaccessible ciphertext and resets the profile to file mode so you can log in again.
- `--clear-marker`: Clears a stuck or corrupt operation marker left behind if a migration was interrupted by a crash or power loss, provided the quarantine directory is clean.

### Relocating or Copying Storage

If you move your `~/.notebooklm-mcp-cli` directory to a new location on the same machine (e.g. changing your home directory path), update the installation identity:

```bash
nlm auth storage relocate
```

**Why a copied folder refuses:** Protected mode relies on your computer's OS keystore. If you copy `~/.notebooklm-mcp-cli` to a different machine, the new computer does not have the hardware-backed encryption key in its keystore. The profile safely refuses to open rather than crashing. To use Gemini Notebook on a new computer, sign in directly with `nlm login`.

### Downgrade Preparation

> [!WARNING]
> Older versions of `notebooklm-mcp-cli` do not understand `credentials.enc`. Before downgrading or installing an older version of the CLI, convert every protected profile back to file mode:
>
>     nlm auth storage set file
>     nlm auth storage set file --profile <name>

### Exporting Plain Credentials

If you need plain-text JSON credentials back (for example, to inspect them or use them with scripts that read `auth.json` or `cookies.json`), run:

```bash
nlm auth storage set file --profile <name>
```

This decrypts the credentials and writes readable JSON files with `0600` permissions.

### macOS "Always Allow" Prompt

Usually, no prompt appears during normal terminal operations. However, macOS prompts for permission when a different Python binary attempts to read an item created by another binary:

1. **Desktop extension users**: the Claude Desktop extension uses `python3` from `PATH` only to run a small launcher (`run_server.py`), which then starts the server with `uvx --from notebooklm-mcp-cli notebooklm-mcp`. `uvx` runs the server in its own environment, separate from `uv tool install`, and may pick a different Python build. If that Python differs from the one that created the key (for example, you enabled Protected mode from the `uv tool` install of `nlm`), macOS shows the Keychain access prompt once when the extension first reads it. Click **Always Allow** (not "Allow").
2. **Python upgrades**: If you upgrade Python (such as when Homebrew or `uv` installs a new Python release with a different binary path or signature), macOS detects the new binary signature and displays the prompt once more. Click **Always Allow** again.
3. Once **Always Allow** is clicked for each binary, macOS silently grants access for all future operations, MCP tool calls, and background token refreshes.

The prompt asks for your **Mac login password**: type it, then click **Always Allow**. If nobody answers within 60 seconds, `nlm` stops waiting and reports "approve the Keychain popup and retry", but the popup stays on screen. Dismiss it, rerun the command, and approve the new prompt.

### Where Protected Mode Cannot Work

Protected mode requires an active, interactive desktop session with an unlocked OS keystore. Remote and headless sessions cannot use the OS keystore (Keychain on macOS, Credential Manager on Windows). Specifically, it cannot work in:
- **Headless Linux servers, Docker containers, or cron jobs** without a D-Bus session bus.
- **Windows SSH sessions, Windows services, or Scheduled Tasks** set to "Run whether user is logged on or not" (Windows error 1312: Windows Credential Manager requires an interactive logon session).
- **macOS over SSH sessions** or prior to user login after a reboot (the macOS Keychain is locked or unavailable over remote SSH sessions).

If you run `nlm auth storage set protected` in an unsupported environment, it safely refuses with a clear error message:
> *Cannot enable protected mode: OS credential store is unavailable or locked.*
> *Remote/SSH sessions can't use the OS keystore (Keychain on macOS, Credential Manager on Windows). Run this from the desktop, or keep this profile in file mode. Your current setup keeps working.*

Your existing setup continues working in file mode. Keep automated, remote, and server profiles in file mode.

### Scheduled Refresh

User-configured scheduler jobs (`launchd`, `cron`, Windows Task Scheduler running `nlm auth refresh`) are the user's responsibility:
- In Protected mode, scheduled refresh only works while the user is logged into the OS desktop session with an unlocked keystore.
- Scheduled jobs running in headless environments, before login, or over SSH fail with a clear error unless the profile is kept in file mode.
- In-process token refresh and rotation (within an active CLI session or running MCP server process) is completely unaffected: it runs in the same Python process and requires no OS prompts.

### Threat Model

- **What it protects against:** Backups, cloud sync folders (Dropbox, iCloud, Google Drive), accidental sharing of repository or storage directories, and other tools or scripts that read files from disk.
- **What it does NOT protect against:** Other code running as your user account on the same computer (any process running under your user session can request the key from the OS keystore).
- **Other credential surfaces:** Protected mode secures `notebooklm-mcp-cli`'s stored credentials. It does not alter browser profiles (`chrome-profiles/`, Firefox SQLite databases), manual `cookies.txt` exports left on disk, `NOTEBOOKLM_COOKIES` environment variables in shell configs, legacy `~/.notebooklm-mcp/auth.json` files, or authenticated debug artifacts (`debug_page.html`).

### Environment Override (`NLM_AUTH_STORAGE`)

You can temporarily force the storage mode for testing using the `NLM_AUTH_STORAGE` environment variable (`file` or `protected`). If `NLM_AUTH_STORAGE` disagrees with the on-disk profile setting, mutating commands (`set` and `resolve`) refuse with an error until the variable is unset.

---

## Where Tokens Are Stored

All data is stored under `~/.notebooklm-mcp-cli/`:

```
~/.notebooklm-mcp-cli/
├── config.toml                    # CLI configuration
├── aliases.json                   # Notebook aliases
├── installation.json              # Installation identity
├── locks/                         # Inter-process profile locks
├── operations/                    # Migration state & quarantine markers
├── profiles/                      # Authentication profiles
│   ├── default/
│   │   ├── metadata.json          # Email, host, timestamps, storage mode
│   │   ├── credentials.enc        # Encrypted cookies & tokens (Protected mode)
│   │   └── cookies.json           # Plaintext cookies (File mode only)
│   ├── work/
│   │   ├── metadata.json
│   │   └── credentials.enc
│   └── personal/
│       ├── metadata.json
│       └── cookies.json
├── auth.json                      # Root mirror of default profile (File mode only)
├── chrome-profile/                # Chrome profile (single-profile users)
└── chrome-profiles/               # Chrome profiles (multi-profile users)
    ├── work/
    └── personal/
```

- In **Protected mode**, credentials live in `credentials.enc`. No plaintext cookies or `auth.json` files exist on disk.
- In **File mode**, credentials live in `cookies.json` (and the configured default profile is mirrored to root `auth.json` for backwards compatibility with external scripts), secured with `0600` permissions.
- In both modes, `metadata.json` stores non-secret profile metadata (account email, base host, build label, and storage mode setting).

---

## After Authentication

Once authenticated, add the MCP to your AI tool:

**Claude Code:**

```bash
claude mcp add gemini-notebook-mcp -- notebooklm-mcp
```

**Gemini CLI:**

```bash
gemini mcp add gemini-notebook-mcp notebooklm-mcp
```

**Manual (settings.json):**

```json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "notebooklm-mcp"
    }
  }
}
```

Then restart your AI assistant.

---

## Token Expiration

- **Cookies:** Generally stable for weeks, but some rotate on each request
- **CSRF token:** Auto-refreshed on each MCP client initialization
- **Session ID:** Auto-refreshed on each MCP client initialization

When you start seeing authentication errors, simply run `nlm login` again to refresh.

---

## Understanding `auth_status`

The MCP `server_info` tool and `nlm login --check` report one of five
`auth_status` values from the multi-probe AuthHealthChecker. Knowing the
difference matters: a `stale` status means you must re-auth, but
an `unverified` status is a network problem, not a credential problem.

> **Caching note:** the `server_info` result is cached for 30 seconds
> (the checker's `CACHE_TTL`) and bypassed on the next call if any auth
> file on disk is rewritten, so an external `nlm login` is reflected
> without waiting for the TTL. `nlm login --check` is always live.

| Status           | Meaning                                                                                                                                                                                          | What to do                                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `configured`     | Live check passed. Credentials are good.                                                                                                                                                         | Nothing.                                                                                                            |
| `not_configured` | No credentials are stored at all (first-time setup).                                                                                                                                             | Run `nlm login`.                                                                                                    |
| `stale`          | Credentials are known-bad: the live check was redirected to `accounts.google.com` (cookies expired), the on-disk profile failed to load, or the last successful validation is older than 7 days. | Run `nlm login` to refresh. Subsequent API calls will fail.                                                         |
| `unverified`     | The live check could not be completed (network timeout, DNS failure, proxy block, non-200 HTTP). Cached credentials on disk are still intact and may work for actual API calls.                  | Retry later, or check your network/proxy. Do not assume the user needs to re-auth — operations often still succeed. |
| `error`          | Unexpected exception inside the check itself (very rare).                                                                                                                                        | File a bug with the traceback.                                                                                      |

> **Heads up for AI agents:** If you see `auth_status = "stale"`, prompt the
> user to re-authenticate. If you see `auth_status = "unverified"` while
> recent operations are succeeding, treat it as a transient monitoring
> failure and continue — re-auth is not required.

---

## Troubleshooting

### "Browser is running but without remote debugging enabled"

Close your browser completely and try again. On Mac, use **Cmd+Q** to fully quit.

### Auto mode fails to connect

Try file mode instead:

```bash
nlm login --manual
```

### "401 Unauthorized" or "403 Forbidden" errors

Your cookies have expired. Run the auth command again to refresh.

### Browser opens with strange branding (e.g., Antigravity IDE)

Some browser extensions or tools modify the browser's behavior. Try a different browser or use file mode:

```bash
nlm login --manual
```

### Cookie file shows "missing required cookies"

Make sure you copied the cookie **value**, not the header name. The value should start with something like `SID=...` not `cookie: SID=...`.

### Authentication loop after `nlm login`

If you keep getting "Authentication expired" even after running `nlm login` or calling `refresh_auth`, check whether `NOTEBOOKLM_COOKIES` is set as an environment variable in your MCP config.

**Why this happens:** When `NOTEBOOKLM_COOKIES` is set in your config (e.g. `claude_desktop_config.json`), it takes priority over default auth sources — `auth.json`, profile cookies, `save_auth_tokens`, and `nlm login`. An explicit CLI `--profile` or MCP `usage_get(profile="work")` uses that saved account instead. When those hardcoded cookies expire, no recovery action can fix a running MCP process because the stale env var is baked into its environment.

**How to check:**

```python
import os
print("NOTEBOOKLM_COOKIES in env:", "YES (used when no explicit profile is selected)" if os.environ.get("NOTEBOOKLM_COOKIES") else "no")
```

**How to fix (pick one):**

1. **Update the cookie value** in your MCP config file with fresh cookies, then restart your AI tool (Claude Desktop, etc.)
2. **Remove the `NOTEBOOKLM_COOKIES` env var** from your config entirely and use `nlm login` instead (recommended — this way auth recovery works automatically)

Similarly, if you have `NOTEBOOKLM_CSRF_TOKEN` or `NOTEBOOKLM_SESSION_ID` in your config, remove them — both are deprecated and auto-extracted. Stale values can prevent auto-refresh from working.

---

### Experimental browser-backed RPC transport

`nlm doctor auth-replay` compares four lanes: saved cookies through httpx,
httpx after a forced cookie rotation, cookies freshly re-extracted from a
live logged-in browser (also replayed through httpx), and an in-page CDP
fetch from that same browser session. The fresh-cookie lane exists to tell
apart two failure modes that look identical if you only compare saved
cookies against the browser: ordinary expired cookies (verdict
`stale_cookies` — run `nlm login`, no transport needed) versus genuine
device-bound replay (verdict `browser_bound_replay` — even fresh cookies
fail outside the browser).

Only opt in to the experimental CDP transport if the verdict is
`browser_bound_replay`:

```bash
NOTEBOOKLM_RPC_TRANSPORT=cdp nlm notebook list
NOTEBOOKLM_RPC_TRANSPORT=cdp nlm query notebook <notebook-id> "Question?"
```

For MCP clients, add the same environment variable to the server config:

```json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "notebooklm-mcp",
      "env": {
        "NOTEBOOKLM_RPC_TRANSPORT": "cdp"
      }
    }
  }
}
```

This runs supported Gemini Notebook form POSTs through `fetch` inside the saved
Gemini Notebook browser profile, so the browser supplies its live cookies. It is
off by default and currently targets normal batchexecute RPCs plus notebook
chat. Uploads, downloads, and artifact file transfers still use the existing
HTTP paths.

If the CDP transport cannot find a saved profile-owned browser session, run
`nlm login` first. Do not use this flag as a general auth refresh shortcut;
use it only for suspected browser-bound replay failures.

---

## Chromium 136+ Compatibility

Chrome 136+ (and other Chromium-based browsers at the same version) restrict remote debugging on the default profile for security reasons. This is handled automatically by:

1. Using dedicated profile directories (`~/.notebooklm-mcp-cli/chrome-profiles/<name>/`)
2. Adding the `--remote-allow-origins=*` flag for WebSocket connections

No action required from users.

---

## Security Notes

- Cookies are stored locally in `~/.notebooklm-mcp-cli/profiles/<name>/auth.json`
- Each browser profile contains your Google login for Gemini Notebook
- Never share your `auth.json` files or commit them to version control
- The `cookies.txt` file in the repo is a template - don't commit real cookies

---

## File: docs/CLI_GUIDE.md

# CLI Guide

Complete command reference for the `nlm` command-line interface.

## Installation

```bash
# Using uv (recommended)
uv tool install notebooklm-mcp-cli

# Using pip
pip install notebooklm-mcp-cli
```

## Authentication

```bash
nlm login                         # Opens browser, extracts cookies automatically
nlm login --profile work          # Named profile for multiple accounts
nlm login --check                 # Check if authenticated
nlm login --storage protected     # New profile: store the login encrypted (or 'file'); skips the question
nlm login switch         # Switch default profile
nlm login profile list            # List all profiles with email addresses
nlm login profile delete <name>   # Delete a profile
nlm login profile rename <old> <new>  # Rename a profile
nlm auth refresh                  # Non-interactive refresh (for schedulers)

# External CDP provider (e.g., OpenClaw-managed browser)
nlm login --provider openclaw --cdp-url http://127.0.0.1:18800
```

For unattended machines, `nlm auth refresh` runs a headless-browser pass that
makes Google reissue the short-lived cookies keeping a session alive — no
interactive login. It exits non-zero on failure, so cron/launchd jobs can react.

Each profile gets its own isolated browser session (supports Chrome, Arc, Dia, Brave, Edge, Chromium, Firefox, and more), so you can stay logged into multiple Google accounts simultaneously.

### Credential Storage (Protected Mode)

Encrypted credential storage using your operating system's native keystore (macOS Keychain, Windows Credential Manager, Linux SecretService):

```bash
# Check storage status
nlm auth storage status
nlm auth storage status --profile work
nlm auth storage status --json

# Set storage mode (file or protected)
nlm auth storage set protected            # in a terminal: pick which saved logins
nlm auth storage set protected --all      # every saved login at once
nlm auth storage set protected --profile work
nlm auth storage set file
nlm auth storage set file --profile work

# Resolve storage conflicts
nlm auth storage resolve file --profile work
nlm auth storage resolve protected --profile work
nlm auth storage resolve file --profile work --discard-inaccessible --yes
nlm auth storage resolve --profile work --clear-marker --yes

# Relocate storage after directory move
nlm auth storage relocate
```

## Command Structure

The CLI supports **two styles** - use whichever feels natural:

```bash
# Noun-first (resource-oriented)
nlm notebook create "Title"
nlm source add <notebook> --url 

# Verb-first (action-oriented)
nlm create notebook "Title"
nlm add url <notebook> 
```

---

## Command Reference

### Notebooks

```bash
nlm notebook list                      # List all notebooks
nlm notebook list --json               # JSON output
nlm notebook create "Title"            # Create notebook
nlm notebook get  --json           # Get details, including notebook emoji
nlm notebook describe              # AI summary
nlm notebook rename  "New Title"   # Rename
nlm notebook delete  --confirm --json  # Delete with structured confirmation
nlm notebook query  "question" --json # Response includes the original question
```

### Sources

`nlm source list --json` and `nlm source get --json` include the original URL
for web and YouTube sources. Use `source list --url` for an ID-to-URL list.
The MCP `source_list_drive` and `source_get_content` responses currently omit
this field; use the CLI when you need source URLs.

```bash
nlm source list <notebook>                         # List sources
nlm source add <notebook> --url "https://..."      # Add URL
nlm source add <notebook> --url "https://..." --json  # Return the new source ID as JSON
nlm source add <notebook> --url "https://..." --wait  # Add and wait until ready
nlm source add <notebook> --text "content" --title "Notes"  # Add text
nlm source add <notebook> --file document.pdf --wait  # Upload file
nlm source add <notebook> --youtube "https://..."  # Add YouTube
nlm source add <notebook> --drive <doc-id>         # Add Drive doc
nlm source get                          # Get content
nlm source describe                     # AI summary
nlm source stale <notebook>                        # Check stale Drive sources
nlm source sync <notebook> --confirm               # Sync stale sources
nlm source delete  --confirm --json     # Structured deletion result
```

### Studio Content Creation

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

For Audio Overviews, Gemini Notebook (formerly Google NotebookLM) has been observed using the BCP-47 region
subtag to select the voice accent. For example, `es` and `es-ES` produce
Spain Spanish, while `es-US` and `es-419` produce Latin-American Spanish.
Changing the focus prompt does not reliably change the accent. This is
observed Gemini Notebook behavior, not a guaranteed API contract.

Set `NOTEBOOKLM_HL=es-419` to use a regional locale as the default artifact
language, or pass `--language es-419` for a specific generation.

### Downloads

```bash
nlm download audio <notebook> --id <artifact-id> --output podcast.m4a
nlm download video <notebook> --id <artifact-id> --output video.mp4
nlm download report <notebook> --id <artifact-id> --output report.md
nlm download mind-map <notebook> --id <artifact-id> --output mindmap.json
nlm download slide-deck <notebook> --id <artifact-id> --output slides.pdf
nlm download infographic <notebook> --id <artifact-id> --output infographic.png
nlm download data-table <notebook> --id <artifact-id> --output data.csv

# Interactive formats (quiz/flashcards)
nlm download quiz <notebook> --id <artifact-id> --format html --output quiz.html
nlm download flashcards <notebook> --id <artifact-id> --format markdown --output cards.md

# Download every completed artifact into a per-notebook folder
nlm download all <notebook> --output-dir ./exports
nlm download all --all-notebooks --output-dir ./exports --skip-existing
```

Audio arrives as AAC in an MP4 container, so it needs a `.m4a` or `.mp4`
suffix; `.mp3` and other mismatched extensions are rejected rather than
written with bytes that contradict the name. Transcode afterwards if you
need MP3:

```bash
nlm download audio <notebook> --id <artifact-id> --output raw.m4a
ffmpeg -i raw.m4a -acodec libmp3lame -q:a 2 podcast.mp3
```


### Research

```bash
nlm research start "query" --notebook-id  --mode fast  # Quick search
nlm research start "query" --notebook-id  --mode deep  # Extended research
nlm research start "query" --notebook-id  --source drive  # Search Drive
nlm research start "query" --notebook-id  --auto-import # Start, poll, and import in one step
nlm research status <notebook> --max-wait 300              # Poll until done
nlm research import <notebook> <task-id>                   # Import sources
nlm research import <notebook> <task-id> --timeout 600     # Custom timeout (default: 300s)
nlm research import <notebook> <task-id> --cited-only      # Import cited deep research sources
```

### Studio Status

```bash
nlm studio status <notebook>           # Check artifact generation status
nlm studio status <notebook> --artifact-id   # Poll one artifact
nlm studio status <notebook> --json --mcp-compatible  # MCP-shaped paginated JSON
nlm video list <notebook>               # List video artifacts only
nlm studio delete <notebook> <artifact-id> --confirm  # Delete artifact
```

The existing `--json` output remains a plain list for script compatibility and
contains both `id` and `artifact_id`. `--mcp-compatible` returns the MCP envelope,
uses lean fields by default, and limits the response to 20 artifacts. Add
`--full`, `--limit`, or `--offset` when detailed or later-page data is needed.

### Sharing

```bash
nlm share status <notebook>                    # View sharing settings
nlm share public <notebook>                    # Enable public link
nlm share private <notebook>                   # Disable public link
nlm share invite <notebook> email@example.com  # Invite viewer
nlm share invite <notebook> email --role editor  # Invite editor
```

### Batch Operations

```bash
nlm batch query "What are the key takeaways?" --notebooks "id1,id2"
nlm batch query "Summarize" --tags "ai,research"          # Query by tag
nlm batch query "Summarize" --all                         # Query ALL notebooks
nlm batch add-source "https://..." --notebooks "id1,id2"
nlm batch create "Project A, Project B, Project C"        # Create multiple
nlm batch delete --notebooks "id1,id2" --confirm          # Delete multiple
nlm batch studio audio --tags "research"                  # Generate across notebooks
```

### Cross-Notebook Query

```bash
nlm cross query "What features are discussed?" --notebooks "id1,id2"
nlm cross query "Compare approaches" --tags "ai,research"
nlm cross query "Summarize everything" --all              # Query ALL notebooks
```

### Pipelines

```bash
nlm pipeline list                                         # List available pipelines
nlm pipeline run ingest-and-podcast --notebook  --input-url "https://..."
nlm pipeline run research-and-report --notebook  --input-url "https://..."
nlm pipeline run multi-format --notebook              # Audio + report + flashcards
```

**Built-in pipelines:** `ingest-and-podcast`, `research-and-report`, `multi-format`

Create custom pipelines: add YAML files to `~/.notebooklm-mcp-cli/pipelines/`

### Tag & Smart Select

```bash
nlm tag add <notebook> --tags "ai,research,llm"           # Add tags
nlm tag add <notebook> --tags "ai" --title "My Notebook"  # With display title
nlm tag remove <notebook> --tags "ai"                     # Remove tags
nlm tag list                                              # List all tagged notebooks
nlm tag select "ai research"                              # Find notebooks by tag match
```

### Chat Configuration

```bash
nlm chat configure <notebook> --goal default --length default
nlm chat configure <notebook> --goal learning_guide --length longer
nlm chat configure <notebook> --goal custom --prompt "You are an expert..."
```

### Plan Usage

```bash
nlm usage                         # Use the configured default profile
nlm usage --profile work          # Inspect a named profile without switching defaults
nlm usage -p personal --json      # Short profile flag with JSON output
```

`--profile` selects credentials only for this usage check; it does not change
`auth.default_profile`. An explicit profile takes priority over
`NOTEBOOKLM_COOKIES`. Without `--profile`, environment cookies still take
priority over the configured default account.

### Configuration

```bash
nlm config show                         # Show all settings
nlm config get auth.default_profile     # Get a specific value
nlm config set auth.default_profile work  # Set default profile
nlm config set output.format json       # Change default output format
```

**Available Settings:**

| Key                    | Default   | Description                                                                                                                                                                                        |
| ---------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `output.format`        | `table`   | Default output format (table, json)                                                                                                                                                                |
| `output.color`         | `true`    | Enable colored output                                                                                                                                                                              |
| `output.short_ids`     | `true`    | Show shortened IDs                                                                                                                                                                                 |
| `auth.browser`         | `auto`    | Preferred browser for login (auto, chrome, arc, brave, edge, edge-beta, chromium, firefox, vivaldi, opera). Falls back to auto if the preferred browser is not found.                                         |
| `auth.default_profile` | `default` | Profile to use when `--profile` not specified. **Note:** MCP tools use the active profile: the saved default, unless the MCP `profile` tool switched this server (until it restarts), environment cookies are set, or `usage_get(profile=...)` selects another account. |

### Aliases (Shortcuts)

```bash
nlm alias set myproject <notebook-id>   # Create alias
nlm alias list                          # List all aliases
nlm alias get myproject                 # Resolve to UUID
nlm alias delete myproject              # Remove alias

# Use aliases anywhere
nlm notebook get myproject
nlm source list myproject
```

### Skills (AI Assistant Integration)

```bash
nlm skill list                           # Show installation status
nlm skill install claude-code            # Install for Claude Code
nlm skill install cursor                 # Install for Cursor AI
nlm skill install <tool> --level project # Install at project level
nlm skill uninstall <tool>               # Remove skill
nlm skill show                           # View skill content
nlm skill package                        # ~/Downloads/nlm-skill.zip for Claude Desktop / claude.ai upload
nlm skill package --output DIR           # Save the zip somewhere else

# Verb-first alternatives
nlm install skill claude-code
nlm list skills
```

**Gemini CLI, Codex, and other agent CLIs** all install to `~/.agents/skills/nlm-skill/` (`.agents/skills/` at project level). These three targets are interchangeable aliases:

```bash
nlm skill install agents      # Generic name
nlm skill install codex       # Alias for OpenAI Codex CLI
nlm skill install gemini-cli  # Alias for Google Gemini CLI
```

**Alef Agent** (CLI target `alef-agent`) is separate: it installs under `~/.alef-agent/workspace/skills/nlm-skill/` with tool-specific skill frontmatter, not under `.agents/skills/`.

```bash
nlm skill install alef-agent
```

**Supported Tools:** `claude-code`, `cursor`, `agents`, `gemini-cli`, `codex`, `opencode`, `antigravity`, `cline`, `openclaw`, `alef-agent`, `other`

**Claude Desktop Chat/Cowork and claude.ai** only load skills uploaded to your
Claude account, not local skill folders (the desktop app's **Code** tab is
Claude Code and uses `~/.claude/skills/`). `nlm skill package` writes
`nlm-skill.zip` (the `nlm-skill/` folder with `SKILL.md` and `references/`) and
prints the upload steps: **Customize → Skills → Add** → pick the file → turn it
on. The packaged `SKILL.md` carries its version under `metadata:` as the skill
upload format requires. Re-upload after updating `nlm`.

### Setup (MCP Server Configuration)

Configure the Gemini Notebook MCP server and optional skills for AI tools:

Running bare `nlm setup` opens an interactive wizard that detects installed tools,
configures MCP servers with private backups, and offers optional skill installation:

```bash
nlm setup                       # Guided wizard (recommended): status, add MCP/skill, remove, copy setup
nlm setup add claude-code       # Direct command: Configure via `claude mcp add`
nlm setup add codex             # Direct command: Configure Codex CLI / ChatGPT desktop
nlm setup add github-copilot --scope user  # Configure VS Code user profile
nlm setup add github-copilot    # Direct command: Configure current workspace (.vscode/mcp.json)
nlm setup add claude-desktop    # Configure detected Claude Desktop profile(s)
nlm setup add claude-desktop --profile 3p  # Select Relay AI / 3P explicitly
nlm setup remove claude-desktop --profile regular  # Remove from regular explicitly
nlm setup add gemini            # Write ~/.gemini/settings.json
nlm setup add cursor            # Write ~/.cursor/mcp.json
nlm setup add windsurf          # Write mcp_config.json
nlm setup add json              # Generate JSON config for any tool

nlm setup remove gemini         # Remove from Gemini CLI

nlm setup list                  # Show all clients and config status
```

Claude Desktop setup never creates a profile that is not detected. When both
regular and Relay AI/3P profiles are present, the command prompts for regular,
3P, or both. Removal uses the same profile selection. User-level skill installs
likewise require the target tool to be detected; project-level installs remain
explicitly scoped to the current project.

The guided wizard's main menu: **Show my tools' status**, **Add the MCP to my
tools/agents**, **Add the skill to my tools/agents**, **Remove an MCP or skill**,
**Credential protection**, **Copy MCP setup for a tool not listed**, and **Exit**. Press **Esc** on any
screen to go back (it quits from the main menu). Only detected tools are listed,
and the connect and remove lists start with nothing selected. Connections that
still use a legacy server name (`notebooklm-mcp`, `notebooklm`) show as
**⚠ old name** in the status view and under **Needs a fix** in the connect list;
ticking one renames it to `gemini-notebook-mcp` and keeps its other settings.

The wizard defaults MCP setup to the supported app/user-level location.
Its skill step defaults to **All my projects (user level)** and offers
**Just this folder (project level)**; it pre-ticks tools you just connected and
available upgrades, and offers a **Claude Desktop / claude.ai** upload file. The Codex CLI and ChatGPT desktop app share
their MCP configuration; selecting this target makes the server available to
both on that host. For GitHub Copilot, the wizard targets the VS Code user
profile, while the direct command without `--scope user` targets the current
workspace.
Removal only offers profiles containing `gemini-notebook-mcp` or a recognized
legacy entry, so unrelated MCP servers are not removed.

Fully quit the selected Claude Desktop profile before adding or removing the
MCP, including when it was launched by Relay AI. The CLI detects running
regular and 3P instances and refuses to write while they are open, since Claude
may rewrite the config and discard the change. Reopen Claude Desktop after the
command completes.

### Guided Removal & Safety

Running `nlm setup` and selecting **Remove** scans all recognized MCP server
configurations and installed skills, presenting a checklist grouped into
**MCP connections** and **Skills** with nothing pre-selected.

- **Separate Confirmations**: Removing MCP configurations requires an explicit
  default-No confirmation (`Remove the selected MCP entries?`). Deleting skill
  folders requires a separate default-No confirmation warning (`Delete the listed skill folders? Personal edits in the active folders will be removed.`).
- **Claude Desktop Profiles**: Regular and Relay AI / 3P profiles appear as distinct
  options and are removed cleanly without nested prompts.
- **Backups & Recovery**: Before modifying any config file or deleting any skill
  folder, a timestamped backup is saved to `~/.notebooklm-mcp-cli/backups/`. If a
  backup cannot be created, the operation aborts safely. To restore a backup, copy
  the backup file or directory from `~/.notebooklm-mcp-cli/backups/` back to its
  original path.
- **JSONC / Comment Preservation**: If a configuration file (such as VS Code's
  user or workspace `mcp.json`) contains comments or custom formatting, `nlm setup`
  safely refuses to modify it and prints the exact path with instructions for
  manual removal.

**Supported Clients:** `claude-code`, `claude-desktop`, `gemini`, `github-copilot`, `cursor`, `windsurf`, `cline`, `antigravity`, `codex`, `opencode`

**For unsupported tools:** Choose **Copy MCP setup for a tool not listed** in `nlm setup`. It copies the standard snippet (full path to `notebooklm-mcp`) to the clipboard; **Advanced options** cover the other formats. `nlm setup add json` also generates a snippet directly. Choose between uvx or regular mode, full path or command name, and whether to include the `mcpServers` wrapper. The result is printed and can be copied to clipboard.

> **Note:** `nlm setup` configures the MCP server transport. Use `nlm skill install` to install skill/reference docs for AI tools that don't use MCP.

### Doctor (Diagnostics)

Run diagnostics to troubleshoot installation, authentication, and configuration issues:

```bash
nlm doctor              # Run all checks
nlm doctor --verbose    # Include additional details (Python version, paths, etc.)
```

**Checks performed:**

| Category       | What it checks                                                     |
| -------------- | ------------------------------------------------------------------ |
| Installation   | Package version, `nlm` and `notebooklm-mcp` binary paths           |
| Authentication | Profile status, cookies present, CSRF token, account email         |
| Browser        | Chromium-based browser installed, saved profiles for headless auth |
| AI Tools       | MCP configuration status for each supported client                 |

Each issue includes a suggested fix (e.g., "Run `nlm login` to authenticate").

---

## Output Formats

| Flag      | Description                                                        |
| --------- | ------------------------------------------------------------------ |
| (none)    | Rich table format                                                  |
| `--json`  | JSON output, including source/Studio creation and deletion results |
| `--quiet` | IDs only                                                           |
| `--title` | "ID: Title" format                                                 |
| `--full`  | All columns                                                        |

---

## Complete Workflow Example

```bash
# 1. Authenticate and configure
nlm login
nlm setup add claude-code       # One-time MCP setup

# 2. Create notebook and set alias
nlm notebook create "AI Research"
nlm alias set ai <notebook-id>

# 3. Add sources (with --wait to ensure ready)
nlm source add ai --url "https://example.com/article" --wait
nlm source add ai --file research.pdf --wait

# 4. Generate podcast
nlm audio create ai --format deep_dive --confirm

# 5. Wait for generation
nlm studio status ai

# 6. Download when ready
nlm download audio ai --id <artifact-id> --output podcast.m4a
```

---

## Tips

- Saved cookies often remain usable for weeks. Run `nlm login` for confirmed stale or missing credentials.
- Use `--confirm` for all create/delete commands in scripts
- Use `--wait` when adding sources to ensure they're ready before querying
- Use aliases for frequently-used notebooks
- Audio/video takes 1-5 minutes; poll with `nlm studio status`
- Use `nlm login switch <name>` to change the default profile
- New profiles ask plain vs protected storage before the browser opens; use `nlm login --storage protected` to skip the question in scripts
- Run `nlm login profile list` to see all profiles with their associated email addresses
- Run `nlm doctor` to diagnose installation, auth, or config issues
- Use `nlm setup add <client>` to quickly configure MCP for your AI tool

---

## Scripting & Automation

### Getting IDs for piping

Use `--quiet` when you only need IDs — it outputs one ID per line, no parsing required:

```bash
# Grab the first notebook ID in a shell script
notebook_id=$(nlm notebook list --quiet | head -1)
nlm notebook query "$notebook_id" "Summarize key points"
```

### Parsing `--json` output

When you need more than just IDs (title, source count, etc.), use `--json`. It emits
clean JSON to stdout — always parse it properly rather than splitting strings:

```python
import json
import subprocess

result = subprocess.run(
    ["nlm", "notebook", "list", "--json"],
    capture_output=True, text=True
)
notebooks = json.loads(result.stdout)
notebook_id = notebooks[0]["id"]   # UUID like "abc12345-..."
```

---

## File: docs/FILE_UPLOAD_IMPLEMENTATION.md

# File Upload Implementation

## Overview

File upload uses **HTTP Resumable Upload** – the reliable, headless method.

## HTTP Resumable Upload

### Implementation

Uses Google's 3-step resumable upload protocol:

1. **Register source intent** (RPC: `o4cbdc`) → get SOURCE_ID
2. **Start upload session** (POST to `/upload/_/`) → get upload URL
3. **Stream file content** → upload URL (memory-efficient streaming)

### Code Structure

- `SourceMixin._register_file_source()` - Step 1: Register and get SOURCE_ID
- `SourceMixin._start_resumable_upload()` - Step 2: Get upload URL
- `SourceMixin._upload_file_streaming()` - Step 3: Stream file content
- `SourceMixin.add_file()` - Public API that orchestrates all 3 steps

### Usage

**Python:**
```python
from notebooklm_tools.core.client import NotebookLMClient

with NotebookLMClient() as client:
    result = client.add_file(notebook_id="...", file_path="document.pdf")
# Returns: {"id": "source-id", "title": "document.pdf"}
```

**CLI:**
```bash
nlm source add <notebook-id> --file document.pdf
```

**MCP:**
```python
source_add(notebook_id="...", source_type="file", file_path="/path/to/file.pdf")
```

### Supported File Types

The local gate admits the official 43-extension contract case-insensitively:

OFFICIAL_FILE_EXTENSIONS: .pdf, .txt, .md, .docx, .csv, .pptx, .epub, .avif, .bmp, .gif, .heic, .heif, .ico, .jp2, .jpe, .jpeg, .jpg, .png, .tif, .tiff, .webp, .3g2, .3gp, .aac, .aif, .aifc, .aiff, .amr, .au, .avi, .cda, .m4a, .mid, .mp3, .mp4, .mpeg, .ogg, .opus, .ra, .ram, .snd, .wav, .wma

This establishes local admission eligibility only. NotebookLM may still reject or
fail to process corrupt files, misleading extensions, inaccessible media, or
reference-only formats such as some `.cda` and `.ram` files. Those outcomes are
provider ingestion errors, not local unsupported-extension errors.

### Advantages

- ✅ No Chrome dependency
- ✅ Works in headless/server environments
- ✅ Memory-efficient streaming for large files
- ✅ Faster and more reliable
- ✅ No browser fingerprinting issues

## Testing

```bash
# Create a test file
echo "Test content" > test.txt

# Upload the file
nlm source add <notebook-id> --file test.txt

# Verify it was added
nlm source list <notebook-id>
```

## Troubleshooting

### "File not found"
- **Cause**: The file path doesn't exist or is inaccessible
- **Fix**: Check the file path spelling and permissions

### "Unsupported file type"
- **Cause**: File extension is not in the supported list
- **Fix**: Convert the file to a supported format

### Upload times out
- **Cause**: Large file or slow connection
- **Fix**: The upload may still complete on the backend; check notebook sources before retrying

---

## Historical Note

A browser-based upload fallback using Chrome automation was previously available (`--browser` flag) but has been removed. Gemini Notebook (formerly Google NotebookLM)'s UI now uses the File System Access API which cannot be automated via CDP. The HTTP method is more reliable anyway.

---

## File: docs/GETTING_STARTED.md

# Getting Started

A practical guide to going from "just installed" to "Gemini Notebook (formerly Google NotebookLM) is wired
into my agent." For background on what the project is and the full feature
list, see the [README](../README.md). For deep command/tool reference, see
the [CLI Guide](CLI_GUIDE.md) and [MCP Guide](MCP_GUIDE.md).

## Contents

- [First-time setup](#first-time-setup)
- [Migrating from another Gemini Notebook MCP](#migrating-from-another-notebooklm-mcp)
- [Troubleshooting](#troubleshooting)

---

## First-time setup

If you have never used `notebooklm-mcp-cli` before, the path is:

1. **Install** — `uv tool install notebooklm-mcp-cli`. This gives you both
   the `nlm` CLI and the `notebooklm-mcp` server binary. See the
   [README → Installation](../README.md#installation) section for
   alternatives (`uvx`, `pip`, `pipx`, source install).
2. **Authenticate** — `nlm login`. The CLI extracts your Google cookies
   from a managed browser session. See the
   [Authentication Guide](AUTHENTICATION.md) for the supported methods
   and how multi-profile auth works.
   > 🔒 **Tip:** On a personal computer, `nlm login` asks before the browser opens whether to keep your login encrypted in your OS keystore (recommended) or as a plain file. Already logged in? Switch any time with `nlm setup` → **Credential protection** or `nlm auth storage set protected`. See [Protected Storage](AUTHENTICATION.md#protected-storage).
3. **Connect an agent** — run the setup wizard and choose your app:
   ```bash
   nlm setup
   ```
   Choose **Add the MCP to my tools/agents**, tick your apps with Space, and
   press Enter. The wizard configures MCP at user/app scope, then offers the
   optional skill for all projects or the current folder. Use **Show my tools'
   status** to check what is connected, and **Copy MCP setup for a tool not
   listed** for a client without a built-in installer. For Claude Desktop's
   Chat and Cowork tabs (and claude.ai), tick **Claude Desktop / claude.ai** in
   **Add the skill** to get an upload file (`~/Downloads/nlm-skill.zip`).
4. **Verify** — restart your agent and call `notebook_list` (MCP) or
   `nlm notebook list` (CLI). If you see your existing notebooks, you are
   good to go.

For deeper coverage, jump to the relevant guide:

- [CLI Guide](CLI_GUIDE.md) — every command, every flag
- [MCP Guide](MCP_GUIDE.md) — every tool, every parameter
- [Authentication Guide](AUTHENTICATION.md) — login, profiles, token
  lifecycle, `auth_status` state meanings

---

## Migrating from another Gemini Notebook MCP

If you previously used a browser-automation–based Gemini Notebook MCP (or any
other third-party Gemini Notebook server) and want to switch to
`notebooklm-mcp-cli` for direct API access, follow these steps. Most agent
frameworks (Hermes Agent, Claude Code, Cursor, etc.) get confused when two
Gemini Notebook servers are configured at the same time because their tool
names overlap (`notebook_create`, `source_add`, …), so a clean swap is
recommended.

### 1. Install the unified CLI/MCP

```bash
uv tool install notebooklm-mcp-cli
```

This installs both `nlm` and the `notebooklm-mcp` server binary.

### 2. Authenticate once

```bash
nlm login
```

Your Google cookies are extracted from a managed browser session and
cached in `~/.notebooklm-mcp-cli/profiles/default/auth.json`. The
`nlm login --check` command verifies that the cached creds still work.

### 3. Register the new MCP server

Recommended: run the guided wizard to configure supported clients and
optionally install the skill:

```bash
nlm setup
```

The skill defaults to all projects (user level); choose the current folder for
project scope. For direct configuration, the existing client-specific commands
remain available. For example:

```bash
# Hermes Agent
nlm skill install hermes

# Claude Code
claude mcp add gemini-notebook-mcp -- notebooklm-mcp

# Gemini CLI
gemini mcp add --scope user gemini-notebook-mcp -- notebooklm-mcp

# Claude Desktop (detects regular and Relay AI/3P profiles)
nlm setup add claude-desktop
# Or select Relay AI / Claude 3P explicitly
nlm setup add claude-desktop --profile 3p
```

For any other MCP client, generate a config snippet:

```bash
nlm setup add json
```

> **Recommended server name:** `gemini-notebook-mcp`. The executable remains
> `notebooklm-mcp`. Avoid
> generic names like `notebooklm` if you also have a legacy server
> registered, or your agent will mix their tools up.

For Claude Desktop, fully quit the selected profile before running setup. If
both regular and Relay AI/3P profiles are detected, the CLI asks which one to
configure; if no profile exists, it creates nothing. Reopen Claude Desktop
after setup completes.

User-level skill installation also requires the target tool to be detected;
use `nlm skill install <tool> --level project` when you intentionally want a
project-local skill without changing user-level tool directories.

### 4. Remove the old MCP server

This is the step most people forget. Leaving both configured is the #1
cause of "Hermes picked the wrong tool" symptoms:

```bash
# Claude Code
claude mcp remove notebooklm          # (or whatever the old name was)

# Gemini CLI
gemini mcp remove notebooklm

# Hermes / others: edit the client config directly
```

> Only removing a *different* Gemini Notebook server needs this step. If the old
> entry is this project's own pre-rebrand name (`notebooklm-mcp` or
> `notebooklm`), `nlm setup` shows it as **⚠ old name** and renames it to
> `gemini-notebook-mcp` for you: **Add the MCP to my tools/agents → Needs a fix**.

If you are not sure what is registered, list everything:

```bash
claude mcp list                       # Claude Code
gemini mcp list                       # Gemini CLI
```

### 5. Restart your agent

Restart Claude Code / Cursor / Gemini / Hermes so the new tool list is
reloaded. Verify with a no-op call such as `notebook_list` (via the MCP)
or `nlm notebook list` (via the CLI).

---

## Troubleshooting

- **Auth setup issues** — see the
  [Authentication Guide → Troubleshooting](AUTHENTICATION.md#troubleshooting).
- **"Hermes picked the wrong tool"** — you almost certainly have two
  Gemini Notebook servers registered. See step 4 of the
  [migration section](#migrating-from-another-notebooklm-mcp) above.
- **`auth_status` says `"stale"`** — re-run `nlm login`. See
  [Understanding `auth_status`](AUTHENTICATION.md#understanding-auth_status)
  for the difference between `stale` and `unverified`.
- **Run the full diagnostic** — `nlm doctor` checks storage, auth, browser,
  and MCP wiring in one go.

---

## File: docs/KNOWN_ISSUES.md

# Known Issues and Fragility

This document describes known limitations and potential failure points in the Gemini Notebook (formerly Google NotebookLM) MCP. Since this project uses undocumented internal APIs, certain breakages are expected over time.

---

## 1. Build Label (`bl`) Parameter

### What it is
The `bl` (build label) parameter is a frontend version identifier required by Gemini Notebook's batchexecute API. It looks like:
```
boq_labs-tailwind-frontend_20260219.16_p2
```

### Current status (v0.3.11+)
**Resolved.** The `bl` value is now auto-extracted from the Gemini Notebook page during `nlm login` and during CSRF token refresh. It stays current automatically without manual intervention.

### Manual override
If you need to force a specific value, set the `NOTEBOOKLM_BL` environment variable:

```bash
export NOTEBOOKLM_BL="boq_labs-tailwind-frontend_YYYYMMDD.XX_pN"
```

The priority order is: env var > auto-extracted > hardcoded fallback.

---

## 2. Cookie Expiration

### What it is
Authentication uses browser cookies extracted from an active Chrome session. These cookies have a limited lifespan.

### When it breaks
Cookies typically expire after 2-4 weeks. Symptoms:
- `ValueError: Cookies have expired. Please re-authenticate...`
- API calls redirect to Google login page
- Authentication errors on previously working operations

### How to fix
Re-extract fresh cookies using one of these methods:

**Option A: nlm login CLI (recommended)**

The built-in authentication CLI automatically launches Chrome, navigates to Gemini Notebook, and extracts cookies:

```bash
nlm login
```

If Chrome is not running, it will be launched automatically. If you're not logged in, the CLI waits for you to complete login in the browser window. Tokens are cached to your profile directory.

**Option B: Chrome DevTools MCP**

If your AI assistant has Chrome DevTools MCP available:
1. Navigate to `notebook.google.com` in Chrome
2. Use Chrome DevTools MCP to extract cookies from any network request
3. Call `save_auth_tokens(cookies=<cookie_header>)`

**Option C: Manual extraction**
1. Open Chrome DevTools on `notebook.google.com`
2. Network tab → find any request → copy Cookie header
3. Set `NOTEBOOKLM_COOKIES` environment variable

---

## 3. Rate Limits

### What it is
The free tier of Gemini Notebook has usage limits enforced server-side.

### Current limits
- ~50 queries per day (approximate, not officially documented)
- Studio content generation may have separate limits

### Symptoms when exceeded
- API returns rate limit errors
- Operations start failing mid-session

### Mitigation
- Space out operations when possible
- Create videos sequentially. Brief automatic retries only cover transient
  failures; after a Studio rate-limit error, wait 1-2 minutes before retrying.
- Avoid tight polling loops
- Poll a known Studio artifact by ID instead of repeatedly listing the notebook.
- Consider batching queries where the API supports it

---

## 4. API Instability (Undocumented Internal APIs)

### What it is
This MCP uses internal, undocumented APIs that Google can change at any time without notice.

### What can break
- RPC IDs (e.g., `wXbhsf` for list notebooks) may be renamed
- Request/response structure may change
- New required parameters may be added
- Endpoints may be deprecated or moved

### Symptoms
- Parsing errors (unexpected response shape)
- `None` results from previously working operations
- New error messages from the API

### What to do when it breaks
1. Check if the issue is widespread (Google may have deployed changes)
2. Use Chrome DevTools to capture current request/response format
3. Update the relevant RPC handling in `api_client.py`
4. Submit a PR or issue if you discover the fix

---

## 5. CSRF Token and Session ID

### What it is
The MCP auto-extracts CSRF token (`SNlM0e`) and session ID (`FdrFJe`) from the Gemini Notebook homepage on first use.

### When it breaks
- If the homepage structure changes, extraction may fail
- Tokens are per-session and must be refreshed if the page is not accessible

### Symptoms
- `ValueError: Could not extract CSRF token from page`
- Debug HTML saved to `~/.notebooklm-mcp-cli/debug_page.html`

### How to fix
If auto-extraction fails:
1. Manually extract tokens from Chrome DevTools Network tab
2. Pass them via `save_auth_tokens(cookies=..., request_body=..., request_url=...)`

---

## 6. Claude Desktop profile setup

### Symptoms

- The MCP does not appear in Claude Desktop after setup.
- `nlm setup add claude-desktop --profile 3p` says Claude is still running even
  though the window was closed.

### Cause

Claude Desktop keeps separate regular and Relay AI/3P profiles. Claude or Relay
AI can also rewrite its configuration while the app is open. macOS may leave a
Crashpad helper process behind after the window closes; that helper is not an
active Claude Desktop instance and is ignored by current `nlm` versions.

### Fix

1. Fully quit the selected Claude Desktop profile and stop the Relay AI launcher
   if it owns the 3P instance.
2. Run `nlm setup add claude-desktop --profile 3p` (or `regular`).
3. Reopen the selected Claude Desktop profile and check its Developer settings.

The CLI never creates a missing profile. Removal only offers profiles that
contain `gemini-notebook-mcp` or a recognized legacy entry, leaving unrelated
MCP servers unchanged.

---

## 7. Claude Desktop Chat or Cowork doesn't use the skill

### Symptoms

- `/nlm-skill` works in Claude Desktop's **Code** tab but the skill is missing
  from **Customize → Skills**, and Chat or Cowork don't use it.

### Cause

The **Code** tab is Claude Code: it reads skills from `~/.claude/skills/`, where
`nlm setup` installs them. **Chat**, **Cowork**, and claude.ai only load skills
uploaded to your Claude account. (The MCP itself reaches Chat and Cowork through
the normal Claude Desktop connection.)

### Fix

1. Run `nlm skill package` (or `nlm setup` → **Add the skill to my tools/agents**
   → tick **Claude Desktop / claude.ai**). It saves `~/Downloads/nlm-skill.zip`.
2. In Claude Desktop or claude.ai open **Customize → Skills → Add**, pick the
   file, and turn the skill on.
3. After updating `nlm`, repeat both steps — `nlm setup` can't see skills
   uploaded to your account, so it can't flag an outdated copy.

---

## 8. Protected Mode & Keystore Environments

### Symptoms
- `BackendUnavailableError: Cannot enable protected mode: OS credential store is unavailable or locked`
- On macOS: authorization popup asking for Keychain access when running the desktop extension.
- On Windows/macOS: refusal over SSH or remote non-interactive sessions.
- On Linux: `D-Bus connection refused` or keystore locked on headless servers or Docker containers.
- Downgrade error: older versions of `notebooklm-mcp-cli` cannot read `credentials.enc`.

### Causes & Fixes
- **macOS "Always Allow" & Desktop Extension:** Usually, no prompt appears during normal terminal operations. However, the Claude Desktop extension starts the server through `uvx` (its `python3` launcher only locates `uvx`), which runs in its own environment and may use a different Python build than the `uv tool` installation. If it does, macOS prompts for Keychain access once when the extension first reads a key created by the other Python. Enter your Mac login password and click **Always Allow**. Likewise, if Python is upgraded (e.g. via Homebrew or uv), do this once more. If the prompt goes unanswered for 60 seconds, the call fails with "approve the Keychain popup and retry" while the popup stays open: dismiss it, retry, and approve the new prompt.
- **Remote / SSH Sessions (macOS and Windows):** Remote/SSH sessions cannot use the OS keystore (Keychain on macOS, Credential Manager on Windows). When attempting migration over SSH, `nlm auth storage set protected` safely refuses. Run from the desktop session, or keep the profile in file mode.
- **Headless Linux / Docker:** Protected mode requires an interactive desktop session with an accessible secret service daemon (D-Bus). For headless machines, servers, or Docker containers, keep the profile in file mode (`nlm auth storage set file`).
- **Downgrading to Older Versions:** Before installing an older version of `notebooklm-mcp-cli`, convert any protected profile back to file mode using `nlm auth storage set file --profile <name>`.

---

## Reporting Issues

When reporting issues, include:
1. The specific tool/operation that failed
2. The error message (redact any sensitive info)
3. Whether the operation worked before
4. The current date (to correlate with potential Google deployments)

---

## File: docs/MCP_CLI_TEST_PLAN.md

# Gemini Notebook (formerly Google NotebookLM) MCP - Comprehensive Test Plan

**Purpose:** Verify all **53 MCP tools** work correctly.

**Version:** 2.7 (Updated 2026-09-27 - synchronized current MCP surface)

**Changes from v2.6:**
- Current tool count: 53 tools — adds `profile`, `alias` and `chat_save_to_note`, plus `pipeline` create and profile info in `server_info` (v0.15.0).

**Historical changes from v2.1:**
- The test plan previously covered 39 tools, including consolidated notes, labels, async query, batch, pipeline, tags, and server_info.

**Historical changes from v1:**
- Tools were consolidated from 45+ to 29 (-36%) before the current 43-tool surface.
- `source_add(source_type=...)` replaces 4 source tools
- `studio_create(artifact_type=...)` replaces 9 creation tools
- `download_artifact(artifact_type=...)` replaces 9 download tools

**Prerequisites:**
- MCP server installed: `uv cache clean && uv tool install --force .`
- Valid authentication cookies saved
- **Restart IDE MCP server** after installation

---

## Automated Testing with AI Tools

**Simply ask your AI assistant to:**
```
Run the automated tests from docs/MCP_TEST_PLAN.md. For the Drive sync test,
pause before checking freshness, ask me to make a change to the doc, then
verify both staleness detection and sync functionality work correctly.
```

### How Automation Works

1. **Execute tests sequentially** - Run through each test group automatically
2. **Track progress** - Use a todo list to show what's being tested
3. **Pause for user input** - Stop at Drive sync test to ask you to modify a document
4. **Report results** - Provide a comprehensive summary of all tests

---

## Test Group 1: Authentication & Basic Operations

### Test 1.1 - Refresh Auth
**Tool:** `refresh_auth`
**CLI:** `nlm login --check` (validates tokens)

**Prompt:**
```
Refresh authentication tokens for NotebookLM.
```

**Expected:** Success message with auth status.

---

### Test 1.1b - Non-Interactive Session Refresh (unattended)
**CLI:** `nlm auth refresh` (headless refresh; `--profile <name>` for a named profile)

**Prompt:**
```
Refresh my NotebookLM session without opening a browser window.
```

**Expected:** With a saved Chrome profile and a working login, prints
`✓ Session refreshed for profile '<name>'` and exits 0. With no saved profile
or a dead login, prints a clear failure and exits non-zero (usable in
cron/launchd). Refuses up front when `NOTEBOOKLM_COOKIES` is set in the
environment (that value overrides saved credentials).

---

### Test 1.1c - Credential Storage Management (Protected Mode)
**CLI:** `nlm auth storage status`, `nlm auth storage set protected`, `nlm auth storage set file`

**Prompt:**
```
Check my NotebookLM credential storage mode and test switching between file mode and protected mode.
```

**Expected:**
1. `nlm auth storage status`: Shows profile name, storage mode (`file` or `protected`), and presence of ciphertext or legacy files.
2. `nlm auth storage set protected`: Encrypts credentials to `credentials.enc` using the OS keystore (macOS Keychain, Windows Credential Manager, Linux SecretService) and removes plaintext files.
3. `nlm auth storage set file`: Decrypts credentials back to `cookies.json` with 0600 permissions.

---

### Test 1.1d - Profiles (`profile` tool)
**Tool:** `profile` (action: list, status, switch)

**Prompt:**
```
List my Gemini Notebook profiles, switch to my other profile for this session, then tell me how many notebooks I have. Do not change my default.
```

**Expected:**
1. `profile(action="list")` returns every saved profile with email, `storage_mode`, `is_saved_default`, `is_active`.
2. `profile(action="switch", name=...)` returns `scope: "session"`; later tools use that account and results carry `active_profile_note`.
3. The saved default (`nlm config get auth.default_profile`) is unchanged.
4. Quitting and reopening the app returns to the saved default.
5. `make_default=true` changes `default_profile` in `~/.notebooklm-mcp-cli/config.toml` (switch it back afterwards).
6. With `NOTEBOOKLM_COOKIES` set in the server's environment, switching returns an error.

---

### Test 1.1e - Aliases, chat-to-note, pipeline create
**Tools:** `alias`, `chat_save_to_note`, `pipeline` (action: create)

**Expected:** `alias(action="set", name="nb", value="<notebook id>")` then `notebook_get(notebook_id="nb")` resolves the alias; `chat_save_to_note` creates a note from a chat; `pipeline(action="create", ...)` saves a pipeline that `pipeline(action="list")` shows.

---

### Test 1.2 - Interactive Login (Primary)
**Tool:** `save_auth_tokens` (Fallback)
**CLI:** `nlm login` (Launches Chrome for automated extraction)
**WSL2 CLI:** `nlm login --wsl` (Supports NAT and mirrored networking modes)

**Prompt:**
```
I need to authenticate with NotebookLM.
```

**Expected:** Chrome opens, logs in, and tokens are saved. On WSL2, Windows Chrome opens and
the CDP connection succeeds in both NAT and mirrored networking modes.

**Anonymous-session regression (#350):**
1. Run login with a new profile. Before signing in, the browser must remain open;
   a public landing page, build label, or anonymous `NID` cookie must not finish login.
2. Complete Google sign-in. Login saves signed-in cookies and a CSRF token, then
   closes the browser it launched. Verify the profile with `nlm login --check`.
3. If sign-in times out, login fails without saving anonymous credentials.
4. For a profile previously saved with only `NID`, rerunning login opens the
   sign-in browser. `--check` must not report success based on a public HTTP 200 page.
5. An API authentication rejection must fail validation; a genuine network timeout
   remains inconclusive and must not be mislabeled as credential expiry.

---

### Test 1.3 - List Notebooks
**Tool:** `notebook_list`
**CLI:** `nlm notebook list`

**Prompt:**
```
List all my NotebookLM notebooks.
```

**Expected:** List of notebooks with counts (owned, shared).

---

### Test 1.3E - Enterprise List and Query

Before running this test, set `NOTEBOOKLM_BASE_URL`, `NOTEBOOKLM_PROJECT_ID`,
and `NOTEBOOKLM_LOCATION` for a Gemini Notebook Enterprise deployment. The
project setting is required; the location defaults to `global`. Authenticate
the profile and make it the default profile used by MCP:

```bash
nlm login --profile enterprise
nlm login switch enterprise
nlm notebook list
nlm notebook query <enterprise-notebook-id> "Summarize the sources" --new-conversation
```

**Expected:** Login opens the configured regional project URL, list returns
Enterprise notebooks, and the query uses the Enterprise streamed endpoint.
This test is not complete until it is run against an Enterprise deployment with
a redacted request/response capture; the automated suite only verifies routing
and payload contracts.

---

### Test 1.4 - Create Notebook
**Tool:** `notebook_create`
**CLI:** `nlm notebook create "MCP Test Notebook"`

**Prompt:**
```
Create a new notebook titled "MCP Test Notebook".
```

**Expected:** New notebook created with `notebook_id` and URL.

**Save:** Note the `notebook_id` for subsequent tests.

---

### Test 1.5 - Get Notebook Details
**Tool:** `notebook_get`
**CLI:** `nlm notebook get [notebook_id]`

**Prompt:**
```
Get the details of notebook [notebook_id from Test 1.4].
```

**Expected:** Notebook details with empty sources list, timestamps.

---

### Test 1.6 - Rename Notebook
**Tool:** `notebook_rename`
**CLI:** `nlm notebook rename [notebook_id] "MCP Test - Renamed"`

**Prompt:**
```
Rename notebook [notebook_id] to "MCP Test - Renamed".
```

**Expected:** Success with updated title.

---

## Test Group 2: Adding Sources (Consolidated source_add)

### Test 2.1 - Add URL Source
**Tool:** `source_add`
**CLI:** `nlm source add [notebook_id] --url https://en.wikipedia.org/wiki/Artificial_intelligence`

**Prompt:**
```
Add a URL source to notebook [notebook_id]:
- source_type: url
- url: https://en.wikipedia.org/wiki/Artificial_intelligence
```

**Expected:** Source added with `source_id`, `source_type: url`.

---

### Test 2.2 - Add Text Source
**Tool:** `source_add`
**CLI:** `nlm source add [notebook_id] --text "This is a test document..." --title "Test Document"`

**Prompt:**
```
Add a text source to notebook [notebook_id]:
- source_type: text
- title: "Test Document"
- text: "This is a test document about machine learning. Machine learning is a subset of artificial intelligence."
```

**Expected:** Text source added with `source_id`, `source_type: text`.

---

### Test 2.3 - Add Drive Source (Optional)
**Tool:** `source_add`
**CLI:** `nlm source add [notebook_id] --drive [doc_id] --type doc --title "My Drive Doc"`

**Prompt:**
```
Add a Drive document to notebook [notebook_id]:
- source_type: drive
- document_id: [your_doc_id]
- title: "My Drive Doc"
- doc_type: doc
```

**Expected:** Drive source added (or skip if no Drive doc available).

---

### Test 2.4 - Add File Source (Optional)
**Tool:** `source_add`
**CLI:** `nlm source add [notebook_id] --file /path/to/document.pdf`

**Prompt:**
```
Add a file to notebook [notebook_id]:
- source_type: file
- file_path: /path/to/document.pdf
```

**Expected:** File uploaded with `source_id`, `method: resumable`.

---

### Test 2.5 - List Sources with Drive Status
**Tool:** `source_list_drive`
**CLI:** `nlm source list [notebook_id] --drive`
**CLI (fast list):** `nlm source list [notebook_id] --drive --skip-freshness`

**Prompt:**
```
List all sources in notebook [notebook_id] and check their Drive freshness status.
```

**Expected:** List showing sources by type, Drive sources show freshness.

**Large notebook variant:** Call `source_list_drive` with `skip_freshness=True`, or use
`nlm source list [notebook_id] --drive --skip-freshness`. Expected: sources are listed
without per-source freshness checks; stale status may be unknown.

**Drive-picker file variant:** Include a PDF, text, Markdown, Word, or PowerPoint file
that was imported from Google Drive. Expected: type-14 files with Drive metadata appear
in `drive_sources` with their Drive IDs and `can_sync: true`; directly uploaded type-14
files without that metadata remain in `other_sources`. `can_sync` marks eligibility to
attempt a manual sync, not a guarantee that every source's RPC will succeed. Only
manually sync entries the tool marks `can_sync: true`.

**Save:** Note a `source_id` for next tests.

---

### Test 2.6 - Describe Source
**Tool:** `source_describe`
**CLI:** `nlm source describe [source_id]`

**Prompt:**
```
Get an AI-generated summary of source [source_id].
```

**Expected:** AI summary with keywords.

---

### Test 2.7 - Get Source Content
**Tool:** `source_get_content`
**CLI:** `nlm source content [source_id]`

**Prompt:**
```
Get the raw text content of source [source_id].
```

**Expected:** Raw text content with title, source_type, char_count.

---

### Test 2.8 - Delete Source
**Tool:** `source_delete`
**CLI:** `nlm source delete [source_id] --confirm`

**Prompt:**
```
Delete source [source_id] with confirm=True.
```

**Expected:** Source permanently deleted.

---

## Test Group 3: AI Features

### Test 3.1 - Describe Notebook
**Tool:** `notebook_describe`
**CLI:** `nlm notebook describe [notebook_id]`

**Prompt:**
```
Get an AI-generated summary of what notebook [notebook_id] is about.
```

**Expected:** AI summary with suggested topics.

---

### Test 3.2 - Query Notebook
**Tool:** `notebook_query`
**CLI:** `nlm query [notebook_id] "What is artificial intelligence?"`

**Prompt:**
```
Ask notebook [notebook_id]: "What is artificial intelligence?"
```

**Expected:** AI answer with conversation_id.

---

### Test 3.2a - Query Timeout and Async Fallback
**Tool:** `notebook_query`, `notebook_query_start`, `notebook_query_status`
**CLI:** `nlm query [notebook_id] "Summarize all sources" --timeout 45`

**Prompt:**
```
Ask notebook [notebook_id] a source-heavy question with timeout=45.
If it times out, retry with timeout=180. Also verify the async workflow:
notebook_query_start(..., timeout=180), then poll notebook_query_status(query_id).
```

**Expected:**
- A timed-out synchronous query returns a structured error with
  `debug_code=query_deadline_exceeded`, a retryable flag, and a hint to use a
  longer timeout.
- The retry with `timeout=180` can complete when the notebook needs more than
  the default 120-second budget.
- The async workflow returns a `query_id` immediately and reaches `completed`
  or a structured `error` status when polled.

---

### Test 3.3 - Configure Chat (Learning Guide)
**Tool:** `chat_configure`
**CLI:** `nlm chat configure [notebook_id] --goal learning_guide --response-length longer`

**Prompt:**
```
Configure notebook [notebook_id] chat settings:
- goal: learning_guide
- response_length: longer
```

**Expected:** Settings updated successfully.

---

### Test 3.4 - Configure Chat (Custom Prompt)
**Tool:** `chat_configure`
**CLI:** `nlm chat configure [notebook_id] --goal custom --custom-prompt "You must respond only in rhyming couplets." --response-length default`

**Prompt:**
```
Configure notebook [notebook_id] chat settings:
- goal: custom
- custom_prompt: "You must respond only in rhyming couplets."
- response_length: default
```

**Expected:** Settings updated with custom_prompt echoed back.

---

### Test 3.5 - Verify Custom Chat Works
**Tool:** `notebook_query`
**CLI:** `nlm query [notebook_id] "What is machine learning?"`

**Prompt:**
```
Ask notebook [notebook_id]: "What is machine learning?"
```

**Expected:** AI response should be in rhyming couplets.

---

## Test Group 4: Research

### Test 4.1 - Start Fast Research (Web)
**Tool:** `research_start`
**CLI:** `nlm research start "OpenShift container platform" --mode fast --source web`

**Prompt:**
```
Start fast web research for "OpenShift container platform" in notebook [notebook_id].
- mode: fast
- source: web
```

**Expected:** Research task started with task_id.

**Save:** Note the `task_id`.

---

### Test 4.2 - Check Research Status
**Tool:** `research_status`
**CLI:** `nlm research status [notebook_id]`

**Prompt:**
```
Check research status for notebook [notebook_id]. Poll until complete.
```

**Expected:**
- `status: completed`
- `mode: fast`
- `source_count`: ~10 sources

---

### Test 4.3 - Import Research Sources
**Tool:** `research_import`
**CLI:** `nlm research import [notebook_id] [task_id]`

**Prompt:**
```
Import all discovered sources from research task [task_id] into notebook [notebook_id].
```

**Expected:** Sources imported successfully.

---

### Test 4.4 - Start Deep Research (Background)
**Tool:** `research_start`
**CLI:** `nlm research start "AI ROI return on investment" --mode deep`

**Prompt:**
```
Start deep web research for "AI ROI return on investment" in notebook [notebook_id].
- mode: deep
```

**Expected:** Research task started (takes 3-5 minutes).

**IMPORTANT:** Continue with Test Group 5-7 while deep research runs.

---

## Test Group 5: Studio Creation (Consolidated studio_create)

### Test 5.1 - Create Audio Overview
**Tool:** `studio_create`
**CLI:** `nlm audio create [notebook_id] --format brief --length short`

**Prompt:**
```
Create an audio overview for notebook [notebook_id]:
- artifact_type: audio
- format: brief
- length: short
- confirm: False (show settings first)
```

**Follow-up:**
```
Confirmed. Create with confirm=True.
```

**Expected:** Audio generation started with artifact_id.

---

### Test 5.1b - Create Audio with Custom Prompt (Verify Extraction)
**Tool:** `studio_create`
**CLI:** `nlm audio create [notebook_id] --focus "Explain this to a 5 year old." --confirm`

**Prompt:**
```
Create an audio overview for notebook [notebook_id] with a focus prompt:
- artifact_type: audio
- focus_prompt: "Explain this to a 5 year old."
- confirm: True
```

**Expected:** Audio generation started. We will verify the prompt "Explain this to a 5 year old" appears in Test 5.10.

---

### Test 5.2 - Create Video Overview
**Tool:** `studio_create`
**CLI:** `nlm video create [notebook_id] --format brief --style classic --confirm`

**Prompt:**
```
Create a video overview for notebook [notebook_id]:
- artifact_type: video
- format: brief
- visual_style: classic
- confirm: True
```

**Expected:** Video generation started.

---

### Test 5.3 - Create Report
**Tool:** `studio_create`
**CLI:** `nlm report create [notebook_id] --format "Briefing Doc" --confirm`

**Prompt:**
```
Create a report for notebook [notebook_id]:
- artifact_type: report
- report_format: Briefing Doc
- confirm: True
```

**Expected:** Report generation started.

---

### Test 5.3b - Create Interactive Report
**Tool:** `studio_create`
**CLI:** `nlm report create [notebook_id] --format Interactive --template learning_overview --prompt "Summarize the key ideas with a quiz" --confirm`

**Prompt:**
```
Create an interactive report for notebook [notebook_id]:
- artifact_type: report
- report_format: Interactive
- report_template: learning_overview
- custom_prompt: Summarize the key ideas with a quiz
- confirm: True
```

**Expected:** Interactive report generation starts (status "queued" -> "in_progress"
-> "completed" in `studio_status`). The artifact type shows as `interactive_report`.

---

### Test 5.3c - Read Interactive Report
**Tool:** `report` (action=get)
**CLI:** `nlm report get [notebook_id] [artifact_id]` (or `--json`, `--output report.md`)

**Prompt:**
```
Read interactive report [artifact_id] in notebook [notebook_id]:
- call report(action="get")
- show the first section of the markdown and the element list
```

**Expected:** Markdown content, prompt and the embedded elements
(mind map / infographic / flashcards / slide deck / quiz) with status
"suggested" before they are generated.

---

### Test 5.3d - List and Generate Report Elements
**Tool:** `report` (action=elements, generate)
**CLI:** `nlm report elements [notebook_id] [artifact_id]` then
`nlm report element create [notebook_id] [artifact_id] --type quiz --confirm`

**Prompt:**
```
List the embedded elements of interactive report [artifact_id], then generate
the quiz element.
```

**Expected:** Element list shows ids/types/statuses; generating one flips its
status from "suggested" to generation states and the element also appears in the
notebook Studio panel.

---

### Test 5.3e - Report Element Settings Sweep
**Tool:** `report` (action=generate, one-item plan)
**CLI:** `nlm report element create [notebook_id] [artifact_id] --type quiz --setting difficulty=hard --setting question_amount=more --confirm`

**Prompt:**
```
Generate an interactive report quiz element with settings difficulty=hard and question_amount=more.
```

**Expected:** Generation starts with custom settings passed into generation options (`difficulty=3`, `question_amount=3`).

---

### Test 5.3f - Batch Element Plan Validation (Invalid Item Stops All)
**Tool:** `report` (action=generate)
**CLI:** `nlm report element create-batch [notebook_id] [artifact_id] --plan invalid_plan.json --confirm`

**Prompt:**
```
Validate and attempt to run a batch plan containing 2 valid element IDs and 1 non-existent or invalid element ID:
[
  {"element_id": "valid-id-1"},
  {"element_id": "invalid-id-xyz"}
]
```

**Expected:** Validation fails before any RPC mutation occurs; no elements change status or start generation.

---

### Test 5.3g - Batch Generation Stops on Quota Exhaustion
**Tool:** `report` (action=generate)
**CLI:** `nlm report element create-batch [notebook_id] [artifact_id] --plan plan.json --confirm`

**Prompt:**
```
Run a multi-element batch plan when quota is exhausted.
```

**Expected:** Generation terminates immediately when encountering `RESOURCE_EXHAUSTED`; result reports `stopped_reason="quota"` and remaining planned items stay `not_started`.

---

### Test 5.3h - Element Bounded Waiting and Timeout
**Tool:** `report` (action=elements)
**CLI:** `nlm report elements [notebook_id] [artifact_id] --wait [element_id] --timeout 10`

**Prompt:**
```
Wait on an in-progress element for up to 10 seconds.
```

**Expected:** Returns status before timeout if completed/failed, or returns `timed_out: true` with current status if timeout elapses without raising an error.

---

### Test 5.3i - Inline Content for Element Review
**Tool:** `report` (action=elements)
**CLI:** `nlm report elements [notebook_id] [artifact_id] --content`

**Prompt:**
```
List report elements with include_content=True to review completed quiz, flashcard, or mind map elements.
```

**Expected:** Returns inline structured content for review with label "Checked against the plan and the report section, not against the original sources."

---

### Test 5.4 - Create Flashcards
**Tool:** `studio_create`
**CLI:** `nlm flashcards create [notebook_id] --difficulty medium --focus "Focus on definitions" --confirm`

**Prompt:**
```
Create flashcards for notebook [notebook_id]:
- artifact_type: flashcards
- difficulty: medium
- confirm: True
```

**Expected:** Flashcards generation started.

---

### Test 5.5 - Create Quiz
**Tool:** `studio_create`
**CLI:** `nlm quiz create [notebook_id] --count 2 --difficulty 2 --focus "Focus on integration testing" --confirm`

**Prompt:**
```
Create a quiz for notebook [notebook_id]:
- artifact_type: quiz
- question_count: 2
- difficulty: medium
- confirm: True
```

**Expected:** Quiz generation started.

---

### Test 5.6 - Create Infographic
**Tool:** `studio_create`
**CLI:** `nlm infographic create [notebook_id] --orientation landscape --detail standard --style professional --confirm`

**Prompt:**
```
Create an infographic for notebook [notebook_id]:
- artifact_type: infographic
- orientation: landscape
- detail_level: standard
- infographic_style: professional
- confirm: True
```

**Expected:** Infographic generation started with professional visual style.

**Visual Styles Available:** auto_select, sketch_note, professional, bento_grid, editorial, instructional, bricks, clay, anime, kawaii, scientific

---

### Test 5.7 - Create Slide Deck
**Tool:** `studio_create`
**CLI:** `nlm slides create [notebook_id] --format detailed_deck --length short --confirm`

**Prompt:**
```
Create a slide deck for notebook [notebook_id]:
- artifact_type: slide_deck
- format: detailed_deck
- length: short
- confirm: True
```

**Expected:** Slide deck generation started.

---

### Test 5.8 - Create Mind Map
**Tool:** `studio_create`
**CLI:** `nlm mindmap create [notebook_id] --title "AI Concepts" --confirm`

**Prompt:**
```
Create a mind map for notebook [notebook_id]:
- artifact_type: mind_map
- title: "AI Concepts"
- confirm: True
```

**Expected:** Mind map created immediately.

---

### Test 5.9 - Create Data Table
**Tool:** `studio_create`
**CLI:** `nlm data-table create [notebook_id] "Key features and capabilities" --confirm`

**Prompt:**
```
Create a data table for notebook [notebook_id]:
- artifact_type: data_table
- description: "Key features and capabilities"
- confirm: True
```

**Expected:** Data table generation started.

---

### Test 5.10 - Check Studio Status
**Tool:** `studio_status`
**CLI:** `nlm studio status [notebook_id]`

**Prompt:**
```
Check studio content generation status for notebook [notebook_id].
```

**Expected:**
- Default MCP response contains at most 20 artifacts and only lean status fields.
- `pagination` contains `returned`, `offset`, `limit`, and `has_more`.
- Polling with `artifact_id` returns only the selected artifact.
- Calling with `include_details=True` exposes rich fields and the artifact from
  Test 5.1b shows `custom_instructions: "Explain this to a 5 year old"`.
- `nlm studio status [notebook_id] --json` remains a plain list containing both
  `id` and `artifact_id`.
- `nlm studio status [notebook_id] --json --mcp-compatible` returns the MCP envelope.
- `nlm video list [notebook_id] --json` returns only video artifacts.

---

### Test 5.11 - Rename Studio Artifact
**Tool:** `studio_status` (with action="rename")
**CLI:** `nlm studio rename [artifact_id] "My Renamed Podcast"`

**Prompt:**
```
Rename artifact [artifact_id] in notebook [notebook_id] to "My Renamed Podcast".
- action: rename
- artifact_id: [artifact_id from studio_status]
- new_title: "My Renamed Podcast"
```

**Expected:** Artifact renamed successfully.

---

### Test 5.12 - Revise Slide Deck
**Tool:** `studio_revise`
**CLI:** `nlm slides revise <artifact_id> --slide '1 Make the title larger' --confirm`

**Prompt:**
```
Revise the slide deck artifact [artifact_id] in notebook [notebook_id]:
- slide_instructions: [{"slide": 1, "instruction": "Make the title larger and bolder"}]
- confirm: true
```

**Expected:**
- New artifact created with status "in_progress"
- New artifact_id returned (different from original)
- Original artifact unchanged
- **Verify:** Run `studio_status` — new deck appears with "(2)" suffix in title.

---

## Test Group 6: Downloads (Consolidated download_artifact)

### Test 6.1 - Download Report
**Tool:** `download_artifact`
**CLI (Verb):** `nlm download report [notebook_id] --output /tmp/report.md`

**Prompt:**
```
Download the report from notebook [notebook_id]:
- artifact_type: report
- output_path: /tmp/report.md
```

**Expected:** Report downloaded as markdown.

---

### Test 6.2 - Download Flashcards (JSON)
**Tool:** `download_artifact`
**CLI:** `nlm download flashcards [notebook_id] --format json --output /tmp/flashcards.json`

**Prompt:**
```
Download flashcards from notebook [notebook_id]:
- artifact_type: flashcards
- output_path: /tmp/flashcards.json
- output_format: json
```

**Expected:** Flashcards downloaded as JSON.

---

### Test 6.3 - Download Quiz (Markdown)
**Tool:** `download_artifact`
**CLI:** `nlm download quiz [notebook_id] --format markdown --output /tmp/quiz.md`

**Prompt:**
```
Download quiz from notebook [notebook_id]:
- artifact_type: quiz
- output_path: /tmp/quiz.md
- output_format: markdown
```

**Expected:** Quiz downloaded as markdown.

---

### Test 6.4 - Download Audio
**Tool:** `download_artifact`
**CLI:** `nlm download audio [notebook_id] --output /tmp/podcast.m4a`

**Prompt:**
```
Download audio from notebook [notebook_id]:
- artifact_type: audio
- output_path: /tmp/podcast.m4a
```

**Expected:** Audio downloaded as M4A/MP4.

---

### Test 6.5 - Download Slide Deck
**Tool:** `download_artifact`
**CLI:** `nlm download slide-deck [notebook_id] --output /tmp/slides.pdf`

**Prompt:**
```
Download slide deck from notebook [notebook_id]:
- artifact_type: slide_deck
- output_path: /tmp/slides.pdf
```

**Expected:** Slides downloaded as PDF.

---

### Test 6.6 - Download Data Table Excel Export
**Tool:** `download_artifact`

**Prompt:**
```
Download the Excel data-table export from notebook [notebook_id]:
- artifact_type: data_table_xlsx
- output_path: /tmp/data-table.xlsx
```

**Expected:** The completed type-10 artifact is downloaded byte-for-byte as XLSX.

---

### Test 6.7 - Download All Artifacts
**Tool:** `download_all_artifacts`
**CLI:** `nlm download all [notebook_id] --output-dir /tmp/exports`

**Prompt:**
```
Download all completed artifacts from notebook [notebook_id]:
- output_dir: /tmp/exports
```

**Expected:** A subdirectory named after the notebook title created under the
output dir, containing every completed artifact named after its title
(report → .md, mind_map → .json, video → .mp4, slide_deck → .pdf, ...).
In-progress/failed artifacts are listed as skipped; result reports
downloaded/failed/skipped counts. Also test `--types video,report` filtering.

---

### Test 6.7 - Sweep All Notebooks (Incremental)
**Tool:** `download_all_artifacts` with `all_notebooks=True, skip_existing=True`
**CLI:** `nlm download all --all-notebooks --skip-existing --output-dir /tmp/exports`

**Prompt:**
```
Download all artifacts from every notebook into /tmp/exports, skipping
files that already exist.
```

**Expected:** One subdirectory per notebook. On a second run with
`skip_existing`, previously downloaded artifacts are reported as skipped
("already downloaded") and only new artifacts are fetched. A failure on one
notebook doesn't stop the sweep; result includes per-notebook counts and
`errored_notebooks`.

---

## Test Group 7: Sharing

### Test 7.1 - Get Share Status
**Tool:** `notebook_share_status`
**CLI (Noun):** `nlm share status [notebook_id]`

**Prompt:**
```
Get sharing status for notebook [notebook_id].
```

**Expected:** Status with `is_public`, `access_level`, `collaborators`.

---

### Test 7.2 - Enable Public Link
**Tool:** `notebook_share_public`
**CLI:** `nlm share public [notebook_id]`

**Prompt:**
```
Enable public link for notebook [notebook_id].
- is_public: True
```

**Expected:** Public link returned.

---

### Test 7.3 - Disable Public Link
**Tool:** `notebook_share_public`
**CLI:** `nlm share private [notebook_id]`

**Prompt:**
```
Disable public link for notebook [notebook_id].
- is_public: False
```

**Expected:** Public link disabled.

---

### Test 7.4 - Invite Collaborator (Optional)
**Tool:** `notebook_share_invite`
**CLI:** `nlm share invite [notebook_id] test@example.com --role viewer`

**Prompt:**
```
Invite collaborator to notebook [notebook_id]:
- email: test@example.com
- role: viewer
```

**Expected:** Invitation sent (or error if email invalid).

---

## Test Group 8: Drive Sync (Optional)

### Test 8.1 - Sync Drive Sources
**Tool:** `source_sync_drive`
**CLI (Verb):** `nlm source sync [notebook_id] --confirm`

**Prompt:**
```
Check if any Drive sources in notebook [notebook_id] are stale using source_list_drive.
If any are stale, sync them using source_sync_drive with confirm=True.
```

**Expected:** Sources synced if any were stale.

---

## Test Group 9: Deep Research Verification

**TIMING:** By now, deep research from Test 4.4 should be complete.

### Test 9.1 - Check Deep Research Status
**Tool:** `research_status`
**CLI (Noun):** `nlm research status [notebook_id]`

**Prompt:**
```
Check deep research status for notebook [notebook_id] with max_wait=60.
```

**Expected:**
- `status: completed`
- `mode: deep`
- `source_count`: ~40-50 sources
- `report` field present

---

### Test 9.2 - Import Deep Research Sources
**Tool:** `research_import`
**CLI (Noun):** `nlm research import [notebook_id] [task_id]`

**Prompt:**
```
Import all deep research sources from task [task_id] into notebook [notebook_id].
```

**Expected:** Sources imported successfully.

---

## Test Group 10: Cleanup

### Test 10.1 - Delete Studio Artifacts
**Tool:** `studio_delete`
**CLI (Noun):** `nlm studio delete [notebook_id] [artifact_id] --confirm`

**Prompt:**
```
Get studio status for notebook [notebook_id], then delete each artifact with confirm=True.
```

**Expected:** All artifacts deleted.

---

### Test 10.2 - Delete All Sources
**Tool:** `source_delete`
**CLI (Noun):** `nlm source delete [source_id] --confirm`

**Prompt:**
```
List sources in notebook [notebook_id], then delete each with confirm=True.
```

**Expected:** All sources deleted.

---

### Test 10.3 - Delete Notebook
**Tool:** `notebook_delete`
**CLI (Noun):** `nlm notebook delete [notebook_id] --confirm`

**Prompt:**
```
Delete notebook [notebook_id] with confirm=True.
```

**Expected:** Notebook deleted successfully.

---

## Test Group 11: Notes Management (Unified `note` tool)

### Test 11.1 - Create Note
**Tool:** `note`
**CLI (Noun):** `nlm note create [notebook_id] --content "Test" --title "Title"`

**Prompt:**
```
Create a note in notebook [notebook_id]:
- action: create
- content: "This is a test note about AI"
- title: "AI Note"
```

**Expected:** Note created with `note_id`.

**Save:** Note the `note_id` for subsequent tests.

---

### Test 11.2 - List Notes
**Tool:** `note`
**CLI (Noun):** `nlm note list [notebook_id]`

**Prompt:**
```
List all notes in notebook [notebook_id]:
- action: list
```

**Expected:** Array of notes including the one just created, with previews.

---

### Test 11.3 - Update Note
**Tool:** `note`
**CLI (Noun):** `nlm note update [notebook_id] [note_id] --content "Updated"`

**Prompt:**
```
Update note [note_id] in notebook [notebook_id]:
- action: update
- note_id: [note_id]
- content: "Updated: AI is transforming technology"
```

**Expected:** Success confirmation.

---

### Test 11.4 - Delete Note
**Tool:** `note`
**CLI (Noun):** `nlm note delete [notebook_id] [note_id] --confirm`

**Prompt:**
```
Delete note [note_id] in notebook [notebook_id]:
- action: delete
- note_id: [note_id]
- confirm: True
```

**Expected:** Deletion confirmation message.

---

## Test Group 12: Server Info

### Test 12.1 - Get Server Info
**Tool:** `server_info`

**Prompt:**
```
Get Gemini Notebook MCP server version and check for updates.
```

**Expected:**
- `version`: Current version
- `latest_version`: Latest PyPI version (or null if offline)
- `update_available`: Boolean
- `update_command`: Upgrade command

---

## Test Group 13: Plan Usage

### Test 13.1 - Get Remaining Usage
**Tool:** `usage_get`
**CLI (Noun):** `nlm usage` (add `--json` for raw output)

**Prompt:**
```
How much of my Gemini Notebook usage allowance is left?
```

**Expected:**
- `windows`: two entries, `rolling` first then `weekly`
- each with `percent_used`, `percent_remaining` and `resets_at` (ISO 8601 UTC)
- `tier`: subscription tier string, e.g. `NOTEBOOKLM_TIER_PRO_CONSUMER_USER`

**Also verify:**
- Run it twice. The reported order stays `rolling`, `weekly` even though the API
  returns the two entries in an unstable order.
- `percent_used` and `percent_remaining` sum to 100 for a partly consumed window.
- With an expired session it reports an authentication error and hints at
  `nlm auth refresh`. It must NOT report the allowance as exhausted.

### Test 13.2 - Check Work and Personal Profiles
**Tool:** `usage_get(profile="work")`, then `usage_get(profile="personal")`
**CLI:** `nlm usage --profile work`, then `nlm usage -p personal --json`

**Expected:**
- Each call reads the selected saved account's usage, even when
  `NOTEBOOKLM_COOKIES` is set for a different account.
- The configured default and the account used by other MCP tools are unchanged.
- Missing profiles return an error without falling back to environment cookies
  or the default account.
- Omitting the profile keeps the existing environment/default authentication.

---

## Test Group 14: Setup Wizard (CLI only)

Automated: `uv run pytest -m wizard_e2e` drives the real `nlm setup` in a
pseudo-terminal against a sandboxed HOME (fake `claude`/`codex`/`ps`/`pbcopy`/
`open`) and checks the files written. Run it after any change to
`cli/commands/setup.py`, `setup_wizard.py`, `skill.py` or `cli/skill_package.py`.
The manual checks below cover what the sandbox can't: the real `claude` and
`codex` CLIs, and the real Claude Desktop upload. Run `nlm setup` in a real
terminal (not an agent's shell — it refuses non-interactive sessions).

### Test 14.1 - Status and Esc
**CLI:** `nlm setup` → **Show my tools' status**

**Expected:**
- Only installed tools are listed, with ✓ set up / ✗ not yet / ⚠ old name and
  the skill version (⬆ when an upgrade exists).
- Esc on every screen returns to the main menu within ~0.1s; Esc on the main
  menu quits.

### Test 14.2 - Connect and rename
**CLI:** `nlm setup` → **Add the MCP to my tools/agents**

**Expected:**
- Nothing is pre-ticked; already-connected tools are shown but not selectable.
- Tools whose entry is still `notebooklm-mcp` appear under **Needs a fix**;
  ticking them reports `repaired · Renamed to gemini-notebook-mcp`.
- Verify: `claude mcp list` shows `gemini-notebook-mcp ✔ Connected`;
  `~/.codex/config.toml` has `[mcp_servers.gemini-notebook-mcp]` and kept any
  extra keys (e.g. `enabled = true`); Claude Desktop configs (both profiles if
  chosen) contain `gemini-notebook-mcp` with the full binary path.
- With Claude Desktop open, connecting it is refused ("still running").

### Test 14.3 - Skill and Claude Desktop upload file
**CLI:** `nlm setup` → **Add the skill to my tools/agents** (or `nlm skill package`)

**Expected:**
- No "Add the skill?" question — it opens on "Where should the skill live?".
- The picker includes **Claude Desktop / claude.ai · creates a file to upload**,
  not pre-ticked. Ticking it saves `~/Downloads/nlm-skill.zip`, reveals it in
  Finder, and shows the upload steps last.
- Upload via Claude Desktop **Customize → Skills → Add**: accepted; the skill
  appears on the Skills page and loads in Chat and Cowork.

### Test 14.4 - Remove and copy setup
**CLI:** `nlm setup` → **Remove an MCP or skill**, then **Copy MCP setup for a tool not listed**

**Expected:**
- Remove: grouped MCP connections / Skills, nothing pre-ticked, two separate
  default-No confirmations; only ticked items change; backups appear in
  `~/.notebooklm-mcp-cli/backups/`.
- Copy: the clipboard holds `{"mcpServers": {"gemini-notebook-mcp": {"command": "<full path>/notebooklm-mcp"}}}`.

### Test 14.5 - Credential protection
**CLI:** `nlm setup` → **Credential protection**

**Expected:**
- With no saved logins: "No saved logins yet. Run 'nlm login' first."
- With plain logins: lists each as `plain` and offers **Protect saved logins (recommended)**; with several logins a checkbox picker appears.
- With protected logins: offers **Restore protected logins to plain files** (asks to confirm, default No); never offers Protect for an already-protected login.
- Mixed: offers both.
- Afterwards `nlm auth storage status` matches, and no protection tip appears for a protected login.

### Test 14.6 - First login on a new profile (new-user flow)
**CLI:** `nlm login` on a machine (or sandbox) with no saved logins

**Expected:**
1. Before the browser opens: "Where should your saved login live?" with Protected (recommended) and Plain file.
2. Choosing 1 stores the login encrypted (`credentials.enc`, no `cookies.json`) and ends with "✓ Login stored in protected mode." with no further tips or questions.
3. A profile name protected mode can't use (`my work`, `john@work.com`) offers a working name (`my-work`) or keeping the name as a plain file.
4. Ctrl+C or closing the flow leaves no profile folder, no saved answer, and closes the Chrome window it opened.

---

## Summary: 31 Consolidated Tools

| Category | Tools | Count |
|----------|-------|-------|
| **Auth** | `refresh_auth`, `save_auth_tokens` | 2 |
| **Notebooks** | `notebook_list`, `notebook_get`, `notebook_describe`, `notebook_create`, `notebook_rename`, `notebook_delete` | 6 |
| **Sources** | `source_add`, `source_list_drive`, `source_sync_drive`, `source_delete`, `source_describe`, `source_get_content` | 6 |
| **Sharing** | `notebook_share_status`, `notebook_share_public`, `notebook_share_invite` | 3 |
| **Research** | `research_start`, `research_status`, `research_import` | 3 |
| **Studio** | `studio_create`, `studio_status`, `studio_delete`, `studio_revise` | 4 |
| **Downloads** | `download_artifact`, `download_all_artifacts` | 2 |
| **Exports** | `export_artifact` | 1 |
| **Chat** | `notebook_query`, `chat_configure` | 2 |
| **Notes** | `note` (unified: list, create, update, delete) | 1 |
| **Server** | `server_info` | 1 |
| **Total** | | **31** |

---

## Quick Copy-Paste Test Prompts

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