The read-only test notebook may have a conversation ID but no actual
chat turns, causing IndexError on empty turns_data. Skip gracefully
with a descriptive message instead of crashing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The `with_client` decorator caught all `FileNotFoundError` exceptions and
treated them as authentication errors. When `source add --type file` was
used with a non-existent file, the `FileNotFoundError` from `add_file()`
was caught by this broad handler, showing "Not logged in" instead of the
actual file-not-found error.
Narrowed the `FileNotFoundError` catch to only wrap the auth step
(`get_auth_tokens`), so file-not-found errors from command logic are
properly propagated as regular errors.
Fixes#153https://claude.ai/code/session_01WsnjDnXMBz76e6sNRjqXGH
Co-authored-by: Claude <noreply@anthropic.com>
* fix(notebooks): correct SUMMARIZE response parsing for get_description/get_summary
The VfAZjd (SUMMARIZE) RPC returns a triple-nested structure:
result = [[[summary_string], [[topics]], ...]]
The previous parsing assumed:
- Summary at result[0][0] (a string) — actually a list
- Topics at result[1][0] — actually at result[0][1][0]
This caused get_description() to always return an empty summary and no
suggested topics, showing "No summary available" in the CLI.
Fixed parsing to use:
- Summary: result[0][0][0]
- Topics: result[0][1][0]
Updated all affected unit/integration tests to use the correct response
structure (confirmed against the real API cassette in notebooks_get_summary.yaml).
Fixes#147
* test(e2e): strengthen summary assertions to catch empty results
* refactor(notebooks): simplify SUMMARIZE response parsing per review feedback
* docs(rpc-reference): fix SUMMARIZE response structure (triple-nested)
* fix(ci): capture exit code correctly in RPC health check workflow
GitHub Actions `shell: bash` implicitly uses `set -eo pipefail`. The
previous `set -o pipefail` was redundant, and `set -e` caused the shell
to exit immediately when the health check script returned non-zero,
before `exit_code=${PIPESTATUS[0]}` could run. This meant the exit_code
output was never set, so the conditional issue-creation steps (for RPC
mismatch or auth failure) never fired.
Fix: use `set +e` before the pipeline so the script's exit code is
captured into PIPESTATUS, then `set -e` to restore strict mode.
https://claude.ai/code/session_01Bbbf9yHDaWv6gvvqEZwZqH
* fix(ci): replace removed LIST_CONVERSATIONS with GET_LAST_CONVERSATION_ID in health check
LIST_CONVERSATIONS was renamed to GET_LAST_CONVERSATION_ID and
GET_CONVERSATION_TURNS in the chat refactor (#141). The health check
script still referenced the old name, causing an AttributeError.
https://claude.ai/code/session_01Bbbf9yHDaWv6gvvqEZwZqH
* chore(ci): remove redundant set -e in health check workflow
The only statements after exit_code capture are echo and exit,
so restoring strict mode adds no value.
https://claude.ai/code/session_01Bbbf9yHDaWv6gvvqEZwZqH
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(test): unpack server_conversation_id from chat response tuples
_parse_ask_response_with_references now returns 3 values (answer, refs,
server_conv_id) and _extract_answer_and_refs_from_chunk returns 4 values
(text, is_answer, refs, conv_id), but 19 test call sites were still
unpacking the old 2/3-value signatures, causing ValueError on all CI
platforms.
* test: assert conv_id is None in chat response unpack tests
Address reviewer feedback: replace _conv_id throwaway with conv_id and
add explicit assertions to verify the returned server_conversation_id is
None in all test cases where no string conversation ID is present in the
response payload.
* chore: release v0.3.3
* docs: fix slide-deck download description (Output path, not directory)
* fix(chat): raise ChatError on rate limit, use server conv ID, add e2e guards
Three bugs fixed in the chat ask() flow:
1. UserDisplayableError now raises ChatError instead of silently returning
an empty answer. When Google's API returns a rate-limit or quota error
(UserDisplayableError in item[5] of the wrb.fr chunk), ask() now raises
ChatError with a clear message rather than logging a warning and returning
answer="". Adds _raise_if_rate_limited() helper.
2. ask() now uses the server-assigned conversation_id from first[2] of the
response instead of the locally generated UUID. This keeps
get_conversation_id() and get_conversation_turns() in sync with the
returned conversation_id, fixing the ID mismatch in TestChatHistoryE2E.
3. E2E test hardening:
- _skip_on_chat_rate_limit autouse fixture in test_chat.py converts
ChatError into pytest.skip(), matching the pattern used for generation
tests (no cascade failures when quota is exhausted).
- pytest_runtest_teardown now adds a 5s delay between chat tests to
reduce the chance of hitting rate limits under normal usage.
Unit tests added for both new behaviors (TDD: red then green).
* test(e2e): fix skip logic and add rate-limit guards
- test_chat.py: _skip_on_chat_rate_limit now only skips on actual
rate-limit ChatErrors (matching 'rate limit'/'rejected by the api');
other ChatErrors (HTTP failures, auth errors) now surface as failures
- test_artifacts.py: skip test_suggest_reports on RPCTimeoutError
- uv.lock: reflect version bump to 0.3.3
* test(e2e): make chat history tests read-only and non-flaky
Switch TestChatHistoryE2E from asking new questions to reading
pre-existing conversation history in a read-only notebook. This
eliminates flakiness caused by conversation persistence delays
and reduces rate-limit pressure on the API.
* test(e2e): skip test_poll_rename_wait that exceeds 60s timeout
Generation + wait_for_completion exceeds the 60s pytest timeout.
Individual operations are already covered by other tests.
* refactor(chat): remove exchange_id, --new flag, simplify history API
- Remove exchange_id from AskResult, ask(), _parse_ask_response_with_references()
- Remove --new flag from CLI ask command (CLI-created convs never persist server-side)
- Remove get_current_exchange_id/set_current_exchange_id from cli/helpers.py
- Rename get_last_conversation_id → get_conversation_id (server returns only one)
- Remove _get_conversation_ids helper (inlined into get_conversation_id)
- Revert get_history to return flat list[tuple[str, str]] (Q&A pairs, oldest-first)
instead of grouped list[tuple[str, list[tuple[str,str]]]] per PR 140
- CLI history_cmd: fetch conv_id once via get_conversation_id, pass to get_history
- Replace _format_conversations with simpler _format_history
- Update all tests to match new API
* docs: update rpc-reference and SKILL.md for exchange_id removal
- Rename GET_LAST_CONVERSATION_ID → GET_CONVERSATION_ID in rpc-reference.md
- Document that server ignores limit param, always returns one ID
- Update response structure note (single entry, not multi-ID list)
- Remove --new flag from SKILL.md quick reference table
- Replace 'Save one conversation as note' row with '-c <id>' chat row
- Update parallel safety note to drop --new mention
- Bump last-updated date in rpc-reference.md
* refactor: tighten get_conversation_id logging and set_current_notebook docstring
- Add debug log in get_conversation_id when API response has no valid ID
(helps diagnose future Google API structure changes)
- Move inline comment in set_current_notebook into the docstring where it belongs
* fix(chat): fix stale docstring and add error handling in get_history
- Remove exchange_id from _extract_answer_and_refs_from_chunk docstring
(exchange_id was removed from params in a prior commit)
- Wrap get_conversation_turns() call in try/except in get_history so a
single RPC failure returns [] instead of propagating to the caller
- Clarify conversation_id arg docstring: defaults to most recent conversation
* fix(chat): catch specific exceptions in get_history instead of bare Exception
- Add _get_conversation_ids() to fetch up to 20 conversation IDs from
the GET_LAST_CONVERSATION_ID RPC (previously only fetched limit=1)
- get_history() now returns list[tuple[str, list[tuple[str,str]]]]
grouped by conversation ID, newest conversation first
- Use asyncio.gather(return_exceptions=True) to fetch all conversation
turn data in parallel; failed conversations are logged and skipped
- notebooklm history display groups Q&A turns under conversation UUID
headers; turn numbers restart at 1 within each conversation
- JSON output changed to nested conversations array structure
- history --save preserves conversation boundaries via _format_conversations
- Remove dead _format_all_qa function (replaced by _format_conversations)
Integration tests added:
- test_get_history_multiple_conversations
- test_get_history_skips_failed_conversations
Incorporates non-history improvements from #138:
- _DEFAULT_BL constant for BL parameter
- Fixed params layout with position comments for GenerateFreeFormStreamed
* fix(chat): persist conversations server-side and show conv ID in history
- Include notebook_id at params[7] in GenerateFreeFormStreamed requests
so conversations are persisted to the server and visible in the GUI
- Move exchange_id to params[6] (was incorrectly at [7])
- Update build label to 20260301.03_p0
- Change get_history() return type to tuple[str | None, list[tuple]]
so callers receive both the conversation ID and Q&A pairs
- Show conversation ID in 'notebooklm history' output and JSON
* refactor(cli): simplify history header by building string instead of branching
* refactor(chat): extract build label into module-level constant
* feat: persist exchange_id across CLI ask invocations for conversation context
The Web UI achieves follow-up context by sending the server-assigned
exchange UUID (first[2][1]) back as params[7] in subsequent requests.
This enables server-side context lookup without replaying history.
Changes:
- Add exchange_id field to AskResult dataclass
- Parse exchange_id from streaming response chunks in single pass
- Add exchange_id parameter to ChatAPI.ask() for follow-up requests
- Fix params[3] to match Web UI structure ([2,None,[1],[1]])
- Extend params with [None,None,exchange_id,1] for follow-ups
- Persist exchange_id in context.json alongside conversation_id
- Clear exchange_id on notebook switch and new conversations
- Extract _get_context_value/_set_context_value shared helpers
* fix: address PR review findings for exchange_id persistence
- Remove dead _parse_exchange_id_from_response method (flagged by all reviewers)
- Fix truthiness checks: use `is not None` instead of `if value:` to correctly
handle empty strings in _set_context_value and ask() exchange_id param
- Fix stale exchange_id bug: only use stored exchange_id when conversation_id
came from local cache, not from explicit --conversation-id flag
- Add logger.warning in _get/_set_context_value catch blocks instead of bare pass
- Add exchange_id to AskResult docstring
- Update tests to exercise production code path instead of removed dead code
* refactor: use loop for preserving context keys on notebook switch
Simplify repeated if-checks into a loop over keys to preserve.
* fix: make exchange_id persistence symmetric with conversation_id guard
Only persist exchange_id when conversation_id is also valid. Clear
exchange_id when conversation_id is absent to avoid mismatched state.
* fix(history): populate Q&A previews using khqZz conversation turns API
The `notebooklm history` command displayed empty Question and Answer
preview columns because the GET_CONVERSATION_HISTORY RPC (hPTbtc) only
returns conversation IDs, not message content.
Fixed by discovering and implementing the GET_CONVERSATION_TURNS RPC
(khqZz), which accepts a conversation ID and returns the full message
turns (type=1 for user questions, type=2 for AI answers). The history
command now calls this per conversation to populate the table previews.
Changes:
- Add GET_CONVERSATION_TURNS = "khqZz" to RPCMethod enum
- Add ChatAPI.get_conversation_turns() method with params [[], null, null, conv_id, limit]
- Update history_cmd to call get_conversation_turns per conversation,
parsing turn[3] (question) and turn[4][0][0] (answer)
- Update unit tests to reflect real API format (conv IDs only from
history) and mock get_conversation_turns with turn data
* test(chat): add integration/e2e tests for GET_CONVERSATION_TURNS and rename LIST_CONVERSATIONS
- Rename GET_CONVERSATION_HISTORY → LIST_CONVERSATIONS (hPTbtc only returns IDs)
- Add integration tests for get_conversation_turns (khqZz RPC)
- Add e2e tests for TestChatHistoryE2E with Q&A turn verification
- Add VCR cassette chat_get_conversation_turns.yaml for notebook f59447f4
- Add CLI VCR test test_history_shows_qa_previews verifying table output
- Update docs/rpc-reference.md with LIST_CONVERSATIONS and GET_CONVERSATION_TURNS sections
* test(chat): add e2e history flow tests and health check for GET_CONVERSATION_TURNS
- Add TestChatHistoryE2E.test_history_flow_lists_conversation: verifies ask()
creates a conversation visible in get_history() (LIST_CONVERSATIONS RPC)
- Add TestChatHistoryE2E.test_history_flow_get_turns_from_history: exercises
the full flow (ask → get_history → get_conversation_turns) end-to-end
- Add GET_CONVERSATION_TURNS (khqZz) to check_rpc_health.py get_test_params()
so its RPC method ID is verified in CI health checks
* fix(history): fix broken save, multi-conversation parsing, and Windows CI
- Fix `history --save` by fetching Q&A via GET_CONVERSATION_TURNS before
formatting (LIST_CONVERSATIONS only returns IDs, not content)
- Fix history showing only 1 conversation by adding `_extract_conversations()`
that handles both API response structures (single-group and multi-group)
- Fix Windows CI failure by adding custom VCR `rpcids` matcher for
deterministic batchexecute request matching (replaces fragile sequential
play-count ordering)
- Extract shared `_extract_qa_from_turns()` helper and use `asyncio.gather`
for parallel turn fetching
- Update integration test mocks to reflect real API behavior (IDs-only
LIST_CONVERSATIONS + separate GET_CONVERSATION_TURNS calls)
* fix(history): fix turn ordering and add --json/--show-all to history
- Reverse API turns (newest-first) to chronological before Q→A parsing,
fixing off-by-one answer mismatch and reversed display order
- Add --json flag for machine-readable output (scripting/LLM agents)
- Add --show-all flag for full Q&A content instead of truncated preview
- Update cassette and tests to match real API turn order (A before Q)
Adds two new ways to persist chat content as notebook notes:
- `notebooklm ask "..." --save-as-note [--note-title "Title"]`
Saves the answer inline after getting a response. Works with or
without --json. Gracefully handles note creation failure (prints
warning instead of traceback, since the answer was already shown).
- `notebooklm history --save [-c <conv_id>] [--note-title "Title"]`
Saves all conversation history (or a single conversation) as a note.
Fetches up to 1000 conversations when saving to avoid truncation.
Also enhances `history` display: shows Question and Answer preview
columns alongside Conversation ID.
Fixes:
- Correct GET_CONVERSATION_HISTORY mock nesting in integration tests
(was [[conv1, conv2]], matching the real API structure)
- Align _format_all_conversations filter with display loop
Tests: 7 new unit tests, 2 new httpx-mock integration tests,
3 VCR stubs (skipped pending cassette recording with real credentials)
* feat: add --append option to generate report for template customization (issue #116)
Add extra_instructions parameter to generate_report() and generate_study_guide()
in _artifacts.py, and a corresponding --append CLI option to notebooklm generate
report. This allows users to append custom instructions to built-in format templates
(briefing-doc, study-guide, blog-post) without losing the format type or switching
to a generic custom report.
- Append is silently ignored (with warning) when --format custom is used explicitly
or when smart detection promotes the format to custom via the description argument
- generate_study_guide() wrapper passes extra_instructions through to generate_report()
- Adds tests for all built-in formats, both warning paths, and kwarg forwarding
* docs: update SKILL.md with --append option for generate report
* test: add API-layer tests for extra_instructions prompt concatenation and CUSTOM isolation
* fix: use immutable dict copy for config mutation per review feedback
* fix: support partial artifact ID in download commands
The -a/--artifact flag in download commands was doing exact match only,
so partial UUIDs silently failed with "not found". Add
resolve_partial_artifact_id() to download_helpers.py and call it in
_download_artifacts_generic before passing to select_artifact, keeping
select_artifact as a pure exact-match selector.
* fix: address PR review findings for partial artifact ID
- Extend partial ID resolution to quiz/flashcard download paths
(_download_interactive now fetches and filters artifacts when -a is used)
- Improve ambiguous match error to include artifact titles alongside IDs
- Fix test_download_by_full_artifact_id to use a 20+ char ID that actually
exercises the bypass path
- Add CLI test: full-length ID not in list still errors gracefully
- Add unit test: resolve_partial_artifact_id with empty list raises
- Add unit test: ambiguous error message includes titles
- Fix fragile "mbiguous" assertion to "Ambiguous"
* chore: sync uv.lock to version 0.3.2
* style: fix ruff formatting in test_download.py
* refactor: extract _get_completed_artifacts_as_dicts helper
Eliminate duplicated fetch/filter/convert logic between
_download_artifacts_generic and _download_interactive.
* feat: add revise_slide() API with REVISE_SLIDE RPC method (KmcKPe)
* feat: add 'generate revise-slide' CLI command for individual slide revision
* feat: add PPTX format support to download_slide_deck()
* feat: add --format pptx option to 'download slide-deck' CLI command
* feat: add REVISE_SLIDE to RPC health check and VCR integration test
* fix: address PR review issues in slide deck revision and download
- Add slide_index < 0 validation in revise_slide()
- Log warning when REVISE_SLIDE returns null result
- Rename format -> output_format in download_slide_deck() to avoid
shadowing Python built-in
- Move _download_url() call outside try/except to narrow the catch
scope to only metadata parsing (IndexError, TypeError)
- Warn to stderr when --format pptx is used with a non-.pptx output path
- Wrap fetch_tokens() in diagnose_get_notebook.py with proper error handling
- Replace silent except pass with informative print in diagnose_get_notebook.py
* docs: update SKILL.md with revise-slide and PPTX download
* fix: add _DownloadFn type annotation to resolve mypy operator errors in download.py
* fix: move _DownloadFn alias after imports to fix ruff E402
* fix: validate download URL domain and scheme in _download_url; simplify PPTX partial
- Add HTTPS scheme check and Google domain allowlist (.google.com,
.googleusercontent.com, .googleapis.com) in _download_url() to prevent
auth cookie exfiltration via attacker-controlled artifact metadata URLs
- Replace nested async def _download_pptx with functools.partial for clarity
- Update test URLs to use trusted Google domains
* fix: ruff format test_artifact_downloads.py
* fix: mock load_httpx_cookies in test_download_slide_deck_pptx for CI
* fix(rpc): improve error diagnostics for GET_NOTEBOOK failures (#114)
The generic "No result found for RPC ID" error was ambiguous — it could
be triggered by 4 distinct server response scenarios (empty response,
non-RPC JSON, null result data, short items) but reported the same
message for all of them. This made Issue #114 impossible to diagnose
remotely.
- Distinguish null-result-data from no-RPC-data in decoder error messages
- Add unit tests reproducing all 4 failure scenarios
- Add integration tests exercising the full client pipeline
- Add scripts/diagnose_get_notebook.py for live API debugging
* fix: address review feedback on diagnostic script
- Use granular httpx timeouts (10s connect, 60s read) for better
network resilience
- Narrow broad Exception catch to (IndexError, TypeError)
* fix: route health check errors to stdout and use distinct exit codes
Auth/infra failures now exit with code 2 instead of 1, allowing the
workflow to distinguish them from actual RPC ID mismatches. Error
messages are printed to stdout so they are captured in the report file
used for issue creation.
Fixes#111
* fix(ci): distinguish auth failures from RPC mismatches in health check
- Capture stderr in report file with 2>&1
- Use PIPESTATUS to preserve Python exit code through tee
- Create different issues for RPC mismatches (exit 1) vs auth failures (exit 2)
- Auth failure issues get title 'Authentication Failure' instead of 'RPC ID Mismatch'
Fixes#111
* fix: keep error messages on stderr per review feedback
Restore file=sys.stderr for error/warning prints. The workflow's 2>&1
redirection captures stderr in the report file, so errors will appear
in issue bodies while following the standard convention of separating
error output from normal output.
The START_DEEP_RESEARCH RPC frequently hits Google's rate limit in CI,
causing flaky nightly failures. Mark as xfail so it still runs but
doesn't fail the suite.
* fix: add runtime Python version check for clear error on <3.10 (#117)
Users on Python 3.9 get a cryptic TypeError from PEP 604 union syntax
(str | None) instead of a helpful message. Add a version guard that runs
before any such syntax is evaluated, in both the library __init__ and
the CLI entry point.
* fix: remove redundant module-level version check call
The explicit calls in __init__.py and notebooklm_cli.py are sufficient.
* fix: ask returns empty answer when API response marker changes (#118)
The answer parser had two brittle gates that caused empty answers:
1. `_MIN_ANSWER_LENGTH = 20` threshold prevented short answers from
being returned at all (e.g., user asks "Respond with exactly: OK")
2. `type_info[-1] == 1` answer marker was required for text to be used
as the answer. When Google changes this undocumented marker, all
answers silently fail with "No answer extracted from response".
Fix: Remove the minimum length gate and add a two-tier fallback
strategy — prefer chunks with the known answer marker, but fall back
to the longest text chunk when the marker is absent or changed.
Closes#118
* fix: add fallback logging and improve test coverage for answer extraction
- Add logger.warning when falling back to unmarked answer text,
signaling the API response format may have changed
- Elevate "No answer extracted" from debug to warning level
- Add test: shorter marked answer wins over longer unmarked text
- Add test: empty/None/non-string first[0] values correctly skipped
* refactor(tests): extract chat_api fixture to reduce test boilerplate
* test(integration): add tests for ask() fallback when API marker is absent
* fix(cli): sync server language setting to local config after login (#121)
When users set their global language on the server (e.g. via the web UI),
generate commands defaulted to 'en' because the local config had no
language entry. After login, we now fetch the server's language setting
and persist it locally so resolve_language() picks it up.
Closes#121
* fix(test): use importlib to bypass Click group shadowing on Python 3.10
On Python 3.10, patch("notebooklm.cli.language.get_config_path") resolves
to the Click group object instead of the language.py module. Use importlib
+ patch.object to get the actual module, matching the existing pattern in
conftest.py's get_cli_module helper.
* fix(docs): correct broken language commands anchor in python-api.md
The link to CLI language commands used #language-commands which doesn't
match the actual heading anchor. Updated to match the generated anchor
for "Language Commands (`notebooklm language <cmd>`)" heading.
Closes#120
* fix(docs): update language count for accuracy
9 languages are listed explicitly, so "over 70 other languages" is more
accurate than "80+ other languages" given 81 total supported languages.
Addresses gemini-code-assist review feedback.
X.com has anti-scraping protections that prevent NotebookLM from directly
fetching content. This results in error pages being parsed instead of the
actual article content.
Added a new 'Protected Website Content Issues' section with:
- Clear symptoms of the issue
- Root cause explanation
- Step-by-step solution using bird CLI
- Alternative methods
- Verification steps
Fixes: Users can now properly import X.com content by pre-fetching with
bird CLI and adding the local markdown file.
Author: Claude
Resolves#105
The `generate report` CLI command was missing the --language option
even though the underlying API already supported it. This adds the
missing option to allow users to specify output language per-request.
Changes:
- Added --language click option to generate_report_cmd
- Pass language parameter to client.artifacts.generate_report()
- Language defaults to config setting or 'en' if not specified
Testing:
- Pre-commit checks pass (ruff, mypy)
- Verified with `notebooklm generate report --help`
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Standard tier has 50 sources per notebook, but Plus (100), Pro (300),
and Ultra (600) tiers have higher limits. Added link to official Google
help article and clarified that the CLI doesn't enforce these limits.
Fixes#106
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The pytest_runtest_teardown hook was using item.fspath.name to check
the test filename, but py.path.local objects use .basename, not .name.
This caused an AttributeError that broke all e2e tests.
Use item.path.name instead, which returns a pathlib.Path (pytest 7+).
Fixes regression introduced in #95 by incorrect code review suggestion.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): don't reuse conversation ID when switching notebooks
Two related fixes for issue #94:
1. In ask command: When using `--notebook <id>` to query a different notebook
than the cached one, don't use the cached conversation_id.
2. In set_current_notebook: When switching notebooks via `notebooklm use`,
clear the cached conversation_id since conversations are notebook-specific.
Both fixes prevent attempting to continue a conversation from one notebook
when actually querying a different notebook.
Refactored ask command logic into helper functions for clarity:
- _determine_conversation_id(): Decides which conversation ID to use
- _get_latest_conversation_from_history(): Fetches recent conversation as fallback
Fixes#94
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: simplify set_current_notebook by reading context file once
Address code review feedback: avoid double-reading context file by
reading it once and checking notebook_id directly from the parsed context.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(e2e): add 15-second delay between generation tests to avoid rate limits
The CI test account was hitting Google's rate limits for expensive
artifact types (infographics, slide decks). This adds a pytest hook
that inserts a 15-second delay after each generation test to spread
out API calls.
The delay only applies to tests in test_generation.py that use the
generation_notebook_id fixture, and only when there's a next test
(avoids unnecessary delay at the end).
Fixes rate limit failures in nightly E2E tests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: address review feedback for generation test delay
- Use item.fspath.name for robust filename matching
- Add logging.info before delay for debugging CI failures
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(encoding): add explicit UTF-8 encoding to all file I/O operations
Fix Windows UnicodeDecodeError by adding encoding="utf-8" to all
read_text(), write_text(), and open() calls.
On Windows, Python defaults to the system locale encoding (typically
CP1252) instead of UTF-8, causing UnicodeDecodeError when reading
files containing non-ASCII characters like CJK text or em dashes.
Files updated:
- cli/skill.py: Package resource read and file I/O for SKILL.md
- auth.py: Storage state JSON reads
- cli/helpers.py: Context file reads/writes
- cli/session.py: Context and storage state reads
Fixes#83
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: add ensure_ascii=False for readable JSON context files
Address review feedback from gemini-code-assist: when notebook titles
contain non-ASCII characters (CJK, etc.), ensure_ascii=False writes them
directly instead of escaping as \uXXXX sequences, improving readability.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): restore ProactorEventLoop for Playwright login on Windows
Fixes#89: `notebooklm login` fails on Windows with Python 3.12+ due to
event loop policy conflict between:
- Issue #79 fix: WindowsSelectorEventLoopPolicy (prevents CLI hanging)
- Playwright requirement: ProactorEventLoop (for subprocess spawning)
Solution: Add `_windows_playwright_event_loop()` context manager that
temporarily restores the default event loop policy for Playwright, then
switches back to WindowsSelectorEventLoopPolicy.
This is a documented Playwright limitation - SelectorEventLoop does not
support asyncio.create_subprocess_exec() on Windows, which Playwright
uses to spawn the browser driver process.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(tests): move imports to module level per PEP 8
Address gemini-code-assist review feedback: consolidate asyncio,
unittest.mock.patch, and _windows_playwright_event_loop imports
at the top of the test module for better readability.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add Playwright smoke tests to catch Windows integration issues
Add TestPlaywrightSmokeTest class that actually invokes sync_playwright()
to verify it works with our event loop configuration. This would have
caught issue #89 before release.
Tests:
- test_playwright_initializes_with_context_manager: Windows-only test
that verifies Playwright works with _windows_playwright_event_loop()
- test_playwright_initializes_on_non_windows: Verifies Playwright works
normally on other platforms
These smoke tests run on CI and catch real integration issues that unit
tests with mocked platform checks would miss.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore: release v0.3.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* ci: add custom branch support to nightly E2E workflow
- Trigger from any branch → tests that branch automatically
- Trigger from main → tests main only
- Trigger from develop → tests develop only
- Optional custom_branch input to override
- Scheduled runs unchanged (main at 6 AM UTC, develop at 11 AM UTC)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Downloads now write to a .tmp file first and only rename to the final
path on success. On any failure (network error, timeout, disk full),
the temp file is cleaned up to avoid leaving corrupted partial files.
Before: Failed download mid-stream leaves corrupted partial file
After: Failed download cleans up, no corrupted file remains
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* ci: add regression tests and timeout improvements for bug prevention
- Add tests/unit/test_windows_compatibility.py with regression tests for
Windows-specific fixes (issue #75, #79, #80):
- Test that SelectorEventLoopPolicy is set on Windows (skipped on other platforms)
- Test that UTF-8 mode is enabled on Windows (skipped on other platforms)
- Test encoding resilience for common CLI characters (runs everywhere)
- Add pytest-timeout to dev dependencies with 60s global timeout
to prevent CI hangs from undetected infinite loops
- Improve HTTPX client timeouts in _core.py:
- Add separate connect_timeout (10s default) for faster network issue detection
- Use httpx.Timeout with granular connect/read/write/pool timeouts
- Shorter connect timeout helps detect network issues faster while
longer read/write timeouts accommodate slow RPC responses
- Document Windows #75 issues in docs/troubleshooting.md:
- CLI hanging due to ProactorEventLoop/IOCP issues
- Unicode encoding errors on non-English locales
- Solutions for both CLI and Python API usage
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): implement Windows compatibility fixes for asyncio and UTF-8
Implements the actual Windows fixes that were documented but not implemented:
- Set WindowsSelectorEventLoopPolicy on Windows at module import time
to avoid ProactorEventLoop hanging at IOCP layer (issue #75, #79)
- Set PYTHONUTF8=1 environment variable on Windows to prevent
UnicodeEncodeError on non-English locales (issue #75, #80)
These fixes run at module import time, before any async code executes,
ensuring the CLI works correctly on Windows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(sources): add VCR cassette for add_drive function
- Add test_add_drive to TestSourcesAPI with recorded cassette
- Validates single-wrapped params [source_data] fix from PR #73
- Uses public Google Doc for reproducible testing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(downloads): use streaming for large file downloads
Replace blocking download with streaming to handle large audio/video files:
- Uses per-chunk timeout (30s) instead of total timeout (60s)
- Streams directly to disk instead of loading into memory
- Handles files of any size without timeout issues
- Detects network failures quickly (30s) vs waiting for full timeout
Before: 200MB video @ 10Mbps would timeout (needs >27Mbps)
After: Works at any speed, uses minimal memory
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(downloads): correct async context manager for streaming
Fix the combined async with statement that doesn't work because
client.stream() returns a coroutine that must be awaited before
becoming an async context manager. Use nested async with blocks
with noqa comment explaining the dependency.
Also update test_download_url_direct to mock streaming response
instead of the old blocking response approach.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): add partial ID resolution to all CLI commands
Previously, partial ID resolution for notebooks, sources, and artifacts only
worked in a few commands (notebook.py, share.py). This left most CLI commands
broken when using partial IDs like `-n abc123` instead of full UUIDs.
Changes:
- Add resolve_notebook_id() to all CLI modules that accept -n/--notebook
- Add resolve_source_id() to commands accepting -s/--source (generate, chat)
- Update test conftest.py with mock objects for partial ID resolution
Affected modules:
- artifact.py: 8 commands now resolve notebook/artifact IDs
- chat.py: 3 commands now resolve notebook/source IDs
- download.py: All download commands now resolve notebook ID
- generate.py: 10 commands now resolve notebook/source IDs
- note.py: 6 commands now resolve notebook/note IDs
- research.py: 2 commands now resolve notebook ID
- source.py: 13 commands now resolve notebook/source IDs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(cli): consolidate resolve_source_ids helper
Move resolve_source_ids() from generate.py to helpers.py for reuse.
Simplifies chat.py by using the consolidated helper function.
Removes redundant conditional block in artifact.py.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes CLI commands hanging indefinitely on Windows after successful
authentication when making network requests (list, create, source add, etc.).
Root cause: Python's default ProactorEventLoop on Windows can block
indefinitely at _overlapped.GetQueuedCompletionStatus() during IOCP
operations, causing the CLI to appear frozen.
Solution: Use WindowsSelectorEventLoopPolicy on Windows, which is more
reliable for network I/O operations.
Tested on Windows 11 with Chinese locale - all CLI commands now complete
successfully without hanging.
Co-authored-by: wreuon <wreuon@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix Unicode encoding errors on non-English Windows systems
Fixes UnicodeEncodeError when displaying CLI output on Windows systems
with legacy encodings (Chinese, Japanese, Korean, Thai, Arabic, etc.).
Error example:
UnicodeEncodeError: 'cp950' codec can't encode character '\u2713'
Root cause: The rich library uses Unicode characters (✓, ✗, box drawing)
in table output. On Windows with non-UTF-8 locales (cp950, cp932, cp936,
cp949, etc.), these characters cause encoding errors.
Solution: Set PYTHONUTF8=1 to force UTF-8 encoding, which supports all
Unicode characters. This is Python's recommended approach for Windows
applications and will become the default in Python 3.15+.
Tested on Windows with Chinese locale (cp950) - all output now displays
correctly without encoding errors.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: move imports to top and apply UTF-8 fix only on Windows
Address review feedback:
- Move `import os` and add `import sys` to module imports
- Apply PYTHONUTF8 fix only on Windows (sys.platform == "win32")
- Use double quotes for string consistency
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: wreuon <wreuon@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Teng Lin <teng.lin@gmail.com>
- Move setup inside try block so cleanup runs if setup fails after creating notebook
- Wrap each cleanup operation in try/except to isolate failures
- Always attempt DELETE_NOTEBOOK even if other cleanup operations fail
- Print warning with notebook ID if deletion fails for manual cleanup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Main branch: 6 AM UTC
- Develop branch: 11 AM UTC (5 hours later)
- Manual dispatch supports selecting specific branch or both
- Uses matrix exclude with github.event.schedule to determine branch
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes two issues discovered while investigating source freshness:
1. check_freshness() bug - API returns different structures:
- URL sources: [] (empty array) for fresh
- Drive sources: [[null, true, [source_id]]] for fresh
Now handles both response formats correctly.
2. add_drive() bug - Wrong nesting level:
- Was: [[source_data]] (double wrapped)
- Now: [source_data] (single wrapped, matches web UI)
This fix enables adding Google Drive documents as sources.
Code changes:
- Fix check_freshness() to handle multiple response formats
- Fix add_drive() params nesting to match web UI
- Simplify check_freshness() logic (extract variable for readability)
Test changes:
- Add VCR cassette for Drive source freshness (nested response format)
- Add integration test for Drive source nested freshness response
- Add integration test for empty array response
- Strengthen VCR and E2E tests with explicit value assertions
- Update unit test for corrected add_drive payload structure
Documentation:
- Add check_freshness() to python-api.md SourcesAPI table
- Add usage example for check_freshness + refresh workflow
- Update rpc-reference.md with accurate response formats
- Add ADD_SOURCE - Google Drive RPC documentation
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The API returns responses with 3 levels of array nesting
[[[null, [summary], [[keywords]], []]]] but the parser only unwrapped
2 levels, causing empty summary and keywords to be returned.
- Add additional array unwrapping level in get_guide()
- Update all test mocks to match real API response structure
- Add non-empty value assertions to E2E and VCR tests
Fixes#70
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(scripts): handle null response in RPC health check
Add null check for response_text in make_rpc_call() to prevent
AttributeError when the server returns an empty/null response.
This fixes the crash that caused issue #69 to be incorrectly created
with "RPC ID Mismatch Detected" when the actual problem was a null
response handling bug in the health check script.
Fixes#69
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(scripts): add null check to test_rpc_method_with_data
Address review feedback: the same potential AttributeError exists
in test_rpc_method_with_data which also calls make_rpc_request
and uses response_text without null checking.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore: release v0.3.0
- Update CHANGELOG with v0.3.0 release notes (deprecations for type access)
- Update docs/stability.md migration guide for v0.3.0
- Update docs/releasing.md to use worktree + PR workflow
Deprecated (to be removed in v0.4.0):
- Source.source_type -> use .kind instead
- Artifact.artifact_type -> use .kind instead
- SourceFulltext.source_type -> use .kind instead
- StudioContentType -> use ArtifactType instead
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: address review feedback and reorder release workflow
- Add Artifact.variant to deprecation list in CHANGELOG.md
- Add Artifact.variant to deprecation table in stability.md
- Restore Artifact.variant migration guide section
- Reorder release workflow: TestPyPI verification before merge to main
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add E2E test step before TestPyPI publish
Run E2E tests on release branch to catch integration issues
before publishing to TestPyPI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove inaccurate StudioContentType entry from Changed section
StudioContentType is deprecated (listed in Deprecated section),
not renamed. Removed duplicate/inaccurate entry from Changed section.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The GET_SUGGESTED_REPORTS RPC uses [[2], notebook_id] params, not just
[notebook_id]. This aligns with the implementation in _artifacts.py.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(rpc): remove unused POLL_ARTIFACT and GET_ARTIFACT
Investigation confirmed that POLL_ARTIFACT (gArtLc with artifact_id params)
never worked - it always returned None. The same RPC ID works only with
LIST_ARTIFACTS params format. GET_ARTIFACT was also unused in the library.
Changes:
- Remove POLL_ARTIFACT and GET_ARTIFACT from RPCMethod enum
- Simplify poll_status() to directly use _list_raw() instead of trying
POLL first and falling back
- Update check_rpc_health.py to use LIST_ARTIFACTS for polling
- Update tests to use LIST_ARTIFACTS response format
- Update docs/rpc-reference.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(rpc): replace ACT_ON_SOURCES with GENERATE_MIND_MAP and fix suggest_reports
- Remove ACT_ON_SOURCES alias from rpc/types.py, keeping only GENERATE_MIND_MAP
- Fix suggest_reports() to use GET_SUGGESTED_REPORTS RPC which works correctly
- The previous ACT_ON_SOURCES with "suggested_report_formats" command did not work
- Remove --source option from CLI suggest_reports (RPC doesn't support source filtering)
- Update tests and documentation
Investigation found that ACT_ON_SOURCES only works with "interactive_mindmap" command,
not with "suggested_report_formats". GET_SUGGESTED_REPORTS is the correct RPC for
getting suggested reports.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove redundant length check in suggest_reports
The outer condition `len(item) >= 5` already guarantees item[4] exists,
so the `len(item) > 4` check is redundant.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(compat): add deprecation shims for backward compatibility
Add deprecated property shims that emit DeprecationWarning:
- Source.source_type → use .kind (returns SourceType enum)
- Artifact.artifact_type → use .kind (returns ArtifactType enum)
- Artifact.variant → use .kind, .is_quiz, or .is_flashcards
- SourceFulltext.source_type → use .kind
- StudioContentType → use ArtifactType
The shims return backward-compatible values (e.g., source_type returns
"text", "url", "youtube", "text_file" as before) while emitting warnings.
All deprecated items will be removed in v0.4.0.
Also adds comprehensive migration guide to docs/stability.md.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove unreachable GOOGLE_DRIVE_AUDIO/VIDEO from compat map
Address code review feedback: these SourceType values cannot be
produced by _SOURCE_TYPE_CODE_MAP, so the entries were dead code.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The test_generate_mind_map test creates a new mind map each nightly run
but never cleaned up, causing 21+ mind maps to accumulate over time.
Now deletes existing mind maps before creating a fresh one to keep
only one mind map in the generation notebook.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>