183 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
notebooklm skill package— Claude-uploadable skill archive (#1856 item 2; the docs half landed in #1857). Builds a deterministic ZIP whose root contains thenotebooklm/SKILL.mdskill folder (version-stamped, frontmatter-first), ready for upload via Claude Settings → Capabilities in chat and Claude Cowork — the supported hand-off for sandboxed agent environments that cannot runskill installagainst a local directory. Supports-o/--output(file or directory),--forceoverwrite protection, and--json(success payload{"path", "version", "entries", "size_bytes"}; failures use the standard error envelope with codesSKILL_SOURCE_MISSING/OUTPUT_EXISTS/WRITE_FAILED). Release workflow now also attachesnotebooklm-skill.zipto GitHub releases alongside the.mcpbbundle.AccountLimits.tier— a correct account-tier signal, sourced from the authoritative quota block. The v0.8.0 removal ([#1738]) dropped a tier that came fromGET_USER_TIER(FetchRecommendations, a promotions endpoint) which could not tell free from paid. This addstierread fromGET_USER_SETTINGSlimits[4] — the same block asnotebook_limit/source_limit, so no extra RPC. It is an opaque enum key (not an ordinal):1=Free,2=Pro,4=Plus,3/6=Ultra (20 TB / 30 TB),5=Expanded (legacy); only1and2are live-confirmed. The MCP and RESTserver_info(include_account=True)account blocks now include atierkey. The removedplan_namelabel and the promotions RPC/AccountTiertype stay gone. (#1738)
Fixed
-
Artifact-generation validation footguns ([#1874]). Three input-validation hardenings on the artifact surface: (A) the REST
POST /artifacts(ArtifactGenerate) body now rejects unknown fields (extra="forbid") with a 422 instead of silently ignoring a typo ({"stylee": …}) and starting a default artifact; (B)generate_reportnow coerces/validatesreport_formatat the boundary, raising a clearValidationError(that namessource_ids) instead of an opaqueTypeErrordeep in the payload builder when a caller misplaces a positionalsource_idslist; (C)export()enforces exactly-one-of (artifact_id,content) instead of firing a meaningless no-op RPC when both default toNone. Breaking:export()'scontentis now keyword-only so its positional slots matchexport_report/export_data_table(title in slot 3) — this fixes a silent title→content misbind; passcontent=…explicitly. (#1874) -
MCP source tools no longer swallow fatal errors or fan out unbounded. The MCP
source_addbatch andsource_waithad drifted from the REST route's policy: a batchsource_addcaught every per-URL exception — so an expired auth / rate limit / upstream 5xx was reported as a per-item "error" inside a success envelope, hiding it from the agent; and multi-sourcesource_waitspawned one poller per source with no concurrency limit. The shared policy (the batch/wait caps, the fatal-vs-isolate classifier, and the bounded multi-source wait pool) now lives in the transport-neutral_appcore (_app.source_batch/_app.source_wait) and is used by both adapters, with a parity guardrail (tests/_guardrails/ test_source_policy_parity.py) that prevents future drift. Behavior change: a fatal batch item now aborts the wholesource_addcall (so an agent can re-auth/retry), only per-URL 4xx-input failures isolate;source_waitnow bounds concurrency at 8, caps the source count at 100 on both the explicit-subset and the omitted-sourceswait-all path (enforced at one shared chokepoint so the adapters can't drift), caps timeout at 3600s, and rejects non-finite (NaN/Infinity) timeout/interval — all via a shared_appvalidator used by the REST route too. Per-URL SSRF/validation isolation is unchanged. (#1871) -
Direct-PDF-URL sources no longer show the raw URL as their title. Adding a source whose URL points straight at a
.pdfleft the full request URL in the title slot (the server extracts<title>for HTML pages but not for a direct PDF link), while HTML sources got proper titles. A PDF source whose title is exactly the source URL (the server degradation — not a user-set title that merely resembles a URL) now falls back to the URL path basename — e.g.https://host/papers/SomePaper.pdf→SomePaper(query and fragment ignored; percent-encoding decoded; a URL whose basename has no.pdfextension, such as…/download?file=x.pdf, keeps the raw URL rather than a misleadingdownload). Applied consistently acrosssource list/get (Source.from_row) andsource fulltext(SourceContentRenderer). No network I/O and no PDF dependency; PDF/Titlemetadata extraction is out of scope. Because the fix lives in the shared read paths it applies retroactively — a source added before this release displays the derived title on the next read, so callers that relied onsource delete-by-title "<the raw URL>"should use the new basename (URL-add idempotency is unaffected — it keys off the sourceurl, not title). (#1850) -
Drive-hosted PDFs no longer list as
google_spreadsheet. The backend returns type code14for both native Google Sheets and Drive-hosted PDFs, and Drive sources carry no URL, so the read paths (source_list/GET_NOTEBOOKandsource fulltext/source_read/GET_SOURCE) mislabeled Drive PDFs askind="google_spreadsheet".kindnow disambiguates code14by the source MIME (metadata[19]/metadata[9][2]from a live capture):application/pdf→pdf, while real Google Sheets (application/vnd.google-apps.spreadsheet) are unchanged. This completes the read-path half of the fix started in #1831 (the add path). (#1832)
Documentation
- Per-tier quota & limit reference (
docs/quota-limits.md). A new reference documenting NotebookLM's published static plan limits — notebooks, sources, chats, and studio artifacts across consumer (5 tiers), Workspace (5 levels), and Enterprise — keyed to theAccountLimits.tierint, with evidence classes for the features Google leaves unquantified and a note that live per-account remaining/reset counters are not API-exposed. Distilled from the research in #1825; cross-linked from thetierfield,rpc-reference.md,mcp-guide.md, andpython-api.md. - Sandboxed agents (Claude Cowork / headless). Documented running
notebooklm-pyfrom Claude Cowork and similar no-display sandboxes inSKILL.mdanddocs/installation.md: per-sessionpip installbootstrap (no[browser]needed for queries — that extra is only for the interactivelogin, which runs on a host machine), and reusing a host-generatedstorage_state.jsonvia the root--storageflag orNOTEBOOKLM_AUTH_JSON. (#1856)
0.8.0
The headline of 0.8.0 is integrations: NotebookLM is now reachable from AI
agents and HTTP clients through two new adapters built over the shared _app/
core (ADR-0021) — an MCP server and an experimental single-tenant REST API
server — plus a remote MCP connector you can self-host for claude.ai
(and Claude Desktop / Code / Cursor) behind a Cloudflare Tunnel or Tailscale
Funnel, gated by a single password.
0.8.0 also lands the breaking half of the ADR-0019 error contract (the
#1346 umbrella):
"absence and refusal raise; only success and async-lifecycle state are
returned." Every flip previewed under NOTEBOOKLM_FUTURE_ERRORS in v0.7.0 is
now the default, and the preview flag — together with the dict-subscript /
get-returns-None / kwarg-alias deprecation machinery — has been removed
(#1365). See the Upgrading to v0.8.0 guide and the
Breaking section below.
⚠
NOTEBOOKLM_FUTURE_ERRORSis gone. It was the v0.7.0 forward-compat preview gate; its target behavior is now unconditional, so the flag is a no-op (setting it changes nothing). Remove it from your environment / CI config.
Added
-
Short-form video format across the client, CLI, MCP, and REST (#1805). Video generation now takes a format selector so you can request a short video in addition to the default full-length one. The server ignores a custom style on the short format, so passing one is rejected up front rather than silently dropped (#1805 follow-up).
-
MCP
source_upload_bytes— in-channel small-file upload (#1803). Adds a source by sending the file bytes directly through the tool call, for network-boxed agents that cannot reach the signed browser-upload URL. -
MCP
source_add_and_wait— a single-call composite ofsource_add+source_waitfor single-source adds (#1807). -
Compact roster mode for
source_listandstudio_list(#1806) — a terser listing shape that fits more entries into an agent's context window. -
MCP strict IDs-only mode + canonical-id echoes (#1808). Opt into rejecting name-based refs and having tools echo the canonical id they resolved, so an agent can pin every subsequent call to an unambiguous id.
-
Near-miss candidates on failed name lookups (#1787). When a notebook/source/note/artifact name doesn't resolve, the error now lists the closest matches instead of a bare not-found.
-
Experimental: MCP server (#1484, opt-in via the
mcpextra). A Model Context Protocol server exposing NotebookLM to MCP clients (Claude Desktop / Code, Cursor, Windsurf) as 34 tools across notebooks, sources, chat, notes, studio artifacts, and research — built as a transport-neutral sibling adapter over the_app/layer (ADR-0021), so it behaves identically to the equivalentnotebooklmCLI command. Run it with thenotebooklm-mcpconsole script (stdio by default, or loopback HTTP via--transport http); wire it into a client withnotebooklm mcp install <client>or the one-click.mcpbdesktop bundle. Notebook- and source-scoped tool arguments accept a name or an id; destructive tools requireconfirm=true(returning aneeds_confirmationpreview otherwise); long-running generation is non-blocking (*_generatereturns atask_idto poll via*_status, then download). The MCP tool surface is experimental and not covered by the library's semver guarantees — names, parameters, and output shapes may change between releases.pip install notebooklm-pyis unaffected: the server and its dependencies (fastmcp) arrive only with themcpextra. See docs/mcp-guide.md. -
Experimental: remote MCP connector — self-host NotebookLM for claude.ai, Claude Desktop, and Cursor (#1645, #1647). The MCP server now also runs as a remote HTTP connector, not just a local stdio process:
notebooklm-mcp --transport httpserves the same tools over HTTP behind a bearer token, anddeploy/ships a one-command Docker stack (make dev) that exposes it through a Cloudflare Tunnel or a Tailscale Funnel — no public IP, no open port, no TLS cert to manage. For claude.ai (whose connector UI is OAuth-only and has no bearer field), the server can run its own tiny OAuth authorization server gated by a single password (NOTEBOOKLM_MCP_OAUTH_PASSWORD) — no external IdP, no JWT template; registered clients and tokens persist across restarts. Opt-in and additive: leave the OAuth vars unset to stay bearer-only (Claude Code / Desktop unaffected). The server fail-closed refuses to start on a non-loopback bind with no auth, or on partial / weak / non-HTTPS OAuth config. Single-tenant, self-hosted, one container per Google account. The remote-deployment surface is experimental and may change between releases. See deploy/README.md and docs/mcp-guide.md. -
Experimental: single-tenant REST API server (#1538, opt-in via the
serverextra; console scriptnotebooklm-server). A FastAPI app exposing guarded/v1routes — notebooks, sources, chat, notes, studio artifacts, sharing, and research — over the same transport-neutral_app/core the CLI and MCP server use (ADR-0021), so behavior matches the equivalent CLI command. Loopback-bound by default, requiresNOTEBOOKLM_SERVER_TOKEN, and refuses an unauthenticated non-loopback bind. Native multipart source upload andFileResponseartifact download (no signed-URL broker needed). Follow-up work closed the remaining REST↔MCP capability and hardening gaps (#1620 notes CRUD, #1767). The REST surface is experimental and not covered by the library's semver guarantees.pip install notebooklm-pyis unaffected — FastAPI / uvicorn arrive only with theserverextra. See docs/installation.md#rest-api-server. -
Master-token headless auth (#1638, #1640; ADR-0023; opt-in via the new
headlessextra). NotebookLM's browser cookies are short-lived, and until now the only way to re-acquire them was a human re-runningnotebooklm loginin a browser — fatal for long-lived headless / CI / server use. A durable Google master token (minted once vianotebooklm login --master-token, then stored0600beside the profile) can now re-mint fresh web cookies on demand with no per-session browser. Recovery is automatic: when amaster_token.jsonis present, an expired session re-mints in-process as the final layer of the refresh ladder (single-flight, so concurrent RPCs coalesce one re-mint) instead of raising "run 'notebooklm login'".notebooklm auth checksurfaces the master-token identity and--master-token-refreshre-mints on demand. This is the foundation that makes the remote MCP connector and unattended deployments viable. Security: the master token is a full-account, long-lived credential — store it like a password and prefer a dedicated / throwaway account for servers. It and its dependency (gpsoauth) arrive only with theheadlessextra. See docs/installation.md and docs/auth-cookie-lifecycle.md. -
Source labels (#1474). A new
client.labelsnamespace and alabelCLI command group bring NotebookLM's source-label (tag) feature to the client: list / create / rename / delete labels and assign or clear them on sources, then narrow a listing withsource list --label <name>(the MCPsource_listtool also gained a label filter). Additive new public surface across the Python API and CLI. -
Suggested prompts (#1612, #1616). New
client.chat.suggest_prompts(...)returns model-generated starter prompts for a notebook (backed by theGeneratePromptSuggestionsRPC), surfaced as anotebooklm suggest-promptsCLI command and, later, an MCPsuggest_promptstool (#1726). Amodeselector targets the surface the suggestions are for (ask / audio / video / quiz / flashcards / …). Additive new public surface. -
Opt-in
curl_cffibrowser-impersonation transport (#1632). An alternative HTTP backend that mimics a real browser's TLS fingerprint, selected viaNOTEBOOKLM_TRANSPORT=curl_cffi(requires thecurl_cffipackage), for environments where the defaulthttpxtransport is fingerprint-blocked. It sits behind the existingasync_client_factoryseam; the default transport is unchanged. -
--jsonon every local CLI command (#1643). The remaining local commands gained a--jsonflag — now enforced by a coverage guardrail — so anynotebooklmcommand can emit machine-readable output for scripting and automation. -
MCP soft-404 body-pattern detection. The
source_waitcontent-sanity check (#1698) now also flags a READY web page that sails past the thin-text threshold but whose short body matches a dead-link / error-page boilerplate phrase (e.g. "Whoops! broken link") — a soft-404 that ingests as a full-bodied 200. Body-only (titles are never scanned), gated to sub-2000-char bodies, advisory-only (never blocks), zero extra RPC (the body is already fetched). The batchsource_add(urls=[...])now surfaces the same warning per synchronously-ready web-page item. -
MCP artifact rename & delete tools. Two new MCP tools close the artifact CRUD gap (the server previously exposed create + read only):
artifact_rename(title-only update) andartifact_delete(destructive, two-stepconfirm). Both accept a notebook/artifact name or ID, cover every studio artifact type including both mind-map kinds — note-backed maps route through the note system, interactive maps and regular artifacts through the artifact RPC — over the shared kind-aware_app.artifactscores the CLI already uses. The MCP tool surface is now 28 tools. -
Live-API e2e coverage for the MCP server and the CLI binary. The nightly E2E job now installs
--extra mcp, so the MCP/CLI layers run against the real NotebookLM API once per release instead of being silentlyimportorskip-ped. New suites: per-domain MCP tool round-trips plus a 28-tool→test matrix (tests/e2e/test_mcp.py); the HTTP transport, bearer gate,.well-knowndiscovery, and signed-URL upload/download routes driven in-process overhttpx.ASGITransport(tests/e2e/test_mcp_http.py); live-only contract checks — thesource_idsomitted-==-[]-==-all collapse (#1652), not-found resolution, destructive confirm-gating, and the MCP error shape (tests/e2e/test_mcp_contracts.py); and a CLI-binary--jsonsmoke including stdout-purity on a live failure (tests/e2e/test_cli_live.py). A standalonescripts/mcp_live_smoke.pyruns the upload+download round-trip against a deployed server (PASS/FAIL) to bootstrap the new per-release manual "MCP connector smoke" checklist indocs/releasing.md. -
Remote MCP file upload & download (ADR-0024). Over the remote (HTTP) connector — where the claude.ai browser can't carry the MCP credential and the JSON-RPC channel can't carry bytes —
source_add type=fileandartifact_downloadnow broker a short-lived HMAC-signed URL served by the same container, and the browser does the byte transfer out-of-band (the established remote-MCP pattern; MCP has no native upload primitive and its native binary-Resource download is capped far below a podcast/video). Upload accepts a raw body overPOST/PUT(a browser file-picker page or a code-execution-sandboxcurl), bounded by a 200 MiB per-request cap and an in-flight-upload limit; download returns a clickableresource_link. Enabled byNOTEBOOKLM_MCP_PUBLIC_URL(falls back toNOTEBOOKLM_MCP_OAUTH_BASE_URL); unset → the two tools return a clear "not configured" error and the server still starts. stdio (local) installs are unchanged — they keep reading and writing real local paths. The RESTserverextra already supports native multipart upload +FileResponsedownload and is unaffected. See docs/mcp-guide.md. -
Retrieve the generation prompt behind an artifact (#1571). New
client.artifacts.get_prompt(notebook_id, artifact_id)returns the free-text prompt an artifact was generated from, and a matchingartifact get-promptCLI command prints it (with--json). Works for every studio artifact type — audio, report, video, quiz, flashcards, interactive mind map, infographic, slide deck, and data table — by reading the prompt already present in theLIST_ARTIFACTSresponse through the newArtifactRow.generation_promptaccessor (no new RPC). ReturnsNonefor an artifact with no stored prompt (e.g. a note-backed mind map) and raisesArtifactNotFoundErrorfor an unknown id. The transport-neutral_app.artifacts.get_artifact_promptexposes the same behaviour to the MCP/HTTP adapters. -
Custom prompt for interactive mind maps.
instructionsis now sent for interactive (studio-artifact) mind maps, not just note-backed ones. The interactiveCREATE_ARTIFACTpayload carries the free-text prompt at the[9][1][2]slot of its options block — the same slot quizzes and flashcards use — and the NotebookLM server honors it for variant 4 (verified live: the prompt steers the generated node tree, and reads back viaartifacts.get_prompt). Previouslyclient.mind_maps.generate(..., kind=INTERACTIVE, instructions=...)andnotebooklm generate mind-map --kind interactive --instructions ...silently dropped the prompt with a warning; both now apply it. The no-prompt request shape is unchanged. (Note-backed maps still passinstructionsthroughGENERATE_MIND_MAP, but the server does not reliably act on them.) -
Passive auth validation for unattended monitors (#1569). New
notebooklm.auth.fetch_tokens_passive(...)validates the cookies on disk with a strictly read-only token fetch — it never runsNOTEBOOKLM_REFRESH_CMD, never fires the layer-1 keepalive rotation poke, and never writesstorage_state.json(additive public symbol; the activefetch_tokens_with_domainsis unchanged).notebooklm auth check --test --passiveroutes the token probe through it, so a systemd/cron health check can answer "do the cookies currently authenticate?" without mutating state, spawning a subprocess, or racing real work. The transport-neutralrun_auth_check(AuthCheckPlan(..., passive=True))exposes the same probe to the MCP/HTTP adapters. -
notebooklm auth refresh --verify(#1569). After a refresh completes, runs the passive token probe to confirm the resulting cookies actually authenticate, exiting non-zero if they still redirect to sign-in. A successful refresh command alone does not prove the post-refresh cookies work — this is especially valuable with--browser-cookies, which rewrites the cookie jar but does not otherwise verify it. -
macOS login recovery hint. When the bundled-Chromium interactive login times out (
Login not detected within 5 minutes), the message now suggests retrying withnotebooklm login --browser chrometo reuse an already signed-in system Chrome session — which often detects immediately and sidesteps bundled-Chromium issues on macOS. -
Layer-3 headless re-auth:
client.refresh_auth(allow_headless=True)(#1525, P2; P1 was #1512). When NotebookLM's first-party cookies are fully dead — the homepage GET 302s to the Google login page and neither token refresh (L1) norRotateCookiesrotation (L2) can help — a persisted browser profile may still hold a live Google SSO session that outlivesstorage_state.json. The new opt-in re-auth layer drives an unattended headless browser against that profile to silently re-mint cookies, then retries. It is explicit by default:refresh_auth(allow_headless=True)(additive keyword-only, defaults toFalse) triggers it on demand, and a mid-RPC auto-fire happens only whenNOTEBOOKLM_HEADLESS_REAUTH=1is set — L3 never fires by default, so behavior with no opt-in and no profile is unchanged. Outcomes are typed and honest (re-minted / profile-session-also-dead / unavailable) and it never reports success on dead tokens. SECURITY: the persistent profile is an account-equivalent credential; L3 is local-unattended-only and must not be the auth path for a remote / hosted MCP server. It reuses the existing cookie-domain allowlist and never logs a captured cookie value. -
client.mind_maps.list_note_backed(notebook_id)— typed list of only the note-backed mind maps (everykindisNOTE_BACKED,treepopulated, deleted rows excluded) via a singleGET_NOTES_AND_MIND_MAPSRPC — noLIST_ARTIFACTS. Factored out ofmind_maps.list()(which now builds on it) and used by the CLIartifact deletecarve-out probe so the note-backed membership check is fully typed while keeping the historical single-RPC call set (recorded cassettes replay unchanged). -
Schema-drift observability:
rpc_decode_errorscounter + chat drift canary (#1492). Wire-schema drift is the stated #1 breakage class, but decode/drift failures (DecodingError/UnknownRPCMethodError) were invisible to metrics — they did not even reach the transport-legrpc_calls_failedcounter (the middleware chain wraps only the transport leg; decode happens after).ClientMetricsSnapshotnow exposes a dedicatedrpc_decode_errorscounter (additive, defaults to0, appended at the end of the dataclass so existing positional construction is unaffected), incremented at the executor's response-decode boundary whenever a decoded response envelope is rejected as drift — both the wrapped shape-drift case (bad JSON / missing key-or-index) and a surfacedDecodingError/UnknownRPCMethodErrorfrom the envelope decoder. A decoded semantic error (rate-limit, not-found, auth) is not drift and does not bump the counter; a drift error recovered by refresh-and-retry is not counted. (Positional drift raised later by feature-layersafe_indexnavigation, afterrpc_callreturns, is not yet routed through this counter — a tracked follow-up.) Operators can now alert on "Google reshaped a response" distinctly from ordinary 5xx / network failures. Separately,scripts/check_rpc_health.pynow probes the streamed-chat orchestration RPCGenerateFreeFormStreamed— aPATH_NOT_METHOD(v1URL) endpoint with no obfuscated method ID — by asserting a 200 plus a recognizable stream frame, closing the gap where the chat surface escaped the daily drift canary.
Changed
-
MCP name refs resolve by unique title prefix (#1786). Notebook, source, note, and artifact name arguments now match on a unique title prefix, not just an exact title, so an agent can address an entity by the shortest unambiguous fragment; an ambiguous prefix is a resolution error listing the candidates.
-
MCP research tools now accept the poll id under one name,
poll_task_id.research_status,research_import, andresearch_cancelpreviously took the value thatresearch_start/research_statussurface aspoll_task_idunder mismatched parameter names (task_idon status/import,run_idon cancel), forcing docstring warnings about which id goes where and inviting copy-paste mistakes. All three now accept it aspoll_task_id, so the value pastes verbatim from one tool's output into the next. The oldtask_id/run_idnames still work as deprecated aliases (removed in v0.9.0): passing one emits aDeprecationWarningand adds adeprecationnote to the result; passing both names with different values is a validation error. (#1789) -
MCP
source_waitnow returns one unified per-source aggregate (#1669). Both modes — waiting for a singlesourceor for every source in the notebook — return the same shape:{notebook_id, ok, ready, timed_out, failed, not_found}.readycarries the sources that reached READY (withkind/status_labellabels); the three error buckets carry{source_id, error}entries;okistrueiff all error buckets are empty. Previously the single-source mode returned{status: ready|not_found|failed| timeout}while the all-sources mode threw on the first failure and discarded every source that had already become ready — the all-sources mode now reports partial progress instead. (Asourcereference that does not resolve still raises NOT_FOUND before the wait — an input error, distinct from a resolved source the backend reports missing/failed/slow.) -
Regenerable test baselines (Phase 1; contributor-facing, no public API change). Frozen public-surface snapshots that were hand-typed copies of values the code already derives —
_FROZEN_TYPES_ALLand_UNGATED_PUBLIC_ALL_SNAPSHOTintests/_guardrails/test_public_surface_manifest.py— are now derived baselines committed undertests/fixtures/baselines/(types_all.json,ungated_surface.json) and registered intests/_baselines/registry.pyalongside the existing CLI contract. A single freeze test diffs each committed file againstderive(); a dev-only--update-baselinespytest flag (wrapped bypython scripts/regen_baselines.py) regenerates them. Adding a public symbol is now one regen command plus a reviewed diff instead of hand-editing several literals. CI never regenerates — it only diffs (the regen flag is refused underCI). The authored_DOCUMENTED_PUBLIC_IMPORTS(promised-import intent) and_TOP_LEVEL_TYPE_EXPORTS(fuzzy derivation) stay hand-curated. See ADR-0022. -
notebooklm.rpcpublic surface narrowed to the two documented power-user imports (#1589).notebooklm.rpc.__all__now lists onlyRPCMethodandresolve_rpc_id; the ~47 other names it used to advertise (the batchexecute wire helpersencode_rpc_request/decode_response/extract_rpc_result/ …, the endpoint URL constants and helpers,safe_index, and the enum / exception re-exports that remain public under their canonical names — most enums asnotebooklm.<X>/notebooklm.types.<X>, the exceptions asnotebooklm.<X>/notebooklm.exceptions.<X>, withArtifactStatus/artifact_status_to_strnotebooklm.types-only andArtifactTypeCodehaving no public alias) are no longer part of the blessed, compat-gated public surface. This aligns the audited surface withdocs/stability.md, which has always markednotebooklm.rpc.*internal. Not a removal: every name stays importable asnotebooklm.rpc.<name>for back-compat — only the public-API advertisement shrank. New code should import the canonical public name (orRPCMethod/resolve_rpc_idfor raw-RPC power use). See docs/deprecations.md. -
notebooklm.authpublic surface narrowed (PR-1) (#1592).auth.__all__drops 23 internal re-exports that only first-partysrc/tests imported (the cookie-snapshot/storage helperssave_cookies_to_storage/snapshot_cookie_jar/CookieSnapshot*/CookieSaveResult/advance_cookie_snapshot_after_save, the WIZ-extraction helpersextract_csrf_from_html/extract_session_id_from_html/extract_wiz_field,authuser_query/format_authuser_value,load_httpx_cookies/normalize_cookie_map,ALLOWED_COOKIE_DOMAINS/MINIMUM_REQUIRED_COOKIES, the env/URL constantsKEEPALIVE_ROTATE_URL/NOTEBOOKLM_REFRESH_CMD_ENV/NOTEBOOKLM_REFRESH_CMD_USE_SHELL_ENV/NOTEBOOKLM_DISABLE_KEEPALIVE_POKE_ENV,load_auth_from_storage,fetch_tokens, andrecover_psidts_in_memory). These were migration leftovers from the_auth/*extraction (ADR-0003 → ADR-0014);docs/stability.mdhas always markednotebooklm.auth.*internal. Not a removal: every name stays importable asnotebooklm.auth.<name>for back-compat — first-party code now imports them from theirnotebooklm._auth.<sub>home. The documented imports (AuthTokens,convert_rookiepy_cookies_to_storage_state, the cookie-domain constants) and the cohesive operations (enumerate_accounts,fetch_tokens_with_domains,fetch_tokens_passive, …) are unchanged. See docs/deprecations.md.
Removed
SettingsAPI.get_account_tier()and theAccountTiertype (BREAKING).client.settings.get_account_tier(),notebooklm.AccountTier/notebooklm.types.AccountTier, and the underlyingGET_USER_TIERRPC are removed. The tier came fromFetchRecommendations, a promotions endpoint, and was a promotion-eligibility signal that could not distinguish free from paid — both a free and a Pro account reportedNOTEBOOKLM_TIER_PRO_CONSUMER_USER. Useclient.settings.get_account_limits()(AccountLimits.notebook_limit/source_limit) for quota decisions instead. The MCPserver_info(include_account=True)account block drops itstierandplan_namekeys (now{email, authuser, available, notebook_limit, source_limit, output_language}). See docs/deprecations.md.
Fixed
-
Remote MCP connector: pin
fastmcp==3.4.2so claude.ai can connect. fastmcp 3.4.3 regressed the RFC 9728 protected-resource-metadata route (/.well-known/oauth-protected-resource/mcpstarted returning 404), which dead-ends the claude.ai remote-connector OAuth discovery — the connector fails with "Couldn't connect to the server." Because themcpextra previously allowedfastmcp>=3.0,<4, the published0.8.0a2Docker image floated to the broken 3.4.3 at build time even though the client code was unchanged. Themcpextra is now pinned to the last-goodfastmcp==3.4.2; the pin will be lifted once a fixed fastmcp ships. -
First-class human/mobile upload path in
upload_required(#1801). When a source add needs a browser upload, theupload_requiredresponse now surfaces the human/mobile hand-off path as a first-class option rather than burying it. -
server_infoand the profile probe report the resolved profile, not the raw active one (#1790). The REST server'sserver_infoand its startup profile probe now bind and report the profile that was actually resolved for the request. -
The
.mcpbdesktop bundle now ships for pre-releases and launches the pinned pre-release. The bundled launcher (run_server.py) always ranuvx --from "notebooklm-py[mcp]", which resolves the latest stable server — so a pre-release bundle would have run the stable release, not the pre-release (which is whypublish-mcpb.ymlskipped pre-releases). The launcher now reads its version from the bundledmanifest.jsonand, for avX.Y.ZaNbundle, pins the exact version (uvx --from "notebooklm-py[mcp]==X.Y.ZaN"— the explicit==pin resolves the pre-release under uv's default resolver, without loosening transitive dependency resolution); stable bundles stay unpinned and track the latest stable server.publish-mcpb.ymlnow ships a bundle for pre-releases too. -
notebooklm doctorno longer greenlights a session that is missing__Secure-1PSIDTS(#1753). The auth check previously passed onSIDpresence alone, so a login that captured only half the Tier-1 cookie set — a common outcome on Windows, where Chrome 127+ App-Bound Encryption blocks--browser-cookiesdecryption and an automation-detected Playwright login can be served a session without the rotating token-binding cookie — reported "All checks passed" even though every real RPC then failed withMissing required cookies: __Secure-1PSIDTS. Doctor now emits a warn row (not a hard fail — the cookie legitimately rotates and can be re-minted at runtime, so a static offline probe must not false-negative a recoverable session) pointing at the Firefox--browser-cookiesand--master-tokenworkarounds.auth check --testalready reported the real error and is unchanged. New troubleshooting.md section documents the Windows App-Bound Encryption limitation and its workarounds. -
notebooklm auth checktext mode now exits non-zero when an executed check fails, matching--jsonmode (#1569). Previously the Rich-table renderer printed the failed checks but always exited0, so an unattended health check usingauth check --test(without--json) silently treated expired auth as healthy — text and JSON modes had different process contracts. Both modes now exit0only when every executed check passes (skipped checks — e.g. the token fetch without--test— do not count as failures) and non-zero (1) otherwise. Behavioral fix to the CLI exit-code contract; no API change. -
Notebook.created_atnow reflects the true creation time instead of the last-modified time;Notebook.modified_atis newly exposed. The notebook metadata block carries two timestamps — the creation instant atdata[5][8][0](pinned across edits) and the last-modified instant atdata[5][5][0](advances on every modification).Notebook.from_api_responseread the modified slot (data[5][5]) and labeled itcreated_at, so the field — and everything built on it (--jsonoutput,metadataexport, theCreatedtable column) — silently reported the last edit time.created_atnow reads the creation slot (data[5][8]), and the last-modified time is surfaced additively asNotebook.modified_at(new field, defaults toNone, appended at the end so positional construction is unaffected; also added toNotebookMetadata.to_dict()and the notebook--jsonenvelopes). This applies to bothnotebooks.get()andnotebooks.list()(the homepage/recent feed), which share the decode path. No signature change —created_atkeeps its type, only its source value is corrected — so this is a behavioral fix, not an API-compat break, andmodified_atis purely additive. -
Decoded
created_attimestamps are now tz-aware UTC instead of naive host-local time (#1519). The shared decoder_datetime_from_timestamp(backingNotebook/Source/Artifact/MindMap.created_at) calleddatetime.fromtimestamp(value)with notz, producing a naive datetime in the host's local zone — so the same epoch surfaced as a different wall-time string per machine, andcreated_at.isoformat()/--jsonoutput, thestrftimetable cells, etc. mis-stated the absolute instant (a notebook created13:40:05UTC rendered as the offset-less08:40:05underAmerica/New_York). It now returnsdatetime.fromtimestamp(value, tz=utc): the value is tz-aware UTC and host-independent..timestamp()round-trips unchanged, so internal sort/dedup/download ordering is provably unaffected — only the rendered string changes (now offset-aware, identical everywhere). This is the production sibling of the timezone slip pinned out of the #1511 golden VCR test. -
Artifact downloads re-validate every redirect hop against the trusted-host allowlist (SSRF-adjacent) (#1521). Both download clients (
download_urlsingle +download_urls_batch) usefollow_redirects=True, but the host-allowlist + HTTPS gate validated only the initial URL. A trusted Google URL whoseLocationpointed off-allowlist — a non-HTTPS hop, or a private/link-local host such as169.254.169.254/localhost— was followed and its body written to the caller'soutput_path, defeating the explicit allowlist. (Google session cookies were already not leaked to a non-Google redirect host — the stdlib cookie policy is domain-scoped — so this never exposed credentials; the residual harm was attacker-influenced bytes landing on the filesystem.) Both clients now attach an httpxrequestevent hook that re-checks every hop's host + scheme before the request is sent, raisingArtifactDownloadErroron the first off-allowlist or non-HTTPS hop so the untrusted host never receives a connection. Legitimate trusted→trusted redirects (Google signed-URL CDNs already on the allowlist) are unaffected. The host-allowlist check (_is_trusted_download_host) also no longer percent-decodes the host before matching: decoding created a parser differential whereevil%2egoogleapis.com(%2e→.) was judged trusted while httpx connected to the raw, non-Google host — the guard now matches the exact host httpx connects to and rejects any host containing%. -
source deletenow honors exact-id-wins over prefix matching, in lockstep withsource get/rename/refresh(#1522). The delete-path resolver (_app/source_mutations.resolve_source_for_delete) built its prefix-match list and branched solely onlen(matches)with no exact-id short-circuit, sosource delete abcraisedAMBIGUOUS_IDwhen a notebook held bothabcandabcdef— even though the shared resolvers (cli.resolve.resolve_partial_id_in_itemsand its_apptwin_app.resolve.resolve_ref) both return on an exact (case-insensitive) id match before evaluating prefixes, so the other verbs resolved the same input. The delete resolver now mirrors that Rule 3: a source whose id equals the input (case-insensitively) wins over any longer-id prefix match (and is not treated as a partial expansion, so no "Matched:" prose is emitted). Genuine prefix ambiguity (two strict prefixes, no exact match) and the not-found / title-instead-of-id paths are unchanged. -
Note.created_atand note-backedMindMap.created_atare now populated (#1529). Both fields were declareddatetime | Nonebut never filled in, even though the rawGET_NOTES_AND_MIND_MAPS/CREATE_NOTEresponses carry the creation timestamp in the note metadata envelope atrow[1][2][2][0]— the same slot the artifact path already decodes.NoteRowgains acreated_at_raw/created_atproperty pair (mirroringArtifactRow) that centralizes the descent behind named position constants, and every note-creation surface now reads it:notes.list/notes.get,notes.create(both the wrapped[[id, …]]and the flat[id, …]CREATE_NOTE response shapes),chat.save_answer_as_note, and the note-backedmind_maps.list_note_backed/mind_maps.generate.Artifact.from_mind_mapwas lifted to reuse the sharedNoteRowextraction so the position knowledge lives in one place. Additive: absent / legacy rows still yieldNone; no signature change. -
profilefilesystem commands now surface a friendly error + exit 1 instead of a raw traceback (#1520). Theprofile delete(shutil.rmtree),profile create(directory materialization),profile rename(os.rename), and text-modeprofile listpaths performed their pure-filesystem operations unguarded, so anOSError— a half-deleted or locked profile directory, a read-only mount, or a browser-profile file held by AV/the browser on Windows — escapedSectionedGroup.main(which only catchesClickException/Abort) and printed a Python traceback. Each now mirrorsprofile switch's existingexcept OSError -> click.ClickExceptionidiom, yielding the documented friendly-message + exit-1 CLI contract. The--json profile listpath keeps its existinghandle_errorsenvelope (an unexpectedOSErrorthere stays theUNEXPECTED_ERROR/ exit-2 contract automation relies on). -
Runtime secret redaction now derives from one canonical registry, closing several credential-disclosure gaps (#1517, #1518). The logging redaction cookie-name alternation in
_logging.pywas hand-enumerated and had drifted from the project's own cassette sanitizer: the session cookiesNID,LSOLH, and__Host-GAPS(classified must-scrub bytests/cassette_patterns.py) were absent, so a bareNID=g.a000-…token — exactly what_auth/refresh.pylogs at DEBUG from refresh-command stdout/stderr through the redacting logger — passed throughscrub_secretsverbatim (#1517). Google API keys (AIza…) and any future__Secure-*/__Host-*cookie carrying an opaque (non-token-shaped) value were also not redacted at runtime. Separately,UnknownRPCMethodError.data_at_failurewas spliced unscrubbed with!rinto__str__/__repr__/ tracebacks (a string splice that bypasses the loggingRedactingFilter), unlike the siblingraw_responsewhich was already scrubbed, so a credential-shaped indexed value leaked through every rendering regardless ofNOTEBOOKLM_DEBUG(#1518). All are fixed by a single canonical runtime registry (notebooklm._secrets):RUNTIME_SESSION_COOKIES(the bare must-scrub cookie names the redaction alternation now derives from),SECURE_HOST_UMBRELLA_PATTERNS(__Secure-*/__Host-*prefix umbrellas whose name class spans the full RFC 6265tokencharset — any future secure/host cookie name fails closed by construction), andAUTH_TOKEN_SHAPE_PATTERNS(carrier-agnosticg.a000-/sidts-/ya29.token catch-alls plus theAIza…Google API-key shape, ported from the cassette registry as defense in depth so a secret under an unknown carrier name still fails closed).data_at_failure(and the already-scrubbedAuthExtractionError.payload_preview) are routed throughscrub_secrets, so all three additions cover the exception surfaces too. Every name-anchored value pattern (cookies, the__Secure-*/__Host-*umbrellas, and theat=/csrf=/f.sid=/upload_id=/OAuth/Bearerquery + header forms) now also redacts an RFC 6265 / JSON double-quoted value (SID="opaque",f.sid="opaque"): the value class excluded", so a quoted value made the whole pattern miss and leaked verbatim — the optional surrounding quotes redact the inner value while preserving the quotes. A parity guardrail (tests/_guardrails/test_runtime_secret_registry_parity.py) asserts the runtime registry stays in lockstep with the cassette sanitizer on every axis: bare-cookie superset, secure/host umbrella coverage by construction, and regex-string equality of the credential-shape set — so a new must-scrub shape added to the cassette registry forces the runtime registry to keep up. -
Playwright login: closing the browser during the final storage-state capture now shows the browser-closed help instead of a bug-report prompt (#1514, deferred from the #1512 review). Every in-flow Playwright call in the login flow (page recovery, the navigation retry loop, the login wait, cookie-forcing) already mapped
TargetClosedErrorto the friendlyBROWSER_CLOSED_HELPtext + exit 1, but a closure in the narrow window during the finalcontext.storage_state()capture fell through the outer handler's bareraiseand exited 2 ("Unexpected error … please report a bug"). The outer handler inrun_browser_capturenow recognizes TargetClosed and surfaces the same help + exit 1; every other unexpected failure keeps the exit-2 bug-report contract. -
Playwright storage-state filter hardened against malformed cookie rows and exact-duplicate identities (#1513, deferred from the #1512 review).
filter_storage_state_cookies_by_domain_policyno longer crashes the whole persist when rookiepy / Playwright emits a malformed row: non-dict entries, cookies whosedomainis not a str, and cookies whosenameis not a non-empty str are skipped with one boundedlogger.warningper row (reprlibpreview) instead of raising in.get/.lstrip. It also dedups rows sharing an exact RFC 6265 identity(name, domain, path)(path normalized viaor "/", matching every loader): the last occurrence in capture order wins whole (fields are never merged), mirroring the persistence-merge rule insave_cookies_to_storagewhere the newer observation overwrites the stored row for the same key. Same-name rows on different domains or paths are all kept — cross-domain same-name resolution remains a load-time concern (the flat loaders rank by_auth_domain_priority); deduping by bare name at write time would starve the(name, domain, path)-keyed runtime loader (build_httpx_cookies_from_storage), which legitimately holds e.g. the per-productOSIDcookie onnotebooklm.google.comandmyaccount.google.comas distinct jar entries. -
Split-state PSIDTS recovery no longer writes a duplicate
__Secure-3PSIDTSrow tostorage_state.json(#1523). On the--browser-cookiespath, when__Secure-1PSIDTSis missing/expired (so recovery fires) but a fresh__Secure-3PSIDTSis already in the source jar, Google'sRotateCookiesPOST returns both rotated SIDTS cookies and the in-memory recovery append loop emitted a second__Secure-3PSIDTS(and a stale__Secure-1PSIDTStwin) entry with no analog in any real browser jar. Auth still worked (the row is deduped on load), but the on-disk artifact diverged from the true cookie set.recover_psidts_in_memorynow keys the source jar by RFC 6265 identity(name, domain, path)(path normalized viaor "/", matching every loader) and overwrites the existing row in place with the rotated occurrence instead of appending — exactly one row per key, carrying the fresh value, mirroring the last-occurrence-wins dedup added tofilter_storage_state_cookies_by_domain_policyin #1513. -
sources.add_textno longer swallows typed transport errors intoSourceAddError. Its bareexcept RPCErrorwrapped everything — including theRPCErrorsubclassesRateLimitError,AuthError, andServerError— so callers could not catch a rate-limitedadd_textto back off viaretry_after(or re-login onAuthError). It now re-raises the narrow transport types unwrapped before wrapping only the residual broadRPCError, matching the ADR-0019 catch ordering its siblingsadd_url/add_drivealready follow. The rule is now enforced, not just documented: a new AST guardrail (tests/_guardrails/test_error_contract_catch_ordering.py) fails anyexcept RPCErrorclause that wrap-and-raises a different exception class without a preceding narrow-transport re-raise clause in the sametry(scope:src/notebooklm/**minus therpc/protocol layer, where the transport subtree originates). -
notebooklm note create --jsonno longer reports failure on every successful create. It previously emitted{"id": null, "created": false, "error": "Creation may have failed"}for every note it successfully created: a leftover raw-shape decoder in the_applayer went dead whennotes.createwas typed to return aNote(it expected the retired raw-list RPC shape and yieldedNonefor a typedNote). The bug was masked in the unit suite by stale raw-list mocks ofnotes.create. The CLI now emits the real note id with"created": true; facade failures propagate as exceptions through the standard CLI error handler instead of a soft-failure envelope. -
14 positional-decode sites no longer fabricate wrong-but-valid values silently on wire drift. Guarded single-level reads of decoded
batchexecutepayloads could swallow a Google reshape into a plausible default — an empty notebook / mind-map id, an empty share email, a deleted mind map leaking as live, a silently-empty chat history, aLIST_NOTEBOOKSwrapper mis-dispatch feeding garbage rows, an unvalidated source type code, and a note lookup flipping found → not-found. Per the #1485 absence-vs-malformed policy, genuine absence (short rows,Noneslots, legitimately-empty containers) keeps its soft degrade, while present-but-malformed data is now loud: the chat conversation-history walk moved behind a newConversationTurnRowadapter and raisesUnknownRPCMethodErroron a truthy non-list payload or turns container (malformed individual turn rows and unrecognized role codes are skipped with a DEBUG diagnostic);notebooks.list()raisesDecodingErroron an unrecognized payload shape; mind-map rows are decoded throughNoteRowand WARN when a null content slot lacks the soft-delete sentinel; notebook-id, share-email, and source-type-code slots WARN with a bounded payload preview when present-but-wrong-type (keeping list parsing alive); andnotes.get_or_noneid matching reads throughNoteRow.id. One behavior nuance:SharedUser.emailis now always astr— aNoneemail slot normalizes to""instead of leakingNonethrough thestr-typed field. -
Chat citation-structure drift is no longer swallowed at DEBUG (#1505 continuity — the last named survivor of that drift-swallow class). A Google reshape of the streamed-chat citation structure previously degraded to "answers with no citations" via a blanket
except → logger.debug → []inparse_citations— invisible, and it also discarded already-parsed citations. Per the absence-vs-malformed policy: genuine absence (no type block, short type block,None/empty citation slot — the routine "answer without citations" shapes on real traffic) stays completely silent; a truthy non-list where the citation container belongs (first[4][3]) is structural wire drift and raisesUnknownRPCMethodError(matching the parser's existinginner_data[0]raise andunwrap_conversation_turns); a present-but-unusable individual citation row now logs at least one bounded WARNING and is skipped, so a good answer keeps its surviving citations. Surviving citations keep their raw wire ordinal ascitation_number(a skipped row leaves a hole; with nothing skipped this equals the dense numbering always produced), so the answer's literal[N]markers never shift onto a different citation. Correspondingly, save-as-note's positional marker fallback (references[N-1]) now applies only when that positional reference carries nocitation_number: a holed marker drops its anchor with a warning instead of anchoring the wrong chunk.
Breaking
sources/artifacts/notes/mind_maps.get()raise on a miss (#1247). A genuine miss now raises the matching*NotFoundError(SourceNotFoundError/ArtifactNotFoundError/NoteNotFoundError/MindMapNotFoundError) instead of returningNone(and the v0.7.0DeprecationWarningis gone), matchingnotebooks.get. Return annotations narrow fromX | NonetoX. Use the unchanged, warning-freeget_or_none()for the sanctionedNone-on-miss lookup, or wrap intry/except *NotFoundError.- Typed research / mind-map / guide returns are attribute-only (#1251). The
MappingCompatMixindict-subscript bridge is removed fromResearchTask/ResearchStart/MindMapResult/SourceGuide/ResearchSource:result["key"]raisesTypeError;result.get(...)/.keys()/.items()/.values()raiseAttributeError;"k" in result/iter(result)/len(result)raiseTypeError. Only attribute access (result.status,guide.keywords, …) andto_public_dict()survive.ResearchStatusstays astr-enum, sostatus == "completed"keeps working. research.wait_for_completion(interval=...)removed (#1254). The deprecatedinterval=keyword alias is gone (its v0.7.0DeprecationWarningcycle is complete); passing it now raises the standardTypeErrorfor an unexpected keyword. Useinitial_interval=(same poll cadence).generate mind-mapdefaults to interactive (#1272). The CLInotebooklm generate mind-map <nb>(andartifact/downloadmind-map paths) now default--kindtointeractiveinstead of the note-backed JSON map. Pass--kind note-backedto keep the note-backed behavior.sources.refresh()/chat.delete_conversation()returnNone(#1290). Both previously returnedTrueon success (uninformative — any failure raised first); they now returnNoneand their annotations change from-> boolto-> None.chat.clear_cache(...)is deliberately unchanged and stays-> bool(its bool is meaningful).- Synchronous generation-kickoff refusals raise (#1342).
artifacts.generate_*andrevise_slideno longer swallow aUSER_DISPLAYABLE_ERRORrefusal into aGenerationStatus(status="failed")— they re-raise the underlyingRateLimitError/RPCError._parse_generation_resultraisesArtifactFeatureUnavailableError/DecodingErroron a missing artifact id.research.startraisesDecodingErroron an empty / non-list payload or a falseytask_id(return type narrows fromResearchStart | NonetoResearchStart). The publicartifacts.with_rate_limit_retryhelper retries only on a raisedRateLimitErrorand re-raises on budget exhaustion (a returned rate-limited status is no longer a retry signal). - Derived-read / lister drift raises
DecodingError(#1344). A structurally-unrecognized RPC payload that previously collapsed to an empty value now raisesDecodingError, so callers can distinguish a genuine miss from server-side shape drift:sources.check_freshness(), the note lister, and the artifact raw lister reject malformed-but-truthy payloads. Legitimate empty / stale shapes are unchanged. - Mutate-existing ops fail loud on a missing target (#1362).
notes.updatepreflights existence and raisesNoteNotFoundErrorbefore firing the update RPC;sources.rename(..., return_object=False)andartifacts.rename(..., return_object=False)run the existence preflight on theFalsepath and raiseSourceNotFoundError/ArtifactNotFoundErroron a miss.return_object=Falsestill returnsNoneon success. NotebooksAPI.share()removed + research poll/wait raise on ambiguity (#1363). The deprecatedclient.notebooks.share()is gone — useclient.sharing.set_public(...)+client.notebooks.get_share_url(...).research.poll(task_id=None)/wait_for_completion(task_id=None)now raise the newAmbiguousResearchTaskErrorwhen two or more tasks are in flight (instead of warning and guessing); with a single in-flight task they resolve it silently.- Removed
NOTEBOOKLM_FUTURE_ERRORSand the deprecation machinery (#1365). The forward-compat preview gate and thewarn_get_returns_none/deprecated_kwarg/MappingCompatMixindeprecation helpers are deleted now that every break they previewed is the default.warn_deprecatedandNOTEBOOKLM_QUIET_DEPRECATIONSremain for future one-off deprecations.
0.7.3 - 2026-06-29
Maintenance patch on the 0.7.x line. Backports fixes from main
(cherry-picked ahead of the v0.8.0 breaking release).
Fixed
- Markdown (
.md) uploads no longer fail on Python 3.10 (backport of #1628; fixes #1627).mimetypes.guess_typereturnsNonefor.mdon Python 3.10 and on hosts without a populated/etc/mime.types, so the upload fell back toapplication/octet-stream; NotebookLM could not infer a parser and processing failed server-side (status=ERROR), surfacing as "Error uploading source" /SourceProcessingError..md/.markdownare now pinned totext/markdownbefore the opaque fallback. - Clearer error when NotebookLM redirects to its region / anti-abuse access
gate (backport of #1630). A redirect to
notebooklm.googleis now classified and surfaced with an actionable message instead of an opaque failure. - Interactive
notebooklm loginno longer hangs 5 minutes and silently fails to save auth (backport of #1700; fixes #1697). The login-success detector used Playwright's defaultwait_until="load", butnotebooklm.google.comis a streaming SPA that never fires theloadevent (readyStatestaysinteractive), so the wait blocked until timeout even though sign-in had succeeded — printing "Login not detected within 5 minutes" and never writingstorage_state.json. The initial navigation and the success detector now passwait_until="commit"(the same level the existing cookie-forcing navigations already use), so login is detected as soon as the authenticated host is reached.
0.7.2 - 2026-06-18
Maintenance patch on the 0.7.x line. Backports fixes from main
(cherry-picked ahead of the v0.8.0 breaking release).
Fixed
-
Video Overview visual-style values corrected to match the live Web UI (#1594; backport of #1597).
VideoStyleused stale numeric wire values, so several named styles (Whiteboard, Kawaii, Anime, Watercolor, Heritage, Paper-craft, Classic, Custom) serialized as the wrong style on the wire. Values now match live Web UI captures, andCUSTOMis encoded the Web-UI way (the style enum slot is omitted/defaulted and the custom visual-style prompt is appended). Note: this changes the integer values ofVideoStylemembers — code that passed the raw ints must update; passing the enum members (VideoStyle.WHITEBOARD, …) is unaffected. -
Video (and other artifact) generation no longer fails immediately on cohorts that reject the legacy client-options shape (#1594; backport of #1583).
CREATE_ARTIFACTsent a minimal param-0 of[2]; the live web client sends the fuller capability envelope ([2, null, null, [1, …, [1]], [[1, 4, 8, 2, 3, 6]]]). On some accounts the backend began rejecting the short form for video generation specifically — the artifact was created and then immediately marked failed, while audio and infographic (which the backend still accepted) kept working. All artifact builders (audio, video, cinematic video, report, quiz, flashcards) now send the full envelope, matching the browser. The VCR body matcher normalizes the old and new client-options shapes so existing generation cassettes still replay. -
notebooks.create()andsources.add_url()no longer fail on already-migrated cohorts (#1548; backport of #1546). Google migrated theCREATE_NOTEBOOK(CCqFvf) and URLADD_SOURCE(izAoDd) payloads to a nested wire shape during the gradual rollout: the flat trailing template block ([2],[1]for create;[2],None,Nonefor url-add) collapsed into a nested[2, None, None, [1, …, [1]]]block, and the URL source spec gained a trailing1. Backends on already-migrated accounts began rejecting the old flat shape (status=3for create,5/9for add-source), while read paths were unaffected. Both payload builders now emit the nested shape, which is forward-compatible across migrated and un-migrated cohorts (verified live). Scope is limited to the create + add-url RPCs with exact Web-UI captures; the youtube/text/drive/fileADD_SOURCEvariants are unchanged. -
Empty notebook summary no longer raises
UnknownRPCMethodError(#1485). A brand-new, source-less notebook has no summary yet, so theSUMMARIZERPC returns an absent/Nonepayload.notebooks.get_summary()andnotebooks.get_description()now treat that routine "no summary yet" state as an empty summary ("") instead of mis-classifying it as wire-schema drift. Genuinely-malformed payloads (a present-but-non-listresult[0], a scalar, or a string where a nested list is expected) still raise.get_summarynow shares the_extract_summarydescent withget_description, so both agree on every shape. As part of the fix,safe_indexrejects astr/bytesvalue at an intermediate descent hop (it is indexable but never a valid container, so descending it would smuggle a single character past drift detection). -
download <type>no longer exits 1 with no file written (#1488). The download path listed artifacts twice — the executor listed to select the target, then each per-type download re-listed to re-find it by id — so the secondLIST_ARTIFACTScould not replay against a single-interaction VCR cassette and aborted the download. The executor now lists once and threads the already-fetched rows into the download method (which skips its redundant second list); studio downloads also no longer trigger the note-backed mind-map sub-fetch they never needed. Live behavior is unchanged for directclient.artifacts.download_*()calls.
0.7.1 - 2026-06-06
Maintenance patch on the 0.7.x line. Backports two fixes from main
(cherry-picked ahead of the v0.8.0 breaking release).
Fixed
- Chat reads no longer time out on slow shared-notebook streams (#1466,
#1468).
client.chat.asknow uses a chat-specific per-read HTTP timeout instead of inheriting the general clienttimeout, so shared notebooks that are slow to emit the first streamed byte stop raising spurious timeouts. A new optionalchat_timeoutkeyword onNotebookLMClient(...)/NotebookLMClient.from_storage(...)(and a matchingchat askCLI flag) tunes the window; it defaults to 180 s. Behavior note: chat reads now wait up to 180 s by default rather than inheriting the prior client timeout — passchat_timeout=Noneto restore the old inherit-timeoutbehavior. - Dropped a misleading byte-count-mismatch WARNING from the chunked RPC parser (#1469). The decoder no longer logs a spurious size-mismatch warning during normal chunked parsing.
Added
chat_timeoutkeyword argument on the client constructors and a correspondingchat askCLI option (additive; defaults preserve existing call sites).
0.7.0 - 2026-06-04
Highlights
- v0.8.0 error-contract runway. This release lands the additive half of a
cross-SDK convergence on "absence and refusal raise; only success and
async-lifecycle state are returned." You can adopt the forward-compatible form
today and run on both 0.7.0 and 0.8.0 with no flag day:
- Test your code against 0.8.0 today — set
NOTEBOOKLM_FUTURE_ERRORS=1to opt your process into the v0.8.0 error contract (get()raises*NotFoundErroron a miss, all dict-style access on the typed returns ([...],.get(),in,.keys(), …) raises, and the deprecatedwait_for_completion(interval=...)alias raises) without changing default behavior. Run your test suite with it on to find breakage before you upgrade. This is the "test-before-you-migrate" mechanism paired with the Upgrading to v0.8.0 guide. get_or_none()— a new, silent optional lookup onsources/artifacts/notes/mind_mapsthat returns the object orNoneand never warns. It is the sanctioned replacement for the now-softget()-returns-Nonepattern.get()now warns on a miss (still returnsNonethis release) and will raise the typed*NotFoundErrorfor its domain in v0.8.0 (#1247).- Typed
*NotFoundErrorper domain —NoteNotFoundError/MindMapNotFoundErrorjoin the existing source / artifact / notebook errors, all catchable via theNotFoundErrorumbrella.
- Test your code against 0.8.0 today — set
- Breaking:
rename()returns the renamed object;delete()returnsNone.rename()now re-fetches and returns the live object (raising*NotFoundErroron a missing target), anddelete()returnsNoneand is idempotent on an already-absent target. See Breaking changes below before upgrading. - Typed dataclass returns for
research.poll/start/wait_for_completion,artifacts.generate_mind_map, andsources.get_guide(ResearchStatus,ResearchTask,ResearchSource,ResearchStart,MindMapResult,SourceGuide) — attribute access instead of untyped dicts, with a backward-compatible read-only mapping bridge. - Unified
client.mind_mapssurface over both backends (note-backed + interactive), plusclient.artifacts.retry_failed()to retry a failed Studio artifact in place (and a matchingnotebooklm artifact retrycommand).
Breaking changes
⚠ BREAKING —
rename()returns the renamed object;delete()returnsNone.These return-type changes ship now, as a clean break with no deprecation runway, because the old returns were never usable contracts a caller could depend on in good faith. (Contrast
get()'sNone-on-miss, which is a real, documented contract and keeps its full deprecation runway to v0.8.0 — see issue #1247. The coherent story: reads/renames are missing-strict; deletes are absence-idempotent.)
rename()→ returns the renamed object, raises*NotFoundErroron a missing target (issues #1255, #1256):
artifacts.renamepreviously returnedNoneeven on success (an unusable return); it now re-fetches and returns the renamedArtifact, raisingArtifactNotFoundErrorwhen the target is absent.sources.renamepreviously fabricated an unverifiedSource(id, title)when the RPC echoed nothing (a silent-false-success bug); it now prefers theUPDATE_SOURCEecho, falls back to an internal fetch, returns the realSource, and raisesSourceNotFoundErrorwhen the target is absent. The fabrication is gone.notebooks.renamealready returned the re-fetchedNotebook(the reference behavior) — unchanged.mind_maps.rename(both note-backed and interactive backings) now returns the renamedMindMapand raises on a missing target.- Error taxonomy: only genuine absence (empty-payload / absent-from-list, detected via a content/list lookup — not a transport 404) maps to a
*NotFoundError. Transport /429/5xx/ auth errors propagate as themselves and are never laundered into a synthetic*NotFoundError.- Bulk opt-out: every
rename()acceptsreturn_object: bool = True. Passreturn_object=Falseto skip the hydrate re-fetch and returnNone(artifacts' re-fetch is a fullLIST_ARTIFACTS, so bulk renamers that ignore the return should opt out to avoid N extra list calls).
delete()→ returnsNone, idempotent on a missing target (issue #1211):
notebooks/sources/artifacts/notes.deleteandnotes.delete_mind_map(andmind_maps.delete) previously returned a hardcodedTrue; they now returnNone. The oldTruewas a tautology (neverFalse), butTrue → Noneis a real, observable flip from truthy to falsy:Drop the# BEFORE (entered the block; delete always returned True) if await client.sources.delete(nb_id, src_id): ... # this branch no longer runs — delete() now returns None (falsy)if; calldelete()for its effect. Useget()first if you need to assert existence.- Idempotent: deleting an already-absent target succeeds (returns
None); it does not raise*NotFoundError. This matches HTTPDELETEidempotency and keeps retry/teardown loops clean. (The one exception ismind_maps.deletewithout an explicitkind, which must list to pick the right RPC family and so raisesValueErrorfor an unknown id; passkind=to delete idempotently.)- Real failures still raise:
allow_null=Truetolerates only a null result, not an RPC/HTTP error — a403/5xx/ auth / transport failure on delete still propagates. "Idempotent on missing" is not "swallow all errors."
⚠ BREAKING — lapsed v0.6.0-targeted deprecations removed.
These deprecation shims advertised removal in v0.6.0, which has shipped, so they have now been removed. This is a pre-1.0 breaking change. See
docs/deprecations.md"Removed in v0.7.0".
- Positional
wait/wait_timeoutonSourcesAPI.add_url/add_text/add_file/add_drive— these parameters are now keyword-only. Passing them positionally raisesTypeError.# BEFORE (deprecated, emitted DeprecationWarning) await client.sources.add_url(nb_id, url, True, 45.0) # AFTER await client.sources.add_url(nb_id, url, wait=True, wait_timeout=45.0)ArtifactsAPI.wait_for_completion(poll_interval=...)— the deprecatedpoll_intervalalias was removed; useinitial_interval=...(same cadence). Passingpoll_intervalraisesTypeError.# BEFORE await client.artifacts.wait_for_completion(nb_id, task_id, poll_interval=5.0) # AFTER await client.artifacts.wait_for_completion(nb_id, task_id, initial_interval=5.0)NOTEBOOKLM_STRICT_DECODE=0soft-mode opt-out — removed. Strict decoding is now the only mode: schema-drift helpers (notablysafe_index) always raiseUnknownRPCMethodErroron shape drift instead of warn-and-returningNone/[]. The env var is now ignored (no-op). Callers that previously relied on the soft fallback should handleUnknownRPCMethodError(a subclass ofRPCError/DecodingError).NotesAPI.create_from_chat(...)— removed (deprecated since v0.5.0, two MINOR cycles of warnings served; the documented removal target was v0.7.0). It was a pure forwarder. UseChatAPI.save_answer_as_note(...), the canonical citation-rich saved-from-chat method and data owner (ADR-0013):await client.chat.save_answer_as_note(nb_id, ask_result). The now-unusedsave_chat_answerinjection plumbing onNotesAPIwas removed with it.Not removed:
SourcesAPI.add_file(mime_type=...)andnotebooklm source add --mime-type(file sources) were reassessed and kept —mime_typewas re-wired to set the resumable-upload content-type header (overriding filename-extension inference), so it is a supported parameter, not a dead shim. Its staleDeprecationWarninghad already been removed; the documentation now reflects this.Not removed: awaiting
NotebookLMClient.from_storage(...)still works — its deprecation targets v1.0, not v0.6.0.
Added
get_or_none()— the sanctioned silent optional lookup, added toclient.sources/client.artifacts/client.notes/client.mind_maps. It returns the entity (Source/Artifact/Note/MindMap) orNonefor a genuine absence and never warns, making it the drop-in migration target for the now-deprecatedget()-returns-Nonepattern (see Deprecated below; issue #1247). Unlikeget(), it does not swallow transport, auth, or decode faults — only a real "not found" yieldsNone.Additive (ADR-0019; issue #1247).# Silent optional lookup (no DeprecationWarning): src = await client.sources.get_or_none(nb_id, source_id) if src is None: ...NOTEBOOKLM_FUTURE_ERRORSopt-in preview flag — run the v0.8.0 error contract early to test forward-compatibility before the breaking flips ship (ADR-0019 / umbrella #1346). Default-off and byte-identical to current v0.7.0 behavior; when truthy (1/true/yes/on) the three warn-runways adopt their v0.8.0 raise-target:sources.get/artifacts.get/notes.get/mind_maps.getraise the matching*NotFoundErroron a miss (#1247), the wholeMappingCompatMixinmapping surface —[...]subscript plus the silentget/keys/items/values/len/in/itershims — raises the exact error a bare dataclass would (#1251), and the deprecatedResearchAPI.wait_for_completion(interval=...)alias raisesTypeError(#1254). Takes precedence overNOTEBOOKLM_QUIET_DEPRECATIONS(a runway raises regardless of quiet). The fourget()methods are now routed through a single_lookup.resolve_getbridge, eliminating the hand-duplicated warn-on-miss pattern. Helper:notebooklm._deprecation.future_errors_enabled. The flag now also previews the purely-behavioral v0.8.0 changes that have no warn-runway (#1405): the uninformativeboolreturns ofsources.refreshandchat.delete_conversationbecomeNone(#1290); a synchronous generation refusal raises the decoder'sRateLimitError/RPCError/DecodingError/ArtifactFeatureUnavailableErrorinstead of being swallowed intoGenerationStatus(status="failed")/ returnedNone— across_call_generate,revise_slide,_parse_generation_result, andresearch.start(#1342); and the mutate-existing opsnotes.updateandsources/artifactsrename(return_object=False)fail loud with a*NotFoundErroron a missing target (#1362). These previews are runtime-only — no public return annotation changes until the v0.8.0 flip — so default-off stays byte-identical. Does not close #1247/#1251/#1254/#1290/#1342/#1362 — the runways and current behavior remain until the v0.8.0 flip. Seedocs/deprecations.md. Additive (issues #1346, #1405).client.artifacts.retry_failed(notebook_id, artifact_id)— retry a failed Studio artifact in place (the web UI "Retry" action), via the newRETRY_ARTIFACT(Rytqqe) RPC. The artifact is not deleted first and the sameartifact_idis preserved, so existingpoll_status()/wait_for_completion()flows keep working. Follows the ADR-0019 "async kickoff" contract: an accepted retry returnsGenerationStatus(status="in_progress"), while a synchronous refusal (USER_DISPLAYABLE_ERROR— rate limit / quota / not-retryable) raises the underlyingRateLimitError/RPCErrorrather than returning astatus="failed"handle. Newnotebooklm artifact retry <artifact_id> [--wait] [--json]CLI command. Additive (issues #1319, #1346).notebooklm.artifacts.with_rate_limit_retrynow also retries when the wrapped callable raisesRateLimitError(backing off and re-raising once the retry budget is exhausted), so it can wrap the newretry_failed. The existing returned-rate-limited-GenerationStatuspath (used bygenerate_*) is unchanged — this is a backward-compatible addition (issue #1319).- New public exception types for the note and mind-map domains, mirroring the
existing
SourceError/SourceNotFoundErrorshape:NoteError+NoteNotFoundErrorandMindMapError+MindMapNotFoundError. Each*NotFoundErroris a triple-base(NotFoundError, RPCError, <Domain>Error), so it is catchable via the cross-domainNotFoundErrorumbrella, at transport-levelexcept RPCErrorcall sites, and at domain-levelexcept NoteError/except MindMapErrorcall sites. These are the prerequisite for the mind-map not-found work (ADR-0019; issues #1291, #1346).MindMapNotFoundErroris now raised by themind_mapsmutation paths (see Changed below);NoteNotFoundErroris not raised by any method yet. ResearchStatus.NOT_FOUND— a typed lifecycle sentinel for the poll-observed absence of a specific requested research task, distinct fromNO_RESEARCH("nothing in flight").research.poll(notebook_id, task_id=...)now returnsResearchTask.not_found(task_id)(statusNOT_FOUND, carrying the requested id) when a non-empty pinnedtask_idmatches no in-flight task; the unfilteredtask_id=Noneempty poll still returnsNO_RESEARCHunchanged. Additive and non-breaking — the poll never raises for an absent task (ADR-0019 Rule 4; issues #1344, #1346).- Typed return values for the research / mind-map / source-guide methods.
research.poll/research.start/research.wait_for_completion,artifacts.generate_mind_map, andsources.get_guidenow return typed dataclasses instead of untypeddict[str, Any], with a newResearchStatusstr-enum for the status field. The new public types are exported fromnotebooklmandnotebooklm.types:ResearchStatus,ResearchTask,ResearchSource,ResearchStart,MindMapResult, andSourceGuide.This is backward-compatible:from notebooklm import ResearchStatus result = await client.research.poll(nb_id) if result.status == ResearchStatus.COMPLETED: # also == "completed" for source in result.sources: print(source.title, source.url) guide = await client.sources.get_guide(nb_id, src_id) print(guide.summary, guide.keywords) mind_map = await client.artifacts.generate_mind_map(nb_id) print(mind_map.note_id, mind_map.mind_map)ResearchStatusis astrenum (sostatus == "completed"still holds), and the returned dataclasses keep working as read-only mappings —result["status"]/result.get("status")/result.keys()/"status" in resultall still work (subscript emits aDeprecationWarning; see Deprecated below). The dict-subscript bridge is removed in v0.8.0. WaitTimeoutError— one catchable base for every wait/poll timeout. A new public exception (notebooklm.WaitTimeoutError) is the common base ofSourceTimeoutError,ArtifactTimeoutError(and itsArtifactPendingTimeoutError/ArtifactInProgressTimeoutErrorsubclasses), and the newResearchTimeoutError, so a singleexcept WaitTimeoutErrorclause catches a wait timeout from any domain. It mixes in the built-inTimeoutError, so this is fully backward-compatible: existingexcept TimeoutErrorclauses keep catching every wait timeout unchanged.from notebooklm import WaitTimeoutError try: await client.sources.wait_until_ready(nb_id, src_id) await client.artifacts.wait_for_completion(nb_id, task_id) await client.research.wait_for_completion(nb_id, research_task_id) except WaitTimeoutError: # was three separate / inconsistent timeout types ...ResearchError/ResearchTimeoutError. The research domain gained a catchable base (ResearchError, mirroringSourceError/ArtifactError) and a domain timeout (ResearchTimeoutError).ResearchAPI.wait_for_completionpreviously raised the bare built-inTimeoutError; it now raisesResearchTimeoutError, aWaitTimeoutError(and therefore still aTimeoutError), exposingnotebook_id/task_id/timeout/timeout_seconds/last_status. (ResearchTaskMismatchErrorstays aValidationError— it is caller-input validation, not a wait timeout.)
Changed / Deprecated
ArtifactTimeoutErrornow declares its bases umbrella-first (WaitTimeoutError, ArtifactError), matchingSourceTimeoutErrorandResearchTimeoutError. This is a cosmetic reorder with no behavior change:isinstance/exceptagainst either base is unaffected.client.mind_mapsmutation sites now raiseMindMapNotFoundErrorinstead of a bareValueErroron a missing target, so callers canexcept NotFoundError(orexcept MindMapError) uniformly across namespaces.rename(and the underlying note-backedrename_mind_map) raise it;MindMapNotFoundErrormulti-inheritsValueError's siblingNotFoundError, notValueErroritself, so existingexcept ValueErrorrename callers must switch toexcept NotFoundError/except MindMapNotFoundError.delete(kind=None)is now idempotent — deleting an already-absent mind map returnsNonerather than raising (matchingsources/artifacts/notesdelete, and thekind-supplied path).get_treereturnsNonefor a missing mind map (it is a derived read that does not police parent existence) — previouslykind=Noneraised on an unknown id. Shape-drift in the interactive payload still raisesUnknownRPCMethodError(ADR-0019; issues #1291, #1346).client.mind_maps.generate(kind=MindMapKind.INTERACTIVE)now raisesArtifactFeatureUnavailableError(instead of a bareArtifactError) when theCREATE_ARTIFACTcall returns no artifact id — no generation task was created. Non-breaking forexcept ArtifactError:ArtifactFeatureUnavailableErroris a subclass ofArtifactError, so that catch still works. (It also multi-inheritsRPCError, so a handler that doesexcept RPCErrorbeforeexcept ArtifactErrorwill now take theRPCErrorbranch — the same MRO the siblinggenerate_*/retry_failednull-create paths already produce.) This aligns the interactive async kickoff with that sibling null-create contract (ADR-0019 "async kickoff"; issue #1359).- Documented two pre-existing
client.mind_mapsread semantics (docs-only, no behavior change):list()populatesMindMap.treeonly for note-backed entries — interactive entries carrytree=None("not fetched", not "empty"; callget_tree(..., kind=MindMapKind.INTERACTIVE)to fetch one); and the explicitget_tree(..., kind=MindMapKind.INTERACTIVE)path delegates absence detection to the RPC, so a missing id's value is server-dependent (returnsNonetoday) rather than enforced client-side (issues #1355, #1359). ResearchAPI.wait_for_completion(interval=...)→initial_interval=.... The research waiter's poll-cadence keyword is nowinitial_interval, matchingSourcesAPI.wait_until_readyandArtifactsAPI.wait_for_completion. The oldinterval=keyword still works as a deprecated alias (warns in 0.7.0, removed in v0.8.0): passing a non-default value emits aDeprecationWarning(suppressible withNOTEBOOKLM_QUIET_DEPRECATIONS=1), and passing bothintervalandinitial_intervalraisesTypeError. Default-shape calls stay silent and the signature is otherwise unchanged, so the public-API compatibility audit stays clean. Seedocs/deprecations.mdfor the migration.Decision —
wait_timeoutkept. Thewait_timeoutkeyword on theSourcesAPI.add_*family was deliberately not renamed totimeout: on those add methodstimeoutwould be ambiguous with a per-request HTTP timeout, whereas the dedicated waiter methods already spell their budgettimeout. The researchinterval→initial_intervalrename was the only standardization with a clear, unambiguous win.
Deprecated
Every deprecation below is on a compatibility runway to v0.8.0. The consolidated Upgrading to v0.8.0 guide is the single reference for moving your code across the boundary; set
NOTEBOOKLM_FUTURE_ERRORS=1to exercise the v0.8.0 behavior in your tests today.
client.mind_maps.get()returningNonefor a missing mind map is now deprecated, closing the runway gap that leftmind_mapsas the only #1247-cohort namespace without one. It now emits aDeprecationWarningon a miss while still returningNone(behavior unchanged this release), matchingsources.get()/artifacts.get()/notes.get(). In v0.8.0 it will instead raiseMindMapNotFoundError. Useget_or_none()for the sanctioned optional lookup (it stays silent), or migrate theNone-check to atry/except MindMapNotFoundError. The warning fires only on a miss; suppress it withNOTEBOOKLM_QUIET_DEPRECATIONS=1. Tracking issue: #1247 (gap: #1358). Seedocs/deprecations.md.sources.get()/artifacts.get()/notes.get()returningNonefor a missing entity is deprecated. These three methods now emit aDeprecationWarningon a miss while still returningNone(behavior is unchanged this release). In v0.8.0 they will instead raise the matching*NotFoundError(SourceNotFoundError/ArtifactNotFoundError/NoteNotFoundError), unifying the not-found contract withnotebooks.get(), which already raisesNotebookNotFoundError. Tracking issue: #1247.The warning fires only on a miss; successful lookups stay silent. Suppress it with# Migrate the None-check to a try/except before v0.8.0: # BEFORE (deprecated) src = await client.sources.get(nb_id, source_id) if src is None: ... # AFTER try: src = await client.sources.get(nb_id, source_id) except SourceNotFoundError: ...NOTEBOOKLM_QUIET_DEPRECATIONS=1. Seedocs/deprecations.md.- Dict-subscript access on the new typed research / mind-map / guide
returns is deprecated. Now that
research.poll/research.start/research.wait_for_completion,artifacts.generate_mind_map, andsources.get_guidereturn typed dataclasses (see Added), the legacyresult["status"]dict-subscript access emits aDeprecationWarningand will be removed in v0.8.0. Migrate to attribute access (result.status). The silentresult.get(...)/result.keys()/"x" in resultmapping shims also disappear in v0.8.0. Suppress the warning withNOTEBOOKLM_QUIET_DEPRECATIONS=1. Seedocs/deprecations.md.# BEFORE (still works in 0.7.0, warns on subscript) if result["status"] == "completed": sources = result["sources"] # AFTER if result.status == "completed": sources = result.sources
Fixed
- CLI now emits the
NOT_FOUNDerror envelope for the*NotFoundErrorfamily from the centralized handler, instead of the genericNOTEBOOKLM_ERROR. AnyNotebookNotFoundError/SourceNotFoundError/ArtifactNotFoundError/NoteNotFoundError/MindMapNotFoundErrorthat reachescli/error_handler.py(e.g.notebooks.get()on a missing notebook, or arenamewhose target was deleted mid-operation) now exits1with the typed{"error": true, "code": "NOT_FOUND", ...}JSON envelope carrying the missing resource id — matching the per-commandsource/artifact/note getconvention (the documented CLI not-found contract since v0.5.0). The per-commandgetpaths already usedget_or_noneand are unaffected. This also makes theNOTEBOOKLM_FUTURE_ERRORS=1preview faithful at the CLI boundary, pre-positioning it for the v0.8.0get()→ raise / mutate-existing fail-loud flips (issues #1364, #1247, #1362). Source.from_api_responsenow reports the real processingstatus. TheADD_SOURCE/ rename parsing path previously never read the status block and always fell back toSourceStatus.READY, whileclient.sources.list()/get()and the source poller read the decoded status. Both parsers now funnel through a singleSource.from_rowconstruction site, so aSourceproduced from an add/rename response carries the samestatus(andurl/created_at) as the listing path. TheSource.statusfield annotation was also corrected frominttoSourceStatus(still anint-compatible enum).
0.6.0 - 2026-05-29
Breaking changes
⚠ BREAKING — exception hierarchy symmetry restored.
SourceNotFoundErrorandArtifactNotFoundErrornow inherit fromRPCErrorin addition to their respective domain bases (SourceError,ArtifactError), restoring symmetry withNotebookNotFoundErrorwhich has mixed inRPCErrorsince the 0.5.x series. Combined with the newNotFoundErrorumbrella (see Added below), the class declarations are now:class NotebookNotFoundError(NotFoundError, RPCError, NotebookError): ... class SourceNotFoundError(NotFoundError, RPCError, SourceError): ... # new RPCError mixin in 0.6.0 class ArtifactNotFoundError(NotFoundError, RPCError, ArtifactError): ... # new RPCError mixin in 0.6.0Migration. Code that catches the broad
RPCErrorbefore a more specificSourceNotFoundError/ArtifactNotFoundErrorclause now routes "not found" through the broad branch instead of falling through to the specific one. Reorder yourexceptclauses so the more specific exceptions come first.The example below uses
client.sources.get_fulltext(...), which raisesSourceNotFoundErrorfor a missing source. (client.sources.get(...)returnsNoneand does not raise, so it doesn't demonstrate the change.)# BEFORE — in 0.5.x this layout worked: SourceNotFoundError was NOT an # RPCError, so it fell through the broad `except RPCError` to the specific # handler. In 0.6.0 the broad handler catches it first, leaving the # specific `except SourceNotFoundError` clause unreachable. try: fulltext = await client.sources.get_fulltext(notebook_id, source_id) except RPCError as e: # ← in 0.6.0 this also catches SourceNotFoundError handle_rpc_failure(e) except SourceNotFoundError: # ← in 0.6.0 this branch becomes unreachable handle_missing_source() # AFTER — put the specific exception first so the broad branch only sees # other RPC failures. try: fulltext = await client.sources.get_fulltext(notebook_id, source_id) except SourceNotFoundError: handle_missing_source() except RPCError as e: handle_rpc_failure(e)Code that catches
SourceNotFoundError/ArtifactNotFoundErrordirectly, or catches via the domain bases (SourceError,ArtifactError), or via the sharedNotebookLMErrorbase, continues to behave exactly as before. Only theRPCError-before-specific ordering is affected.
SourceNotFoundError.__init__andArtifactNotFoundError.__init__also now accept keyword-onlymethod_id/raw_responseparameters (forwarded to theRPCErrorparent), matching theNotebookNotFoundErrorsignature. All positional call sites remain source-compatible.
notebooklm source stale <ID>now follows the standard CLI exit-code convention by default. Exit0indicates the freshness check succeeded (regardless of whether the source is fresh or stale); exit1indicates an error. Previously the command used an inverted predicate (0= stale,1= fresh) so the shell idiomif notebooklm source stale ID; then refresh; fiworked naturally. Migration: scripts that depended on the inverted predicate can opt back into the legacy semantics with the new--exit-on-staleflag (if notebooklm source stale --exit-on-stale ID; then refresh; fi). Scripts written for the new default should branch on the JSONstale/freshfields or stdout text. Seedocs/cli-exit-codes.mdfor the full rationale + the newExit code semanticssummary.NotebookLMClient.rpc_call(...)no longer acceptssource_path,_is_retry, oroperation_variant— the three kwargs deprecated in v0.5.0 (docs/deprecations.md) were removed after one MINOR cycle. The public escape hatch's primary contract (client.rpc_call(method, params)) is unchanged and the default-shape call keeps working with no migration. Migration:- Keyword callers: drop the removed kwarg from the call. The previous default-shape behavior (
source_path="/",_is_retry=False,operation_variant=None) is now what every call gets unconditionally —source_pathwas a leaky internal seam,_is_retrywas an internal retry-loop flag, andoperation_variantis part of the mutating-RPC idempotency registry. Calls that genuinely needed a non-"/"source_pathor a specificoperation_variantwere already on the wrong layer; build a typed method on a sub-client instead, or open an issue describing the workflow. - Positional callers (rare): the positional order of the remaining parameters is
(method, params, allow_null, *, disable_internal_retries=...), so a previously-positionalsource_path/_is_retryargument now binds to a different parameter slot. A pre-cutclient.rpc_call(method, params, "/", True)(which passedsource_path="/",allow_null=True) becomesclient.rpc_call(method, params, allow_null=True)after the cut — switch to keyword arguments forallow_nullto avoid this footgun. - There is no public replacement for the removed internal-only kwargs (
_is_retry,operation_variant); they were never part of the supported surface in the first place.
- Keyword callers: drop the removed kwarg from the call. The previous default-shape behavior (
source add --urlrejects internal hosts by default (SSRF guard).localhost,127.0.0.1, RFC-1918, and link-local URLs — and any non-http(s)scheme — are now refused before ingestion. Migration: pass the new--allow-internalflag to ingest an internalhttp(s)URL intentionally (the scheme allowlist still applies). Full detail in Security below (#1114).sourceCLI--jsonoutput shape changed.source get --jsonnow emits the bare kind value ("type": "url") instead of the leaked Python enum repr ("type": "SourceType.URL"), andsource fulltext --jsonemits a fixed{source_id, title, kind, content, url, char_count}payload instead of a rawasdict(SourceFulltext)dump. Migration:--jsonconsumers parsingsource get'stypefield, or relying on extrafulltextkeys, must update. Full detail in Fixed below (#1129).- Post-parse CLI validation errors exit
1(was2) and print a JSON envelope on stdout under--json. Fordownloadflag conflicts,generatevalidation,research wait --cited-only, andask --new+--conversation-id, a--jsoninvocation now emits{"error": true, "code": "VALIDATION_ERROR", ...}on stdout and exits1instead of Click's stderr usage text + exit2. Text-mode behavior is unchanged. Migration: automation parsing these--jsonfailures should branch on exit1+ the JSON body. Full detail in Changed below (ADR-0015; #1112, #1115, #1117).
Added
notebooklm source stale --exit-on-staleflag — opt-in back-compat for the legacy inverted-predicate exit codes (0= stale,1= fresh). The default behavior is now the standard CLI convention (see Breaking changes above); pass--exit-on-staleto keepif notebooklm source stale --exit-on-stale ID; then refresh; fishell idioms working.Exit code semanticssummary section indocs/cli-exit-codes.md. A normative one-line table —0= succeeded as documented,1= failed or queried target not found,2= Click parser-time error — backing the convention every command obeys outside the documented intentional exceptions. Cross-references the existing tables and ADR-0015.NotFoundErrorcross-domain umbrella exception. CatchNotFoundErrorto handle any "resource not found" case across notebooks, sources, and artifacts in oneexceptclause — replacingexcept (NotebookNotFoundError, SourceNotFoundError, ArtifactNotFoundError):.NotebookNotFoundError,SourceNotFoundError, andArtifactNotFoundErrorall inherit fromNotFoundError. The umbrella itself is additive; the asymmetric inheritance noted on its original introduction has been resolved in the same release — all three subclasses also mix inRPCError(see Breaking changes above for theexcept-ordering migration).notebooklm notebook delete --json(#1167).notebook deletewas the last delete command (and the onlylist/create/metadatasibling) without a JSON envelope — passing--jsoncrashed withNo such option. It now emits the typed success/cancel envelope, refuses to prompt in--jsonmode (requiring--yes, else aVALIDATION_ERRORenvelope + exit1), and surfacescontext_cleared: truewhen the deleted notebook was the active context (#1193).notebooklm skill install --dry-run/--no-clobber/--force(#1109). Project-scope installs now classify each target as create / up-to-date / overwrite. A target that would be overwritten with different content exits1and lists the conflicts unless--force(overwrite) or--no-clobber(skip differing, still create missing) is passed;--dry-runpreviews intended writes without touching disk. Writes go through an atomic temp-file +os.replaceso a crash can't leave a partialSKILL.md. User scope keeps the historical always-overwrite behavior (the new flags error when paired with--scope user).GenerationStatus.is_removed+status="removed"(#1168). A delisted or quota-removed artifact now reportsstatus="removed"(is_removed=True) instead of a synthesized"failed", so callers can distinguish a transient list omission from a server-marked FAILED artifact.is_failedstaysFalsefor a removal;is_rate_limitedstill treats a quota-worded removal as retryable, and CLI exit behavior is unchanged (#1195).- Structured media-timeout diagnostics. When an accepted media task (audio / video / cinematic-video / infographic / slide-deck) stays queued or running past the
--wait/wait_for_completionbudget, the artifact APIs now raise a typed timeout exception that preserves the last poll-status transition and media-not-ready metadata (also surfaced in--json) instead of a bare timeout (#1094).
Changed
- Media
--waitdefault timeouts raised.generate audio --waitnow defaults to 1200 s (#1140) and the video / cinematic-video wait defaults were increased to match empirical generation durations (#1088, #1094), so long generations no longer time out before the artifact is ready under default settings.docs/now documents the media wait budgets and the manualartifact waitrecovery path. notebooklm doctorexits1when any check fails (#1160). It previously builtstatus: "fail"entries but always exited0, so CI health checks,set -escripts, and monitoring probes read a broken install as green. Overall health is now computed from the final check states (after any--fix) and the process exits1if any check still fails (warnings stay non-fatal). The exit happens after the payload/table is emitted, so machine-readable--jsonoutput is unaffected;doctorprofile JSON errors are also now wrapped in the typed envelope (#1179, #1146).- Post-parse CLI validation errors emit the typed JSON envelope under
--json(ADR-0015).downloadflag conflicts (--force+--no-clobber,--latest+--earliest,--all+--artifact),generatevalidation (cinematic--format/--styleconflicts, invalid--language/NOTEBOOKLM_HL),research wait --cited-onlywithout--import-all, andask --new+--conversation-idnow route through{"error": true, "code": "VALIDATION_ERROR", ...}on stdout and exit1under--json, instead of Click's parser bypassing the envelope to exit2with usage text on stderr. Text-mode behavior (usage text, exit2) is unchanged. Flagged under Breaking changes above for--jsonautomation (#1112, #1115, #1117).
Fixed
notebooklm artifact delete <id> --jsonnow requires--yesbefore deleting (#1197). Without--yes, the command emits the typedVALIDATION_ERRORenvelope, includes"deleted": false, exits1, and leaves the artifact untouched, matching the other destructive delete commands.- HTML file uploads now fail client-side with a clear validation error (#1127).
notebooklm source add ./article.htmlandclient.sources.add_file(..., "article.html")previously reached NotebookLM's upload endpoint astext/htmland surfaced a cryptic upstream400 Bad Request. The upload pipeline now rejects.html/.htm/.xhtml/.xht/ HTML MIME uploads before registering a source, with guidance to convert the page to.txt,.md, or.pdf. notebooklm source fulltext -o FILEno longer silently overwrites existing files (#1173). Existing output paths now auto-rename by default (FILE->FILE (2), etc.); pass--forceto overwrite intentionally or--no-clobberto fail when the path already exists.sources.list()raises on a malformedGET_NOTEBOOKresponse under strict-decode (the default) (#1159). A drifted or error-enveloped response was previously folded into an empty list, so a sync script could conclude every source had vanished and re-add them all. The hand-rolled list-shape checks now honorNOTEBOOKLM_STRICT_DECODE(logging the drift warning, then raisingRPCError); a genuinely empty notebook (aNonesources slot) still returns[]. SetNOTEBOOKLM_STRICT_DECODE=0for the legacy warn-and-return-[]fallback (#1178).client.rpc_call(..., allow_null=True)raises on method-ID drift and anti-bot walls (#1158). The decoder gated its entire null-handling block behindnot allow_null, so opt-in null callers (CREATE_ARTIFACT,GENERATE_MIND_MAP,DELETE_SOURCE,GET_SUGGESTED_REPORTS, …) silently receivedNonewhen Google rotated a method ID or served a redirect / anti-bot page. An absent RPC ID (drift) and a body with no RPC frames (anti-bot wall) now always raise; only a present-but-nullwrb.frframe returnsNone. Null-result error messages now embed the discoveredfound_ids(#1176).- Auth-refresh replay no longer re-issues non-idempotent writes (#1157). After a mid-flight auth error (HTTP 401/403, or an auth-shaped decoded
RPCError) on a probe-then-create method (CREATE_NOTEBOOK,CREATE_ARTIFACT,CREATE_NOTE,ADD_SOURCE,SHARE_NOTEBOOK,GENERATE_MIND_MAP), the refresh-and-retry path could duplicate the resource, invite email, or generation quota when the error landed after the server committed the write. Both replay paths (theAuthRefreshMiddleware401/403 leg and theRpcExecutordecode-time leg) now honor the effectivedisable_internal_retriesclassification and propagate the original auth error so the caller's probe-then-create wrapper can disambiguate a commit-lost write (#1177). client.notes.createraises whenCREATE_NOTEreturns no usable note id (#1162). It previously fell through to a success-shapedNote(id="")that was never finalized viaUPDATE_NOTEor persisted server-side, so any later operation keyed on the empty id silently misbehaved. It now raisesRPCError, matching the siblingadd_source/notebooks.createpaths (#1186).- Stale authed-POST envelope rebuilt after a
401 → refresh → 429 → retryflow (#1096). The terminal freshness guard's snapshot-equality short-circuit could POST the pre-refresh URL / headers / body against the refreshed cookie jar; the envelope is now rebuilt from a freshly captured auth snapshot on every terminal attempt (byte-identical on the happy path, load-bearing on the post-refresh retry). NotebookLMClient.close(drain=True)no longer hangs on in-flight artifact polls (#1161). Registered drain hooks (which cancel polls parked inoperation_scope) now fire before the drain wait, soclose()short-circuits a pending poll instead of blocking up to the poll's own 300 s timeout (#1182).Kernel.open()closes the httpx client if the open-time cookie snapshot raises (#1163). A failure while capturing the open snapshot previously propagated with a live, never-closed client (Python skips__aexit__after a failed__aenter__), leaking the connection pool.open()nowaclose()s the partial client and resets it so a retry rebuilds cleanly (#1187).- RPC concurrency semaphore gains the loop-affinity guard + close→reopen reset its siblings already have (#1169). The per-client
max_concurrent_rpcssemaphore was the only loop-bound primitive without an affinity guard or reset, so reopening a capped client on a different event loop reused the stale semaphore and could raise "bound to a different event loop" or mispark waiters on Python 3.10/3.11 (masked on 3.12+). It is now guarded by the bound-loop assertion and discarded on any bound-loop change (#1196). - New conversations are serialized per notebook (#1144). Concurrent
chat.ask()calls with noconversation_idagainst the same notebook are serialized so they no longer race to create duplicate server-side conversations. - Auth-refresh lock released if the lock-wait metric raises (#1164).
await_refreshrecorded the lock-wait metric betweenacquire()and thetry, so a metric-side exception left the auth-refresh lock held forever, deadlocking every subsequent refresh. The metric call moved inside thetry/finally: release(), matching the siblingupdate_auth_tokens(#1188). - Source-upload registration fails closed on an unparseable source id (#1143). The resumable-upload path now raises instead of silently accepting a response it can't parse a source id from, while still tolerating the legacy filename-first row shapes.
- Artifact-generation defaults and null responses hardened (#1063, #1088). Omitting infographic options on the Python
client.artifacts.generate_*calls now sends concrete visual defaults (matching the CLI) instead of producing a nullCREATE_ARTIFACTresult, and a null artifact-generation response is now classified asArtifactFeatureUnavailableError. sourcecommand--jsonoutput shape corrected and stabilized (#1129).source get --jsonpreviously leaked the Python enum repr ("type": "SourceType.URL") and now emits the bare kind value ("type": "url");source fulltext --jsonnow emits a fixed{source_id, title, kind, content, url, char_count}payload instead of a rawasdict(SourceFulltext)dump, and its-oenvelope gains akindfield.--jsonconsumers that parsedsource get'stypefield or relied on extrafulltextkeys must update (flagged under Breaking changes above). Shared serializers keep the shape consistent across the source subcommands going forward.notebooklm source add -(stdin) rejects a non-text--type. Piping content from stdin with an explicit non-text source type now fails with a clear validation error instead of mis-routing the content.notebooklm agent showroutes errors to stderr (#1175) so they no longer pollute stdout.- Auth-error classification hardened (#1142) — empty RPC code labels no longer slip past the auth-error matcher.
- Malformed
batchexecutechunk records are now counted (#1141) rather than silently dropped, so theclient.metricssurface reflects partial-response drift.
Removed
NotebookLMClient.rpc_call(source_path=...),NotebookLMClient.rpc_call(_is_retry=...),NotebookLMClient.rpc_call(operation_variant=...)— see Breaking changes above. The correspondingDeprecationWarningemitters inclient.pyand thetests/unit/test_rpc_call_public_surface.pywarning-surface tests were retired in the same change.
Security
- SSRF guard on
source add --url(#1114). The prefix-onlystartswith(("http://", "https://"))check was replaced with a structuralurlsplitparse + scheme allowlist (http/httpsonly) plus a private / loopback / link-local IP guard and alocalhost-literal guard. Behavior change:http://localhost,http://127.0.0.1, RFC-1918 hosts, andhttp://169.254.169.254are now rejected by default — pass the new--allow-internalflag to ingest an internal URL intentionally (the scheme allowlist still applies). DNS is never resolved at validation time. Flagged under Breaking changes above. - Resumable upload URLs validated and redacted (#1130). The server-returned upload session / cancel URLs are validated before use and redacted in error and log output so a credentialed upload URL can't leak.
- Artifact download allowlist validated by hostname (#1172). Download host-allowlisting now parses the URL hostname structurally instead of matching a string prefix, closing a bypass where a crafted URL (including encoded-slash hosts, hardened further in #1199) could satisfy a prefix check while pointing at an untrusted host.
httpx/urllib3logs redacted for library consumers (#1166).configure_logging()now attaches a logger-levelRedactingFilterto thehttpxandurllib3loggers at import, so a consumer who enables httpx DEBUG (e.g.logging.basicConfig(level=logging.DEBUG)) no longer sees the session id in?f.sid=...request lines. Pure defense-in-depth — no handler is added, so consumers who never enable those loggers see no behavior change (#1191).- Bare CSRF / session-id token values redacted in logs (#1165). The scrubber now redacts bare
SNlM0e(CSRF) andFdrFJe(session-id)WIZ_global_datamarkers, thecsrf=form alias, and standaloneAF1_QpN-CSRF tokens — credential-equivalent shapes that previously passed throughscrub_secrets()unredacted (#1189). - Playwright login subprocess output sanitized (#1111).
ensure_chromium_installednow strips ANSI control sequences and redacts inherited environment-variable secret values (including JSON-nested leaves such asNOTEBOOKLM_AUTH_JSON) from captured subprocess stderr/stdout before surfacing install diagnostics (meta-audit G4).
0.5.0 - 2026-05-23
The first release after the v0.4.x auth cookie lifecycle series. Headline user-facing work: a top-to-bottom CLI UX overhaul (uniform --json, exit-code policy, shell completion, stdin pipes, SIGINT-resume), auth and cookie reliability hardening (inline PSIDTS cold-start recovery, fail-closed notebooklm use, concurrent-upload safety), and the v0.3-era deprecation removal cycle. Read Breaking changes below before upgrading.
Breaking changes
Items that need attention when upgrading from 0.4.x. Full migration prose lives in the natural sections below.
NOTEBOOKLM_STRICT_DECODEnow defaults to1— RPC shape drift raisesUnknownRPCMethodError(subclass ofRPCError) at the decoder boundary instead of warning and returningNone. Set=0to opt back into the legacy behavior for one release window (the soft-mode fallback itself now emitsDeprecationWarningand is scheduled for removal in v0.6.0).rate_limit_max_retriesdefault raised from0to3with exponential-backoff fallback. Programmatic users now inherit smart-retry behavior matching the CLI. Passrate_limit_max_retries=0to restore the previous immediate-RateLimitErrorbehavior. Mutating create RPCs already opt out viadisable_internal_retries=True.server_error_max_retriesdefault raised from0to3with the same exponential-backoff fallback, covering HTTP 5xx + retryable network errors (#629). Passserver_error_max_retries=0to restore immediate failure on 5xx.max_concurrent_rpcssemaphore added with default16(#630). High-fan-out callers (e.g.asyncio.gatherover 100 RPCs) are now throttled by default instead of saturating the connection pool. Passmax_concurrent_rpcs=Noneto restore unbounded fan-out. Must satisfymax_concurrent_rpcs <= ConnectionLimits.max_connections.notebooklm use <id>fails closed when the notebook doesn't exist.usenow verifies the id withNotebooksAPI.get(id)before persisting and exits1without writing tocontext.jsonon a missing notebook / wire failure / auth-expiry. Pass--forceto bypass verification.NotebookNotFoundErrornow inherits from bothRPCErrorandNotebookError.source get/artifact get/note getexit1on not-found (was0). Matches the rest of the CLI's user-error convention so scripts can branch on the exit code.--jsonfailure body uses the standard{"error": true, "code": "NOT_FOUND", ...}envelope.generate cinematic-video --format <non-cinematic>exits2with a UsageError instead of silently overriding the conflict. Drop the conflicting flag, or usegenerate video --format <value>if a non-cinematic format was intended.NOTEBOOKLM_REFRESH_CMDdefaults toshell=False(security hardening for the shell-injection footgun when the env var is sourced from CI configs). Now parsed withshlex.splitand invoked withsubprocess.run(argv, shell=False, ...). SetNOTEBOOKLM_REFRESH_CMD_USE_SHELL=1(literal"1"only) to opt back into the legacyshell=True.source addno longer follows symlinks by default. A workspace symlink like~/Downloads/foo.pdf → /etc/passwdpreviously resolved and uploaded the target with no warning. The path now refuses symlink traversal with aClickException(exit1) unless--follow-symlinksis explicit. Scripts that point at symlink-resolved paths must add the flag (#476).- YouTube cookies no longer scraped or trusted by default at login / refresh. The cookie-domain allowlist split into REQUIRED (NotebookLM + Drive + RotateCookies) and OPTIONAL (YouTube / Docs / Mail / myaccount). Pass
--include-domains=youtube(or=all) onlogin/auth refresh --browser-cookies <browser>/auth inspectto opt YouTube back in; pass=docs/=mail/=myaccountto opt those sibling domains in explicitly (#483). - Artifact generation without
language=now honors the configured language. The Pythonclient.artifacts.generate_*methods now resolve omittedlanguageviaNOTEBOOKLM_HL/ global config /"en"instead of hard-coding"en"at the signature. Passlanguage="en"for a fixed English payload. --storage <path>no longer shares the default profile's notebook context. A previously-runnotebooklm use <id>against the default profile is invisible to a laternotebooklm --storage X.json <cmd>(and vice versa) because--storagenow derives a sibling<path>.context.json. Set the active notebook explicitly vianotebooklm --storage <path> use <id>,-n/--notebook, orNOTEBOOKLM_NOTEBOOKenv var (#467).login --browser-cookies --account EMAILnow writes the active/default profile by default instead of creating a profile from the email local-part. Use--profile-name NAMEto write a separate named profile, or--storage PATHfor an exact file. Existing profile auth for a different or unknown account prompts before overwrite (#987).- v0.3-era deprecated APIs removed —
Source.source_type,Artifact.artifact_type,Artifact.variant,SourceFulltext.source_type,StudioContentType,DEFAULT_STORAGE_PATH,notebooklm.cli.language.save_config. Migrate to the.kindproperty andnotebooklm.paths.get_storage_path(). See Removed below. - Cookie identity widened to
(name, domain, path)per RFC 6265 §5.3. Writes remain backward-compatible (flat dicts / legacy 2-tuples still accepted); reads ofauth.cookieswith the old 2-tuple key now raiseKeyError. Useauth.cookies[("SID", ".google.com", "/")],auth.flat_cookies["SID"], orauth.cookie_header.
Added
Auth and reliability
- Inline
__Secure-1PSIDTScold-start recovery. When a storage file has__Secure-1PSIDbut no__Secure-1PSIDTS, a preflight POST toaccounts.google.com/RotateCookiesmints a fresh token before any RPC traffic, so cold-start workers no longer fail on the first call. Cross-process flock serializes concurrent cold starts; respectsNOTEBOOKLM_DISABLE_KEEPALIVE_POKE=1(#865, #872). NOTEBOOKLM_BASE_URLenv var for enterprise NotebookLM deployments (#402). Routes RPC + auth traffic through a non-google.combase URL; cookie-domain allowlist auto-extends to the enterprise host. Previously enterprise users had to monkey-patch internals.NOTEBOOKLM_RPC_OVERRIDESenv-var escape hatch. When Google rotates abatchexecutemethod ID, set e.g.NOTEBOOKLM_RPC_OVERRIDES='{"LIST_NOTEBOOKS": "newId123"}'to keep working until a patch ships. Overrides are gated tonotebooklm.google.com/accounts.google.combase hosts so a redirected base can't pivot them (#486).ConnectionLimitsdataclass for httpx pool tuning. PassConnectionLimits(max_connections=200, ...)toNotebookLMClient(...)for long-running agents and high-fan-out workers — no more monkey-patching internals (#527).max_concurrent_rpcsconstructor arg (default16, #630). Bounds simultaneous in-flight RPCs to protect the connection pool under fan-out.Noneopts out — see Breaking changes for the default-shift note.--include-domainsflag onlogin/auth refresh --browser-cookies <browser>/auth inspect. Backs the REQUIRED/OPTIONAL cookie-domain split described in Breaking changes — passing=youtube/=docs/=mail/=myaccount(or=all) opts those OPTIONAL domains back in. Accepts repeated-flag or comma-separated syntax (#483).- In-memory
__Secure-1PSIDTSrecovery during--browser-cookiesextraction (#990, #991). Whenrookiepyreturns a partial cookie set (most often when the browser hasn't rotated__Secure-1PSIDTSyet), a singleRotateCookiesPOST against the live browser cookies mints the missing token before persistence. Recovery declines surface scenario-specific hints (No SID → "You are not signed in to Google in <browser>",PSIDTS missing + secondary binding intact → "RotateCookies recovery did not succeed. Open https://notebooklm.google.com in <browser>") instead of the previous generic "No valid Google authentication cookies found".
Chat
client.chat.delete_conversation(notebook_id, conversation_id)+notebooklm ask --newis now genuinely destructive (#824). Captures the web UI's "Delete history" action (J7GthcRPC) so callers can force-end a server-side conversation; the nextask()with noconversation_idstarts a brand-new thread. ⚠ Deleted turns are not recoverable. CLI prompts for confirmation;--jsonimplies--yes.notebooklm ask --newflag (previously promised in the docstring but undeclared) — starts a fresh conversation, mutually exclusive with--conversation-id.notebooklm ask --timeoutper-invocation HTTP timeout, mirroringsource add --timeout.ChatReference.answer_range+.score(#686). Every reference now exposes the answer-text span it grounds (start/end char positions) and the model's relevance score — useful for highlighting cited passages and ranking sources.chat savepreserves inline citation hover anchors (closes #660, #675). Saved notes retain[citation]-style anchors so users can hover-preview the source passage that grounded each claim in the NotebookLM web UI.
CLI ergonomics
- Uniform
--jsonenvelopes on every detail and mutating command:artifact get/rename/delete/poll/export, eightsourcesubcommands (delete/rename/refresh/clean/get/delete-by-title/add-drive/stale),note get/save/create/delete/rename,notebooklm configure, andnotebook use. Detail commands mirror the underlying dataclasses; mutating commands emit{"id": ..., "renamed|deleted|exported": true, ...}. - Standard download flag set on
download quiz/download flashcards—--all,--latest,--earliest,--name,--dry-run,--force,--no-clobber,--json— so one wrapper script works across every artifact type. - Uniform
--timeout/--intervalongenerate <kind> --wait,artifact wait, andsource wait. --limit=Nand--no-truncateon everylistcommand, plus--no-truncateonchat history.chat history --no-truncatelifts the hardcoded 50-char preview on Question/Answer columns.- Shell completion + ID-aware completers.
notebooklm completion <bash|zsh|fish>prints a completion script; once sourced,-n/--notebook,-s/--source, and-a/--artifactTAB-complete live IDs from the active profile. - SIGINT resume hint on long-running
--waitops. Ctrl-C exits 130 withCancelled. Resume with: notebooklm artifact poll <task_id>(or the parallelsource wait <source_id>) instead of dumping aKeyboardInterrupttraceback. Under--json:{"error": true, "code": "CANCELLED", "resume_hint": "..."}. - Unix
-stdin convention onask,note create,source add, and--prompt-file.echo "what is X?" | notebooklm ask -and similar pipelines now compose without temp files. NOTEBOOKLM_NOTEBOOKenv var + global--quietflag.NOTEBOOKLM_NOTEBOOK=<id> notebooklm ask "..."works without-n/--notebookor a priornotebooklm use.--quietsuppresses status output, raises the package logger floor to ERROR, and remains mutually exclusive with-v/-vv.source addwarns when a path-shaped argument doesn't exist. A typo like./missin.mdpreviously fell through to inline-text ingestion silently; an advisory stderr warning now fires before the source is added.--follow-symlinksopt-in onsource add. See Breaking changes above; scripts that point at symlink-resolved paths must add the flag to keep working (#476).source cleancommand (#261). Bulk-delete failed/stale sources in a notebook; pairs withsource stalefor inspection. Supports--all,--latest,--earliest,--dry-run, and--json.notebooklm create --useflag (#220, #413).create --use "title"makes the new notebook the active context in one step. (Plaincreateno longer auto-switches the context —--useis the explicit opt-in.)- Chromium-profile selectors on
login --browser-cookies chromium:<profile>(#648). Pick a specific Chrome user profile (e.g.chromium:Profile_1) instead of always defaulting to the first profile. Useful for users with multiple Google accounts in one browser install. auth login --updateon--all-accounts(#594). Replaces the stored state for an already-logged-in account instead of refusing on conflict.
Python API
- Source fulltext markdown format.
client.sources.get_fulltext(..., output_format="markdown")andsource fulltext -f markdown(closes #222). Requires the optionalmarkdownifyextra (pip install "notebooklm-py[markdown]"). - Public
client.rpc_call(method_id, params)(#646). A documented escape hatch for invoking anybatchexecuteRPC method directly when no high-level API wraps it yet. Pairs withNOTEBOOKLM_RPC_OVERRIDESfor community self-patching while waiting on a fix. - Observability hooks + drain API on
NotebookLMClient(#643). Newon_rpc_eventcallback (per-call timing + status),client.metricssnapshot, andawait client.drain()for graceful shutdown. Designed for long-running agents needing visibility without monkey-patching. - Correlation IDs + categorized logging (#430, #431). Every RPC carries an
X-Correlation-ID(also surfaced on log records); log records are categorized (rpc.call,rpc.retry,auth.refresh,upload.chunk, …) for filtering. Credential redaction now covers every log surface by default. - Per-call upload timeouts on
sources.add_file/add_drive(#618). Newupload_timeout/chunk_timeoutkeyword args for tuning large-file uploads against slow networks. ResearchAPI.wait_for_completion(notebook_id, task_id=None, *, timeout=1800, interval=5)(#970). Polls until research reaches a terminal state (completed/failed) or the timeout fires; passes throughtask_idon subsequent polls once the backend assigns one to prevent a later concurrent task from substituting its sources/report. Surfaces a new terminalfailedstatus so wait loops no longer spin until timeout after the backend rejects a task.notebooklm.artifacts.with_rate_limit_retry(callable, *, max_retries=3, ...)(#969). Shared retry helper for theclient.artifacts.generate_*family — catches generation-timeRateLimitError, honorsretry_after, and falls back to exponential backoff. Replaces the per-caller try/except/sleep boilerplate previously suggested indocs/python-api.md.__all__declared onnotebooklm.paths,notebooklm.migration, andnotebooklm.notebooklm_cli(#958). ADR-0012 marks all three as public modules;__all__now pins the exported surface (12 names onpaths, 3 onmigration,cli+mainon the CLI entry point) sofrom notebooklm.paths import *is well-defined and the public API compatibility audit can lock it.
Changed
- Custom
--storagedownloads now use the selected auth file (#838, #888).ArtifactDownloadServicepreviously snapshotted the session's storage path at construction time, so--storageoverrides applied after construction were silently ignored on download. CLI--storageflag and mid-process profile switches are now inherited reliably. --storage <path>derives a sibling<path>.context.jsonper file (#467). Two--storageinvocations against different files no longer leak notebook state through the default profile. Precedence: explicit--storage> profile > legacy home-root. (See Breaking changes for the script-impact note.)- Conversation IDs are now server-assigned (#659, #667).
ChatAPI.ask()returns whatever the server creates instead of minting a local UUID. Previously-saved conversation IDs from a v0.4.x session remain valid against the server. - Cross-event-loop reuse fails fast with
RuntimeError(#633). OneNotebookLMClientinstance is bound to itsopen()-time event loop; reusing it from a different loop (common in hot-reload servers, worker pools) now raises on the first authed POST instead of failing with cryptic httpx errors. notebook usesurfaces the typed auth-aware error on expired credentials. Text mode shows the canonical "Not logged in" walkthrough with thenotebooklm loginremediation;--jsonemits the standardAUTH_REQUIREDenvelope.download <type>exception paths route through the typed error handler.--jsonis honored on the exception path;RateLimitError.retry_aftersurfaces as both a JSON field and a "Retry after Ns" text line;AuthErrorshows the canonical re-auth hint.notebooklm loginandnotebooklm auth refreshno longer leak Python tracebacks on unexpected failures. Unexpected exceptions become a single friendly line + bug-report URL with exit code2; original traceback remains available at-vv.--waitpaths show a transient spinner with elapsed timer and an empirical typical-duration hint where known (e.g.typically 30-40 minfor cinematic-video). No-op under--json.- CLI group docstrings synced with the live registered subcommand set.
source,download,artifact, andnotegroup--helpblocks now enumerate every registered subcommand (previously missedadd-drive,add-research,clean,wait,cinematic-video,quiz,flashcards,suggestions,rename). notebooklm --helpbins five previously-orphaned top-level commands into primary sections:auth→ Session;metadata→ Notebooks;agent/skill/language→ Command Groups.artifact pollvsartifact wait--helpclarified on ID kind.poll <task_id>straight fromgenerate;wait <artifact_id>resolved againstartifact list.- First-run profile migration no longer races concurrent invocations (#478). Previously two
notebooklminvocations starting under a fresh home (container start-up races, parallel test runs, MCP worker pools) could both run the copy-and-delete migration. Lock waits past 30 s raise a domain-specificMigrationLockTimeoutError(RuntimeError). RPCError.raw_responsepreviews capped at 80 chars;NOTEBOOKLM_DEBUG=1opts into full body. Previously embedded a 500-char preview of the upstream response — noisy in CI and capable of leaking large server payloads (#479).RPCError.rpc_idandRPCError.codedeprecations revoked. Both are now permanent aliases formethod_id/rpc_code— removing exception diagnostic aliases can mask the original exception insideexcepthandlers.- BREAKING:
note delete --jsonwithout--yesandnote renamelose-the-race now exit1(was0). Two parallel surgical fixes tocli/note.pymatching the broader--jsonexit-code convention (audit P1.T5).notebooklm note delete <id> --jsonwithout--yesnow emits{"error": true, "code": "VALIDATION_ERROR", "message": "Pass --yes to confirm deletion in --json mode", "id": ..., "notebook_id": ...}+ exit1(was the same payload as{deleted: false, error: ...}+ exit0).notebooklm note rename <id> "new"when the note vanishes between the partial-ID resolve and the underlyingget(e.g. a concurrentnote delete) now emits the standard{"error": true, "code": "NOT_FOUND", "message": "Note not found", "id": ..., "notebook_id": ...}envelope + exit1(was{renamed: false, error: ...}+ exit0). Migration: scripts branching on the exit code now correctly catch both misconfigurations; scripts parsing the JSON body must switch fromdata["deleted"] == false/data["renamed"] == falsechecks todata["error"] == true(or branch ondata["code"]).
Deprecated
await NotebookLMClient.from_storage(...)form.from_storagenow returns an awaitable async-context-manager wrapper that supports both the legacyasync with await NotebookLMClient.from_storage(...) as client:pattern (and bareawait NotebookLMClient.from_storage(...)) and the new canonicalasync with NotebookLMClient.from_storage(...) as client:pattern. Awaiting the call emitsDeprecationWarning; the await form will be removed in v1.0. Migration: drop theawaitkeyword fromasync with await NotebookLMClient.from_storage(...) as client:call sites.NotebookLMClient.rpc_callkwargs_is_retry,source_path,operation_variant. EmitDeprecationWarning; removal targets v0.6.0.NotesAPI.create_from_chat. UseChatAPI.save_answer_as_note; removal targets v0.6.0.- Positional
wait/wait_timeoutonSourcesAPI.add_url/add_text/add_file/add_drive. Calls likeclient.sources.add_url(nb_id, url, True)still work in v0.5.0 but emitDeprecationWarning; passwait=True/wait_timeout=...as keywords. Removal targets v0.6.0. CLI is unaffected. SourcesAPI.add_filemime_typeparameter. Never wired into the resumable-upload RPC — the server derives MIME from the filename extension. Passing a non-Nonevalue now emitsDeprecationWarning; removal targets v0.6.0. The separateadd_drive(..., mime_type=...)parameter is unaffected.notebooklm source add --mime-typeon the file-source path. A no-op when the resolved source type isfile; using it now emits a stderr deprecation note (suppress viaNOTEBOOKLM_QUIET_DEPRECATIONS=1). Removal targets v0.6.0. The same flag onsource add-driveis unaffected.ArtifactsAPI.wait_for_completion(poll_interval=...). Useinitial_interval=...;poll_intervalremains accepted until v0.6.0.NotebooksAPI.share(). Useclient.sharing.set_public(). Scheduled for removal in a future major release.NOTEBOOKLM_STRICT_DECODE=0soft-mode fallback. Each use emitsDeprecationWarningnaming the decoder source; the soft-mode path is scheduled for removal in v0.6.0.ResearchAPI.poll(task_id=None)default on multi-task notebooks. When multiple research tasks are in flight,poll()with notask_idnow emitsDeprecationWarning(single-task notebooks: no warning, current behavior preserved). Scheduled for removal in a future major release.
Removed
- v0.3-era deprecation cycle complete. Removed
Source.source_type,SourceFulltext.source_type,Artifact.artifact_type(use.kind);Artifact.variant(use.kind,.is_quiz,.is_flashcards);notebooklm.StudioContentType(useArtifactType);notebooklm.DEFAULT_STORAGE_PATH(usenotebooklm.paths.get_storage_path());notebooklm.cli.language.save_config(now private). - RPC raw-code
StudioContentTypealiases.notebooklm.rpc.types.StudioContentTypeandnotebooklm.rpc.StudioContentTyperemoved; useArtifactTypefor public code andArtifactTypeCodeonly for low-level RPC internals. RPCMethod.DISCOVER_SOURCESandRPCMethod.QUERY_ENDPOINT.DISCOVER_SOURCESwas an unused enum entry never exercised by anyclient.*API.QUERY_ENDPOINTwas an endpoint URL path, not a batchexecute RPC method; usenotebooklm.rpc.get_query_url()for the configured streamed-chat endpoint.
Fixed
- Artifact generation language compatibility restored. Omitting
languageon publicclient.artifacts.generate_*calls again defaults artifact output to"en"; passlanguage=Noneto opt in toNOTEBOOKLM_HLdefault-language resolution. - Source upload auth/MIME routing (#984). The resumable-upload path skipped a redundant env-auth lookup and now classifies media MIME types case-insensitively;
application/mp4is included in the media-MIME set so.mp4uploads route through the media upload path instead of the generic file path. - Source upload rejection with status
3now hints at the per-notebook source cap (#977). Previously surfaced as a bareRPCError; the error message now suggests checking the notebook source count when the server returns the cap-rejection code. - Windows atomic-replace races on cookie/profile writes (#983).
os.replaceon Windows can transiently fail withERROR_ACCESS_DENIED(5) orERROR_SHARING_VIOLATION(32) when the destination is briefly held open by AV scanners or backup software. Bounded retry with backoff handles the transient cases; persistent failures still surface. - IO event-loop blocking and chunked-download throughput (#981). Sync
Path.resolve()/open()/os.fstat()on the upload path are now wrapped inasyncio.to_thread, keeping the loop responsive under the upload semaphore on slow filesystems. Chunked downloads use a single dedicated writer thread fed by a boundedqueue.Queue(≈512 KiB buffered) instead of spawning oneto_threadcall per 64 KiB chunk. A bug whereArtifactDownloadError(raised bydownload_urls_batch()for invalid scheme / untrusted host / auth failure / HTML payload) aborted the entire batch instead of landing inDownloadResult.failedis also fixed. notebooklm login --browser-cookieshardening (#974). Tightened Chromium account enumeration, cookie-jar normalization, and refresh writes so partial extractions surface a clear error instead of silently writing an incompletestorage_state.json. Pairs with the in-memory__Secure-1PSIDTSrecovery shipped in #990 / #991.notebooklm login --browser-cookiesPlaywright account metadata (#989). The Playwright login path now writes account metadata to the profile and validates it on subsequent refresh (rejecting bool-shaped corruption from earlier buggy writes), sonotebooklm auth refresh --all-accountsand--account EMAILcan target the right profile without manual cleanup.- Playwright account metadata repair runs after the sync context exits (#1000, #1002).
notebooklm loginpreviously invokedrepair_playwright_account_metadata()whilesync_playwright()'s event loop was still active, which raised onrun_async(). The repair is now deferred until after the Playwright context closes, using the captured page HTML and saved storage path. source add-research --waittimeout path (#971). The CLI service now wraps the research wait in a typed timeout error and surfaces a resumable hint (notebooklm research poll <task_id>) instead of hanging until the global request timeout.notebooklm auth refresh --all-accountslanguage sync runs once (#976). Previously re-issued thenotebooklm.SetLanguageRPC once per account; now coalesces to a single sync at the end of the multi-account loop.- Loop-affinity guard on
sources.add_fileandclient.drain()admission (#952). Cross-event-loop reuse already failed fast on authed RPC POSTs (#633); upload admission and drain admission now also raiseRuntimeErrorinstead of silently mis-binding to the wrong loop. NotebookLMClient.close()no longer leaks the httpx pool if cancelled mid-drain (#950). ACancelledErrorraised during drain previously skippedhttpx.AsyncClient.aclose(); close now shields the transport cleanup so the connection pool is released on every cancellation path.- Deep-research source import no longer requires leaving the "Add sources?" modal (#315, #882). The deep-research flow used to discover sources but skip the modal-confirm step, leaving sources pending until a separate UI action committed them. The CLI /
ResearchAPI.import_sourcesnow commits directly. DELETE_NOTEno longer races shieldedUPDATE_NOTEat cancel time (#876). Cancellation during an in-flightNotesAPI.update(...)could land a delete before the shielded write completed, then have the update resurrect the note. Cancel-time cleanup is now ordered soDELETE_NOTEwaits for any shieldedUPDATE_NOTEto settle.- Client close preserves the original exception (#526).
NotebookLMClient.__aexit__previously masked the original body exception whenaclose()itself raised. Body exceptions are now preserved (chained via__cause__) while close-time failures still propagate; an inner shield guarantees the underlying httpx client is closed on every path. - Unique temp file per concurrent artifact download (#523). Two parallel
download_*calls against the same artifact used to share<dest>.tmpand clobber each other's bytes. Each invocation now allocates a unique temp file (PID + uuid suffix) and atomically renames into place. add_fileTOCTOU fix +max_concurrent_uploadsknob (#595).SourcesAPI.add_fileused to open the source file twice — a path swap between the two opens could substitute a different file into a successful upload. The file is now opened once; the FD is held across size check + registration + upload. Newmax_concurrent_uploads: int | None = 4onNotebookLMClientcaps simultaneous in-flight uploads (doubles as an FD-exhaustion guard forasyncio.gatherfan-outs).- Research
task_idcross-wire on concurrent in-flight tasks (#619). Two research sessions in flight on the same notebook could letResearchAPI.poll(notebook_id)silently return the latest task, mis-attributing source provenance to the caller's task.poll()gains an optionaltask_iddiscriminator;import_sources()raises the newResearchTaskMismatchError(subclass ofValidationError) when aresearch_task_idon any source disagrees with the caller'stask_id. RPCHealthsurfaceshttpxexception class name on empty error messages (#874). Somehttpxexception classes raise with emptystr(exc), which previously surfaced as a blank line. Health output now prefixes the class name (e.g.ConnectTimeout:).notebooklm logininstall hint stripped the[browser]extra (#416). Rich interpreted[browser]as a style tag, so the "Playwright not installed" message rendered aspip install "notebooklm-py"with no extras. Fixed bymarkup=False; also corrected the package name fromnotebooklmtonotebooklm-py.- Per-create-RPC idempotency hardening (#801, #806, #808, #809, #813). Six-policy idempotency registry with probe-then-retry semantics for
ADD_SOURCE,ADD_SOURCE_FILE,CREATE_NOTE,CREATE_ARTIFACT,GENERATE_MIND_MAP, andSTART_RESEARCH/IMPORT_SOURCES. Resolves duplicate-create on transient retries while still raising clear errors for genuine probe failures.
Security
- Comprehensive secret-leak audit closed across logging, auth, and URL handling (#746, #803, #903). A multi-iteration sweep tightening every surface that could leak credentials or grant codes:
payload_preview,final_url, and share-URL IDs scrubbed in error paths (#746).repr()redaction on auth objects,NOTEBOOKLM_REFRESH_CMDstdout/stderr redaction, Playwright cookie-jar domain filter, atomic profile-state writes (#803).- Standalone
__Secure-1PSIDTS/__Secure-3PSIDTS/__Secure-1PAPISID/__Secure-3PAPISIDcookie redaction in_logging.py(previously only caught insideCookie:/Set-Cookie:header values);_safe_urlredacts the URL path with/<redacted>on Google OAuth hosts (accounts.google.com,oauth2.googleapis.com,oauth2.googleusercontent.com) and subdomains, so opaque grant codes in paths like/o/oauth2/auth/<token>no longer leak throughValueErrorinterpolations or CSRF / session-id drift surfaces (#903).
0.4.1 - 2026-05-11
Compatibility note. Despite a few additive items (
notebooklm auth refreshCLI,keepalive=constructor argument onNotebookLMClient,NOTEBOOKLM_REFRESH_CMDenv var, two new dataclass fields), 0.4.1 is shipped as a patch release because the dominant work — and the reason to ship now — is auth/cookie stability remediation. Bumping to v0.5.0 would force the long-deferred removal of v0.3-era deprecated APIs (see Stability) earlier than scheduled; we'd rather keep that change isolated from the auth cookie lifecycle work. All additive items are backward compatible — existing code keeps working without changes.
Added
notebooklm auth refreshCLI command - One-shot keepalive that opens a session, triggers the layer-1 SIDTS rotation poke againstaccounts.google.com, persists the rotated cookies tostorage_state.json, and exits. Designed to be scheduled by the OS (launchd / systemd / cron / Task Scheduler / k8s CronJob) to keep an idle profile from staling out between user-driven calls. Pairs naturally with--quietfor log-only-on-error cron output. Requires file/profile-backed authentication — explicitly refuses to run whenNOTEBOOKLM_AUTH_JSONis set (no writable backing store). Seedocs/troubleshooting.mdfor per-OS scheduler recipes (#336).- Periodic keepalive task on
NotebookLMClient- Long-lived clients (agents, workers, multi-hourasync withblocks) can opt into a background task that periodically POSTsRotateCookiesto drive__Secure-1PSIDTSrotation, then persists rotated cookies tostorage_state.jsonimmediately so a crash doesn't lose the freshness. Disabled by default — passkeepalive=<seconds>toNotebookLMClient(...)orNotebookLMClient.from_storage(...)to enable. Values belowkeepalive_min_interval(default 60 s) are clamped up to that floor. The loop swallows transient errors at DEBUG and continues; cancellation on__aexit__is clean. Persistence runs off-loop viaasyncio.to_threadso the loop never blocks on disk I/O. Closes the gap left by the per-call layer-1 poke for clients that never re-callfetch_tokens(#297, #312, #341). - Auto-refresh on auth expiry -
fetch_tokensnow optionally runs a user-provided shell command when a Google session cookie has expired, reloads cookies from the same storage path, and retries once. Opt in by setting theNOTEBOOKLM_REFRESH_CMDenvironment variable to a command that rewritesstorage_state.json(e.g. a sync script reading from a cookie vault). Refresh commands receiveNOTEBOOKLM_REFRESH_STORAGE_PATHandNOTEBOOKLM_REFRESH_PROFILEso profile-aware scripts can target the active auth file. Covers every CLI entry point without changing the public API. Retry guards prevent refresh loops (#336). examples/refresh_browser_cookies.py- SampleNOTEBOOKLM_REFRESH_CMDscript that re-extracts cookies from a live local browser vianotebooklm login --browser-cookies. Provides a recovery path for unattended automation when the in-process keepalive isn't enough (idle gaps, force-logout, password change).Source.created_atandGenerationStatus.urlpublic dataclass fields -Source.created_atis now populated for both nested and deeply-nested response paths.GenerationStatus.urlis now populated bypoll_statusfor media artifact types (audio, video, infographic, slide-deck PDF) so callers can stream the asset as soon as the status flips to ready (#349, #356).ALLOWED_COOKIE_DOMAINSextended for sibling Google products - The browser-cookie import path now accepts cookies from Google's sibling product domains, restoring--browser-cookiesflows for users whose active Google session lives on a sibling surface rather thannotebooklm.google.comdirectly (#362).
Fixed
- Cookies could silently stale out under sustained use -
fetch_tokensnow POSTs tohttps://accounts.google.com/RotateCookies(Chrome's dedicated unsigned rotation endpoint) before hittingnotebooklm.google.comto drive__Secure-1PSIDTS/__Secure-3PSIDTSrotation. Empirically validated against both DBSC-bound (Playwright-minted) and unbound (Firefox-imported) profiles. RPC traffic againstnotebooklm.google.comalone does not appear to trigger rotation, so a keepalive that hit NotebookLM alone could silently stale out. The rotatedSet-Cookielands in the livehttpxjar and is persisted viasave_cookies_to_storage()along thefetch_tokens_with_domains/AuthTokens.from_storagepaths. A 60 s mtime guard rate-limits the layer-1 poke — the POST is skipped when storage was recently rotated. Failures log at DEBUG and never abort token fetch. Disable withNOTEBOOKLM_DISABLE_KEEPALIVE_POKE=1(e.g. networks that blockaccounts.google.com). Closes #312 (#345, #346). - Concurrent
RotateCookiespoke stampede - The 60 s mtime guard only debounces sequential invocations; underasyncio.gatherfan-out, parallel CLI loops, or MCP worker pools, all callers see the same stalestorage_state.jsonmtime and stampede the POST. Three layered protections inside_poke_session: a per-event-loop, per-storage-path async lock registry plus a sync state lock for in-process dedup (anasyncio.gatherof 10 fires exactly one POST), a non-blockingLOCK_EX | LOCK_NBflock on the new.storage_state.json.rotate.locksentinel for cross-process dedup (parallel CLI loops / MCP workers skip silently when another process is rotating), and a failure-stampede protection where the timestamp updates regardless of POST outcome — so a 15 s timeout against a hungaccounts.google.comdoesn't let 10 fanned-out callers each wait the full timeout. The layer-2 keepalive loop now calls the bare_rotate_cookiesdirectly (it's already self-paced viakeepalive_min_interval) andNOTEBOOKLM_DISABLE_KEEPALIVE_POKEcontinues to disable both layers (#347, #348). Notebook.sources_countparsed but never surfaced - Thesources_countfield on the publicNotebookdataclass is now populated fromdata[1]on both LIST and GET notebook shapes; previously it always read as0regardless of actual source count (#350).Artifact.urlunpopulated for media artifacts - Theurlfield on the publicArtifactdataclass is now populated for media types (audio, video, infographic; slide-deck exposes the PDF URL — usedownload_slide_deck(output_format="pptx")for PPTX) so callers no longer need to drop down todownload_*to obtain the asset URL (#349, #356).- Cross-process and refresh-path save races - Close lifecycle and refresh-path saves now serialize correctly with the keepalive writer; concurrent writers no longer overwrite each other's rotated cookies (#344).
- Keepalive ↔ close serialization; stop mutating caller
Auth- The keepalive task no longer races with__aexit__, and no longer mutates theAuthinstance the caller passed in. Callers that share anAuthacross multiple clients now get the isolation the API documented (#343). - Snapshot keepalive cookie jar; normalize explicit
storage_path- The keepalive task now snapshots the livehttpxjar before writing (avoiding torn writes when an RPC is mid-flight); an explicitstorage_path=argument toNotebookLMClientis normalized onto theAuthinstance so the keepalive task writes to the file the caller actually pointed at (#342). - Per-domain cookie scoping on file upload - File-upload requests now send only cookies whose
Domainattribute applies to the upload host, instead of the full jar. Prevents upload rejection when the jar mixes cookies forgoogle.com,notebooklm.google.com, andgoogleusercontent.com(#373, #374). - Two-tier cookie validation pre-flight - Auth loaders now distinguish "missing-but-recoverable" from "fatal" cookie states before attempting an RPC, surfacing clearer errors and avoiding doomed requests against Google's identity surface (#372).
- Preserve cookie attributes on load -
Domain,Path,Secure,HttpOnly, andSameSiteattributes round-trip through storage load, restoring behaviors that depended on cross-host scoping (#365, #368). - Unify flat-cookie selection across loaders - Legacy flat-cookie and modern Playwright storage shapes now share a single selection contract; subtle mismatches between the two paths are eliminated (#375, #376).
- Tolerate non-numeric / out-of-range timestamp values on dataclasses -
Notebook.created_at,Source.created_at, andArtifact.created_atnow catchTypeError,ValueError,OSError, andOverflowErrorfromdatetime.fromtimestampand resolve toNoneinstead of raising on edge-case server responses (#357). examples/refresh_browser_cookies.py--profileplacement - The example invoked... login --browser-cookies <b> --profile <p>but--profileis a top-level Click option and was rejected afterlogin(Error: No such option: --profile). Now invokes... --profile <p> login --browser-cookies <b>and works end-to-end against profile-backed storage.
Infrastructure
- Consolidated URL extraction -
_extract_artifact_url, per-type extractors (audio/video/infographic/slide-deck), and_is_valid_artifact_urlmoved totypes.py. Readiness checks,Artifact.url,GenerationStatus.url, and the download paths now share one URL-selection contract:mp4quality-4 > anymp4> first valid URL for video.SourcesAPI.get_fulltextfixed for YouTube fulltext URLs atmetadata[5][0]along the way (#349, #356). - Removed redundant
ArtifactsAPIURL helpers - Private_is_valid_media_urland_find_infographic_urlshim methods removed; tests now exercise the canonicaltypes.pyhelpers (#358). - E2E
--profilepytest flag -pytest --profile <name>scopes the E2E notebook ID cache to a named profile, so parallel multi-profile test runs don't collide on the cached notebook fixture (#340).
0.4.0 - 2026-05-09
Added
- Multi-account profiles - Switch between Google accounts without re-authenticating (#227)
notebooklm profile create/list/switch/rename/deletecommands- Global
--profile/-pflag andNOTEBOOKLM_PROFILEenvironment variable to scope any command to a profile - Per-profile storage paths under
~/.notebooklm/profiles/<name>/ - Implicit default profile preserved for backward compatibility; existing
~/.notebooklm/storage_state.jsonis auto-detected as the default profile (no manual migration needed)
notebooklm doctordiagnostic command -notebooklm doctor [--fix] [--json]checks profile setup, auth, and migration status; reports actionable issues- Microsoft Edge SSO login -
notebooklm login --browser msedgefor organizations that require Edge for SSO (#204) - Browser cookie import - Reuse cookies from your existing browser session without driving Playwright
notebooklm login --browser-cookies <browser>(chrome, edge, firefox, safari, etc.)- New
convert_rookiepy_cookies_to_storage_state()Python helper - Optional
[cookies]extra installsrookiepy(pip install "notebooklm-py[cookies]") - Honors the active profile:
notebooklm --profile <name> login --browser-cookies <browser>writes to that profile'sstorage_state.json. Note that cookie extraction always pulls the source browser's currently-active Google account forgoogle.com/notebooklm.google.com— to populate multiple profiles from the same browser, switch the active Google account in the browser between runs (or use a separate browser per profile).
- EPUB source type - Upload
.epubfiles as notebook sources (#231) - Agent skill installation - Install the bundled NotebookLM skill into local AI agents (#206, #207)
notebooklm skill install- Install into~/.claude/skills/notebooklmand~/.agents/skills/notebooklmnotebooklm skill status- Check installation statenotebooklm agent show codex/notebooklm agent show claude- Print bundled agent templates
- Mind map customization -
client.artifacts.generate_mind_map()now acceptslanguageandinstructionsparameters (#252) note list --json- Machine-readable note listings (#259)- Bare status codes in decoder errors - Decoder surfaces server status codes on null RPC results for clearer diagnostics (#114, #294)
Fixed
- Cross-domain cookie preservation - Login storage state retains cookies across
google.comandnotebooklm.google.comsubdomains, restoring sessions for regional domains - NotebookLM subdomain cookies - Subdomain cookies are no longer dropped during login (#334)
- Video artifact detection - Correctly detect completed video media URLs in polling responses (#333)
- Research import on unavailable snapshots - CLI gracefully handles missing source snapshots during research import (#335)
- Source import retry - Filtered partial-import retry payloads and tightened verification to avoid false positives (#321, #327)
- Server-state verification on timeout - Prevents duplicate inflation when source imports time out (#319)
- Playwright navigation interruption - Handles updated Playwright behavior on already-authenticated sessions (#214, #322)
- Login subprocess on Windows - Use
sys.executablefor Playwright subprocess calls (#279) - Legacy Windows Unicode output - Sanitized output streams for legacy Windows consoles (#324)
- Settings quota errors - Use account limits when reporting create-quota failures (#328)
- Chat references - Emit references only from the winning chunk to avoid >600-element duplication (#300, #310)
- Login retry mechanism - Resolved race conditions and improved error handling on retry (#243)
- Quota detection during polling - Detect quota / daily-limit failures during artifact polling (#240)
- Google account switching - Fixed switching between Google accounts at login time (#246)
- YouTube URL extraction - Extract YouTube URLs at deeply-nested response positions (#265)
- Bare-HTTP URL fallback - Disabled brittle bare-HTTP fallback in
sources.list()(#294) - Logout context cleanup - Clear the active notebook context on
notebooklm logout - Infographic URL extraction - Aligned with download-path logic; added regression test (#229)
- Custom storage path for downloads - Artifact downloads now respect custom auth storage paths (#235)
- Windows file permissions - Skip Unix-only
0o600calls on Windows and rely on Python 3.13+ ACL behavior (#225) - TOCTOU protection - Hardened directory creation in
session.py(#225)
Changed
rookiepyis an optional[cookies]extra - Excluded from[all]to avoid Python 3.13+ install issues; install withpip install "notebooklm-py[cookies]"- Login error detection - Improved detection of missing browser binaries (e.g.,
msedgenot installed) - Skill installation paths - Hardened to handle alternative
~/.claudeand~/.agentslayouts - Deprecation removal deferred to v0.5.0 - The deprecated APIs originally scheduled for removal in v0.4.0 —
StudioContentType,Source.source_type,SourceFulltext.source_type,Artifact.artifact_type,Artifact.variant, andDEFAULT_STORAGE_PATH— continue to work and emitDeprecationWarning. Removal is now planned for v0.5.0 to give downstream users an extra release to migrate.
Infrastructure
- Pinned
ruff==0.8.6in dev deps to match pre-commit configuration - Bumped
python-dotenv(#299) - Bumped
pytestin theuvgroup - Added contribution templates and PR quality guidelines for issues and PRs
0.3.4 - 2026-03-12
Added
- Notebook metadata export - Added notebook metadata APIs and CLI export with a simplified sources list
- New
notebooklm metadatacommand with human-readable and--jsonoutput - New
NotebookMetadataandSourceSummarypublic types - New
client.notebooks.get_metadata()helper
- New
- Cinematic Video Overview support - Added cinematic generation and download flows
notebooklm generate video --format cinematic
- Infographic styles - Added CLI support for selecting infographic visual styles
source delete-by-title- Added explicit exact-title deletion command for sources
Fixed
- Research imports on timeout - CLI research imports now retry on timeout with backoff
- Metadata command behavior - Aligned metadata output and implementation with current CLI patterns
- Regional login cookies - Improved browser login handling for regional Google domains
- Notebook summary parsing - Fixed notebook summary response parsing
- Source delete UX - Improved source delete resolution, ambiguity handling, and title-vs-ID errors
- Empty downloads - Raise an error instead of producing zero-byte files
- Module execution - Added
python -m notebooklmsupport
Changed
- Documentation refresh - Updated release, development, CLI, README, and Python API docs for current commands, APIs, and
uvworkflows - Public API surface - Exported
NotebookMetadata,SourceSummary, andInfographicStyle
0.3.3 - 2026-03-03
Added
ask --save-as-note- Save chat answers as notebook notes directly from the CLI (#135)notebooklm ask "question" --save-as-note- Save response as a notenotebooklm ask "question" --save-as-note --note-title "Title"- Save with custom title
history --save- Save full conversation history as a notebook note (#135)notebooklm history --save- Save history with default titlenotebooklm history --save --note-title "Title"- Save with custom titlenotebooklm history --show-all- Show full Q&A content instead of preview
generate report --append- Append custom instructions to built-in report format templates (#134)- Works with
briefing-doc,study-guide, andblog-postformats (no effect oncustom) - Example:
notebooklm generate report --format study-guide --append "Target audience: beginners"
- Works with
generate revise-slide- Revise individual slides in an existing slide deck (#129)notebooklm generate revise-slide "prompt" --artifact <id> --slide 0
- PPTX download for slide decks - Download slide decks as editable PowerPoint files (#129)
notebooklm download slide-deck --format pptx(web UI only offers PDF)
Fixed
- Partial artifact ID in download commands - Download commands now support partial artifact IDs (#130)
- Chat empty answer - Fixed
askreturning empty answer when API response marker changes (#123) - X.com/Twitter content parsing - Fixed parsing of X.com/Twitter source content (#119)
- Language sync on login - Syncs server language setting to local config after
notebooklm login(#124) - Python version check - Added runtime check with clear error message for Python < 3.10 (#125)
- RPC error diagnostics - Improved error reporting for GET_NOTEBOOK and auth health check failures (#126, #127)
- Conversation persistence - Chat conversations now persist server-side; conversation ID shown in
historyoutput (#138) - History Q&A previews - Fixed populating Q&A previews using conversation turns API (#136)
generate report --language- Fixed missing--languageoption for report generation (#109)
Changed
- Chat history API - Simplified history retrieval; removed
exchange_id, improved conversation grouping with parallel fetching (#140, #141) - Conversation ID tracking - Server-side conversation lookup via new
hPTbtcRPC (GET_LAST_CONVERSATION_ID) replaces local exchange ID tracking - History Q&A population - Now uses
khqZzRPC (GET_CONVERSATION_TURNS) to fetch full Q&A turns with accurate previews (#136)
Infrastructure
- Bumped
actions/upload-artifactfrom v6 to v7 (#131)
0.3.2 - 2026-01-26
Fixed
- CLI conversation reset - Fixed conversation ID not resetting when switching notebooks (#97)
- UTF-8 file encoding - Added explicit UTF-8 encoding to all file I/O operations (#93)
- Windows Playwright login - Restored ProactorEventLoop for Playwright login on Windows (#91)
Infrastructure
- Fixed E2E test teardown hook for pytest 8.x compatibility (#101)
- Added 15-second delay between E2E generation tests to avoid rate limits (#95)
0.3.1 - 2026-01-23
Fixed
- Windows CLI hanging - Fixed asyncio ProactorEventLoop incompatibility causing CLI to hang on Windows (#79)
- Unicode encoding errors - Fixed encoding issues on non-English Windows systems (#80)
- Streaming downloads - Downloads now use streaming with temp files to prevent corrupted partial downloads (#82)
- Partial ID resolution - All CLI commands now support partial ID matching for notebooks, sources, and artifacts (#84)
- Source operations - Fixed empty array handling and
add_drivenesting (#73) - Guide response parsing - Fixed 3-level nesting in
get_guideresponses (#72) - RPC health check - Handle null response in health check scripts (#71)
- Script cleanup - Ensure temp notebook cleanup on failure or interrupt
Infrastructure
- Added develop branch to nightly E2E tests with staggered schedule
- Added custom branch support to nightly E2E workflow for release testing
0.3.0 - 2026-01-21
Added
- Language settings - Configure output language for artifact generation (audio, video, etc.)
- New
notebooklm language list- List all 80+ supported languages with native names - New
notebooklm language get- Show current language setting - New
notebooklm language set <code>- Set language (e.g.,zh_Hans,ja,es) - Language is a global setting affecting all notebooks in your account
--localflag for offline-only operations (skip server sync)--languageflag on generate commands for per-command override
- New
- Sharing API - Programmatic notebook sharing management
- New
client.sharing.get_status(notebook_id)- Get current sharing configuration - New
client.sharing.set_public(notebook_id, True/False)- Enable/disable public link - New
client.sharing.set_view_level(notebook_id, level)- Set viewer access (FULL_NOTEBOOK or CHAT_ONLY) - New
client.sharing.add_user(notebook_id, email, permission)- Share with specific users - New
client.sharing.update_user(notebook_id, email, permission)- Update user permissions - New
client.sharing.remove_user(notebook_id, email)- Remove user access - New
ShareStatus,SharedUserdataclasses for structured sharing data - New
ShareAccess,SharePermission,ShareViewLevelenums
- New
SourceTypeenum - Newstr, Enumfor type-safe source identification:GOOGLE_DOCS,GOOGLE_SLIDES,GOOGLE_SPREADSHEET,PDF,PASTED_TEXT,WEB_PAGE,YOUTUBE,MARKDOWN,DOCX,CSV,IMAGE,MEDIA,UNKNOWN
ArtifactTypeenum - Newstr, Enumfor type-safe artifact identification:AUDIO,VIDEO,REPORT,QUIZ,FLASHCARDS,MIND_MAP,INFOGRAPHIC,SLIDES,DATA_TABLE,UNKNOWN
.kindproperty - Unified type access acrossSource,Artifact, andSourceFulltext:# Works with both enum and string comparison source.kind == SourceType.PDF # True source.kind == "pdf" # Also True artifact.kind == ArtifactType.AUDIO # True artifact.kind == "audio" # Also TrueUnknownTypeWarning- Warning (deduplicated) when API returns unknown type codesSourceStatus.PREPARING- New status (5) for sources in upload/preparation phase- E2E test coverage - Added file upload tests for CSV, MP3, MP4, DOCX, JPG, Markdown with type verification
--retryflag for generation commands - Automatic retry with exponential backoff on rate limitsnotebooklm generate audio --retry 3- Retry up to 3 times on rate limit errors- Works with all generate commands (audio, video, quiz, etc.)
ArtifactStatus.FAILED- New status (code 4) for artifact generation failures- Centralized exception hierarchy - All errors now inherit from
NotebookLMErrorbase class- New
SourceAddErrorwith detailed failure messages for source operations - Granular exception types for better error handling in automation
- New
- CLI
sharecommand group - Notebook sharing management from command linenotebooklm share- Enable public sharingnotebooklm share --revoke- Disable public sharing
- Partial UUID matching for note commands -
note get,note delete, etc. now support partial IDs
Fixed
- Silent failures in CLI - Commands now properly report errors instead of failing silently
- Source type emoji display - Improved consistency in
source listoutput
Changed
- Source type detection - Use API-provided type codes as source of truth instead of URL/extension heuristics
- CLI file handling - Simplified to always use
add_file()for proper type detection
Removed
detect_source_type()- Obsolete heuristic function replaced bySource.kindpropertyARTIFACT_TYPE_DISPLAY- Unused constant replaced byget_artifact_type_display()
Deprecated
The following emit DeprecationWarning when accessed and were originally scheduled for removal in v0.4.0.
See Migration Guide for upgrade instructions.
Note: Removal was subsequently deferred one release; see the 0.4.0 entry above. These names will now be removed in v0.5.0.
Source.source_type- Use.kindproperty instead (returnsSourceTypestr enum)Artifact.artifact_type- Use.kindproperty instead (returnsArtifactTypestr enum)Artifact.variant- Use.kind,.is_quiz, or.is_flashcardsinsteadSourceFulltext.source_type- Use.kindproperty insteadStudioContentType- UseArtifactType(str enum) for user-facing code
0.2.1 - 2026-01-15
Added
- Authentication diagnostics - New
notebooklm auth checkcommand for troubleshooting auth issues- Shows storage file location and validity
- Lists cookies present and their domains
- Detects
NOTEBOOKLM_AUTH_JSONandNOTEBOOKLM_HOMEusage --testflag performs network validation--jsonflag for machine-readable output (CI/CD friendly)
- Structured logging - Comprehensive DEBUG logging across library
NOTEBOOKLM_LOG_LEVELenvironment variable (DEBUG, INFO, WARNING, ERROR)- RPC call timing and method tracking
- Legacy
NOTEBOOKLM_DEBUG_RPC=1still works
- RPC health monitoring - Automated nightly check for Google API changes
- Detects RPC method ID mismatches before they cause failures
- Auto-creates GitHub issues with
rpc-breakagelabel on detection
Fixed
- Cookie domain priority - Prioritize
.google.comcookies over regional domains (e.g.,.google.co.uk) for more reliable authentication - YouTube URL parsing - Improved handling of edge cases in YouTube video URLs
Documentation
- Added
auth checkto CLI reference and troubleshooting guide - Consolidated CI/CD troubleshooting in development guide
- Added installation instructions to SKILL.md for Claude Code
- Clarified version numbering policy (PATCH vs MINOR)
0.2.0 - 2026-01-14
Added
- Source fulltext extraction - Retrieve the complete indexed text content of any source
- New
client.sources.get_fulltext(notebook_id, source_id)Python API - New
source fulltext <source_id>CLI command with--jsonand-ooutput options - Returns
SourceFulltextdataclass with content, title, URL, and character count
- New
- Chat citation references - Get detailed source references for chat answers
AskResult.referencesfield contains list ofChatReferenceobjects- Each reference includes
source_id,cited_text,start_char,end_char,chunk_id - Use
notebooklm ask "question" --jsonto see references in CLI output
- Source status helper - New
source_status_to_str()function for consistent status display - Quiz and flashcard downloads - Export interactive study materials in multiple formats
- New
download quizanddownload flashcardsCLI commands - Supports JSON, Markdown, and HTML output formats via
--formatflag - Python API:
client.artifacts.download_quiz()andclient.artifacts.download_flashcards()
- New
- Extended artifact downloads - Download additional artifact types
- New
download reportcommand (exports as Markdown) - New
download mind-mapcommand (exports as JSON) - New
download data-tablecommand (exports as CSV) - All download commands support
--all,--latest,--name, and--artifactselection options
- New
Fixed
- Regional Google domain authentication - SID cookie extraction now works with regional Google domains (e.g., google.co.uk, google.de, google.cn) in addition to google.com
- Artifact completion detection - Media URL availability is now verified before reporting artifact as complete, preventing premature "ready" status
- URL hostname validation - Use proper URL parsing instead of string operations for security
Changed
- Pre-commit checks - Added mypy type checking to required pre-commit workflow
0.1.4 - 2026-01-11
Added
- Source selection for chat and artifacts - Select specific sources when using
askorgeneratecommands- New
--sourcesflag accepts comma-separated source IDs or partial matches - Works with all generation commands (audio, video, quiz, etc.) and chat
- New
- Research sources table -
research statusnow displays sources in a formatted table instead of just a count
Fixed
- JSON output broken in TTY terminals -
--jsonflag output was including ANSI color codes, breaking JSON parsing for commands likenotebooklm list --json - Warning stacklevel -
warnings.warncalls now report correct source location
Infrastructure
- Windows CI testing - Windows is now part of the nightly E2E test matrix
- VCR.py integration - Added recorded HTTP cassette support for faster, deterministic integration tests
- Test coverage improvements - Improved coverage for
_artifacts.py(71% → 83%),download.py, andsession.py
0.1.3 - 2026-01-10
Fixed
- PyPI README links - Documentation links now work correctly on PyPI
- Added
hatch-fancy-pypi-readmeplugin for build-time link transformation - Relative links (e.g.,
docs/troubleshooting.md) are converted to version-tagged GitHub URLs - PyPI users now see links pointing to the exact version they installed (e.g.,
/blob/v0.1.3/docs/...)
- Added
- Development repository link - Added prominent source link for PyPI users to find the GitHub repo
0.1.2 - 2026-01-10
Added
- Ruff linter/formatter - Added to development workflow with pre-commit hooks and CI integration
- Multi-version testing - Docker-based test runner script for Python 3.10-3.14 (
/matrixskill) - Artifact verification workflow - New CI workflow runs 2 hours after nightly tests to verify generated artifacts
Changed
- Python version support - Now supports Python 3.10-3.14 (dropped 3.9)
- CI authentication - Use
NOTEBOOKLM_AUTH_JSONenvironment variable (inline JSON, no file writes)
Fixed
- E2E test cleanup - Generation notebook fixture now only cleans artifacts once per session (was deleting artifacts between tests)
- Nightly CI - Fixed pytest marker from
-m e2eto-m "not variants"(e2e marker didn't exist) - macOS CI fix for Playwright version extraction (grep pattern anchoring)
- Python 3.10 test compatibility with mock.patch resolution
Documentation
- Claude Code skill: parallel agent safety guidance
- Claude Code skill: timeout recommendations for all artifact types
- Claude Code skill: clarified
-nvs--notebookflag availability
0.1.1 - 2026-01-08
Added
NOTEBOOKLM_HOMEenvironment variable for custom storage locationNOTEBOOKLM_AUTH_JSONenvironment variable for inline authentication (CI/CD friendly)- Claude Code skill installation via
notebooklm skill install
Fixed
- Infographic generation parameter structure
- Mind map artifacts now persist as notes after generation
- Artifact export with proper ExportType enum handling
- Skill install path resolution for package data
Documentation
- PyPI release checklist
- Streamlined README
- E2E test fixture documentation
0.1.0 - 2026-01-06
Added
- Initial release of
notebooklm-py- unofficial Python client for Google NotebookLM - Full notebook CRUD operations (create, list, rename, delete)
- Research polling CLI commands for LLM agent workflows:
notebooklm research status- Check research progress (non-blocking)notebooklm research wait --import-all- Wait for completion and import sourcesnotebooklm source add-research --no-wait- Start deep research without blocking
- Multi-artifact downloads with intelligent selection:
download audio,download video,download infographic,download slide-deck- Multiple artifact selection (--all flag)
- Smart defaults and intelligent filtering (--latest, --earliest, --name, --artifact-id)
- File/directory conflict handling (--force, --no-clobber, auto-rename)
- Preview mode (--dry-run) and structured output (--json)
- Source management:
- Add URL sources (with YouTube transcript support)
- Add text sources
- Add file sources (PDF, TXT, MD, DOCX) via native upload
- Delete sources
- Rename sources
- Studio artifact generation:
- Audio overviews (podcasts) with 4 formats and 3 lengths
- Video overviews with 9 visual styles
- Quizzes and flashcards
- Infographics, slide decks, and data tables
- Study guides, briefing docs, and reports
- Query/chat interface with conversation history support
- Research agents (Fast and Deep modes)
- Artifact downloads (audio, video, infographics, slides)
- CLI with 27 commands
- Comprehensive documentation (API, RPC, examples)
- 96 unit tests (100% passing)
- E2E tests for all major features
Fixed
- Audio overview instructions parameter now properly supported at RPC position [6][1][0]
- Quiz and flashcard distinction via title-based filtering
- Package renamed from
notebooklm-automationtonotebooklm - CLI module renamed from
cli.pytonotebooklm_cli.py - Removed orphaned
cli_query.pyfile
⚠️ Beta Release Notice
This is the initial public release of notebooklm-py. While core functionality is tested and working, please note:
- RPC Protocol Fragility: This library uses undocumented Google APIs. Method IDs can change without notice, potentially breaking functionality. See Troubleshooting for debugging guidance.
- Unofficial Status: This is not affiliated with or endorsed by Google.
- API Stability: The Python API may change in future releases as we refine the interface.
Known Issues
- RPC method IDs may change: Google can update their internal APIs at any time, breaking this library. Check the RPC Development Guide for how to identify and update method IDs.
- Rate limiting: Heavy usage may trigger Google's rate limits. Add delays between bulk operations.
- Authentication expiry: CSRF tokens expire after some time. Re-run
notebooklm loginif you encounter auth errors. - Large file uploads: Files over 50MB may fail or timeout. Split large documents if needed.