All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
cdp.py that created a race condition where a newly opened WebSocket could be prematurely closed. Thanks to @insane66613 for the PR!conftest.py with autouse fixtures to isolate test execution from user storage, preventing tests from silently overwriting real user credentials in ~/.notebooklm-mcp-cli/auth.json. (Cherry-picked from PR #252)NOTEBOOKLM_RPC_TRANSPORT=cdp) — Routes normal batchexecute RPCs and notebook chat through fetch inside the saved NotebookLM browser profile so Chrome supplies live browser-bound cookies. Off by default; use only when nlm doctor auth-replay shows cdp_in_page succeeds but normal replay fails. See docs/AUTHENTICATION.md for usage.GenerateFreeFormStreamed queries are routed through the experimental CDP transport when the flag is enabled, with longer CDP response waits to handle long answers.notebook_query cancellation crash — The query tool is now async and dispatches blocking I/O to a thread via anyio.to_thread.run_sync with abandon_on_cancel=True, so MCP client cancellation no longer leaves the server in an inconsistent state.nlm doctor auth-replay CDP probe uses the shared fetch helper — The diagnostic path now exercises the same fetch_form_in_page helper used by the real experimental transport, keeping the two code paths consistent.execute_cdp_command accepts a response_timeout parameter — The default 30 s WebSocket wait is now configurable so long-running in-page fetches (e.g. streamed notebook queries) do not time out prematurely.find_existing_nlm_chrome accepts include_headless — Interactive login keeps the existing False default; the browser-backed RPC transport passes True to reuse profile-owned headless browsers.nlm doctor auth-replay diagnostic command — Compares saved-cookie replay through normal httpx, httpx replay after a forced Google RotateCookies call, and optional in-page CDP fetch from the saved Chrome profile. Verdicts distinguish ordinary cookie freshness failures from likely browser-bound replay behavior, giving actionable next steps for issue #248.RotateCookies support during auth token refresh — When refreshing auth tokens, the client now attempts a RotateCookies call to keep cookies fresh before replay.httpx requests were producing false browser_bound_replay verdicts; probes now use the same headers as normal NotebookLM RPC calls.nlm login --check confirms auth redirects with a real RPC (Fixes #250) — The homepage probe now validates with a lightweight NotebookLM RPC before reporting cookies as expired, eliminating false-positive expiry reports. Thanks to @laofun for reporting and contributing this fix!.google.com cookie values when flattening duplicates (Fixes #249) — When browser cookies contain duplicates, the .google.com-domain value is now preferred, preventing stale subdomain values from overriding the correct one. Thanks to @laofun for reporting and contributing this fix!nlm login --check crash on slow accounts (Fixes #243, PR #245) — Auth checks now use the lightweight NotebookLM homepage probe instead of the full notebook-list RPC, and notebook counts are fetched as a best-effort extra so large accounts no longer crash the command with a raw timeout. Connect-phase RPC failures are retried safely without retrying read/write timeouts that may have already reached NotebookLM. Thanks to @LesleyMurfin for the report, careful fix, and regression coverage!find_existing_nlm_chrome() now verifies mapped Chrome PIDs still use the NLM profile's --user-data-dir and the expected --remote-debugging-port, skips headless automation browsers on all platforms, and clears stale port-map entries instead of reconnecting to unrelated CDP listeners such as agent-browser or Playwright. Thanks to @syf2211 for the focused Windows CDP fix and tests!--user-data-dir and --remote-debugging-port must match exactly, preventing prefix matches such as chrome-profile-other or port 92222 from being accepted as the intended NLM profile/port.video_format="short" (CLI: nlm video create <id> --format short) generates NotebookLM's new ~60-second, vertical "bite-sized overview" video format, announced by Google on 2026-06-30. Like Cinematic, Short has no visual style picker (--style/--style-prompt map into --focus) and is currently English-only, 18+, and rolling out gradually to Pro/Ultra accounts. The request payload was verified via a live network capture against a production NotebookLM account.studio_status and nlm studio status --json --full now include each artifact's source_ids, allowing callers to trace generated podcasts, videos, reports, slide decks, infographics, quizzes, flashcards, and data tables back to their source documents. The parser handles the nested source-ID shapes returned by current NotebookLM responses. Thanks to @tonhuu96 for identifying the missing capability and contributing the end-to-end implementation!Could not add file source. The hint clarifies that paths must be accessible on the machine running nlm or the MCP server, which makes different-host and remote MCP failures diagnosable.es/es-ES selects Spain Spanish, while es-US/es-419 selects Latin-American Spanish. The same regional locale can be supplied through NOTEBOOKLM_HL.ALL_PROXY=socks5://... or socks5h://... no longer fail with a missing socksio import before the first request. Sync and async client initialization are covered by regression tests and were verified through a live SOCKS5 proxy.source_list_drive now accepts skip_freshness=True, and nlm source list <notebook-id> --drive --skip-freshness returns the source list without making one freshness API call per Drive source. This gives users with very large notebooks a fast listing path when freshness status is not needed. Skipped freshness is reported as unknown (stale: null / is_stale: null), not fresh.RPCDriftError was bypassing the service layer error wrapping, exposing raw exception types instead of the friendly ServiceError payload. It is now correctly wrapped while perfectly preserving the actionable NOTEBOOKLM_RPC_OVERRIDES hot-patch instructions for the user.INFO level. Thanks to @Grobiou for the cleanups!research_status: auto_import parameter — Pass auto_import=True and the tool automatically imports discovered sources on completion. No separate research_import call needed. When False (default), the response includes a next_action hint pointing you to the exact research_import call to make. Fixes the empty-notebook-shell problem reported in #231.source_list_drive timeouts on large notebooks (Fixes #232) — Drive-source freshness checks were made one at a time, in sequence. A notebook with 50 Drive sources took ~30 seconds and routinely hit the 30-second timeout. Freshness checks now run in parallel (8 workers). Measured on a live 44-source notebook: 29s sequential → 3.1s parallel, zero rate-limit errors. If one source's freshness check fails, that source reports stale: null instead of failing the whole list.research_status default timeout too short for deep research — The MCP default was 5 minutes; deep research frequently runs longer. Default max_wait bumped to 15 minutes (900s), poll interval stays 30s. The CLI --auto-import flow is now consistent: same 15-minute / 30-second cadence instead of the old mode-specific 10-minute / 10-second split.RPCDriftError doc said "instead of returning silently" but the code still returned None silently when the response had no wrb.fr chunks to compare against. Clarified the behavior in CLAUDE.md and added the MCP server restart requirement to the NOTEBOOKLM_RPC_OVERRIDES hot-patch instructions.NOTEBOOKLM_RPC_OVERRIDES, loud RPCDriftError on rotation detection, and exponential-backoff retry for RESOURCE_EXHAUSTED (code 8) throttling. Thanks to @Grobiou for the contribution!--json output in automation scripts using a JSON parser rather than string splitting. Thanks to @JanaGK2 for the first contribution!source_get_content call wrote files to disk with no cleanup), and unintended behavioral changes to source_get_content and download_artifact for all users. The feature has been reverted. A future release will re-introduce the polling improvements (wait/poll_interval) cleanly, and the ChatGPT integration may return in a hardened form.no_proxy sanitization — The v0.7.2 sanitizer stripped all no_proxy entries containing :, incorrectly removing legitimate IPv6 loopback addresses (::1) and host:port entries. The fix now targets only CIDR-style IPv6 entries (e.g., fe11::/16) that crash httpx's URL parser, preserving all other valid entries.AuthHealthChecker API fallback passed wrong cookie format, causing false stale on semi-stale sessions (PR #225) — The API fallback flattened profile.cookies to a dict, dropping domain-specific duplicates and omitting session_id / build_label. The API probe now passes profile.cookies unchanged with all session fields, matching nlm login --check. Thanks to @insane66613!server_info, refresh_auth, and studio_create disagreed on semi-stale auth (PR #225, Fixes #224) — These three MCP paths each ran independent auth checks, so server_info could report stale and studio_create refuse to run while notebook_list and CLI tools worked fine. All MCP auth gates now share credentials_are_usable() (AuthHealthChecker + live API confirmation), eliminating split-brain results. Thanks to @insane66613!no_proxy environment variable (PR #221) — Fixed package import crashes on Windows systems caused by CIDR-style IPv6 entries in no_proxy that httpx cannot parse. Thanks to @insane66613!server_info reported "stale" for valid semi-stale cookies (PR #219) — Google enforces different session lifecycles for the NotebookLM homepage (a navigation endpoint) and the RPC API endpoints. Cookies that are "semi-stale" — rejected by the homepage with a redirect to accounts.google.com, but still 100% accepted by the RPC API — were causing the MCP server_info tool (and nlm login --check) to falsely report "stale" even though every actual API tool would work fine. The new AuthHealthChecker runs a homepage probe first and, on expired / http_401 / http_403, falls back to a live NotebookLMClient.list_notebooks() call before deciding. The homepage headers were also upgraded to use the full browser-like _PAGE_FETCH_HEADERS (including Sec-Fetch-Dest / Sec-Fetch-Mode / Sec-Fetch-Site / Sec-Fetch-User) — without them Google was bot-detecting the homepage check and redirecting even fresh cookies, producing false "stale" reports on first probe. Results are cached for 30 seconds with mtime-based bypass, so an external nlm login is reflected without waiting for the TTL. The CLI and MCP now share the cache via the get_auth_health_checker() singleton in services/auth.py. Thanks to @SERDAR-AKIN for the original multi-probe design and PR #219!AuthHealthChecker lives in services/auth.py, not core/auth.py — the multi-probe orchestration, 30-second cache, and verdict aggregation are business logic and belong in the services layer per the layering rule in CLAUDE.md / AGENTS.md / GEMINI.md. core/auth.py stays focused on the low-level auth primitives (AuthManager, AuthTokens, check_auth, save_tokens_to_cache, load_cached_tokens, _fetch_notebooklm_homepage).server_info docstring no longer calls auth_status a "live check" — it is cached for 30 seconds with mtime-based bypass. docs/AUTHENTICATION.md documents the same contract for nlm login --check (always live) vs server_info (cached).AuthHealthReport.valid is now verdict == "configured" (was !=) — the field was inverted: it returned True for stale / unverified / not_configured reports. Pinned by TestReportValid in tests/services/test_auth_health.py._probe_api now catches httpx.TimeoutException and httpx.RequestError explicitly and emits the "network_error:" prefix so _determine_verdict can route them to "unverified". Previously all exceptions emitted f"{type(e).__name__}: {e}", which meant a real transport error on the API path was misclassified as an auth failure ("stale"). Pinned by TestProbeApiErrorClassification in tests/services/test_auth_health.py.references/studio-prompting-guide.md (per-artifact decision trees, prompt parameters) and references/studio-prompt-examples.md (copy-paste templates). SKILL.md and workflows.md updated throughout.video_format=cinematic is now documented and supported in the skill, CLI, and MCP guide. Cinematic videos take the full creative brief via focus_prompt / --focus; --style is not applicable.ANSWER: C from a multiple-choice prompt, nlm discarded it and returned the longest thinking chunk instead. The 20-char guard in _extract_answer_from_chunk (both the list-form and string-form branches) was redundant with the existing type indicator at first_elem[4][-1] (1 = answer, 2 = thinking), which is the authoritative discriminator. Removed both guards. The type indicator still routes short thinking chunks to the thinking bucket, so behavior for normal answers is unchanged. 4 new regression tests in TestShortAnswerRegression cover: short answer wins over a longer thinking chunk, single-character answers, short thinking chunks still filtered as thinking, and the string-form first_elem path.nlm notebook create missing --json (Issue #215) — Every other notebook verb (list, get, describe, query) already supported --json, but create did not. This was a real friction point for agent workflows that need to capture the new notebook ID reliably. Added the flag; the output is the same notebook_id / title / url / message dict that other verbs return, so scripting just works. Thanks to @SimonMallas for the report and end-to-end test in the issue!auth_status = "stale" was misleading (Issue #215) — The MCP server_info tool (and nlm login --check) reported "stale" for any non-configured / non-not_configured outcome, which silently grouped three very different conditions together: (a) credentials are actually expired and operations will fail, (b) the live check hit a network error/timeout/non-200 and cached creds may still work, and (c) the saved profile failed to load. The new state machine splits this into two distinct values: "stale" is reserved for cases (a) and (c) where the user genuinely needs to nlm login, and a new "unverified" reports case (b) so agents don't pester users to re-auth on transient network blips. Unknown future reasons stay conservative ("stale"). No raw AuthCheckResult.reason strings are exposed; only the 5 stable status values. The new Understanding auth_status section in docs/AUTHENTICATION.md documents each state and what to do.format_item silently discarded plain-dict results — All three formatter classes (TableFormatter, JsonFormatter, CompactFormatter) checked for model_dump / __dict__ before isinstance(item, dict). Because TypedDict instances are plain dict at runtime (no model_dump, no per-instance __dict__ in CPython), they fell through to the worst-case path: JsonFormatter wrapped the payload as {"value": {...}} instead of printing it flat, and CompactFormatter emitted str(item) (the raw dict repr) instead of the notebook ID. Fixed by adding isinstance(item, dict) as the first branch in all three format_item methods. nlm notebook create --json and pipe capture now work correctly."unverified" in server_info — The reason.startswith("http_") catch-all in _check_auth_status mapped every non-200 response — including definitive credential-rejection codes 401 and 403 — to "unverified" ("cached credentials may still work, do not prompt re-auth"). Added explicit http_401 / http_403 → "stale" guards before the general http_ branch so agents correctly prompt re-authentication when cookies are genuinely rejected by NotebookLM.docs/GETTING_STARTED.md guide — First-time setup, agent registration, and a full 5-step migration path from a browser-automation–based NotebookLM MCP (the kind of setup reported in Issue #215). The migration section explicitly calls out removing any legacy notebooklm server config as the #1 cause of "Hermes picked the wrong tool" symptoms. README is no longer carrying the migration content; the docs index now points at the new guide. (Issue #215, items 3 and 5)MCP_GUIDE.md server-naming note — Recommends the default notebooklm-mcp server name and warns against generic names that collide with legacy MCPs. (Issue #215, item 5)Auth-guard stale-TTL window when tokens change on disk during the cached period — The 60s auth-guard introduced in 0.6.14 cached the "auth is valid" result for 60s. If you ran nlm login (or any flow that rewrote the auth file) during that window, the guard would still report valid and your server would use the stale tokens until the TTL elapsed. Fixed by services.auth.get_active_auth_mtime(): the guard now records the latest mtime of the active auth storage (the legacy auth.json plus every cookies.json under profiles/) and invalidates the cache when any of them changes. A write to ANY profile's file invalidates the guard, regardless of which profile the CLI/MCP session is using. 5 new tests cover modern profile layout, legacy fallback, mid-migration (both files exist), fresh install (no files), and config-error defensiveness.
Auth-guard mtime check was watching the wrong file (caught by live testing) — The first iteration of the mtime fix only watched the config's default_profile's cookies.json. But the active profile for a CLI/MCP session can be overridden with --profile, while the config-level default_profile stays put. If you ran nlm login --profile <other> externally, the active profile's cookies.json would be rewritten but the guard never saw it. The fix above resolves this by globbing all profiles/*/cookies.json files. Live testing against a real Chrome login + real NotebookLM API confirmed the fix.
services/auth.py is now a full shim, not a single-symbol re-export — The 0.6.14 release added services/auth.py to route check_auth through the services layer. This release extends it to cover all 6 auth symbols that the cli/ and mcp/ layers were importing directly from core/: check_auth, load_cached_tokens, save_tokens_to_cache, get_cache_path, validate_cookies, plus the two class symbols AuthTokens and AuthManager via PEP 562 __getattr__. The shim is a thin layer with no business logic of its own; behavior is unchanged. The only remaining direct core.auth import in cli/ or mcp/ is in utils/cdp.py, which has a circular-import guard and is explicitly outside the layering rule's scope.nlm login crash on fully expired auth (PR #211 / Issue #210) — When the stored Google session/cookies were fully expired, _validate_saved_profile() raised ClientAuthenticationError, which does not inherit from NLMError. The except NLMError: clause in login_callback missed it, so the exception bubbled up to cli_main() and exited the process before ever launching Chrome for interactive sign-in. Users had to manually delete all profiles as a workaround. Fixed by also catching ClientAuthenticationError in the validation catch. Thanks to @insane66613 for the fix!refresh_auth() returned status: "success" after reloading dead tokens from disk. A disk reload is not a successful re-auth — now runs check_auth(live=True) after the reload and returns status: "expired" with an actionable nlm login hint if tokens are dead.studio_create() had no pre-flight auth check, so it returned status: "success" with an artifact_id that failed seconds later — sending agents into pointless polling loops. Now runs check_auth(live=True) after the network-free confirmation preview and artifact-type validation; invalid auth returns status: "error" with an nlm login hint before any doomed request is fired.studio_status() surfaced status: "failed" artifacts with every other field null and no reason, so callers had no way to know why. The raw gRPC payload carries no error string, so get_studio_status() now synthesizes a non-null error_reason for failed artifacts while preferring any real error_reason / failure_reason / failure_code / error key if a future API version exposes one. 12 new tests cover the full matrix (one is parametrized with 4 cases). Thanks to @idankatz64-commits for the comprehensive PR and tests!check_auth(live=True) adds ~1 homepage fetch on the confirm=True path of studio_create and on every refresh_auth. A network-free preview path is preserved for confirm=False.ArtifactInfo gains an optional error_reason field — Returned by the MCP studio_status tool. None for healthy artifacts; a synthesized string for failed artifacts (with a hint to re-check auth); verbatim from the API if a real error_reason/failure_reason/failure_code/error key is present. Backward-compatible: existing callers that only inspect status are unaffected.0 to disable that specific cap):NOTEBOOKLM_CONVERSATION_MAX_TURNS (default 50) — max turns kept per conversation. Older turns are FIFO-dropped; survivors are renumbered 1..N so turn_number stays a stable 1-indexed position in the current list.NOTEBOOKLM_CONVERSATION_MAX_CONVS (default 500) — max distinct conversations cached. On overflow, the least-recently-used conversation is evicted. Writes and reads both promote to MRU.NOTEBOOKLM_CONVERSATION_MAX_CHARS_PER_TURN (default 100000) — per-turn answer character cap as a safety net against pathological payloads. Queries are user input and not truncated.get_conversation_cache_stats() returns {conversations, total_turns, max_turns_per_conversation, max_conversations, max_chars_per_turn}.0=unlimited, negative-clamp-to-zero, FIFO trim with renumber, LRU eviction on insert, LRU promotion on read, LRU promotion on write, LRU promotion when migrating to a pre-existing key, answer truncation, stats accuracy, and clear compatibility.--stateless flag and NOTEBOOKLM_MCP_STATELESS env var continue to control the MCP HTTP transport layer only — they do not affect this cache.auth.json, cookies.json, metadata.json, port map) were previously written with default permissions and then chmod'd to 0o600 — leaving a brief window where the file was world-readable. Fixed using os.open() + os.fdopen() so the file descriptor is created with 0o600 from the start. Thanks to @Amy-Ra-lph for the PR and thorough implementation!notebooklm-mcp --transport http --host 0.0.0.0 (or --transport sse) previously emitted a warning but still bound to the external address. The server now refuses to start unless NOTEBOOKLM_ALLOW_EXTERNAL_BIND=1 is explicitly set, preventing accidental cookie exposure on untrusted networks. The guard now covers both HTTP and SSE transports.actions/checkout, astral-sh/setup-uv, pypa/gh-action-pypi-publish, softprops/action-gh-release) to their full 40-character commit SHAs with a version comment for readability. Prevents tag-drift supply chain attacks. Thanks to @Amy-Ra-lph for the careful SHA verification!terminate_chrome() null-safety (PR #205) — On double-call, _cached_ws.close() could raise AttributeError because _cached_ws was read after being set to None. The reference is now captured before the try block. Thanks to @Amy-Ra-lph!.strip() to cookie key parsing in save_auth_tokens to handle edge cases with leading/trailing whitespace in cookie headers.check_auth() function with a typed AuthCheckResult, eliminating subtle differences between the MCP and CLI auth status paths. Thanks to @derekszen for the clean refactor!_reconcile_source() (the fallback poller that verifies a source landed after an ambiguous gRPC error) previously used a fixed 1-second delay between polls. Now uses exponential backoff (1s → 2s → 4s, capped at 4s) to reduce unnecessary API calls on slower operations.add_file() now calls .expanduser().resolve() on the input path, so paths like ~/Documents/file.pdf work correctly and symlinks are fully resolved before validation.raw_response field removed from query() return — The raw_response key was included in the dict returned by ConversationMixin.query() but was never read by any caller (services, MCP tools, or CLI). Removing it avoids leaking raw API response text into any future log aggregators or serializers.nlm login --manual:#HttpOnly_ lines as comment rows and silently ignored them, which dropped essential Google authentication cookies like __Secure-1PSIDTS and __Secure-3PSIDTS (resulting in cryptic 401 errors)."" instead of being skipped by ensuring trailing tab characters are not stripped from the end of the line.source_add tool and SKILL.md file-type alignment (PR #197) — Updated the source_add MCP tool docstring and the global SKILL.md guidelines to list all 18 supported file-type extensions (PDF, TXT, MD, DOCX, CSV, EPUB, MP3, M4A, WAV, AAC, OGG, OPUS, MP4, JPG, JPEG, PNG, GIF, WEBP) instead of a restricted subset. Also documented how image-bearing sources are ingested to feed the Studio video generation's visual-crop pipeline to generate on-screen visual aids in Video Overviews. Thanks to @Premshay for the excellent documentation enhancement and detailed research on the visual-crop pipeline!source_add and research_import (Issue #196) — source_add (text, URL, Drive) and research_import were reporting "Could not add ... source." errors even when NotebookLM had successfully accepted the source for asynchronous processing. Root cause: NotebookLM uses the same gRPC error code 3 in the wrb.fr response envelope for both "accepted-pending" (async processing started) and "genuine rejection". Added a _reconcile_source() helper that polls get_notebook_sources_with_types() after a code 3 or 9 error to verify whether the source actually landed. If found → returns success. If not found after polling → re-raises the original error so genuine failures still surface. Also fixed a secondary double-submission bug where URL sources on accounts using the v1 (izAoDd) RPC would trigger a spurious v2 (ozz5Z) call when v1 returned an accepted-pending code 3 — reconciliation now short-circuits the fallback if v1 actually delivered. 12 new unit tests added (total: 875 tests). Thanks to @mdshearer for the detailed report and excellent root cause analysis!~/snap/<snap-name>/common/. Launching with --user-data-dir=~/.notebooklm-mcp-cli/ was failing with Exit code 21: Failed to create a ProcessSingleton. Snap browsers are now detected via /snap/ in the resolved binary path and automatically redirected to ~/snap/chromium/common/notebooklm-mcp-cli/chrome-profiles/. Profile lock, headless auth, and cache cleanup are all snap-aware. Thanks to @ildella for the contribution!nlm download audio were returning HTTP 403 from Google's CDN (lh3.google.com) because _download_url inherited Sec-Fetch-Site: none from the page fetch headers. Google's audio CDN treats that value as an unauthorized address-bar navigation and rejects the request regardless of valid cookies. The fix mirrors the header shape Chrome uses for window.open() — setting Sec-Fetch-Site: cross-site and Referer: https://notebooklm.google.com/ on all cross-domain artifact downloads. Verified: same notebook that returned "Download failed for audio." now produces a complete 41.7 MB AAC file. Thanks to @responsiblefleet for the thorough root cause analysis and fix!nlm doctor to match the login flow.CREATE_NEW_PROCESS_GROUP.--disable-features=msEdgeStartupBoost launch flag to prevent Edge's "Startup Boost" background processes from intercepting CDP sessions.nlm skill install hermes now installs the NotebookLM skill for Hermes Agent by NousResearch. Respects the $HERMES_HOME environment variable for custom install paths..epub files can now be uploaded as notebook sources. Thanks to @mateogon for the contribution!nlm skill install no longer falsely warns that tools are not installed on Windows. Detection now checks for the tool's binary on PATH and root config directory existence instead of only checking the (possibly non-existent) skills subdirectory.safe_mkdir now catches PermissionError and provides an actionable fix command (icacls) instead of a raw traceback. The update-check cache and print_update_notification() no longer crash the CLI when the storage directory has restrictive ACLs.read_text() / write_text() calls now explicitly specify encoding="utf-8" to prevent UnicodeDecodeError on Windows systems that default to cp1252.nlm studio status now routes through the service layer (get_studio_status) so mind maps are included in the output.is_tool_on_system() helper into cli/utils.py, shared by both nlm skill install and nlm setup to eliminate duplicate detection logic.TOOL_CONFIGS in skill.py now uses a ToolConfig TypedDict instead of dict[str, Any] for better type safety..epub and all audio/image formats actually supported by the API.Content-Type: application/x-www-form-urlencoded;charset=UTF-8 to the streamed query endpoint. Thanks to @fabianafurtadoff for the contribution (PR #189)![1]), ranges ([4-6]), and bibliography extraction. The research_status polling output now also annotates sources with a cited: bool field for visibility. Thanks to @zxyasfas for the contribution (PR #188)!nlm download audio now rejects incompatible file extensions (like .mp3 or .wav) instead of writing AAC data with the wrong file extension. A helpful hint now suggests using .m4a or .mp4 and provides an ffmpeg command for conversion.client.post() with a fresh httpx.Client() context in conversation queries to prevent internal connection pool exhaustion or unexpected hangs during repeated querying.Opaque error on capacity throttle (Issue #182) — When NotebookLM returns RPCError code=8 (RESOURCE_EXHAUSTED) with a UserDisplayableError payload, the error message now surfaces the human-readable text instead of the raw protobuf type URL. Added ResourceExhaustedError(RPCError) subclass so callers can catch throttle errors distinctly. Studio artifact creation now provides retry-specific hints. Thanks to @nikolaykazakovvs-ux for the detailed report!
Cinematic video silently ignores --style-prompt (Issue #183) — --style-prompt with cinematic format now maps to custom_instructions (same API field as the web UI's "Customize Video Overview" dialog) instead of being silently dropped. --style still rejects for cinematic since visual style codes don't apply. Validation now runs before source resolution for faster feedback. Thanks to @guia-matthieu for the report!
nlm login CLI and headless authentication flows exited prematurely, resulting in incomplete cookie extraction and subsequent "Authentication expired" errors. Implemented deterministic DOM polling to wait for session tokens (FdrFJe or build label) before extracting cookies.OSID cookies leaked during cross-domain artifact downloads, causing ServiceLogin redirects. The _download_url method now correctly strips service-scoped cookies for external hosts. Thanks to @laofun for this critical fix!is_logged_in() used substring matching on the full URL, so the post-sign-in redirect URL containing original_referer=...accounts.google.com... in the query string was misidentified as a sign-in page. Now parses the URL hostname via urlparse(). Thanks to @SKMKZP for the clear root cause analysis and fix!nlm login — find_any_existing_cdp_browser() would blindly reuse any Chrome with CDP enabled on ports 9222-9231, including headless instances from other tools (e.g. Perplexity MCP). This caused nlm login to silently hang for 5 minutes waiting for sign-in on an invisible browser. Now checks User-Agent for HeadlessChrome and skips automation browsers._fetch_cdp_version() helper to share /json/version logic between get_debugger_url() and find_any_existing_cdp_browser().label MCP tool and nlm label CLI commands. Full action set: auto (AI-generated labels), list, create, rename, set_emoji, move_source, and delete. Multi-label assignment supported — sources can belong to more than one label. Requires 5+ sources for auto-labeling.UnicodeDecodeError in check_firewall_rule() that made nlm login --wsl show a false firewall warning even when the rule already existed. Fixed by adding errors="replace" to the subprocess call. Thanks to @andrepreira for the diagnosis and clean fix!stdio transport strictly requires stdout to be used only for JSON-RPC messages. fastmcp initialization logs (and any other stray print() calls) were corrupting the stdout stream, causing MCP clients to crash with an EOF error on Windows and macOS. Added a dedicated _StdoutToStderrWrapper in server.py that intercepts all standard text output and safely redirects it to stderr, while preserving the underlying binary .buffer for valid JSON-RPC payloads. Thanks to @swiezaczek for the thorough analysis in the issue report!NOTEBOOKLM_COOKIES env var is stale (Issue #170) — refresh_auth now detects when NOTEBOOKLM_COOKIES is set in the environment and returns a clear, actionable error instead of falsely reporting "success" while silently reloading the same stale cookies. Auth failure messages now include a note pointing to the env var when it's the likely cause. Thanks to @nobolso for the thorough root cause analysis!NOTEBOOKLM_CSRF_TOKEN / NOTEBOOKLM_SESSION_ID env vars removed — These were still being read and passed to the client constructor, which caused them to bypass auto-refresh when stale. Both are now always auto-extracted; the deprecated env vars are ignored.AUTHENTICATION.md explaining the NOTEBOOKLM_COOKIES env var override trap, how to diagnose it, and both fix options.typing.TypedDict on Python < 3.12. All 13 service files now import TypedDict from a centralized compat shim (services/_compat.py) that uses typing_extensions on < 3.12. Added typing_extensions as an explicit dependency for Python < 3.12. Thanks to @irvinghu07 for the clear report and suggested fixes!nlm login (Issue #167) — The CDP helper's httpx.Client inherited ALL_PROXY from the environment, causing ImportError: socksio not installed on localhost CDP connections. Fixed with trust_env=False. Thanks to @irvinghu07!notebook_list always returns "Authentication expired" (Issue #169) — save_tokens_to_cache() only wrote to the legacy auth.json, but load_cached_tokens() prioritizes profiles/default/cookies.json. Tokens saved via the MCP save_auth_tokens tool were never read back. Fixed by syncing writes to both auth.json and the active profile. Thanks to @nobolso for the thorough diagnosis and file structure analysis!pathlib.mkdir regression (Issue #169) — Path.mkdir(parents=True, exist_ok=True) raises FileExistsError (WinError 183) on Python 3.14 + Windows. Added safe_mkdir() wrapper applied to all mkdir calls across config.py, auth.py, base.py, and cdp.py. Thanks to @nobolso!--transport http) now defaults to stateless sessions, preventing the MCP SDK double-response crash (AssertionError: Request already responded to) that killed entire sessions on slow Google API calls. Use --no-stateless to opt out. Thanks to @mylaser215 for the thorough root cause analysis (#165)!BooleanOptionalAction — --stateless / --no-stateless and --debug / --no-debug are now proper toggle pairs. Environment variables (NOTEBOOKLM_MCP_STATELESS, NOTEBOOKLM_MCP_DEBUG) accept true/false/0/1/yes/no/on/off (case-insensitive).agents, codex, gemini-cli, and alef-agent with install path details. Fixed missing opencode in setup clients list.codex and gemini-cli agent skills and added Alef Agent specific frontmatter logic. Thanks to the user who reported this issue (#163)!--title when adding a file source (PR #162) — Fixed an issue where adding a file source via CLI ignored the user's custom title. The source upload is now fully awaited before the follow-up rename is fired to guarantee precision. Huge thanks to @CryptoWombat for this excellent contribution and thorough fix!server_info reports local auth state (Issue #160) — Response includes auth_status (configured | stale | not_configured | error) based on cached token presence and age. This is a local disk check only, not a live Google validation; docstring clarifies. Thanks to @josuebustosn for the report and expected behavior.Formatter now use make_console() (safe_box, legacy_windows=False on Windows) instead of bare Console(). Complements the UTF-8 stdio bootstrap from v0.5.25 and avoids UnicodeEncodeError / MCP EOF on legacy Windows code pages when printing API text with arrows, smart quotes, etc. Thanks again to @argonaut-cm for the original EOF / Unicode analysis (v0.5.25 thanked the stdio + Rich bootstrap; this completes CLI-wide coverage).nlm login without manual refresh_auth (Issue #161) — get_client() now invalidates the singleton when on-disk tokens are newer than the running client (extracted_at vs _created_at), not only when the cookie dict changes. Same-profile re-auth updates are picked up automatically. Auth expiry error message mentions refresh_auth as a fallback. Thanks to @josuebustosn for the clear repro and suggested directions.=m140-dv (download variant, fast CDN via drum.usercontent.google.com, ~3 MB/s) and =m140 (streaming transcode via googlevideo.com, ~30 KB/s). The download logic now explicitly prefers the -dv variant for both download_audio and _extract_audio_media_url. Audio downloads that previously failed now complete a 47MB file in ~15 seconds. Thanks to @Victor777777 for the detailed bug report and redirect chain analysis!HTTP_PROXY / HTTPS_PROXY environment variables are set (e.g., Clash, Surge), websocket-client routed localhost CDP connections through the external proxy, crashing nlm login. Fixed by temporarily clearing proxy env vars around websocket.create_connection in execute_cdp_command. The existing httpx fix (#119) only covered HTTP; this completes the WebSocket side. Thanks to @ahmelkholy for identifying the issue and contributing PR #157!→ (U+2192) returned by NotebookLM, killing the MCP server process and causing client EOF disconnects. Fixed by reconfiguring stdout/stderr to UTF-8 with replacement at process startup (both CLI and MCP entry points) and setting legacy_windows=False on Rich Console instances. Thanks to @argonaut-cm for the clear traceback and proposed solutions!NotebookLMClient in package __init__ — from notebooklm_tools import NotebookLMClient now uses __getattr__ to defer the heavy import until first access, keeping the stdio encoding bootstrap lightweight.slides revise fails due to an invalid artifact ID or a rejected revision request, the error now surfaces the specific Google API error code (e.g., INVALID_ARGUMENT) along with a clear hint guiding the user to verify their artifact ID. Previously, these failures produced opaque, unhelpful error messages. Thank you @sickn33 for this fix!--remote-debugging-address=0.0.0.0, restricting the DevTools Protocol to 127.0.0.1 only. This completely broke nlm login --wsl for all WSL2 users. The fix switches to a port proxy approach: Chrome launches on port 9223 (localhost) and WSL connects via port 9222 through a netsh interface portproxy rule. Temp Chrome profiles are now created on the Windows filesystem (%TEMP%) instead of WSL's /tmp to prevent "Profile error occurred" crashes. Updated docs/WSL_SETUP.md with one-time setup instructions. Thank you @casjogreen for this critical fix!os.execvp fails on Windows. Replaced with subprocess.run to prevent immediate crashes on Windows 11 during server startup via the .mcpb bundle. Added explicit stdin=sys.stdin, stdout=sys.stdout, and stderr=sys.stderr to ensure the JSON-RPC stdio channel between Claude Desktop and the server remains properly connected across platforms. Thanks to @m3saros for diagnosing the root cause and providing the exact fix!_normalize_studio_status routine. The JsonFormatter has been updated to dynamically populate returned JSON payloads with newly available artifact fields (such as audio_url, video_url, slide_deck_url, flashcard_count) without breaking the shape expected by downstream consumers. Huge thanks to @sickn33 for this amazing contribution!poll_studio_status bypassing auth recovery — The core poll_studio_status helper function was making raw HTTP calls and dodging the standard _call_rpc pipeline. This caused it to immediately fail on 400 Bad Request exceptions whenever the user's build_label or session tokens went stale. The polling function now correctly wraps its logic in _call_rpc, securing free auth recovery loops, retries, and unified debug capability during studio polling.400 Bad Request instead of 401 or 403 when the internal CSRF token expires. Added 400 to the retryable auth status codes, which fixes auth recovery failures and prevents tracebacks (PR #148).nlm login --wsl securely launches and channels DevTools via 0.0.0.0 to safely bridge the WSL network boundary.nlm login --check command was initiating a client without passing the configured build label, unnecessarily triggering a three-month backward fallback.nlm create infographic crash when --style not specified (Issue #142) — The verb-first route was missing the --style option entirely, causing a TypeError on invocation. Fixed alongside 12 other missing parameters across verb-first wrappers.nlm create, nlm add, nlm describe, nlm query, nlm delete) was missing 13 parameters that existed on the noun-first route: --focus on nlm create quiz and nlm create flashcards; --wait and --wait-timeout on nlm add url, nlm add text, and nlm add drive; --auto-import on nlm research start; --format on nlm download slides; --json on nlm describe notebook, nlm query notebook, and nlm source content; and --confirm on nlm delete alias.fastmcp dependency to >=2.0.0,<4.0 (Issue #141) — The previous upper bound (<3.0) caused a startup crash when fakeredis 2.35.0 was installed alongside fastmcp 2.x. Widening to <4.0 resolves the incompatibility and allows users to use newer FastMCP releases without version conflicts.tests/cli/test_verbs_parity.py) — Automatically compares every verb-first wrapper in cli/commands/verbs.py against its target function to detect missing parameters. CI will now fail if a verb wrapper drifts out of sync with its noun-first counterpart.--auto-import / --wait-and-import flags to nlm research start to automatically wait for research to finish and immediately import.API error (code 5): unknown. This now returns a clean validation error indicating the notebook is empty and needs sources added.NOT_FOUND, PERMISSION_DENIED, etc.) instead of logging as unknown.nlm login --wsl flag launches Windows Chrome from WSL2 for seamless authentication. Includes automatic firewall rule management, cross-boundary CDP communication, and a cleanup mechanism for temporary Chrome profiles. Full setup guide at docs/WSL_SETUP.md. Thanks to @kylebrodeur for the comprehensive implementation!nlm doctor now detects WSL2 environments and reports Chrome availability, Windows interop status, and firewall configuration.threading.Lock to BaseClient protecting mutable state (_reqid_counter, _conversation_cache, _source_rpc_version) from race conditions during parallel MCP tool invocations. Uses double-checked locking for singleton client initialization. Includes 7 new concurrent access tests. Thanks to @xiangyuwang1998 for the implementation!execute_cdp_command() that was inadvertently removed during the WSL2 merge. Prevents infinite hangs on stale/dropped WebSocket connections.chmod 0o600 on the port map file that was inadvertently removed during the WSL2 merge. Ensures the port map is only readable by the owner.--remote-allow-origins restricted to localhost and 127.0.0.1 only, preventing malicious webpages from connecting to the CDP debug port. Previously allowed all origins (*). Thanks to @wccheung11011001 for the security audit!NOTEBOOKLM_BASE_URL environment variable is now validated against an allowlist of known Google domains (HTTPS only), preventing cookie exfiltration via environment injection.validate_output_path() to block downloads from writing to sensitive directories (.ssh, .gnupg, .aws, .kube, .claude, .config) or overwriting sensitive files (authorized_keys, id_rsa, .bashrc, etc.).0o600 for files, 0o700 for the storage directory). Thanks to @wccheung11011001!--style custom --style-prompt "your description" (CLI) or video_style_prompt (MCP). The style prompt is also returned in studio status responses. Thanks to @agarwalvipin for the implementation and live API verification!nlm source add --file now correctly handles audio uploads (m4a, wav, mp3). Audio sources use type code 10, which was previously unrecognized. The --wait flag now handles audio's transient status 3 state (which is not a hard failure for audio, unlike other source types) and a new --wait-timeout flag (default 600s) gives long recordings enough time to finish transcribing. Thanks to @stanleykao72 for the thorough investigation and fix!build_label Data Loss (PR #133) — Profile.to_dict() was silently dropping the build_label field, causing it to be lost across restarts and re-fetched from scratch. Now properly persisted. Thanks to @wccheung11011001!B904 exception chaining in CDP timeout handler, and resolved format violations in sources.py, config.py, test_url_source_fallback.py, and test_studio.py.source_add (URL type) failing with RPC error code 3 (INVALID_ARGUMENT) for some users due to Google migrating the add_source endpoint. Implemented a dual-RPC fallback: the system tries the legacy izAoDd endpoint first, and if it returns code 3, automatically retries with the new ozz5Z endpoint. The working endpoint is cached per session to avoid extra round-trips on subsequent calls. Both single and bulk URL additions (url and urls parameters) benefit from the fallback. Thanks to @Neophen for reporting!notebook_query_start and notebook_query_status MCP tools for querying large notebooks (50+ sources) without hitting MCP client timeouts. notebook_query_start fires the query in a background thread and returns immediately with a query_id. Poll notebook_query_status with the query_id to get the result when ready. Includes automatic TTL cleanup (10 min) for stale entries. The existing notebook_query tool remains unchanged for backward compatibility.nlm research start "query" --title "My Title" (and the corresponding MCP tool parameter title) failed because the internal logic did not automatically trigger new notebook creation when title was provided without a notebook_id.nlm skill (Issue #122) — Fixed a crash when running nlm skill install on Python 3.13, which was caused by using @click.option(type=Literal["user", "project"]). Replaced with standard string validation. Thanks to @zhaoguoqiao for reporting!httpx HTTP2 client was honoring system proxy settings even for the internal 127.0.0.1 CDP WebSocket acquisition call (http://127.0.0.1:9222/json). This caused connections to fail on machines running proxies. Restored the proxy=None argument to explicitly bypass proxies for local loopback connections. Thanks to @sjs33 for discovering and reporting this!research_status Polling Loop (PR #120) — Restored the internal polling loop for research_status when max_wait is set. Previously, the parameters were ignored after a refactor, and it always returned after a single check. The tool now correctly blocks and polls until the research is completed or times out. Thanks to @byingyang for the excellent bug report, full implementation, and test suite!_try_reload_or_headless_auth() gated Layer 2 recovery on auth.json existence. If the legacy file didn't exist, valid profile-based credentials in profiles/default/cookies.json were completely skipped, falling straight through to headless auth (Layer 3). Now always calls load_cached_tokens() which checks the profile directory first, then falls back to auth.json. Thanks to @olaservo for the incredibly detailed report!--cdp-url ignored by builtin provider (Issue #117, Bug 2) — nlm login --cdp-url http://127.0.0.1:9222 was ignored when using the default builtin provider, always launching a new Chrome instead. Now, when --cdp-url is explicitly provided (even with the builtin provider), the CLI auto-routes to the existing-CDP extraction path, matching the user's intent to connect to an already-running browser. Thanks again to @olaservo!SIM105 in auth.py, I001 in base.py, F401 in test_coerce_list.py) that were blocking the CI pipeline for PR #118.auth.json file permissions (PR #116) — Auth token cache files are now written with chmod 600 (owner read/write only), preventing other local users or processes from reading active session cookies. Thanks to @tody-agent for the security audit!NOTEBOOKLM_BASE_URL environment variable. Set to https://notebooklm.cloud.google.com (or your organization's URL) to use NotebookLM with managed Workspace accounts. All API calls, authentication, file uploads, and URL detection are updated to use the configured base URL. Default remains https://notebooklm.google.com for personal accounts (fully backward compatible). Thanks to @Robiton for this contribution!docs/AUTHENTICATION.mdNOTEBOOKLM_BASE_URL to environment variables table in docs/MCP_GUIDE.mdfocus_prompt ignored (Issue #113) — The focus_prompt parameter for quiz and flashcard generation was silently ignored due to an off-by-one error in the RPC payload structure. The backend expects focus_prompt at array index [2] (after a reserved null slot), but it was being placed at index [1]. Both create_quiz and create_flashcards in core/studio.py now use the correct payload layout. Thanks to @ojsed for the detailed analysis!source_ids parameter fails with string input (Issue #111) — MCP clients (Claude Desktop, Cursor, etc.) frequently serialize list parameters as JSON strings ('["a","b"]') or comma-separated strings ('a,b') instead of native Python lists, causing Pydantic validation errors. Added a coerce_list() helper in mcp/tools/_utils.py that normalizes all input forms (JSON strings, comma-separated, single values, native lists) into proper list[T]. Applied to all 6 list parameters across 4 MCP tool files: studio_create, notebook_query, source_add, source_sync_drive, source_delete, and research_import. Thanks to @Carlos-OL for reporting!ensure_ascii=False missing from json.dumps() calls across core and CLI layers, causing Unicode characters to be escaped as \uXXXX in RPC request bodies and file persistence. Thanks to @rujinlong for the fix!coerce_list helper (total: 660 tests)ImportError in the CLI (ArtifactNotReadyError) caused by missed codebase updates during the v0.5.8 structural refactor.ruff linting and formatting standards.ruff linting and formatting refactor (PR #110) AND the GitHub Actions CI Pipeline (PR #109)! These are huge improvements to the codebase quality.ruff format --check, ruff check) and the full pytest suite (uv run pytest) on pull requests and pushes to main. Linting errors now block the build, guaranteeing code quality before merge. Thanks to @nikosavola for this contribution!nlm login --manual --file saved cookies correctly, but subsequent requests to notebooklm.google.com were rejected by Google (302 → login page) because the page-fetch headers included macOS-specific Client Hints (sec-ch-ua-platform: "macOS", sec-ch-ua, sec-ch-ua-mobile). When cookies were captured from a Windows Chrome session, the OS fingerprint mismatch caused Google to reject the session. Removed all three sec-ch-ua* headers (they're optional per spec) and switched to a platform-neutral Linux Chrome UA — making auth platform-agnostic. Also added a multi-pattern CSRF token fallback (SNlM0e → at= → FdrFJe) in _refresh_auth_tokens, and a make_console(safe_box=True) factory to prevent UnicodeEncodeError crashes on Windows cp1251/cp1252 codepage terminals. Thanks to @pakulyaev for the detailed diagnosis and debug output! (5 new regression tests added)nlm login (Issue #108) — On Windows, Chrome's DevTools debugger binds to 127.0.0.1 (IPv4), but websocket-client resolves localhost to ::1 (IPv6), causing PermissionError: [WinError 10013]. Added a _normalize_ws_url() helper that explicitly rewrites ws://localhost: to ws://127.0.0.1: at all 4 WebSocket connection sites in cdp.py. Thanks to @theteleporter for the spot-on diagnosis!download_artifact failing for report/mind_map/data_table (Issue #107) — The MCP download_artifact tool exclusively routed through download_async(), but _dispatch_async() had no handlers for report, mind_map, or data_table (only _dispatch_sync() did). Added these three non-streaming types to the async dispatcher so all artifact types are downloadable via the MCP tool. Thanks to @Neophen for the detailed bug report!poll_research returning None for deep research in multi-task notebooks (Issue #106) — When deep research mutates the task ID internally and the notebook has multiple research tasks, poll_research returned None instead of a valid task. The fallback now prefers any in_progress task, then falls back to the most recent task. Thanks to @Neophen for reporting!nlm delete source TypeError (Issue #104) — nlm delete source <id> --confirm was crashing with TypeError: delete_source() got an unexpected keyword argument 'source_id'. The verb-style CLI layer in verbs.py was passing source_id=source (singular string) but the underlying function expects source_ids (a list). Fixed the parameter name and wrapped the value in a list. The noun-style nlm source delete was unaffected. Thanks to @Le-Yann for the detailed bug report and root cause analysis!handle_error() across all 13 command modules. Errors with --json flag now output structured JSON ({"status": "error", "error": "...", "hint": "..."}).hint fields in error responses for AI agent consumption.ServiceError now carries an optional hint attribute, propagated from NLMError exceptions.NotebookLMError (parent of RPCError, ArtifactError, etc.) now inherits from NLMError, ensuring all low-level API errors are caught and handled gracefully instead of producing raw tracebacks.--json flag) now preserves Unicode characters (e.g., café, こんにちは) instead of escaping them as \uXXXX sequences via a shared print_json() helper with ensure_ascii=False. Thanks to @nickyfoto for the contribution. (PR #100)--mode deep) no longer silently fails with a generic "no confirmation from API" message when Google returns a transient error. The structured error payload (e.g., DeepResearchErrorDetail code 3) is now properly detected and surfaced with an actionable message: "Google API error code 3 (DeepResearchErrorDetail). This is likely a transient issue. Try again in a few minutes, or use --mode fast."start_research() to use the standard _call_rpc() pipeline instead of raw HTTP calls. This gives deep and fast research automatic auth retry, server error retries, and enhanced debug logging for free.RPCError exception class — New structured error type in core/errors.py for Google batchexecute errors with error code, detail type, and detail data attributes. All non-auth RPC errors (not just code 16) are now properly raised.research import now uses a 300-second default timeout (up from 120s), fixing consistent timeouts on notebooks with many sources. The timeout is configurable via --timeout / -t in CLI and timeout parameter in MCP.research start no longer hard-exits when previous research has un-imported sources. Instead, it shows a warning and prompts interactively, so users can choose to proceed or import first. Previously, if import timed out, users were stuck — unable to import or start new research without --force.nlm research import <notebook> <task-id> --timeout 600 for extra-large notebooks. Available in both CLI (--timeout / -t) and MCP (timeout parameter on research_import). Default: 300 seconds.cc-claw as a supported tool for nlm skill install cc-claw (~/.cc-claw/workspace/skills/nlm-skill/).nlm add url <notebook_id> "https://..." on Windows PowerShell incorrectly parsed the URL string into a list of characters, attempting to add dozens of duplicate sources instead of one.nlm notebook query) and the MCP server now perfectly persist their chat history directly into the NotebookLM web UI. All prompts sent via CLI or MCP agents will now appear in the notebook's native chat panel, sharing the same conversational context as the web UI. (Closes #92)nlm setup command (nlm setup add opencode) to automatically configure the NotebookLM MCP server for OpenCode. Includes smart config array injection and parsing. Thanks to @woohyun212 for the comprehensive implementation and thorough unit tests (PR #95, closes #95).nlm login switch <profile>. The server now automatically detects token file changes and gracefully reloads the NotebookLM client in real-time, matching the active profile perfectly.cc-claw as a supported tool for nlm skill install cc-claw (~/.cc-claw/workspace/skills/nlm-skill/)..gemini/skills/ → .agents/skills/ — Starting with Gemini CLI v0.33.1, .agents/skills/ is the recommended cross-tool compatible path (higher priority than .gemini/skills/). nlm skill install gemini-cli and nlm skill install codex are replaced by nlm skill install agents, which installs to ~/.agents/skills/nlm-skill/. This path works for Gemini CLI, Codex, and any tool that reads .agents/skills/. Users with existing gemini-cli or codex installations should run nlm skill install agents to reinstall at the new location.nlm batch query — Query multiple notebooks with the same questionnlm batch add-source — Add a URL to multiple notebooksnlm batch create — Create multiple notebooks at oncenlm batch delete — Delete multiple notebooks (requires --confirm)nlm batch studio — Generate artifacts across multiple notebooksbatch tool with action parameter (query|add_source|create|delete|studio)nlm cross query "question" --notebooks "id1,id2" — Ask across specific notebooksnlm cross query "question" --tags "ai,research" — Query by tagcross_notebook_query toolnlm pipeline list — List available pipelines (3 builtin: ingest-and-podcast, research-and-report, multi-format)nlm pipeline run <notebook> <pipeline-name> — Execute a pipeline~/.notebooklm-mcp-cli/pipelines/pipeline tool with action parameter (run|list)nlm tag add <notebook> --tags "ai,research" — Add tagsnlm tag remove <notebook> --tags "ai" — Remove tagsnlm tag list — List all tagged notebooksnlm tag select "query" — Find notebooks by tag matchtag tool with action parameter (add|remove|list|select)studio_status(action="list_types")note, source_add).batch.py, cross.py, pipeline.py) importing get_client from mcp/tools/_utils instead of cli/utils, violating architecture layeringencoding='utf-8' to 6 file I/O calls in smart_select.py and pipeline.py to prevent UnicodeDecodeError on Windowssources_used field handling in cross.py display formatting (could crash on string values)nlm doctor crash on Windows (Issue #87) — Fixed UnicodeDecodeError when running claude mcp list on Windows systems with non-UTF-8 default encodings (e.g., cp936, cp1252). Added explicit encoding="utf-8" and errors="replace" to the subprocess call. Also added a null check for result.stdout to prevent AttributeError when the subprocess returns no output.launch_chrome_process was silently swallowing all subprocess.Popen exceptions, causing nlm login to report the cryptic "Cannot connect to browser on port XXXX" with no indication of what went wrong. Now logs the browser path, port, and exception details so users get actionable error messages. Added debug logging for successful launches as well.notebook_query dropping cited text when Google returns "direct" citation segments (integer-first elements) alongside the standard "wrapped" format. Both segment variants are now correctly handled. Thanks to @meirtsvi for this contribution!cited_table field with num_columns and rows data. Table segments are indicated by a <cited_table> placeholder in the cited_text field.studio_create for mind maps was returning metadata (root_name, children_count) but dropping the actual mind_map_json. The full JSON now flows through to MCP clients. Thanks to @cowhi for reporting!_extract_text_from_table_rows, _extract_table_from_detail, and cited_table in _extract_citation_data. Total tests: 503.nlm setup add claude-desktop removed — Claude Desktop users should install via the .mcpb extension (download from Releases, double-click to install). The CLI-based config file editing was unreliable compared to the extension approach. nlm setup add claude-code (for the Claude Code CLI) is unchanged..m4a, .wav, .aac, .ogg, .opus (previously only .mp3).gif, .webp (previously only .jpg, .jpeg, .png).flac, .webm, .mov, .avi, .mkv were removed from the original PR as Google's upload server does not accept themnotebook_query responses now include a references array with the actual quoted passage text for each citation. Previously, only the source ID and citation number were returned; the passage text was already in the API response but was being discarded. Thanks to @cbruyndoncx for this contribution!source_id, citation_number, and cited_textsources_used and citations fields unchangednlm setup add all — Interactive multi-tool setup — Scans the system for installed AI tools, shows detection status, and lets you interactively choose which ones to configure with NotebookLM MCPall, specific numbers, or nonenlm setup remove all — Remove NotebookLM MCP from all configured tools at once, with explicit confirmation and safety warnings. Uses CLI-first removal (e.g., claude mcp remove) where available.nlm skill install codex now installs to ~/.agents/skills/nlm-skill/SKILL.md per official Codex docs, replacing the old ~/.codex/AGENTS.md path. Users with the old installation should run nlm skill install codex to reinstall at the correct location.video_format="cinematic", format code 3). This format produces higher-fidelity video overviews and is available to NotebookLM Plus/Ultra subscribers. Thanks to @ovai-felix for the detailed reverse-engineering and verified payload structure (Issue #79).visual_style_code), while Explainer/Brief continue to use 6 elementsstudio_create with video_format="cinematic" nlm video create <notebook> --format cinematic"NotebookLM rejected video creation. Try again later or create from NotebookLM UI for diagnosis." — this is expected behavior, not a bug..mcpb extension disconnects (Issue #78) — The .mcpb bundle was incomplete (only contained manifest.json with no entrypoint) and relied on uvx being in PATH, which Claude Desktop's restricted macOS environment doesn't expose. Fixed by bundling a cross-platform Python launcher (run_server.py) that defensively resolves uvx across common install locations (~/.local/bin, ~/.cargo/bin, /opt/homebrew/bin, etc.) and using ${__dirname} for reliable path resolution. Thanks to @abanoub-ashraf for the detailed diagnosis and reproduction steps.notebook_share_batch) — Invite multiple collaborators to a notebook in a single API call (Issue #73). Supports mixed roles (viewer/editor) per recipient.add_collaborators_bulk(notebook_id, recipients) on SharingMixininvite_collaborators_bulk(client, notebook_id, recipients) with upfront validationnotebook_share_batch tool with recipients list and confirm flagnlm share batch <notebook> "a@gmail.com,b@gmail.com" --role viewer__version__ string in __init__.py to correctly report version (was omitted in 0.3.20 release).threading.Lock) for thread-safe client initialization in MCP tools.logger.debug().str = None -> str | None = None) across the codebase.ValidationError throwing to ensure strict type checking completeness.-j flag (note list, share status, export artifact, config show) were producing invalid JSON due to Rich console wrapping long strings at terminal width. JSON output now bypasses Rich and goes directly to stdout. Thanks to @pjeby for reporting.auto_select, sketch_note, professional, bento_grid, editorial, instructional, bricks, clay, anime, kawaii, scientific. Available via MCP (infographic_style parameter on studio_create), CLI (--style flag on nlm infographic create), and Python API (visual_style_code on create_infographic()). Default is auto_select for backward compatibility.nlm login — nlm login now detects and launches any Chromium-based browser, not just Google Chrome. Supported browsers (in priority order): Google Chrome, Arc (macOS), Brave, Microsoft Edge, Chromium, Vivaldi, Opera. Checks both system and user-local install paths. Error messages now dynamically list supported browsers per platform. Thanks to @devnull03 for this contribution (PR #70).nlm login uses via nlm config set auth.browser <name>. Valid values: auto (default, first found wins), chrome, arc, brave, edge, chromium, vivaldi, opera. Falls back to auto-detection if the preferred browser is not installed. Also settable via NLM_BROWSER env var.nlm research status <nb> --task-id <id> returned "no research found" for deep research because the backend assigns a new task_id internally. Now falls back to returning the only active task when the original task_id doesn't match. Thanks to @danielbrodie for reporting.nlm login could reuse a Chrome instance from a different NLM profile. Implemented port-to-profile mapping to guarantee strict cross-profile isolation.AccountMismatchError, automatically clears the stale Chrome user-data-dir, and relaunches Chrome for a fresh Google sign-in.nlm login profile delete validation: Profile deletion was failing for broken/invalid profiles because it strictly checked for valid cookies. Now it checks if the profile directory exists, allowing deletion of empty/corrupt profiles.nlm setup add json — Interactive JSON config generator — Run nlm setup add json to generate an MCP JSON config snippet for any tool not directly supported. Interactive wizard with numbered prompts lets you choose uvx vs regular mode, full path vs command name, and whether to include the mcpServers wrapper. Prints syntax-highlighted JSON and offers clipboard copy on macOS.type= for source_add, studio_create, and download_artifact, but the actual tool schemas use source_type and artifact_type. AI clients reading the instructions would use wrong parameter names, causing validation errors. Also added value parameter hints for source_add.add_url_sources(notebook_id, urls) on SourceMixinadd_sources(client, notebook_id, sources) — batches URL sources automatically, falls back to individual calls for other typessource_add now accepts optional urls list parameter for bulk URL addnlm source add <notebook> --url https://a.com --url https://b.com (repeatable --url flag)delete_sources(source_ids) on SourceMixindelete_sources(client, source_ids) with validationsource_delete now accepts optional source_ids list parameter for bulk deletenlm source delete <id1> <id2> <id3> --confirm (variadic arguments)add_url_source, add_drive_source, add_text_source, and multiple other methods in core/sources.py to use the unified _call_rpc mechanism instead of raw client.post requests. This ensures that adding sources now properly benefits from the automatic session/CSRF token refresh if authentication unexpectedly expires (Issue #62).list_notebooks and delete_notebook in core/notebooks.py to use _call_rpc, ensuring they recover from expired CSRF tokens just like other core operations. Thanks to @byingyang for identifying this in PR #61.workplace -> workspace) in code and documentation. Thanks to @maxcanada for reporting (Issue #63).create slides default format - Fixed a bug where create slides would error because it used an invalid format fallback. It now correctly defaults to detailed_deck. Added comprehensive tests for all verb defaults. (PR #64)bl) - The bl URL parameter is now automatically extracted from the NotebookLM page during nlm login and CSRF token refresh, instead of using a hardcoded value that goes stale every few weeks. This keeps API requests current with Google's latest build without any manual steps. The NOTEBOOKLM_BL env var still works as an override. The save_auth_tokens MCP tool also extracts bl from the request_url parameter when provided.sources_used now populated in query responses - The sources_used field was always returning [] even when the AI's answer contained citation markers like [1], [2]. Google's response includes citation-to-source mapping data that was present but never parsed. Query responses now correctly return sources_used (list of cited source IDs) and citations (dict mapping each citation number to its parent source ID). This also enables the REPL's citation legend feature. Thanks to @MinhDung2209 for reporting (issue #57).source_rename) — Rename any source within a notebook via new RPC b7Wfje.source_rename with notebook_id, source_id, and new_title paramsnlm source rename <source-id> <title> --notebook <notebook-id>nlm rename source <source-id> <title> --notebook <notebook-id>--clear flag for nlm login - Added a --clear flag that wipes the cached Chrome profile before logging in. This solves an issue where nlm login would auto-login to an old, cached account without letting the user switch profiles or emails.extract_email where the CLI would sometimes grab a shared note author's email off the dashboard instead of the logged-in user. The regex now prioritizes actual internal Google account fields before falling back to generic matching.--clear would cause the CLI to mistakenly run a migration step from older CLI versions, reinstating the wrong account profile.--debug Flag - nlm --debug <command> enables debug logging across all CLI commands, showing raw API responses and internal state. Useful for diagnosing API issues.INVALID_ARGUMENT, UserDisplayableError) instead of an answer, the CLI now surfaces a clear error message instead of returning an empty answer. Previously, queries would succeed with {'answer': ''} and no indication of what went wrong. Thanks to @MinhDung2209 for the detailed debugging that uncovered this (issue #57).NOTEBOOKLM_HL) - Set NOTEBOOKLM_HL env var to control both the API's hl URL parameter and the default artifact creation language. Explicit --language flags still take priority. Thanks to @beausea for this contribution (PR #59, closes #58).nlm notebook query and nlm query notebook now accept --timeout / -t to set query timeout in seconds (default: 120). Useful for long extraction prompts that need more processing time (closes #57).studio_revise) — Revise individual slides in an existing slide deck via new RPC KmcKPe. Creates a new artifact with revisions applied; original is never modified.studio_revise with artifact_id, slide_instructions, and confirm paramsnlm slides revise <artifact-id> --slide '1 Make the title larger' --confirmnlm download slide-deck <notebook> --format pptxdownload_artifact with slide_deck_format="pptx"--force to override.nlm login now warns when connecting to an existing Chrome instance instead of launching a fresh one.nlm login time from ~25s to under 3s. Thanks to @pjeby for this contribution (PR #54).nlm login hang on fresh install - Optimized Chrome port availability scanning (using socket.bind instead of httpx.get) to avoid 20+ second timeouts on systems that drop network packets. Thanks to @pjeby for the diagnosis (closes #52)nlm login and headless authentication now perform a graceful shutdown of Chrome via CDP (Browser.close) rather than abruptly killing the process, resolving crashes on next browser start. Again, great work by @pjeby (fixes #52)~/.openclaw/workspace/skills/ instead of ~/.openclaw/skills/.--focus parameter to nlm quiz create and nlm flashcards create commands to specify custom instructions.studio_status now correctly extracts custom prompts for all artifact types (Audio, Video, Slides, Quiz, Flashcards).nlm skill install support for:~/.cline/skills) - Terminal-based AI agent~/.gemini/antigravity/skills) - Advanced agentic framework~/.openclaw/workspace/skills) - Autonomous AI agent~/.codex/AGENTS.md) - Now with version trackingnlm setup support — Added automatic MCP configuration for:nlm setup add cline)nlm setup add antigravity)nlm skill update command - Update installed AI skills to the latest version. Supports updating all skills or specific tools (e.g., nlm skill update claude-code).nlm update skill works identically to nlm skill update.AGENTS.md formats now support version tracking via injected comments.nlm skill list now correctly identifies outdated skills and prevents "unknown" version status for Codex.0.3.1 to match release tag.services/) — 10 domain modules centralizing all business logic previously duplicated across CLI and MCP:errors.py: Custom error hierarchy (ServiceError, ValidationError, NotFoundError, CreationError, ExportError)chat.py: Chat configuration and notebook query logicdownloads.py: Artifact downloading with type/format resolutionexports.py: Google Docs/Sheets exportnotebooks.py: Notebook CRUD, describe, query consolidationnotes.py: Note CRUD operationsresearch.py: Research start, polling, and source importsharing.py: Public link, invite, and status managementsources.py: Source add/list/sync/delete with type validationstudio.py: Unified artifact creation (all 9 types), status, rename, deletecli/ and mcp/ are now thin wrappers delegating to services/. Neither imports from core/ directly.core/client.py → services/*.py → mcp/tools/*.py + cli/commands/*.py → tests/services/nlm login --provider openclaw --cdp-url <url> allows authentication via an already-running Chrome CDP endpoint (e.g., OpenClaw-managed browser sessions) instead of launching a separate Chrome instance. Thanks to @kmfb for this contribution (PR #47)nlm setup and nlm doctor — Added Setup and Doctor command reference sections, updated workflow example, and added tips. Cherry-picked from PR #48 by @997unixnlm infographic create now exits non-zero with a clear error when the backend returns UserDisplayableError and no artifact, instead of silently reporting success (closes #46). Thanks to @kmfb (PR #47)4 now maps to "failed" instead of "unknown", making artifact failures visible during polling. By @kmfb (PR #47)suppress_origin=True for compatibility with managed Chrome endpoints, with fallback for older websocket-client versionsnlm setup list and nlm doctor now detect NotebookLM when installed as a Claude Desktop Extension (.mcpb), showing version and enabled state.nlm setup add <TAB> crashing with TypeError due to incorrect completion callback signature.--json flag for more commands — Added structured JSON output to notebook describe, notebook query, source describe, and source content. JSON output is also auto-detected when piping. Thanks to @sebsnyk for the request in #43.--json flags..mcpb file from the release page, double-click to install. No manual config editing required.scripts/build_mcpb.py reads version from pyproject.toml, syncs manifest.json, and packages the .mcpb file. Old builds are auto-cleaned..mcpb file is automatically built and attached to GitHub Releases alongside PyPI publish.nlm doctor and nlm setup documentation — Added to AI docs (nlm --ai) and skill file.uvx — Claude Desktop extension now uses uvx --from notebooklm-mcp-cli notebooklm-mcp for universal PATH compatibility.PROJECT_RECAP.md and todo.md (outdated development artifacts).nlm setup command - Automatically configure NotebookLM MCP for AI tools (Claude Code, Claude Desktop, Gemini CLI, Cursor, Windsurf). No more manual JSON editing! Thanks to @997unix for this contribution (PR #39)nlm setup list - Show configuration status for all supported clientsnlm setup add <client> - Add MCP server config to a clientnlm setup remove <client> - Remove MCP server confignlm doctor command - Diagnose installation and configuration issues in one command. Checks authentication, Chrome profiles, and AI tool configurations. Also by @997unix (PR #39)typer.Exit exceptions bypassed the check. Moved print_update_notification() to a finally block so it always runs.import os placement for Windows compatibilityshow_banner=False to mcp.run() to prevent FastMCP banner from corrupting stdio JSON-RPC protocol on Windows (fixes #35)print() with logging in auth.py and notebooks.py to avoid corrupting JSON-RPC outputnlm login --check to use config's default_profile instead of hardcoded "default"nlm chat start failing with TypeError: BaseClient.__init__() got an unexpected keyword argument 'profile'. Now uses proper get_client(profile) utility and handles dict/list API responses correctly. Thanks to @eng-M-A-AbelLatif for the detailed bug report and fix in issue #25!src/notebooklm_mcp/ directory. This legacy code was not packaged or distributed but caused confusion (e.g., PR #29 targeted it thinking it was active). The active MCP server is notebooklm_tools.mcp.server. Thanks to @NOirBRight for PR #29 which helped identify this dead code.notebooklm_mcp package from test suite.This release also acknowledges past community contributions that weren't properly thanked: - @latuannetnam for HTTP transport support, debug logging, and query timeout configuration (PR #12) - @davidszp for Linux Chrome detection fix (PR #6) and source_get_content tool (PR #1) - @saitrogen for the research polling query fallback fix (PR #15)
~/.notebooklm-mcp/ to ~/.notebooklm-mcp-cli/ on first use. Users upgrading from older versions don't need to re-authenticate.~/.notebooklm-mcp-cli/. Previously some code still referenced the old ~/.notebooklm-mcp/ location, causing confusion. Now everything uses the single unified location.notebooklm-mcp-auth standalone command - The standalone authentication tool has been officially deprecated and removed. Use nlm login instead, which provides all the same functionality with additional features like named profiles. The headless auth for automatic token refresh continues to work behind the scenes.notebooklm-mcp-auth stored tokens in a different location than nlm login, causing "Authentication expired" errors. Now there's only one auth path via nlm login.nlm download slides → nlm download slide-deck in CLI guide.nlm login not launching Chrome - Running nlm login without arguments now properly launches Chrome for authentication instead of showing help. Workaround for v0.2.10: use nlm login -p default.nlm download-verb and nlm research-verb (use nlm download and nlm research instead)nlm export to-docs / nlm export to-sheets (not docs/sheets)-o flag for output pathdetailed_deck / presenter_slides (not detailed/presenter)nlm mindmap list from documentationsource_type_name key correctlynote_create, note_list, note_update, note_delete) into a single note(action=...) toolnlm skill tool argumentSKILL.md, command_reference.md, troubleshooting.md, and workflows.md with latest featuresnlm skill install other automatically switching to project levelresearch_status handling of None tasks in responsenlm skill commands)~/.config) and project-level installationnlm skill listnlm skill install othernlm-skill folder naming across all installationsnlm --ai)nlm login profile rename command for renaming authentication profilesnlm login profile listnlm config set auth.default_profile <name> to avoid typing --profile for every commandconsole.print using invalid err=True parameter (now uses err_console = Console(stderr=True))source_ids=None (now defaults to all notebook sources)nlm auth status/list/delete with nlm login --check and nlm login profile list/delete/rename--source-ids parameter (defaults to all sources in notebook)get_alias_manager().resolve())--confirm flag to nlm alias delete commandThis release unifies the previously separate notebooklm-cli and notebooklm-mcp-server packages into a single notebooklm-mcp-cli package. One install now provides both the nlm CLI and notebooklm-mcp server.
notebooklm-mcp-cli package replaces separate CLI and MCP packagesnlm (CLI), notebooklm-mcp (MCP server), notebooklm-mcp-auth (auth tool)--wait parameter to block until source is readynlm export sheets)nlm export docs)nlm note create/list/update/deletenote_create, note_list, note_update, note_deletenlm share status)nlm share public/private)nlm share invite)nlm login --profile <name>)nlm login profile list/delete/renamenlm notebook list, nlm source add, nlm studio createnlm list notebooks, nlm add url, nlm create audionlm skill install <tool> adds NotebookLM expertise to AI assistantsnotebooklm-mcp --transport http --port 8000)notebooklm-mcp --debug)~/.notebooklm-mcp-cli/notebook_add_url/text/drive → unified source_add)poll_research now accepts status code 6 (Imported) as success, fixing "hanging" Fast Research.target_task_id filtering to poll_research to ensure the correct research task is returned (essential for Deep Research).research_status and research_import to use task ID filtering.research_status tool now accepts an optional task_id parameter.SOURCE_TYPE_UPLOADED_FILE, SOURCE_TYPE_IMAGE, and SOURCE_TYPE_WORD_DOC that were omitted in v0.1.13.SOURCE_TYPE_UPLOADED_FILE (11): Direct file uploads (e.g., .docx uploaded directly)SOURCE_TYPE_IMAGE (13): Image files (GIF, JPEG, PNG)SOURCE_TYPE_WORD_DOC (14): Word documents via Google DriveSOURCE_TYPES CodeMapper with uploaded_file, image, and word_doc mappingsDRIVE_SOURCE_TIMEOUT to SOURCE_ADD_TIMEOUT (120s)add_url_source and add_text_sourcenotebooklm-mcp-auth completes, releasing the profile lock and enabling headless auth for automatic token refreshnotebooklm-mcp-authThese fixes resolve "Authentication expired" errors that occurred even after users re-authenticated.
status: "timeout" instead of error when timeout occurs, indicating operation may have succeededDRIVE_SOURCE_TIMEOUT constant in api_client.pyrefresh_auth MCP tool for explicit token reloadrun_headless_auth() function for background authentication (if Chrome profile has saved login)has_chrome_profile() helper to check if profile existslaunch_chrome() now returns subprocess.Popen handle instead of bool for cleanup control_call_rpc() enhanced with _deep_retry parameter for multi-layer auth recoveryconstants.py module as single source of truth for all API code-name mappingsCodeMapper class with bidirectional lookup (name→code, code→name)CodeMapperquiz_create now accepts difficulty: str ("easy"|"medium"|"hard") instead of int (1|2|3)constants.CodeMapper for input validationconstants.CodeMapper for human-readable names_get_*_name helper methods from api_client.py*_codes dictionaries from server.py tool functionsget_notebook_sources_with_typesget_notebook_sources_with_typesstudio_status now includes mind maps alongside audio/video/slidesdelete_mind_map() method with two-step RPC deletionRPC_DELETE_MIND_MAP constant for mind map deletionstudio_delete (fixes #7)notebook_query now accepts source_ids as JSON string for compatibility with some AI clients (fixes #5)list_mind_maps responsesbl version to boq_labs-tailwind-frontend_20260108.06_p0delete_studio_artifact now accepts optional notebook_id for mind map fallbacksource_get_content tool for raw text extraction from sources