NotebookLM Quick Commands Query Google NotebookLM for source-grounded, citation-backed answers. Environment All dependencies and authentication are handled automatically by : - First run creates and installs Python/Node.js dependencies - If Google auth is missing or expired, a browser window opens automatically - No manual pre-flight steps required --- Usage Commands Notebook Management | Command | Description | |---------|-------------| | | Authenticate with Google | | | Show auth and library status | | | List all Google accounts | | | Add a new Google account | | | Switch active account (by…

, input_value):\n return input_value\n\n return None\n\n\nasync def discover_notebook_metadata(notebook_id: str) -> Dict[str, Any]:\n \"\"\"Query notebook to discover its name, description, and topics.\"\"\"\n from notebooklm_wrapper import NotebookLMWrapper, NotebookLMError\n\n result = {\n 'name': 'Untitled',\n 'description': '',\n 'topics': []\n }\n\n async with NotebookLMWrapper() as wrapper:\n # Get notebook title from API\n print(\" Fetching notebook info...\")\n notebooks = await wrapper.list_notebooks()\n for nb in notebooks:\n if nb.get('id') == notebook_id:\n result['name'] = nb.get('title', 'Untitled')\n break\n\n # Query notebook content for description and topics\n print(\" Analyzing notebook content...\")\n try:\n question = (\n \"What is this notebook about? Respond in this exact JSON format only, no other text:\\n\"\n '{\"description\": \"one sentence description\", \"topics\": [\"topic1\", \"topic2\", \"topic3\"]}'\n )\n response = await wrapper.chat(notebook_id, question)\n text = response.get('text', '')\n\n # Extract JSON from response\n json_match = re.search(r'\\{[^{}]*\"description\"[^{}]*\"topics\"[^{}]*\\}', text, re.DOTALL)\n if json_match:\n parsed = json.loads(json_match.group(0))\n result['description'] = parsed.get('description', '')\n result['topics'] = parsed.get('topics', [])\n else:\n # Fallback: use the response as description\n result['description'] = text[:200] if text else ''\n except NotebookLMError as e:\n print(f\" ⚠️ Could not analyze content: {e.message}\")\n except Exception as e:\n print(f\" ⚠️ Could not analyze content: {e}\")\n\n return result\n\n\ndef main():\n \"\"\"Command-line interface for notebook management\"\"\"\n parser = argparse.ArgumentParser(description='Manage NotebookLM library')\n\n subparsers = parser.add_subparsers(dest='command', help='Commands')\n\n # Add command - Smart Add with auto-discovery\n add_parser = subparsers.add_parser('add', help='Add a notebook (auto-discovers metadata)')\n add_parser.add_argument('identifier', nargs='?', help='Notebook ID or URL')\n add_parser.add_argument('--url', help='NotebookLM URL (alternative to positional)')\n add_parser.add_argument('--notebook-id', help='NotebookLM notebook ID (alternative to positional)')\n add_parser.add_argument('--name', help='Override auto-discovered name')\n add_parser.add_argument('--description', help='Override auto-discovered description')\n add_parser.add_argument('--topics', help='Override auto-discovered topics (comma-separated)')\n add_parser.add_argument('--tags', help='Additional tags (comma-separated)')\n\n # List command\n list_parser = subparsers.add_parser('list', help='List all notebooks')\n list_parser.add_argument('--all-accounts', action='store_true',\n help='Show notebooks from all accounts')\n\n # Search command\n search_parser = subparsers.add_parser('search', help='Search notebooks')\n search_parser.add_argument('--query', required=True, help='Search query')\n\n # Activate command\n activate_parser = subparsers.add_parser('activate', help='Set active notebook')\n activate_parser.add_argument('--id', required=True, help='Notebook ID')\n\n # Remove command\n remove_parser = subparsers.add_parser('remove', help='Remove a notebook')\n remove_parser.add_argument('--id', required=True, help='Notebook ID')\n\n # Stats command\n subparsers.add_parser('stats', help='Show library statistics')\n\n args = parser.parse_args()\n\n # Initialize library\n library = NotebookLibrary()\n\n # Execute command\n if args.command == 'add':\n # Smart Add: auto-discover metadata from notebook\n input_value = args.identifier or args.url or args.notebook_id\n\n if not input_value:\n print(\"❌ Error: Provide a notebook ID or URL\")\n print(\" Usage: notebook_manager.py add \u003cnotebook-id-or-url>\")\n return 1\n\n # Extract notebook ID from input\n notebook_id = extract_notebook_id(input_value)\n if not notebook_id:\n print(f\"❌ Error: Cannot extract notebook ID from: {input_value}\")\n return 1\n\n url = f\"https://notebooklm.google.com/notebook/{notebook_id}\"\n\n # Check for duplicates by URL\n for existing in library.notebooks.values():\n if notebook_id in existing.get('url', ''):\n print(f\"❌ Error: Notebook already in library as '{existing['name']}' ({existing['id']})\")\n return 1\n\n print(f\"🔍 Discovering notebook metadata...\")\n\n # Auto-discover metadata using async wrapper\n try:\n discovered = asyncio.run(discover_notebook_metadata(notebook_id))\n except Exception as e:\n print(f\"❌ Error discovering metadata: {e}\")\n return 1\n\n # Use discovered values, allow overrides\n name = args.name or discovered.get('name', 'Untitled')\n description = args.description or discovered.get('description', '')\n topics = [t.strip() for t in args.topics.split(',')] if args.topics else discovered.get('topics', [])\n tags = [t.strip() for t in args.tags.split(',')] if args.tags else []\n\n print(f\" Name: {name}\")\n print(f\" Description: {description[:80]}{'...' if len(description) > 80 else ''}\")\n print(f\" Topics: {', '.join(topics)}\")\n\n notebook = library.add_notebook(\n url=url,\n name=name,\n description=description,\n topics=topics,\n tags=tags\n )\n print(json.dumps(notebook, indent=2))\n\n elif args.command == 'list':\n account_mgr = AccountManager()\n active = account_mgr.get_active_account()\n\n if hasattr(args, 'all_accounts') and args.all_accounts:\n # Show all notebooks grouped by account\n grouped = library.list_all_notebooks_grouped()\n print(\"\\n📚 All Notebooks:\")\n for account_key, notebooks in grouped.items():\n print(f\"\\n {account_key}:\")\n if notebooks:\n for notebook in notebooks:\n active_mark = \" [ACTIVE]\" if notebook['id'] == library.active_notebook_id else \"\"\n print(f\" 📓 {notebook['name']}{active_mark}\")\n print(f\" ID: {notebook['id']}\")\n else:\n print(\" (no notebooks)\")\n else:\n # Show notebooks for active account\n if active:\n print(f\"\\n📧 Active account: [{active.index}] {active.email}\")\n notebooks = library.list_notebooks_for_account()\n else:\n notebooks = library.list_notebooks()\n\n if notebooks:\n print(\"\\n📚 Notebook Library:\")\n for notebook in notebooks:\n active_mark = \" [ACTIVE]\" if notebook['id'] == library.active_notebook_id else \"\"\n print(f\"\\n 📓 {notebook['name']}{active_mark}\")\n print(f\" ID: {notebook['id']}\")\n print(f\" Topics: {', '.join(notebook['topics'])}\")\n print(f\" Uses: {notebook['use_count']}\")\n else:\n print(\"📚 No notebooks for this account. Add notebooks with: notebook_manager.py add\")\n\n elif args.command == 'search':\n results = library.search_notebooks(args.query)\n if results:\n print(f\"\\n🔍 Found {len(results)} notebooks:\")\n for notebook in results:\n print(f\"\\n 📓 {notebook['name']} ({notebook['id']})\")\n print(f\" {notebook['description']}\")\n else:\n print(f\"🔍 No notebooks found for: {args.query}\")\n\n elif args.command == 'activate':\n notebook = library.select_notebook(args.id)\n print(f\"Now using: {notebook['name']}\")\n\n elif args.command == 'remove':\n if library.remove_notebook(args.id):\n print(\"Notebook removed from library\")\n\n elif args.command == 'stats':\n stats = library.get_stats()\n print(\"\\n📊 Library Statistics:\")\n print(f\" Total notebooks: {stats['total_notebooks']}\")\n print(f\" Total topics: {stats['total_topics']}\")\n print(f\" Total uses: {stats['total_use_count']}\")\n if stats['active_notebook']:\n print(f\" Active: {stats['active_notebook']['name']}\")\n if stats['most_used_notebook']:\n print(f\" Most used: {stats['most_used_notebook']['name']} ({stats['most_used_notebook']['use_count']} uses)\")\n print(f\" Library path: {stats['library_path']}\")\n\n else:\n parser.print_help()\n\n\nif __name__ == \"__main__\":\n main()","content_type":"text/x-python; charset=utf-8","language":"python","size":23018,"content_sha256":"b70b42cb73d61b7b249143b76a4bb54ef4e6d595b5ab8e6ce1dee0fadf373a62"},{"filename":"scripts/notebooklm_wrapper.py","content":"#!/usr/bin/env python3\n\"\"\"\nNotebookLM Wrapper - Thin async wrapper over notebooklm-py.\nHandles auth loading, token refresh, and browser fallback for uploads.\n\"\"\"\n\nimport asyncio\nimport json\nimport re\nfrom datetime import datetime, timezone, timedelta\nfrom pathlib import Path\nfrom typing import Optional, Any, List\n\nfrom notebooklm import NotebookLMClient\n\nfrom config import (\n GOOGLE_AUTH_FILE,\n NOTEBOOKLM_TOKEN_STALENESS_DAYS,\n DEFAULT_SESSION_ID,\n)\n\n\nclass NotebookLMError(Exception):\n \"\"\"Base exception for NotebookLM wrapper errors.\"\"\"\n\n def __init__(self, message: str, code: str = \"UNKNOWN\", recovery: str = \"\"):\n self.message = message\n self.code = code\n self.recovery = recovery\n super().__init__(message)\n\n\nclass NotebookLMAuthError(NotebookLMError):\n \"\"\"Raised when authentication fails or tokens are invalid.\"\"\"\n\n def __init__(self, message: str, recovery: str = \"\"):\n super().__init__(\n message,\n code=\"AUTH_ERROR\",\n recovery=recovery or \"Run: python scripts/run.py auth_manager.py setup\",\n )\n\n\nclass NotebookLMWrapper:\n \"\"\"Thin async wrapper over notebooklm-py with auth loading and fallback.\"\"\"\n\n def __init__(self, auth_file: Optional[Path] = None, account_index: Optional[int] = None):\n \"\"\"Initialize wrapper with optional account selection.\n\n Args:\n auth_file: Explicit auth file path (overrides account_index)\n account_index: Account index to use (defaults to active account)\n \"\"\"\n if auth_file:\n self.auth_file = auth_file\n elif account_index is not None:\n from account_manager import AccountManager\n account_mgr = AccountManager()\n account = account_mgr.get_account_by_index(account_index)\n if account:\n self.auth_file = account.file_path\n else:\n raise ValueError(f\"Account not found: {account_index}\")\n else:\n # Use active account\n from account_manager import AccountManager\n account_mgr = AccountManager()\n active_auth_file = account_mgr.get_active_auth_file()\n self.auth_file = active_auth_file or GOOGLE_AUTH_FILE\n\n self._client: Optional[NotebookLMClient] = None\n self._auth_data: Optional[dict] = None\n\n async def __aenter__(self) -> \"NotebookLMWrapper\":\n \"\"\"Load auth and initialize notebooklm-py client.\"\"\"\n # Use from_storage() which handles token extraction internally\n self._client = await NotebookLMClient.from_storage(str(self.auth_file))\n await self._client.__aenter__()\n return self\n\n async def __aexit__(self, exc_type, exc_val, exc_tb):\n \"\"\"Clean up client resources.\"\"\"\n if self._client:\n await self._client.__aexit__(exc_type, exc_val, exc_tb)\n self._client = None\n\n def _load_auth_file(self) -> dict:\n \"\"\"Load auth data from file.\"\"\"\n if not self.auth_file.exists():\n raise NotebookLMAuthError(\n \"Auth file not found\",\n recovery=\"Run: python scripts/run.py auth_manager.py setup\",\n )\n try:\n return json.loads(self.auth_file.read_text())\n except json.JSONDecodeError as e:\n raise NotebookLMAuthError(f\"Invalid auth file: {e}\")\n\n def _is_token_stale(self) -> bool:\n \"\"\"Check if tokens are older than staleness threshold.\"\"\"\n if not self._auth_data:\n return True\n extracted_at = self._auth_data.get(\"extracted_at\")\n if not extracted_at:\n return True\n try:\n timestamp = datetime.fromisoformat(extracted_at.replace(\"Z\", \"+00:00\"))\n age = datetime.now(timezone.utc) - timestamp\n return age > timedelta(days=NOTEBOOKLM_TOKEN_STALENESS_DAYS)\n except (ValueError, TypeError):\n return True\n\n @staticmethod\n def _is_auth_error(error: Exception) -> bool:\n \"\"\"Check if an exception indicates an auth error.\"\"\"\n message = str(error).lower()\n return any(\n token in message\n for token in (\"401\", \"403\", \"unauthorized\", \"not authenticated\", \"invalid token\")\n )\n\n async def _with_retry(self, coro_func, max_retries: int = 1):\n \"\"\"Execute coroutine with token refresh retry on auth errors.\"\"\"\n try:\n return await coro_func()\n except Exception as e:\n if self._is_auth_error(e) and max_retries > 0:\n await self._refresh_tokens()\n return await self._with_retry(coro_func, max_retries - 1)\n raise NotebookLMError(str(e), code=\"API_ERROR\")\n\n async def _refresh_tokens(self):\n \"\"\"Refresh tokens using agent-browser.\"\"\"\n # Import here to avoid circular dependency\n from auth_manager import AuthManager\n\n auth_manager = AuthManager()\n # This is synchronous but we call it from async context\n loop = asyncio.get_event_loop()\n await loop.run_in_executor(None, auth_manager.refresh_notebooklm_tokens)\n\n # Recreate client with fresh tokens using from_storage\n if self._client:\n await self._client.__aexit__(None, None, None)\n self._client = await NotebookLMClient.from_storage(str(self.auth_file))\n await self._client.__aenter__()\n\n # === Notebooks API ===\n\n async def create_notebook(self, name: str) -> dict:\n \"\"\"Create a new notebook. Falls back to browser on failure.\"\"\"\n async def _create():\n notebook = await self._client.notebooks.create(name)\n return {\n \"id\": notebook.id,\n \"title\": notebook.title,\n }\n try:\n return await self._with_retry(_create)\n except NotebookLMError:\n # Fallback to browser creation\n return await self._fallback_create_notebook(name)\n\n async def list_notebooks(self) -> List[dict]:\n \"\"\"List all notebooks.\"\"\"\n async def _list():\n notebooks = await self._client.notebooks.list()\n return [\n {\n \"id\": nb.id,\n \"title\": nb.title,\n }\n for nb in notebooks\n ]\n return await self._with_retry(_list)\n\n async def delete_notebook(self, notebook_id: str) -> bool:\n \"\"\"Delete a notebook.\"\"\"\n async def _delete():\n await self._client.notebooks.delete(notebook_id)\n return True\n return await self._with_retry(_delete)\n\n async def rename_notebook(self, notebook_id: str, new_title: str) -> dict:\n \"\"\"Rename a notebook.\"\"\"\n async def _rename():\n notebook = await self._client.notebooks.rename(notebook_id, new_title)\n return {\n \"id\": notebook.id,\n \"title\": notebook.title,\n }\n return await self._with_retry(_rename)\n\n async def get_notebook_summary(self, notebook_id: str) -> str:\n \"\"\"Get AI-generated summary for a notebook.\"\"\"\n async def _summary():\n return await self._client.notebooks.summary(notebook_id)\n return await self._with_retry(_summary)\n\n async def get_notebook_description(self, notebook_id: str) -> dict:\n \"\"\"Get AI-generated description and suggested topics.\"\"\"\n async def _description():\n desc = await self._client.notebooks.description(notebook_id)\n return {\n \"summary\": desc.summary,\n \"suggested_topics\": [t.question for t in desc.suggested_topics] if hasattr(desc, 'suggested_topics') else [],\n }\n return await self._with_retry(_description)\n\n # === Sources API ===\n\n async def add_file(self, notebook_id: str, file_path: Path) -> dict:\n \"\"\"Upload a file to a notebook. Falls back to browser on failure.\"\"\"\n async def _add():\n source = await self._client.sources.add_file(notebook_id, file_path)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n \"source_type\": source.source_type,\n }\n\n try:\n return await self._with_retry(_add)\n except NotebookLMError:\n # Fallback to browser upload\n return await self._fallback_upload(notebook_id, file_path)\n\n async def add_url(self, notebook_id: str, url: str) -> dict:\n \"\"\"Add a URL source to a notebook.\"\"\"\n async def _add():\n source = await self._client.sources.add_url(notebook_id, url)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n \"source_type\": source.source_type,\n }\n return await self._with_retry(_add)\n\n async def add_youtube(self, notebook_id: str, url: str) -> dict:\n \"\"\"Add a YouTube video source to a notebook.\"\"\"\n async def _add():\n source = await self._client.sources.add_youtube(notebook_id, url)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n \"source_type\": source.source_type,\n }\n return await self._with_retry(_add)\n\n async def add_text(self, notebook_id: str, title: str, content: str) -> dict:\n \"\"\"Add text content as a source to a notebook.\"\"\"\n async def _add():\n source = await self._client.sources.add_text(notebook_id, title, content)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n \"source_type\": source.source_type,\n }\n return await self._with_retry(_add)\n\n async def list_sources(self, notebook_id: str) -> List[dict]:\n \"\"\"List all sources in a notebook.\"\"\"\n async def _list():\n sources = await self._client.sources.list(notebook_id)\n return [\n {\n \"source_id\": src.id,\n \"title\": src.title,\n \"source_type\": src.source_type,\n \"is_ready\": src.is_ready,\n }\n for src in sources\n ]\n return await self._with_retry(_list)\n\n async def get_source(self, notebook_id: str, source_id: str) -> dict:\n \"\"\"Get details of a specific source.\"\"\"\n async def _get():\n source = await self._client.sources.get(notebook_id, source_id)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n \"source_type\": source.source_type,\n \"is_ready\": source.is_ready,\n }\n return await self._with_retry(_get)\n\n async def delete_source(self, notebook_id: str, source_id: str) -> bool:\n \"\"\"Delete a source from a notebook.\"\"\"\n async def _delete():\n await self._client.sources.delete(notebook_id, source_id)\n return True\n return await self._with_retry(_delete)\n\n async def rename_source(self, notebook_id: str, source_id: str, new_title: str) -> dict:\n \"\"\"Rename a source.\"\"\"\n async def _rename():\n source = await self._client.sources.rename(notebook_id, source_id, new_title)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n }\n return await self._with_retry(_rename)\n\n async def refresh_source(self, notebook_id: str, source_id: str) -> dict:\n \"\"\"Refresh a URL source to re-fetch content.\"\"\"\n async def _refresh():\n source = await self._client.sources.refresh(notebook_id, source_id)\n return {\n \"source_id\": source.id,\n \"title\": source.title,\n }\n return await self._with_retry(_refresh)\n\n async def get_source_fulltext(self, notebook_id: str, source_id: str) -> dict:\n \"\"\"Get full indexed text content of a source.\"\"\"\n async def _fulltext():\n fulltext = await self._client.sources.get_fulltext(notebook_id, source_id)\n return {\n \"char_count\": fulltext.char_count,\n \"content\": fulltext.content,\n }\n return await self._with_retry(_fulltext)\n\n async def get_source_guide(self, notebook_id: str, source_id: str) -> dict:\n \"\"\"Get AI-generated summary and keywords for a source.\"\"\"\n async def _guide():\n guide = await self._client.sources.get_guide(notebook_id, source_id)\n return guide\n return await self._with_retry(_guide)\n\n # === Chat API ===\n\n async def chat(self, notebook_id: str, message: str) -> dict:\n \"\"\"Send a chat message to a notebook and get a response. Falls back to browser on failure.\"\"\"\n async def _chat():\n from notebooklm import ChatMode\n\n # Set mode to DETAILED for comprehensive answers (not quick search)\n await self._client.chat.set_mode(notebook_id, ChatMode.DETAILED)\n\n # Use client.chat.ask() - chat is a ChatAPI object, not callable\n response = await self._client.chat.ask(notebook_id, message)\n return {\n \"text\": response.answer, # AskResult uses 'answer' not 'text'\n \"citations\": response.references if hasattr(response, \"references\") else [],\n \"conversation_id\": response.conversation_id if hasattr(response, \"conversation_id\") else None,\n }\n try:\n return await self._with_retry(_chat)\n except NotebookLMError:\n # Fallback to browser-based chat\n return await self._fallback_chat(notebook_id, message)\n\n # === Audio/Podcast API ===\n\n async def generate_audio(\n self,\n notebook_id: str,\n instructions: str = \"\",\n audio_format: str = \"DEEP_DIVE\",\n audio_length: str = \"DEFAULT\",\n ) -> dict:\n \"\"\"Generate audio podcast from notebook content.\"\"\"\n async def _generate():\n from notebooklm import AudioFormat, AudioLength\n\n format_map = {\n \"DEEP_DIVE\": AudioFormat.DEEP_DIVE,\n \"BRIEF\": AudioFormat.BRIEF,\n \"CRITIQUE\": AudioFormat.CRITIQUE,\n \"DEBATE\": AudioFormat.DEBATE,\n }\n length_map = {\n \"SHORT\": AudioLength.SHORT,\n \"DEFAULT\": AudioLength.DEFAULT,\n \"LONG\": AudioLength.LONG,\n }\n\n status = await self._client.artifacts.generate_audio(\n notebook_id,\n audio_format=format_map.get(audio_format.upper(), AudioFormat.DEEP_DIVE),\n audio_length=length_map.get(audio_length.upper(), AudioLength.DEFAULT),\n instructions=instructions or None,\n )\n return {\n \"task_id\": status.task_id,\n \"status\": \"started\",\n }\n return await self._with_retry(_generate)\n\n async def wait_for_audio(\n self,\n notebook_id: str,\n task_id: str,\n timeout: int = 600,\n poll_interval: int = 10,\n ) -> dict:\n \"\"\"Wait for audio generation to complete.\"\"\"\n async def _wait():\n final = await self._client.artifacts.wait_for_completion(\n notebook_id,\n task_id,\n timeout=timeout,\n poll_interval=poll_interval,\n )\n return {\n \"is_complete\": final.is_complete,\n \"is_failed\": getattr(final, 'is_failed', False),\n \"url\": getattr(final, 'url', None),\n \"error\": getattr(final, 'error', None),\n }\n return await self._with_retry(_wait)\n\n async def download_audio(\n self,\n notebook_id: str,\n output_path: str,\n artifact_id: str = None,\n ) -> str:\n \"\"\"Download generated audio file.\"\"\"\n async def _download():\n path = await self._client.artifacts.download_audio(\n notebook_id,\n output_path,\n artifact_id=artifact_id,\n )\n return str(path)\n return await self._with_retry(_download)\n\n async def list_artifacts(self, notebook_id: str, artifact_type: str = None) -> List[dict]:\n \"\"\"List all artifacts in a notebook.\n\n Args:\n notebook_id: The notebook ID\n artifact_type: Optional filter - 'audio', 'video', 'slide-deck', 'infographic'\n \"\"\"\n async def _list():\n artifacts = await self._client.artifacts.list(notebook_id)\n result = []\n for artifact in artifacts:\n # Handle different possible attribute names from the API\n art_type = getattr(artifact, 'artifact_type', None) or getattr(artifact, 'type', None) or getattr(artifact, 'status_str', 'unknown')\n item = {\n \"artifact_id\": artifact.id,\n \"type\": art_type,\n \"title\": getattr(artifact, 'title', None),\n \"status\": getattr(artifact, 'status', None) or getattr(artifact, 'status_str', None),\n \"is_completed\": getattr(artifact, 'is_completed', False),\n \"created_at\": getattr(artifact, 'created_at', None),\n \"url\": getattr(artifact, 'url', None),\n }\n if artifact_type is None or str(art_type).lower() == artifact_type.lower():\n result.append(item)\n return result\n return await self._with_retry(_list)\n\n async def get_artifact(self, notebook_id: str, artifact_id: str) -> dict:\n \"\"\"Get details of a specific artifact.\"\"\"\n async def _get():\n artifact = await self._client.artifacts.get(notebook_id, artifact_id)\n art_type = getattr(artifact, 'artifact_type', None) or getattr(artifact, 'type', None) or getattr(artifact, 'status_str', 'unknown')\n return {\n \"artifact_id\": artifact.id,\n \"type\": art_type,\n \"title\": getattr(artifact, 'title', None),\n \"status\": getattr(artifact, 'status', None) or getattr(artifact, 'status_str', None),\n \"is_complete\": getattr(artifact, 'is_completed', False),\n \"is_failed\": getattr(artifact, 'is_failed', False),\n \"url\": getattr(artifact, 'url', None),\n \"error\": getattr(artifact, 'error', None),\n \"created_at\": getattr(artifact, 'created_at', None),\n }\n return await self._with_retry(_get)\n\n async def delete_artifact(self, notebook_id: str, artifact_id: str) -> bool:\n \"\"\"Delete an artifact from a notebook.\"\"\"\n async def _delete():\n await self._client.artifacts.delete(notebook_id, artifact_id)\n return True\n return await self._with_retry(_delete)\n\n # === Slide Deck / Infographic API ===\n\n async def generate_slide_deck(\n self,\n notebook_id: str,\n instructions: str = \"\",\n slide_format: str = \"DETAILED_DECK\",\n slide_length: str = \"DEFAULT\",\n ) -> dict:\n \"\"\"Generate slide deck from notebook content.\n\n Args:\n notebook_id: The notebook ID\n instructions: Custom instructions for the slide deck\n slide_format: DETAILED_DECK or PRESENTER_SLIDES\n slide_length: SHORT or DEFAULT\n \"\"\"\n async def _generate():\n from notebooklm.rpc.types import SlideDeckFormat, SlideDeckLength\n\n format_map = {\n \"DETAILED_DECK\": SlideDeckFormat.DETAILED_DECK,\n \"PRESENTER_SLIDES\": SlideDeckFormat.PRESENTER_SLIDES,\n }\n length_map = {\n \"SHORT\": SlideDeckLength.SHORT,\n \"DEFAULT\": SlideDeckLength.DEFAULT,\n }\n\n status = await self._client.artifacts.generate_slide_deck(\n notebook_id,\n slide_format=format_map.get(slide_format.upper(), SlideDeckFormat.DETAILED_DECK),\n slide_length=length_map.get(slide_length.upper(), SlideDeckLength.DEFAULT),\n instructions=instructions or None,\n )\n return {\n \"task_id\": status.task_id,\n \"status\": \"started\",\n }\n return await self._with_retry(_generate)\n\n async def generate_infographic(\n self,\n notebook_id: str,\n instructions: str = \"\",\n orientation: str = \"LANDSCAPE\",\n detail_level: str = \"STANDARD\",\n ) -> dict:\n \"\"\"Generate infographic from notebook content.\n\n Args:\n notebook_id: The notebook ID\n instructions: Custom instructions for the infographic\n orientation: LANDSCAPE, PORTRAIT, or SQUARE\n detail_level: CONCISE, STANDARD, or DETAILED\n \"\"\"\n async def _generate():\n from notebooklm.rpc.types import InfographicOrientation, InfographicDetail\n\n orientation_map = {\n \"LANDSCAPE\": InfographicOrientation.LANDSCAPE,\n \"PORTRAIT\": InfographicOrientation.PORTRAIT,\n \"SQUARE\": InfographicOrientation.SQUARE,\n }\n detail_map = {\n \"CONCISE\": InfographicDetail.CONCISE,\n \"STANDARD\": InfographicDetail.STANDARD,\n \"DETAILED\": InfographicDetail.DETAILED,\n }\n\n status = await self._client.artifacts.generate_infographic(\n notebook_id,\n orientation=orientation_map.get(orientation.upper(), InfographicOrientation.LANDSCAPE),\n detail_level=detail_map.get(detail_level.upper(), InfographicDetail.STANDARD),\n instructions=instructions or None,\n )\n return {\n \"task_id\": status.task_id,\n \"status\": \"started\",\n }\n return await self._with_retry(_generate)\n\n async def download_slide_deck(\n self,\n notebook_id: str,\n output_path: str,\n artifact_id: str = None,\n ) -> str:\n \"\"\"Download generated slide deck as PDF.\"\"\"\n async def _download():\n path = await self._client.artifacts.download_slide_deck(\n notebook_id,\n output_path,\n artifact_id=artifact_id,\n )\n return str(path)\n return await self._with_retry(_download)\n\n async def download_infographic(\n self,\n notebook_id: str,\n output_path: str,\n artifact_id: str = None,\n ) -> str:\n \"\"\"Download generated infographic as PNG.\"\"\"\n async def _download():\n path = await self._client.artifacts.download_infographic(\n notebook_id,\n output_path,\n artifact_id=artifact_id,\n )\n return str(path)\n return await self._with_retry(_download)\n\n async def get_audio_status(self, notebook_id: str, task_id: str) -> dict:\n \"\"\"Get status of an audio generation task.\"\"\"\n async def _status():\n status = await self._client.artifacts.poll_status(notebook_id, task_id)\n return {\n \"task_id\": task_id,\n \"status\": getattr(status, 'status', 'unknown'),\n \"is_complete\": getattr(status, 'is_complete', False),\n \"is_failed\": getattr(status, 'is_failed', False),\n \"progress\": getattr(status, 'progress', None),\n \"url\": getattr(status, 'url', None),\n \"error\": getattr(status, 'error', None),\n }\n return await self._with_retry(_status)\n\n async def download_artifact(\n self,\n notebook_id: str,\n artifact_id: str,\n output_path: str,\n artifact_type: str = \"audio\",\n ) -> str:\n \"\"\"Download any artifact type to local path.\n\n Args:\n notebook_id: The notebook ID\n artifact_id: The artifact ID\n output_path: Local path to save the file\n artifact_type: 'audio', 'video', 'slide-deck', 'infographic'\n \"\"\"\n async def _download():\n download_methods = {\n \"audio\": self._client.artifacts.download_audio,\n \"video\": self._client.artifacts.download_video,\n \"slide-deck\": self._client.artifacts.download_slide_deck,\n \"infographic\": self._client.artifacts.download_infographic,\n }\n method = download_methods.get(artifact_type)\n if not method:\n raise NotebookLMError(\n f\"Unknown artifact type: {artifact_type}\",\n code=\"INVALID_TYPE\",\n recovery=\"Use: audio, video, slide-deck, or infographic\",\n )\n path = await method(notebook_id, output_path, artifact_id=artifact_id)\n return str(path)\n return await self._with_retry(_download)\n\n # === Browser Fallback ===\n\n async def _fallback_create_notebook(self, name: str) -> dict:\n \"\"\"Create notebook via browser automation when API fails.\"\"\"\n from agent_browser_client import AgentBrowserClient, AgentBrowserError\n from auth_manager import AuthManager\n\n loop = asyncio.get_event_loop()\n\n def _browser_create():\n auth = AuthManager()\n client = AgentBrowserClient(session_id=DEFAULT_SESSION_ID)\n\n try:\n client.connect()\n auth.restore_auth(\"google\", client=client)\n\n # Navigate to NotebookLM home\n print(\" 🌐 Creating notebook via browser...\")\n client.navigate(\"https://notebooklm.google.com\")\n\n import time\n time.sleep(3)\n\n # Get snapshot and find create notebook button\n snapshot = client.snapshot()\n create_ref = self._find_button_ref(snapshot, [\"create\", \"new notebook\", \"new\"])\n\n if not create_ref:\n raise NotebookLMError(\n \"Create notebook button not found\",\n code=\"ELEMENT_NOT_FOUND\",\n recovery=\"Check if NotebookLM page loaded correctly\",\n )\n\n client.click(create_ref)\n time.sleep(2)\n\n # Get current URL to extract notebook ID\n snapshot = client.snapshot()\n # Look for notebook URL pattern in the page or get from navigation\n # The notebook ID appears in URL after creation\n\n # Wait for notebook to be created and page to update\n time.sleep(3)\n\n # Try to get the notebook ID from the URL\n current_url = client.evaluate(\"window.location.href\")\n notebook_id = None\n if current_url and \"notebook/\" in current_url:\n parts = current_url.split(\"notebook/\")\n if len(parts) > 1:\n notebook_id = parts[1].split(\"/\")[0].split(\"?\")[0]\n\n if not notebook_id:\n # Generate a placeholder - we'll get the real ID from the URL later\n import uuid\n notebook_id = str(uuid.uuid4())\n\n auth.save_auth(\"google\", client=client)\n\n return {\n \"id\": notebook_id,\n \"title\": name,\n \"created_via\": \"browser_fallback\",\n }\n\n except AgentBrowserError as e:\n raise NotebookLMError(e.message, code=e.code, recovery=e.recovery)\n finally:\n client.disconnect()\n\n return await loop.run_in_executor(None, _browser_create)\n\n async def _fallback_upload(self, notebook_id: str, file_path: Path) -> dict:\n \"\"\"Upload file via browser automation when API fails.\"\"\"\n from agent_browser_client import AgentBrowserClient, AgentBrowserError\n from auth_manager import AuthManager\n\n loop = asyncio.get_event_loop()\n\n def _browser_upload():\n auth = AuthManager()\n client = AgentBrowserClient(session_id=DEFAULT_SESSION_ID)\n\n try:\n client.connect()\n auth.restore_auth(\"google\", client=client)\n\n # Navigate to notebook\n notebook_url = f\"https://notebooklm.google.com/notebook/{notebook_id}\"\n print(f\" 🌐 Navigating to notebook for upload...\")\n client.navigate(notebook_url)\n\n # Wait for page load\n import time\n time.sleep(3)\n\n # Get snapshot and find add source button\n snapshot = client.snapshot()\n add_ref = self._find_button_ref(snapshot, [\"add source\", \"add sources\", \"add\"])\n\n if not add_ref:\n raise NotebookLMError(\n \"Add source button not found\",\n code=\"ELEMENT_NOT_FOUND\",\n recovery=\"Check if notebook page loaded correctly\",\n )\n\n print(f\" 📎 Clicking add source button...\")\n client.click(add_ref)\n time.sleep(2)\n\n # Get new snapshot and find upload/file option\n snapshot = client.snapshot()\n upload_ref = self._find_button_ref(snapshot, [\"upload\", \"file\", \"pdf\", \"document\"])\n\n if upload_ref:\n client.click(upload_ref)\n time.sleep(1)\n snapshot = client.snapshot()\n\n # Find file input ref in snapshot\n file_input_ref = self._find_file_input_ref(snapshot)\n\n if not file_input_ref:\n raise NotebookLMError(\n \"File input not found\",\n code=\"ELEMENT_NOT_FOUND\",\n recovery=\"Retry after page loads completely\",\n )\n\n # Upload file using agent-browser upload command with ref\n print(f\" 📤 Uploading {file_path.name}...\")\n client.upload(file_input_ref, [str(file_path)])\n\n # Wait for upload to process\n print(f\" ⏳ Waiting for upload to complete...\")\n time.sleep(10)\n\n auth.save_auth(\"google\", client=client)\n\n return {\n \"source_id\": None, # Unknown from browser upload\n \"title\": file_path.name,\n \"uploaded_via\": \"browser_fallback\",\n }\n\n except AgentBrowserError as e:\n raise NotebookLMError(e.message, code=e.code, recovery=e.recovery)\n finally:\n client.disconnect()\n\n return await loop.run_in_executor(None, _browser_upload)\n\n async def _fallback_chat(self, notebook_id: str, message: str) -> dict:\n \"\"\"Chat via browser automation when API fails.\"\"\"\n from agent_browser_client import AgentBrowserClient, AgentBrowserError\n from auth_manager import AuthManager\n\n loop = asyncio.get_event_loop()\n\n def _browser_chat():\n auth = AuthManager()\n client = AgentBrowserClient(session_id=DEFAULT_SESSION_ID)\n\n try:\n client.connect()\n auth.restore_auth(\"google\", client=client)\n\n # Navigate to notebook\n notebook_url = f\"https://notebooklm.google.com/notebook/{notebook_id}\"\n print(f\" 🌐 Navigating to notebook...\")\n client.navigate(notebook_url)\n\n import time\n time.sleep(3)\n\n # Get snapshot and find query input\n print(\" ⏳ Finding query input...\")\n snapshot = client.snapshot()\n input_ref = self._find_textbox_ref(snapshot)\n\n if not input_ref:\n raise NotebookLMError(\n \"Query input not found\",\n code=\"ELEMENT_NOT_FOUND\",\n recovery=\"Check if notebook page loaded correctly\",\n )\n\n # Type the question\n print(\" ⌨️ Typing question...\")\n client.fill(input_ref, message)\n time.sleep(0.5)\n\n # Press Enter to submit\n client.press_key(\"Enter\")\n\n # Wait for response\n print(\" ⏳ Waiting for answer...\")\n time.sleep(10) # Initial wait\n\n # Poll for response (up to 60 seconds)\n answer = None\n for _ in range(12): # 12 * 5 = 60 seconds max\n snapshot = client.snapshot()\n answer = self._extract_chat_response(snapshot)\n if answer and len(answer) > 50: # Got substantial response\n break\n time.sleep(5)\n\n if not answer:\n raise NotebookLMError(\n \"No response received\",\n code=\"TIMEOUT\",\n recovery=\"Try again or check if notebook has sources\",\n )\n\n print(\" ✅ Got answer!\")\n auth.save_auth(\"google\", client=client)\n\n return {\n \"text\": answer,\n \"citations\": [],\n \"via\": \"browser_fallback\",\n }\n\n except AgentBrowserError as e:\n raise NotebookLMError(e.message, code=e.code, recovery=e.recovery)\n finally:\n client.disconnect()\n\n return await loop.run_in_executor(None, _browser_chat)\n\n @staticmethod\n def _find_textbox_ref(snapshot: str) -> Optional[str]:\n \"\"\"Find textbox/input ref for chat in snapshot.\"\"\"\n for line in snapshot.splitlines():\n lower = line.lower()\n # Look for textbox or input elements related to chat\n if (\"textbox\" in lower or \"textarea\" in lower) and (\"ask\" in lower or \"query\" in lower or \"question\" in lower or \"chat\" in lower):\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n # Fallback: look for any textbox\n for line in snapshot.splitlines():\n if \"textbox\" in line.lower():\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n return None\n\n @staticmethod\n def _extract_chat_response(snapshot: str) -> Optional[str]:\n \"\"\"Extract the latest chat response from snapshot.\"\"\"\n lines = snapshot.splitlines()\n # Look for response content - typically in a section after the input\n response_lines = []\n in_response = False\n\n for line in lines:\n lower = line.lower()\n # Skip input areas\n if \"textbox\" in lower or \"button\" in lower:\n if in_response and response_lines:\n break # End of response section\n continue\n # Look for text content\n if line.strip() and not line.startswith('[') and len(line.strip()) > 20:\n # Clean up the line (remove refs if any)\n clean_line = re.sub(r'\\[ref=\\w+\\]', '', line).strip()\n if clean_line:\n response_lines.append(clean_line)\n in_response = True\n\n if response_lines:\n return '\\n'.join(response_lines)\n return None\n\n @staticmethod\n def _find_file_input_ref(snapshot: str) -> Optional[str]:\n \"\"\"Find file input ref in snapshot.\"\"\"\n for line in snapshot.splitlines():\n lower = line.lower()\n # Look for file input or upload-related elements\n if \"file\" in lower and (\"input\" in lower or \"upload\" in lower):\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n # Fallback: look for any input that might be file-related\n for line in snapshot.splitlines():\n if \"input\" in line.lower() and \"type\" not in line.lower():\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n return None\n\n @staticmethod\n def _find_button_ref(snapshot: str, keywords: List[str]) -> Optional[str]:\n \"\"\"Find button ref in snapshot matching keywords.\"\"\"\n for line in snapshot.splitlines():\n lower = line.lower()\n if \"button\" not in lower:\n continue\n if not any(keyword in lower for keyword in keywords):\n continue\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n return None","content_type":"text/x-python; charset=utf-8","language":"python","size":37158,"content_sha256":"af13dbb4346336cd8a6a65273b3f06c28bcee54f08be369a8d88d5e2e3b74ac9"},{"filename":"scripts/patchright_auth.py","content":"#!/usr/bin/env python3\n\"\"\"\nPatchright-based Google Authentication for nblm\n\nUses Patchright (anti-detection Playwright fork) to bypass Google's\n\"This browser or app may not be secure\" blocking for personal Gmail accounts.\n\nKey techniques:\n- Uses real Chrome executable (not Chrome for Testing)\n- Persistent context maintains session\n- No custom headers that trigger detection\n- Patchright's anti-detection patches\n\"\"\"\n\nimport json\nimport os\nimport shutil\nimport tempfile\nimport time\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Optional, Dict, Any, Tuple\n\nfrom config import (\n GOOGLE_AUTH_FILE,\n SKILL_DIR,\n)\n\n# NotebookLM URL\nNOTEBOOKLM_URL = \"https://notebooklm.google.com\"\n\n\n# Patchright browser profile directory\nPATCHRIGHT_PROFILE_DIR = SKILL_DIR / \"data\" / \"patchright-profile\"\n\n\ndef _find_chrome_executable() -> Optional[str]:\n \"\"\"Find the real Chrome executable path on the current platform.\"\"\"\n import platform\n system = platform.system()\n\n if system == \"Darwin\": # macOS\n paths = [\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n os.path.expanduser(\"~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"),\n ]\n elif system == \"Windows\":\n paths = [\n os.path.expandvars(r\"%ProgramFiles%\\Google\\Chrome\\Application\\chrome.exe\"),\n os.path.expandvars(r\"%ProgramFiles(x86)%\\Google\\Chrome\\Application\\chrome.exe\"),\n os.path.expandvars(r\"%LocalAppData%\\Google\\Chrome\\Application\\chrome.exe\"),\n ]\n else: # Linux\n paths = [\n \"/usr/bin/google-chrome\",\n \"/usr/bin/google-chrome-stable\",\n \"/opt/google/chrome/chrome\",\n ]\n\n for path in paths:\n if os.path.isfile(path):\n return path\n return None\n\n\ndef _extract_storage_state(context) -> Dict[str, Any]:\n \"\"\"Extract cookies and localStorage from browser context.\"\"\"\n # Get cookies\n cookies = context.cookies()\n\n # Get localStorage from NotebookLM origin\n origins = []\n try:\n page = context.pages[0] if context.pages else None\n if page and \"notebooklm.google.com\" in page.url:\n local_storage = page.evaluate(\"() => Object.entries(localStorage)\")\n origins.append({\n \"origin\": \"https://notebooklm.google.com\",\n \"localStorage\": [{\"name\": k, \"value\": v} for k, v in local_storage]\n })\n except Exception:\n pass # localStorage extraction is optional\n\n return {\n \"cookies\": cookies,\n \"origins\": origins,\n }\n\n\ndef _save_auth_state(storage_state: Dict[str, Any]) -> None:\n \"\"\"Save authentication state to google.json.\"\"\"\n GOOGLE_AUTH_FILE.parent.mkdir(parents=True, exist_ok=True)\n\n # Add timestamp\n storage_state[\"notebooklm_updated_at\"] = datetime.now(timezone.utc).isoformat()\n\n with open(GOOGLE_AUTH_FILE, \"w\") as f:\n json.dump(storage_state, f, indent=2)\n\n\ndef _extract_email_from_page(page) -> Optional[str]:\n \"\"\"Extract the logged-in user's email from the page.\"\"\"\n try:\n email = page.evaluate(\"\"\"\n () => {\n // Try aria-label on account button\n const accountBtn = document.querySelector('[aria-label*=\"@\"]');\n if (accountBtn) {\n const match = accountBtn.getAttribute('aria-label').match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}/);\n if (match) return match[0];\n }\n\n // Try data attributes\n const elements = document.querySelectorAll('[data-email], [data-user-email]');\n for (const el of elements) {\n const email = el.getAttribute('data-email') || el.getAttribute('data-user-email');\n if (email && email.includes('@')) return email;\n }\n\n // Try text content in account-related elements\n const accountElements = document.querySelectorAll('[class*=\"account\"], [class*=\"user\"], [class*=\"profile\"]');\n for (const el of accountElements) {\n const match = el.textContent.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}/);\n if (match) return match[0];\n }\n\n return null;\n }\n \"\"\")\n return email\n except Exception:\n return None\n\n\ndef authenticate_with_patchright(\n timeout_seconds: int = 600,\n use_fresh_profile: bool = False\n) -> tuple[bool, Optional[str], Optional[Dict[str, Any]]]:\n \"\"\"\n Perform Google authentication using Patchright.\n\n Opens a real Chrome browser for user to log in manually.\n Waits for successful authentication, then extracts session data.\n\n Args:\n timeout_seconds: Maximum time to wait for user to complete login\n use_fresh_profile: If True, use a temporary profile to force account selection\n\n Returns:\n Tuple of (success, email, storage_state):\n - success: True if authentication succeeded\n - email: The authenticated user's email address (if extractable)\n - storage_state: Browser storage state dict for saving credentials\n \"\"\"\n try:\n from patchright.sync_api import sync_playwright\n except ImportError:\n print(\"❌ Patchright not installed. Run: pip install patchright && patchright install chromium\")\n return False, None, None\n\n # Find real Chrome executable - always use real Chrome\n chrome_path = _find_chrome_executable()\n if not chrome_path:\n print(\"❌ Google Chrome not found. Please install Chrome.\")\n return False, None, None\n\n print(\"🔐 Opening Chrome for Google authentication...\", flush=True)\n print(f\" Using: {chrome_path}\", flush=True)\n print(\" (Patchright anti-detection enabled)\", flush=True)\n if use_fresh_profile:\n print(\" (Fresh session for new account login)\", flush=True)\n print(flush=True)\n\n # Determine profile directory\n temp_profile_dir = None\n if use_fresh_profile:\n # Use a temporary profile for adding new accounts\n # This avoids Chrome's profile switching when signing into a different account\n temp_profile_dir = tempfile.mkdtemp(prefix=\"nblm-auth-\")\n profile_dir = temp_profile_dir\n print(f\"🧹 Using temporary profile for clean login...\", flush=True)\n else:\n # Use persistent profile for normal auth/re-auth\n PATCHRIGHT_PROFILE_DIR.mkdir(parents=True, exist_ok=True)\n profile_dir = str(PATCHRIGHT_PROFILE_DIR)\n\n try:\n with sync_playwright() as p:\n # Launch with anti-detection settings\n # Key: ignore_default_args removes --enable-automation flag\n # args disable additional automation indicators\n launch_args = [\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--disable-dev-shm-usage\",\n \"--no-first-run\",\n \"--no-default-browser-check\",\n # Enable remote debugging so we can detect pages in other windows\n \"--remote-debugging-port=0\",\n ]\n\n # For fresh profile: disable Chrome sign-in and sync to prevent profile switching\n if use_fresh_profile:\n launch_args.extend([\n \"--disable-sync\",\n \"--disable-features=ChromeWhatsNewUI\",\n \"--no-service-autorun\",\n \"--password-store=basic\",\n ])\n\n # Build launch options\n launch_options = {\n \"user_data_dir\": profile_dir,\n \"headless\": False, # Must be visible for auth\n \"no_viewport\": True, # Don't override viewport\n \"ignore_default_args\": [\n \"--enable-automation\", # Removes automation banner\n \"--enable-blink-features=AutomationControlled\",\n ],\n \"args\": launch_args,\n }\n\n # Only set executable_path if using real Chrome\n if chrome_path:\n launch_options[\"executable_path\"] = chrome_path\n\n context = p.chromium.launch_persistent_context(**launch_options)\n\n page = context.pages[0] if context.pages else context.new_page()\n\n # Get CDP session for detecting pages across all windows\n cdp_session = None\n try:\n cdp_session = context.new_cdp_session(page)\n except Exception:\n pass # CDP not available, will use context.pages only\n\n # Navigate to NotebookLM - it will redirect to Google login if needed\n print(f\"🌐 Navigating to {NOTEBOOKLM_URL}...\", flush=True)\n page.goto(NOTEBOOKLM_URL, wait_until=\"domcontentloaded\")\n\n # Wait for user to complete authentication\n print(flush=True)\n print(\"⏳ Please complete login in the browser window...\", flush=True)\n print(\" (DO NOT close the browser - it will close automatically)\", flush=True)\n print(flush=True)\n\n def check_cdp_for_notebooklm() -> bool:\n \"\"\"Use CDP to check if any Chrome page has NotebookLM.\"\"\"\n if not cdp_session:\n return False\n try:\n result = cdp_session.send(\"Target.getTargets\")\n for target in result.get(\"targetInfos\", []):\n url = target.get(\"url\", \"\")\n if \"notebooklm.google.com\" in url and \"accounts.google.com\" not in url:\n return True\n except Exception:\n pass\n return False\n\n start_time = time.time()\n authenticated = False\n auth_page = None\n last_url = \"\"\n check_count = 0\n\n while time.time() - start_time \u003c timeout_seconds:\n try:\n # Check if browser/page is still open\n if not context.pages or len(context.pages) == 0:\n # Browser was closed by user\n print(\"⚠️ Browser was closed manually\", flush=True)\n # If we were on NotebookLM, try to save what we have\n if \"notebooklm.google.com\" in last_url:\n print(\" Attempting to save session from last URL...\", flush=True)\n authenticated = True\n break\n\n # Check ALL pages in context (Google may open new tabs)\n for p in context.pages:\n try:\n current_url = p.url\n # Check if any page reached NotebookLM\n if \"notebooklm.google.com\" in current_url:\n if \"accounts.google.com\" not in current_url and \"/signin\" not in current_url:\n print(\" ✓ Detected NotebookLM homepage!\", flush=True)\n auth_page = p\n time.sleep(2)\n authenticated = True\n break\n except Exception:\n continue # Page might be closing\n\n if authenticated:\n break\n\n # Also check via CDP for pages in other Chrome windows\n if not authenticated and check_cdp_for_notebooklm():\n print(\" ✓ Detected NotebookLM in another window via CDP!\", flush=True)\n time.sleep(2)\n authenticated = True\n break\n\n # Use first page for status display\n current_url = context.pages[0].url if context.pages else \"\"\n check_count += 1\n\n # Debug: show URL periodically\n if check_count % 5 == 1: # Every 5 seconds\n num_pages = len(context.pages)\n page_info = f\" ({num_pages} tabs)\" if num_pages > 1 else \"\"\n print(f\" Checking: {current_url[:60]}{page_info}\", flush=True)\n\n last_url = current_url\n time.sleep(1)\n except Exception as e:\n # Browser might have been closed\n error_msg = str(e)\n if \"closed\" in error_msg.lower():\n print(\"⚠️ Browser was closed\", flush=True)\n if \"notebooklm.google.com\" in last_url:\n authenticated = True\n else:\n print(f\"❌ Browser error: {e}\", flush=True)\n break\n\n # Use the page where auth completed, or first page\n page = auth_page if auth_page else (context.pages[0] if context.pages else None)\n\n if authenticated:\n print(\"✅ Authentication successful!\", flush=True)\n\n # If auth was detected via CDP (another window), navigate our page to NotebookLM\n # to get the cookies (session is shared across windows)\n if page and \"notebooklm.google.com\" not in page.url:\n print(\" Navigating to NotebookLM to capture session...\", flush=True)\n try:\n page.goto(NOTEBOOKLM_URL, wait_until=\"domcontentloaded\")\n time.sleep(2) # Wait for page to load\n except Exception:\n pass\n\n # Extract email and storage state before browser closes\n try:\n email = _extract_email_from_page(page)\n if email:\n print(f\" ✓ Logged in as: {email}\", flush=True)\n storage_state = _extract_storage_state(context)\n except Exception:\n # Browser already closed, can't extract\n print(\" ⚠️ Could not extract session (browser closed)\", flush=True)\n email = None\n storage_state = None\n else:\n print(\"❌ Authentication timed out\", flush=True)\n email = None\n storage_state = None\n\n context.close()\n\n # Clean up temporary profile if used\n if temp_profile_dir and os.path.exists(temp_profile_dir):\n try:\n shutil.rmtree(temp_profile_dir)\n except Exception:\n pass # Best effort cleanup\n\n return authenticated, email, storage_state\n except Exception as e:\n print(f\"❌ Authentication error: {e}\", flush=True)\n # Clean up temporary profile on error\n if temp_profile_dir and os.path.exists(temp_profile_dir):\n try:\n shutil.rmtree(temp_profile_dir)\n except Exception:\n pass\n return False, None, None\n\n\ndef clear_patchright_profile() -> bool:\n \"\"\"Clear the Patchright browser profile for fresh auth.\"\"\"\n import shutil\n\n if PATCHRIGHT_PROFILE_DIR.exists():\n shutil.rmtree(PATCHRIGHT_PROFILE_DIR)\n print(f\" ✓ Cleared Patchright profile\")\n return True\n return False\n\n\nif __name__ == \"__main__\":\n import sys\n\n if len(sys.argv) > 1 and sys.argv[1] == \"clear\":\n clear_patchright_profile()\n else:\n success, email, storage_state = authenticate_with_patchright()\n if success:\n print(f\"Email: {email}\")\n # Save for backward compatibility when run standalone\n if storage_state:\n _save_auth_state(storage_state)\n sys.exit(0 if success else 1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16031,"content_sha256":"65e6fdd3609057c5e61abba84305380038f8c57610e68a0bc538554455803745"},{"filename":"scripts/run.py","content":"#!/usr/bin/env python3\n\"\"\"\nUniversal runner for NotebookLM skill scripts\nEnsures all scripts run with the correct virtual environment\n\"\"\"\n\nimport hashlib\nimport json\nimport os\nimport sys\nimport subprocess\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\n\n\nAGENT_PROCESS_HINTS = (\"codex\", \"claude\", \"claude-code\", \"claude_code\")\nIGNORED_PROCESS_NAMES = {\n # Unix shells\n \"bash\",\n \"dash\",\n \"fish\",\n \"sh\",\n \"zsh\",\n # Windows shells\n \"cmd\",\n \"cmd.exe\",\n \"powershell\",\n \"powershell.exe\",\n \"pwsh\",\n \"pwsh.exe\",\n # Interpreters\n \"python\",\n \"python3\",\n \"python.exe\",\n \"node\",\n \"node.exe\",\n \"npm\",\n \"npm.cmd\",\n}\n\n# Scripts that skip auth pre-check\nSKIP_AUTH_CHECK = {\n \"auth_manager.py\", # Handles its own auth\n \"cleanup_manager.py\", # Cleanup doesn't need auth\n \"setup_environment.py\", # Setup script\n \"init_platform.py\", # Platform initialization\n}\n\n# Timeouts for long-running operations (in seconds)\nTIMEOUT_VENV_SETUP = 600 # 10 minutes\nTIMEOUT_PIP_INSTALL = 600 # 10 minutes\nTIMEOUT_NPM_INSTALL = 600 # 10 minutes\nTIMEOUT_AUTH_SETUP = 600 # 10 minutes (user interaction)\n\n\ndef _get_process_info(pid: int):\n \"\"\"Return (ppid, command) for a PID, or None on failure.\"\"\"\n try:\n result = subprocess.run(\n [\"ps\", \"-p\", str(pid), \"-o\", \"pid=,ppid=,command=\"],\n capture_output=True,\n text=True,\n check=False,\n )\n except Exception:\n return None\n\n if result.returncode != 0:\n return None\n\n line = result.stdout.strip()\n if not line:\n return None\n\n parts = line.split(None, 2)\n if len(parts) \u003c 2:\n return None\n\n try:\n ppid = int(parts[1])\n except ValueError:\n return None\n\n command = parts[2] if len(parts) > 2 else \"\"\n return ppid, command\n\n\ndef _looks_like_agent(command: str) -> bool:\n lower = command.lower()\n return any(hint in lower for hint in AGENT_PROCESS_HINTS)\n\n\ndef _is_ignored_command(command: str) -> bool:\n if not command:\n return True\n base = Path(command.split()[0]).name.lower()\n return base in IGNORED_PROCESS_NAMES\n\n\ndef _detect_owner_pid():\n \"\"\"Best-effort owner PID detection for CLI agents.\"\"\"\n if os.name == \"nt\":\n return os.getppid()\n\n pid = os.getppid()\n fallback_pid = None\n seen = set()\n\n for _ in range(20):\n if pid \u003c= 1 or pid in seen:\n break\n seen.add(pid)\n\n info = _get_process_info(pid)\n if not info:\n break\n\n ppid, command = info\n if _looks_like_agent(command):\n return pid\n if fallback_pid is None and not _is_ignored_command(command):\n fallback_pid = pid\n\n if not ppid or ppid == pid:\n break\n pid = ppid\n\n return fallback_pid\n\n\ndef get_venv_python():\n \"\"\"Get the virtual environment Python executable\"\"\"\n skill_dir = Path(__file__).parent.parent\n venv_dir = skill_dir / \".venv\"\n\n if os.name == 'nt': # Windows\n venv_python = venv_dir / \"Scripts\" / \"python.exe\"\n else: # Unix/Linux/Mac\n venv_python = venv_dir / \"bin\" / \"python\"\n\n return venv_python\n\n\ndef ensure_venv():\n \"\"\"Ensure virtual environment exists\"\"\"\n skill_dir = Path(__file__).parent.parent\n venv_dir = skill_dir / \".venv\"\n setup_script = skill_dir / \"scripts\" / \"setup_environment.py\"\n\n # Check if venv exists\n if not venv_dir.exists():\n print(\"🔧 First-time setup: Creating virtual environment...\")\n print(\" This may take a minute...\")\n\n # Run setup with system Python\n try:\n result = subprocess.run(\n [sys.executable, str(setup_script)],\n timeout=TIMEOUT_VENV_SETUP\n )\n except subprocess.TimeoutExpired:\n print(f\"❌ Venv setup timed out after {TIMEOUT_VENV_SETUP}s\")\n sys.exit(1)\n\n if result.returncode != 0:\n print(\"❌ Failed to set up environment\")\n sys.exit(1)\n\n print(\"✅ Environment ready!\")\n\n return get_venv_python()\n\n\ndef _get_requirements_hash(requirements_file: Path) -> str:\n \"\"\"Compute SHA256 hash of requirements.txt\"\"\"\n if not requirements_file.exists():\n return \"\"\n content = requirements_file.read_bytes()\n return hashlib.sha256(content).hexdigest()\n\n\ndef ensure_pip_deps():\n \"\"\"Ensure pip dependencies are installed and up-to-date\"\"\"\n skill_dir = Path(__file__).parent.parent\n venv_dir = skill_dir / \".venv\"\n requirements_file = skill_dir / \"requirements.txt\"\n hash_file = venv_dir / \".requirements.hash\"\n\n if not requirements_file.exists():\n return # No requirements file\n\n current_hash = _get_requirements_hash(requirements_file)\n\n # Check if hash matches\n if hash_file.exists():\n stored_hash = hash_file.read_text().strip()\n if stored_hash == current_hash:\n return # Dependencies up-to-date\n\n # Install/update dependencies\n print(\"📦 Installing Python dependencies...\")\n venv_python = get_venv_python()\n try:\n result = subprocess.run(\n [str(venv_python), \"-m\", \"pip\", \"install\", \"-r\", str(requirements_file), \"--quiet\"],\n capture_output=True,\n text=True,\n timeout=TIMEOUT_PIP_INSTALL\n )\n except subprocess.TimeoutExpired:\n print(f\"⚠️ pip install timed out after {TIMEOUT_PIP_INSTALL}s\")\n print(\" Try running manually: pip install -r requirements.txt\")\n return\n\n if result.returncode != 0:\n print(f\"⚠️ pip install failed: {result.stderr}\")\n print(\" Try running: pip install -r requirements.txt\")\n else:\n # Save hash on success\n hash_file.write_text(current_hash)\n print(\"✅ Python dependencies installed\")\n # Install Patchright browser if patchright was installed\n _ensure_patchright_browser(venv_python)\n\n\ndef _ensure_patchright_browser(venv_python: Path):\n \"\"\"Ensure Patchright browser is installed for Google auth.\"\"\"\n skill_dir = Path(__file__).parent.parent\n patchright_marker = skill_dir / \".venv\" / \".patchright-browser-installed\"\n\n # Skip if already installed\n if patchright_marker.exists():\n return\n\n # Check if patchright is installed\n try:\n result = subprocess.run(\n [str(venv_python), \"-c\", \"import patchright\"],\n capture_output=True,\n timeout=10\n )\n if result.returncode != 0:\n return # Patchright not installed, skip\n except Exception:\n return\n\n # Install Patchright browser\n print(\"📦 Installing Patchright browser for Google auth...\")\n try:\n patchright_cmd = skill_dir / \".venv\" / \"bin\" / \"patchright\"\n if os.name == 'nt':\n patchright_cmd = skill_dir / \".venv\" / \"Scripts\" / \"patchright.exe\"\n\n result = subprocess.run(\n [str(patchright_cmd), \"install\", \"chromium\"],\n capture_output=True,\n text=True,\n timeout=300 # 5 minutes for browser download\n )\n if result.returncode == 0:\n patchright_marker.write_text(\"installed\")\n print(\"✅ Patchright browser installed\")\n else:\n print(f\"⚠️ Patchright browser install failed: {result.stderr}\")\n except subprocess.TimeoutExpired:\n print(\"⚠️ Patchright browser install timed out\")\n except Exception as e:\n print(f\"⚠️ Patchright browser install error: {e}\")\n\n\ndef _get_npm_command():\n \"\"\"Get the npm command for the current platform.\"\"\"\n if os.name == 'nt': # Windows\n return \"npm.cmd\"\n return \"npm\"\n\n\ndef ensure_node_deps():\n \"\"\"Ensure Node.js dependencies are installed\"\"\"\n skill_dir = Path(__file__).parent.parent\n package_json = skill_dir / \"package.json\"\n node_modules = skill_dir / \"node_modules\"\n\n if not package_json.exists():\n return # No Node.js dependencies needed\n\n if not node_modules.exists():\n print(\"📦 Installing agent-browser...\")\n npm_cmd = _get_npm_command()\n try:\n result = subprocess.run(\n [npm_cmd, \"install\"],\n cwd=str(skill_dir),\n capture_output=True,\n text=True,\n timeout=TIMEOUT_NPM_INSTALL\n )\n except subprocess.TimeoutExpired:\n print(f\"⚠️ npm install timed out after {TIMEOUT_NPM_INSTALL}s\")\n print(\" Please run manually: npm install\")\n return\n\n if result.returncode != 0:\n print(f\"⚠️ npm install failed: {result.stderr}\")\n print(\" Please ensure Node.js and npm are installed\")\n else:\n print(\"✅ agent-browser installed\")\n\n\ndef _prompt_auth_setup():\n \"\"\"Trigger interactive auth setup, return True on success.\"\"\"\n print(\"🔐 Google authentication required. Opening browser...\")\n print(\" Please complete login in the browser window.\")\n print(f\" (Timeout: {TIMEOUT_AUTH_SETUP // 60} minutes)\")\n print()\n\n venv_python = get_venv_python()\n skill_dir = Path(__file__).parent.parent\n auth_script = skill_dir / \"scripts\" / \"auth_manager.py\"\n\n try:\n result = subprocess.run(\n [str(venv_python), str(auth_script), \"setup\", \"--service\", \"google\"],\n timeout=TIMEOUT_AUTH_SETUP\n )\n except subprocess.TimeoutExpired:\n print(f\"❌ Authentication timed out after {TIMEOUT_AUTH_SETUP // 60} minutes.\")\n print(\" Please try again: python scripts/run.py auth_manager.py setup\")\n sys.exit(1)\n\n if result.returncode != 0:\n print(\"❌ Authentication failed. Cannot proceed.\")\n sys.exit(1)\n\n print() # Blank line after auth success\n return True\n\n\ndef _prompt_auth_reauth():\n \"\"\"Trigger interactive reauth for expired credentials, return True on success.\"\"\"\n print(\"🔐 Re-authenticating expired Google session...\")\n print(\" Please complete login in the browser window.\")\n print(f\" (Timeout: {TIMEOUT_AUTH_SETUP // 60} minutes)\")\n print()\n\n venv_python = get_venv_python()\n skill_dir = Path(__file__).parent.parent\n auth_script = skill_dir / \"scripts\" / \"auth_manager.py\"\n\n try:\n result = subprocess.run(\n [str(venv_python), str(auth_script), \"reauth\", \"--service\", \"google\"],\n timeout=TIMEOUT_AUTH_SETUP\n )\n except subprocess.TimeoutExpired:\n print(f\"❌ Re-authentication timed out after {TIMEOUT_AUTH_SETUP // 60} minutes.\")\n print(\" Please try again: python scripts/run.py auth_manager.py reauth\")\n sys.exit(1)\n\n if result.returncode != 0:\n print(\"❌ Re-authentication failed. Cannot proceed.\")\n sys.exit(1)\n\n print() # Blank line after auth success\n return True\n\n\ndef ensure_google_auth():\n \"\"\"Ensure Google authentication is valid and fresh, prompting setup if needed.\"\"\"\n skill_dir = Path(__file__).parent.parent\n TTL_DAYS = 10\n\n # Multi-account structure: check google/index.json first\n index_file = skill_dir / \"data\" / \"auth\" / \"google\" / \"index.json\"\n legacy_auth_file = skill_dir / \"data\" / \"auth\" / \"google.json\"\n\n if index_file.exists():\n # Multi-account mode: find active account's auth file\n try:\n index_data = json.loads(index_file.read_text())\n active_index = index_data.get(\"active_account\")\n if active_index:\n for acc in index_data.get(\"accounts\", []):\n if acc.get(\"index\") == active_index:\n auth_file = skill_dir / \"data\" / \"auth\" / \"google\" / acc.get(\"file\", \"\")\n break\n else:\n auth_file = None\n else:\n auth_file = None\n except (json.JSONDecodeError, IOError):\n auth_file = None\n elif legacy_auth_file.exists():\n auth_file = legacy_auth_file\n else:\n auth_file = None\n\n # Check 1: File exists\n if not auth_file or not auth_file.exists():\n return _prompt_auth_setup()\n\n # Check 2: Valid structure\n try:\n payload = json.loads(auth_file.read_text())\n except (json.JSONDecodeError, IOError):\n return _prompt_auth_setup()\n\n if not payload.get(\"cookies\") and not payload.get(\"origins\"):\n return _prompt_auth_setup()\n\n # Check 3: Freshness (notebooklm_updated_at within TTL)\n updated_at = payload.get(\"notebooklm_updated_at\")\n if updated_at:\n try:\n timestamp = datetime.fromisoformat(updated_at)\n if timestamp.tzinfo is None:\n timestamp = timestamp.replace(tzinfo=timezone.utc)\n age = datetime.now(timezone.utc) - timestamp\n if age > timedelta(days=TTL_DAYS):\n print(f\"⚠️ Google auth expired ({age.days} days old)\")\n return _prompt_auth_reauth() # Reauth existing account, not fresh setup\n except ValueError:\n pass # Invalid timestamp, but cookies exist - proceed\n\n # All checks passed - silent success\n return True\n\n\ndef should_skip_auth_check(script_name: str, script_args: list) -> bool:\n \"\"\"Determine if this invocation should skip auth pre-check.\"\"\"\n # Skip for help flags\n if \"--help\" in script_args or \"-h\" in script_args:\n return True\n\n # Skip for scripts that don't need auth\n if script_name in SKIP_AUTH_CHECK:\n return True\n\n return False\n\n\ndef ensure_owner_pid_env():\n \"\"\"Ensure agent-browser owner PID is set for watchdog cleanup\"\"\"\n if not os.environ.get(\"AGENT_BROWSER_OWNER_PID\"):\n owner_pid = _detect_owner_pid()\n if owner_pid is None:\n owner_pid = os.getppid()\n os.environ[\"AGENT_BROWSER_OWNER_PID\"] = str(owner_pid)\n\n\ndef main():\n \"\"\"Main runner\"\"\"\n # Handle init command for platform initialization\n if len(sys.argv) >= 2 and sys.argv[1] == \"init\":\n skill_dir = Path(__file__).parent.parent\n init_script = skill_dir / \"scripts\" / \"init_platform.py\"\n\n # Pass remaining args to init_platform.py\n init_args = sys.argv[2:]\n cmd = [sys.executable, str(init_script)] + init_args\n result = subprocess.run(cmd)\n sys.exit(result.returncode)\n\n # Handle --check-deps flag for pre-flight dependency check\n if len(sys.argv) >= 2 and sys.argv[1] == \"--check-deps\":\n print(\"🔍 Checking dependencies...\")\n skill_dir = Path(__file__).parent.parent\n\n # Check Python venv\n venv_python = get_venv_python()\n if not venv_python.exists():\n print(\"📦 Setting up Python environment...\")\n ensure_venv()\n else:\n print(\"✅ Python environment ready\")\n\n # Check pip dependencies\n ensure_pip_deps()\n\n # Check Node.js deps\n node_modules = skill_dir / \"node_modules\"\n if not node_modules.exists():\n print(\"📦 Installing Node.js dependencies...\")\n ensure_node_deps()\n else:\n print(\"✅ Node.js dependencies ready\")\n\n print(\"✅ All dependencies installed\")\n sys.exit(0)\n\n if len(sys.argv) \u003c 2:\n print(\"Usage: python run.py \u003cscript_name> [args...]\")\n print(\"\\nAvailable scripts:\")\n print(\" ask_question.py - Query NotebookLM\")\n print(\" notebook_manager.py - Manage notebook library\")\n print(\" session_manager.py - Manage sessions\")\n print(\" auth_manager.py - Handle authentication\")\n print(\" cleanup_manager.py - Clean up skill data\")\n sys.exit(1)\n\n script_name = sys.argv[1]\n script_args = sys.argv[2:]\n\n # Handle both \"scripts/script.py\" and \"script.py\" formats\n if script_name.startswith('scripts/'):\n # Remove the scripts/ prefix if provided\n script_name = script_name[8:] # len('scripts/') = 8\n\n # Ensure .py extension\n if not script_name.endswith('.py'):\n script_name += '.py'\n\n # Get script path\n skill_dir = Path(__file__).parent.parent\n script_path = skill_dir / \"scripts\" / script_name\n\n if not script_path.exists():\n print(f\"❌ Script not found: {script_name}\")\n print(f\" Working directory: {Path.cwd()}\")\n print(f\" Skill directory: {skill_dir}\")\n print(f\" Looked for: {script_path}\")\n sys.exit(1)\n\n # Ensure venv exists and get Python executable\n venv_python = ensure_venv()\n ensure_pip_deps()\n ensure_node_deps()\n ensure_owner_pid_env()\n\n # Auth pre-check (unless skipped)\n if not should_skip_auth_check(script_name, script_args):\n ensure_google_auth()\n\n # Build command\n cmd = [str(venv_python), str(script_path)] + script_args\n\n # Run the script\n try:\n result = subprocess.run(cmd)\n sys.exit(result.returncode)\n except KeyboardInterrupt:\n print(\"\\n⚠️ Interrupted by user\")\n sys.exit(130)\n except Exception as e:\n print(f\"❌ Error: {e}\")\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":17124,"content_sha256":"479e1c163757f0fb9db62f4ded6babfe7818d2f2731561ee58a03011c7cd510c"},{"filename":"scripts/setup_environment.py","content":"#!/usr/bin/env python3\n\"\"\"\nEnvironment Setup for nblm\nManages virtual environment and dependencies automatically\n\"\"\"\n\nimport os\nimport sys\nimport subprocess\nimport venv\nfrom pathlib import Path\n\n\ndef _get_npm_command():\n \"\"\"Get the npm command for the current platform.\"\"\"\n if os.name == 'nt': # Windows\n return \"npm.cmd\"\n return \"npm\"\n\n\nclass SkillEnvironment:\n \"\"\"Manages skill-specific virtual environment\"\"\"\n\n def __init__(self):\n # Skill directory paths\n self.skill_dir = Path(__file__).parent.parent\n self.venv_dir = self.skill_dir / \".venv\"\n self.requirements_file = self.skill_dir / \"requirements.txt\"\n\n # Python executable in venv\n if os.name == 'nt': # Windows\n self.venv_python = self.venv_dir / \"Scripts\" / \"python.exe\"\n self.venv_pip = self.venv_dir / \"Scripts\" / \"pip.exe\"\n else: # Unix/Linux/Mac\n self.venv_python = self.venv_dir / \"bin\" / \"python\"\n self.venv_pip = self.venv_dir / \"bin\" / \"pip\"\n\n def ensure_venv(self) -> bool:\n \"\"\"Ensure virtual environment exists and is set up\"\"\"\n\n # Check if we're already in the correct venv\n if self.is_in_skill_venv():\n print(\"✅ Already running in skill virtual environment\")\n return True\n\n # Create venv if it doesn't exist\n if not self.venv_dir.exists():\n print(f\"🔧 Creating virtual environment in {self.venv_dir.name}/\")\n try:\n venv.create(self.venv_dir, with_pip=True)\n print(\"✅ Virtual environment created\")\n except Exception as e:\n print(f\"❌ Failed to create venv: {e}\")\n return False\n\n # Install/update dependencies\n if self.requirements_file.exists():\n print(\"📦 Installing dependencies...\")\n try:\n # Upgrade pip first\n subprocess.run(\n [str(self.venv_pip), \"install\", \"--upgrade\", \"pip\"],\n check=True,\n capture_output=True,\n text=True\n )\n\n # Install requirements\n result = subprocess.run(\n [str(self.venv_pip), \"install\", \"-r\", str(self.requirements_file)],\n check=True,\n capture_output=True,\n text=True\n )\n print(\"✅ Dependencies installed\")\n\n # Install Node.js dependencies if present\n package_json = self.skill_dir / \"package.json\"\n if package_json.exists():\n print(\"📦 Installing Node.js dependencies...\")\n npm_cmd = _get_npm_command()\n try:\n subprocess.run(\n [npm_cmd, \"install\"],\n check=True,\n capture_output=True,\n text=True,\n cwd=str(self.skill_dir)\n )\n print(\"✅ Node.js dependencies installed\")\n print(\"🌐 Installing Playwright browsers...\")\n try:\n subprocess.run(\n [npm_cmd, \"run\", \"install-browsers\"],\n check=True,\n capture_output=True,\n text=True,\n cwd=str(self.skill_dir)\n )\n print(\"✅ Playwright browsers installed\")\n except subprocess.CalledProcessError as e:\n print(f\"⚠️ Warning: browser install failed: {e}\")\n print(\" You may need to run manually: npm run install-browsers\")\n except subprocess.CalledProcessError as e:\n print(f\"⚠️ Warning: npm install failed: {e}\")\n print(\" Ensure Node.js and npm are installed\")\n\n return True\n except subprocess.CalledProcessError as e:\n print(f\"❌ Failed to install dependencies: {e}\")\n print(f\" Output: {e.output if hasattr(e, 'output') else 'No output'}\")\n return False\n else:\n print(\"⚠️ No requirements.txt found, skipping dependency installation\")\n return True\n\n def is_in_skill_venv(self) -> bool:\n \"\"\"Check if we're already running in the skill's venv\"\"\"\n if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):\n # We're in a venv, check if it's ours\n venv_path = Path(sys.prefix)\n return venv_path == self.venv_dir\n return False\n\n def get_python_executable(self) -> str:\n \"\"\"Get the correct Python executable to use\"\"\"\n if self.venv_python.exists():\n return str(self.venv_python)\n return sys.executable\n\n def run_script(self, script_name: str, args: list = None) -> int:\n \"\"\"Run a script with the virtual environment\"\"\"\n script_path = self.skill_dir / \"scripts\" / script_name\n\n if not script_path.exists():\n print(f\"❌ Script not found: {script_path}\")\n return 1\n\n # Ensure venv is set up\n if not self.ensure_venv():\n print(\"❌ Failed to set up environment\")\n return 1\n\n # Build command\n cmd = [str(self.venv_python), str(script_path)]\n if args:\n cmd.extend(args)\n\n print(f\"🚀 Running: {script_name} with venv Python\")\n\n try:\n # Run the script with venv Python\n result = subprocess.run(cmd)\n return result.returncode\n except Exception as e:\n print(f\"❌ Failed to run script: {e}\")\n return 1\n\n def activate_instructions(self) -> str:\n \"\"\"Get instructions for manual activation\"\"\"\n if os.name == 'nt':\n activate = self.venv_dir / \"Scripts\" / \"activate.bat\"\n return f\"Run: {activate}\"\n else:\n activate = self.venv_dir / \"bin\" / \"activate\"\n return f\"Run: source {activate}\"\n\n\ndef main():\n \"\"\"Main entry point for environment setup\"\"\"\n import argparse\n\n parser = argparse.ArgumentParser(\n description='Setup NotebookLM skill environment'\n )\n\n parser.add_argument(\n '--check',\n action='store_true',\n help='Check if environment is set up'\n )\n\n parser.add_argument(\n '--run',\n help='Run a script with the venv (e.g., --run ask_question.py)'\n )\n\n parser.add_argument(\n 'args',\n nargs='*',\n help='Arguments to pass to the script'\n )\n\n args = parser.parse_args()\n\n env = SkillEnvironment()\n\n if args.check:\n if env.venv_dir.exists():\n print(f\"✅ Virtual environment exists: {env.venv_dir}\")\n print(f\" Python: {env.get_python_executable()}\")\n print(f\" To activate manually: {env.activate_instructions()}\")\n else:\n print(f\"❌ No virtual environment found\")\n print(f\" Run setup_environment.py to create it\")\n return\n\n if args.run:\n # Run a script with venv\n return env.run_script(args.run, args.args)\n\n # Default: ensure environment is set up\n if env.ensure_venv():\n print(\"\\n✅ Environment ready!\")\n print(f\" Virtual env: {env.venv_dir}\")\n print(f\" Python: {env.get_python_executable()}\")\n print(f\"\\nTo activate manually: {env.activate_instructions()}\")\n print(f\"Or run scripts directly: python setup_environment.py --run script_name.py\")\n else:\n print(\"\\n❌ Environment setup failed\")\n return 1\n\n\nif __name__ == \"__main__\":\n sys.exit(main() or 0)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7960,"content_sha256":"0718bc47cb538968b146e366c6f246b3ed7bd3981e0d10c6e8e0035870026fb4"},{"filename":"scripts/source_manager.py","content":"#!/usr/bin/env python3\n\"\"\"\nUnified source ingestion for NotebookLM.\n\"\"\"\n\nimport argparse\nimport asyncio\nimport json\nimport re\nimport sys\nimport tempfile\nfrom pathlib import Path\nfrom typing import List, Optional, Union\n\nfrom agent_browser_client import AgentBrowserClient\nfrom auth_manager import AuthManager\nfrom config import DEFAULT_SESSION_ID\nfrom notebook_manager import NotebookLibrary, extract_notebook_id\nfrom notebooklm_wrapper import NotebookLMWrapper, NotebookLMError\nfrom sync_manager import SyncManager\nfrom zlibrary.downloader import ZLibraryDownloader\nfrom zlibrary import epub_converter\nfrom account_manager import AccountManager\n\n\ndef _resolve_notebook_target(args, file_title: str) -> tuple[Optional[str], bool]:\n \"\"\"Resolve which notebook to use based on CLI args.\n\n Returns:\n (notebook_id, create_new) tuple:\n - (id, False) = use existing notebook with given ID (real NotebookLM UUID)\n - (None, True) = create new notebook\n - Raises SystemExit if invalid combination\n \"\"\"\n library = NotebookLibrary()\n\n # Explicit notebook ID takes priority\n if args.notebook_id:\n return (args.notebook_id, False)\n\n # --use-active flag\n if args.use_active:\n active = library.get_active_notebook()\n if not active:\n print(\"❌ No active notebook set.\", file=sys.stderr)\n print(\" Set one with: python scripts/run.py notebook_manager.py activate --id \u003cid>\", file=sys.stderr)\n print(\" Or use --create-new to create a new notebook\", file=sys.stderr)\n raise SystemExit(1)\n print(f\"📓 Using active notebook: \\\"{active.get('name', 'Unnamed')}\\\"\")\n\n # Extract real NotebookLM UUID from URL (not library ID)\n url = active.get(\"url\")\n if url:\n real_uuid = extract_notebook_id(url)\n if real_uuid:\n return (real_uuid, False)\n\n # Fallback: if no URL or extraction failed, use library ID (may not be UUID)\n return (active.get(\"id\"), False)\n\n # --create-new flag\n if args.create_new:\n print(f\"📓 Will create new notebook: \\\"{file_title}\\\"\")\n return (None, True)\n\n # Neither flag provided - show error with options\n active = library.get_active_notebook()\n print(\"❌ No notebook specified. Please choose one of:\", file=sys.stderr)\n print(file=sys.stderr)\n if active:\n print(f\" --use-active Upload to active notebook: \\\"{active.get('name', 'Unnamed')}\\\"\", file=sys.stderr)\n print(f\" --create-new Create new notebook named after the file\", file=sys.stderr)\n print(f\" --notebook-id Specify notebook ID explicitly\", file=sys.stderr)\n raise SystemExit(1)\n\n\nclass SourceManager:\n \"\"\"Unified source ingestion for NotebookLM.\"\"\"\n\n def __init__(\n self,\n auth_manager: Optional[AuthManager] = None,\n client: Optional[AgentBrowserClient] = None,\n downloader_cls=ZLibraryDownloader,\n converter=epub_converter,\n ):\n self.auth = auth_manager or AuthManager()\n self.client = client or AgentBrowserClient(session_id=DEFAULT_SESSION_ID)\n self.downloader_cls = downloader_cls\n self.converter = converter\n\n @staticmethod\n def _is_zlibrary_url(url: str) -> bool:\n domains = [\"zlib.li\", \"z-lib.org\", \"zlibrary.org\", \"zh.zlib.li\"]\n return any(domain in url for domain in domains)\n\n @staticmethod\n def _sanitize_title(file_path: Path) -> str:\n title = file_path.stem\n title = re.sub(r'_part\\d+

NotebookLM Quick Commands Query Google NotebookLM for source-grounded, citation-backed answers. Environment All dependencies and authentication are handled automatically by : - First run creates and installs Python/Node.js dependencies - If Google auth is missing or expired, a browser window opens automatically - No manual pre-flight steps required --- Usage Commands Notebook Management | Command | Description | |---------|-------------| | | Authenticate with Google | | | Show auth and library status | | | List all Google accounts | | | Add a new Google account | | | Switch active account (by…

, '', title)\n title = title.replace('_', ' ')\n title = re.sub(r'\\[.*?\\]', '', title)\n title = re.sub(r'\\(.*?\\)', '', title)\n title = re.sub(r'\\s+', ' ', title).strip()\n if len(title) > 50:\n title = title[:50] + \"...\"\n return title\n\n @staticmethod\n def _extract_notebook_id_from_url(notebook_url: str) -> Optional[str]:\n if not notebook_url:\n return None\n match = re.search(r\"/notebook/([^/?#]+)\", notebook_url)\n if match:\n return match.group(1)\n return None\n\n async def _wait_for_sources_ready_async(\n self,\n wrapper: NotebookLMWrapper,\n notebook_id: str,\n source_ids: List[str],\n ) -> Optional[dict]:\n \"\"\"Wait for sources to be ready using async wrapper.\"\"\"\n if not source_ids:\n return None\n\n unique_ids = list(dict.fromkeys(source_ids))\n total = len(unique_ids)\n print(f\"Waiting for NotebookLM to process {total} source(s)...\", file=sys.stderr, flush=True)\n\n last_ready = None\n while True:\n sources = await wrapper.list_sources(notebook_id)\n status_by_id = {src[\"source_id\"]: src for src in sources}\n\n ready_count = 0\n for source_id in unique_ids:\n source = status_by_id.get(source_id)\n if not source:\n continue\n if source.get(\"is_ready\"):\n ready_count += 1\n\n if last_ready is None or ready_count != last_ready:\n print(f\"Ready: {ready_count}/{total}\", file=sys.stderr, flush=True)\n last_ready = ready_count\n\n if ready_count >= total:\n return None\n\n await asyncio.sleep(2)\n\n async def add_from_file(\n self,\n file_path: Union[Path, List[Path]],\n notebook_id: Optional[str] = None,\n source_label: str = \"upload\",\n ) -> dict:\n \"\"\"Upload local file(s) to NotebookLM.\"\"\"\n paths = file_path if isinstance(file_path, list) else [file_path]\n for path in paths:\n if not Path(path).exists():\n raise FileNotFoundError(f\"File not found: {path}\")\n\n title = self._sanitize_title(Path(paths[0]))\n created_notebook = False\n\n notebook_url = None\n resolved_notebook_id = notebook_id\n\n if not self.auth.is_authenticated(\"google\"):\n return {\n \"success\": False,\n \"error\": \"Google authentication required\",\n \"recovery\": \"Run: python scripts/run.py auth_manager.py setup\",\n }\n\n async with NotebookLMWrapper() as wrapper:\n if not notebook_id:\n try:\n result = await wrapper.create_notebook(title)\n notebook_id = result[\"id\"]\n created_notebook = True\n resolved_notebook_id = notebook_id\n except NotebookLMError as e:\n return {\n \"success\": False,\n \"error\": e.message,\n \"recovery\": e.recovery,\n }\n else:\n library = NotebookLibrary()\n notebook = library.get_notebook(notebook_id)\n if notebook:\n notebook_url = notebook.get(\"url\")\n resolved_notebook_id = self._extract_notebook_id_from_url(notebook_url) or notebook_id\n else:\n is_uuid = bool(re.fullmatch(r\"[a-f0-9-]{36}\", notebook_id or \"\", re.IGNORECASE))\n if not is_uuid:\n return {\n \"success\": False,\n \"error\": f\"Notebook '{notebook_id}' not found in library\",\n \"recovery\": \"Run: python scripts/run.py notebook_manager.py list\",\n }\n\n if not notebook_url:\n notebook_url = f\"https://notebooklm.google.com/notebook/{resolved_notebook_id}\"\n\n # Upload files using the wrapper\n source_ids = []\n try:\n for path in paths:\n result = await wrapper.add_file(resolved_notebook_id, Path(path))\n if result.get(\"source_id\"):\n source_ids.append(result[\"source_id\"])\n except NotebookLMError as e:\n return {\n \"success\": False,\n \"error\": e.message,\n \"recovery\": e.recovery,\n }\n\n if created_notebook:\n try:\n library = NotebookLibrary()\n description = f\"Imported from {source_label}: {title}\"\n library.add_notebook(\n url=notebook_url,\n name=title,\n description=description,\n topics=[source_label],\n notebook_id=notebook_id, # Use actual UUID from NotebookLM\n )\n library.select_notebook(notebook_id)\n print(f\"✅ Activated notebook: {title}\")\n except Exception as e:\n print(f\"⚠️ Warning: Could not activate notebook: {e}\")\n\n if not source_ids:\n return {\"success\": False, \"error\": \"Upload failed\"}\n\n # Wait for sources to be ready\n try:\n wait_error = await self._wait_for_sources_ready_async(wrapper, resolved_notebook_id, source_ids)\n except NotebookLMError as e:\n return {\n \"success\": False,\n \"error\": e.message,\n \"recovery\": e.recovery,\n }\n if wait_error:\n return wait_error\n\n if len(paths) > 1:\n return {\n \"success\": True,\n \"notebook_id\": notebook_id,\n \"source_ids\": source_ids,\n \"title\": title,\n \"chunks\": len(paths)\n }\n\n return {\n \"success\": True,\n \"notebook_id\": notebook_id,\n \"source_id\": source_ids[0],\n \"title\": title\n }\n\n async def add_from_zlibrary(self, url: str, notebook_id: Optional[str] = None) -> dict:\n \"\"\"Download from Z-Library and upload to NotebookLM.\"\"\"\n if not self.auth.is_authenticated(\"zlibrary\"):\n raise RuntimeError(\n \"Z-Library authentication required. \"\n \"Run: python scripts/run.py auth_manager.py setup --service zlibrary\"\n )\n\n self.client.connect()\n try:\n # Let restore_auth handle navigation - don't navigate here\n self.auth.restore_auth(\"zlibrary\", client=self.client)\n downloader = self.downloader_cls(self.client)\n file_path, file_format = downloader.download(url)\n self.auth.save_auth(\"zlibrary\", client=self.client)\n finally:\n self.client.disconnect()\n\n if file_format == \"epub\" or Path(file_path).suffix.lower() == \".epub\":\n output_path = Path(tempfile.gettempdir()) / f\"{Path(file_path).stem}.md\"\n converted = self.converter.convert_epub_to_markdown(file_path, output_path)\n return await self.add_from_file(converted, notebook_id, source_label=\"zlibrary\")\n\n return await self.add_from_file(Path(file_path), notebook_id, source_label=\"zlibrary\")\n\n async def add_from_url(self, url: str, notebook_id: Optional[str] = None) -> dict:\n \"\"\"Smart routing based on URL pattern.\"\"\"\n if self._is_zlibrary_url(url):\n return await self.add_from_zlibrary(url, notebook_id)\n raise ValueError(f\"Unsupported URL: {url}\")\n\n\nasync def async_main():\n parser = argparse.ArgumentParser(description=\"Add sources to NotebookLM\")\n parser.add_argument(\"command\", choices=[\"add\", \"sync\"], help=\"Command to run\")\n parser.add_argument(\"--url\", help=\"Source URL\")\n parser.add_argument(\"--file\", help=\"Local file path\")\n parser.add_argument(\"--notebook-id\", help=\"Existing notebook ID\")\n parser.add_argument(\"--use-active\", action=\"store_true\",\n help=\"Upload to currently active notebook\")\n parser.add_argument(\"--create-new\", action=\"store_true\",\n help=\"Create a new notebook for the upload\")\n\n # Sync command arguments\n parser.add_argument(\"folder\", nargs=\"?\", help=\"Folder path to sync\")\n parser.add_argument(\"--dry-run\", action=\"store_true\",\n help=\"Show sync plan without executing\")\n parser.add_argument(\"--rebuild\", action=\"store_true\",\n help=\"Force rebuild tracking file (re-hash all files)\")\n\n args = parser.parse_args()\n\n # Validate mutually exclusive options\n if args.use_active and args.create_new:\n print(\"❌ Cannot use both --use-active and --create-new\", file=sys.stderr)\n raise SystemExit(1)\n if args.notebook_id and (args.use_active or args.create_new):\n print(\"❌ Cannot use --notebook-id with --use-active or --create-new\", file=sys.stderr)\n raise SystemExit(1)\n\n manager = SourceManager()\n\n if args.command == \"add\" and args.file:\n path = Path(args.file).resolve()\n if path.is_dir():\n args.command = \"sync\"\n args.folder = str(path)\n args.file = None\n\n if args.command == \"add\":\n if args.url:\n # For URLs, derive title from URL\n file_title = Path(args.url).stem or \"Untitled\"\n notebook_id, create_new = _resolve_notebook_target(args, file_title)\n result = await manager.add_from_url(args.url, notebook_id)\n elif args.file:\n file_title = Path(args.file).stem\n notebook_id, create_new = _resolve_notebook_target(args, file_title)\n result = await manager.add_from_file(Path(args.file), notebook_id)\n else:\n raise SystemExit(\"Provide --url or --file\")\n\n print(json.dumps(result, indent=2))\n\n elif args.command == \"sync\":\n if not args.folder:\n print(\"❌ No folder specified.\", file=sys.stderr)\n print(\" Usage: python scripts/run.py source_manager.py sync \u003cfolder>\", file=sys.stderr)\n raise SystemExit(1)\n\n folder_path = Path(args.folder).resolve()\n if not folder_path.is_dir():\n print(f\"❌ Folder not found: {folder_path}\", file=sys.stderr)\n raise SystemExit(1)\n\n # Resolve notebook target\n folder_name = folder_path.stem\n notebook_id, create_new = _resolve_notebook_target(args, folder_name)\n\n # Get active account\n account_mgr = AccountManager()\n active = account_mgr.get_active_account()\n if not active:\n print(\"❌ No active Google account.\", file=sys.stderr)\n print(\" Run: python scripts/run.py auth_manager.py accounts list\", file=sys.stderr)\n raise SystemExit(1)\n\n # Create notebook if needed\n if create_new:\n async with NotebookLMWrapper() as wrapper:\n nb_result = await wrapper.create_notebook(folder_name)\n notebook_id = nb_result[\"id\"]\n print(f\"📓 Created new notebook: {folder_name}\")\n\n library = NotebookLibrary()\n url = f\"https://notebooklm.google.com/notebook/{notebook_id}\"\n library.add_notebook(url=url, name=folder_name, description=f\"Synced from {folder_name}\", topics=[], notebook_id=notebook_id)\n library.select_notebook(notebook_id)\n print(f\"✅ Activated notebook: {folder_name}\")\n\n if not notebook_id:\n print(\"❌ No notebook specified and create-new not specified.\", file=sys.stderr)\n raise SystemExit(1)\n\n # Create sync manager and run sync\n sync_mgr = SyncManager(str(folder_path))\n\n # Rebuild option - delete tracking file\n if args.rebuild and sync_mgr.tracking_file.exists():\n sync_mgr.tracking_file.unlink()\n print(f\"🗑️ Cleared tracking file for rebuild\")\n\n result = await sync_mgr.execute_sync(\n notebook_id=notebook_id,\n account_index=active.index,\n account_email=active.email,\n dry_run=args.dry_run,\n )\n\n print(json.dumps(result, indent=2))\n\n\ndef main():\n asyncio.run(async_main())\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16108,"content_sha256":"a4ad60c83758674dadf4a141dbb72e06178e6804152ac151f1e339df5046fadc"},{"filename":"scripts/sync_manager.py","content":"#!/usr/bin/env python3\n\"\"\"\nFolder sync manager for NotebookLM.\nScans local folders, tracks file changes, and syncs to NotebookLM notebooks.\n\"\"\"\n\nimport hashlib\nimport json\nimport os\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom config import DATA_DIR\n\nSUPPORTED_EXTENSIONS = {'.pdf', '.txt', '.md', '.docx', '.html', '.epub'}\n\n# Ensure sync directory exists\nSYNC_DIR = DATA_DIR / \"sync\"\nSYNC_DIR.mkdir(parents=True, exist_ok=True)\n\n\nclass SyncAction(Enum):\n \"\"\"Sync action types.\"\"\"\n ADD = \"add\"\n UPDATE = \"update\"\n SKIP = \"skip\"\n DELETE = \"delete\"\n\n\n@dataclass\nclass TrackedFile:\n \"\"\"Represents a tracked file in the sync state.\"\"\"\n filename: str\n hash: str\n modified_at: str\n source_id: Optional[str] = None\n uploaded_at: Optional[str] = None\n\n\n@dataclass\nclass SyncState:\n \"\"\"Represents the sync tracking state.\"\"\"\n version: int = 1\n folder_path: str = \"\"\n notebook_id: Optional[str] = None\n notebook_url: Optional[str] = None\n account_index: Optional[int] = None\n account_email: Optional[str] = None\n last_sync_at: Optional[str] = None\n files: dict[str, TrackedFile] = field(default_factory=dict)\n\n\nclass SyncManager:\n \"\"\"Manages folder-to-notebook synchronization.\"\"\"\n\n def __init__(self, folder_path: str):\n self.folder_path = Path(folder_path).resolve()\n # Store tracking file in data/sync/ (not in the synced folder!)\n folder_hash = hashlib.md5(str(self.folder_path).encode()).hexdigest()[:12]\n self.tracking_file = SYNC_DIR / f\"{folder_hash}.sync.json\"\n self.state = SyncState(folder_path=str(self.folder_path))\n\n def load_state(self) -> bool:\n \"\"\"Load sync state from tracking file.\n\n Returns:\n True if state loaded successfully (creates fresh state if file doesn't exist or is corrupted)\n \"\"\"\n if not self.tracking_file.exists():\n # Create fresh state for new sync folder\n self.state = SyncState(folder_path=str(self.folder_path))\n return True\n\n try:\n data = json.loads(self.tracking_file.read_text())\n\n # Validate version\n if data.get(\"version\") != 1:\n raise ValueError(f\"Unsupported tracking file version: {data.get('version')}\")\n\n # Reconstruct state\n self.state = SyncState(\n version=data.get(\"version\", 1),\n folder_path=data.get(\"folder_path\", str(self.folder_path)),\n notebook_id=data.get(\"notebook_id\"),\n notebook_url=data.get(\"notebook_url\"),\n account_index=data.get(\"account_index\"),\n account_email=data.get(\"account_email\"),\n last_sync_at=data.get(\"last_sync_at\"),\n )\n\n # Reconstruct file entries\n for path, file_data in data.get(\"files\", {}).items():\n self.state.files[path] = TrackedFile(\n filename=file_data.get(\"filename\", \"\"),\n hash=file_data.get(\"hash\", \"\"),\n modified_at=file_data.get(\"modified_at\", \"\"),\n source_id=file_data.get(\"source_id\"),\n uploaded_at=file_data.get(\"uploaded_at\"),\n )\n\n return True\n\n except (json.JSONDecodeError, ValueError) as e:\n print(f\"⚠️ Error loading tracking file: {e}\")\n # Backup corrupted file\n broken = self.tracking_file.with_suffix(\".json.broken\")\n if not broken.exists():\n self.tracking_file.replace(broken)\n print(f\" Backed up corrupted file to: {broken}\")\n # Create fresh state\n self.state = SyncState(folder_path=str(self.folder_path))\n return True\n\n def save_state(self) -> bool:\n \"\"\"Save sync state to tracking file.\n\n Returns:\n True if saved successfully, False on error\n \"\"\"\n try:\n data = {\n \"version\": self.state.version,\n \"folder_path\": self.state.folder_path,\n \"notebook_id\": self.state.notebook_id,\n \"notebook_url\": self.state.notebook_url,\n \"account_index\": self.state.account_index,\n \"account_email\": self.state.account_email,\n \"last_sync_at\": self.state.last_sync_at,\n \"files\": {}\n }\n\n for path, file_info in self.state.files.items():\n data[\"files\"][path] = {\n \"filename\": file_info.filename,\n \"hash\": file_info.hash,\n \"modified_at\": file_info.modified_at,\n \"source_id\": file_info.source_id,\n \"uploaded_at\": file_info.uploaded_at,\n }\n\n # Atomic write via temp file\n temp_file = self.tracking_file.with_suffix(\".json.tmp\")\n temp_file.write_text(json.dumps(data, indent=2))\n temp_file.replace(self.tracking_file)\n return True\n\n except Exception as e:\n print(f\"❌ Error saving tracking file: {e}\")\n return False\n\n def scan_folder(self) -> dict[str, dict]:\n \"\"\"Scan folder for supported files.\n\n Returns:\n Dict mapping relative path -> file info dict with:\n - path: relative path from folder\n - absolute_path: full path\n - filename: file stem (without extension)\n - extension: file extension\n - modified_at: ISO timestamp\n - size: file size in bytes\n \"\"\"\n files = {}\n\n if not self.folder_path.exists():\n print(f\"⚠️ Folder does not exist: {self.folder_path}\")\n return files\n\n for root, dirs, filenames in os.walk(self.folder_path):\n # Skip hidden directories\n dirs[:] = [d for d in dirs if not d.startswith('.')]\n\n for filename in filenames:\n # Skip hidden files\n if filename.startswith('.'):\n continue\n\n path = Path(root) / filename\n relative_path = path.relative_to(self.folder_path)\n\n # Check extension\n ext = path.suffix.lower()\n if ext not in SUPPORTED_EXTENSIONS:\n continue\n\n try:\n stat = path.stat()\n # Normalize paths to POSIX format for cross-platform portability\n posix_path = relative_path.as_posix()\n files[posix_path] = {\n \"path\": posix_path,\n \"absolute_path\": str(path),\n \"filename\": path.stem,\n \"extension\": ext,\n \"modified_at\": datetime.fromtimestamp(stat.st_mtime).isoformat(),\n \"size\": stat.st_size,\n }\n except OSError as e:\n print(f\"⚠️ Could not access {path}: {e}\")\n\n print(f\"📁 Found {len(files)} supported files in {self.folder_path}\")\n return files\n\n def compute_file_hash(self, file_path: Path) -> str:\n \"\"\"Compute SHA-256 hash of a file.\n\n Returns:\n Hash string prefixed with algorithm name, e.g., \"sha256:abc123...\"\n \"\"\"\n sha256 = hashlib.sha256()\n\n with open(file_path, 'rb') as f:\n for chunk in iter(lambda: f.read(8192), b''):\n sha256.update(chunk)\n\n return f\"sha256:{sha256.hexdigest()}\"\n\n def get_sync_plan(self, local_files: dict[str, dict]) -> list[dict]:\n \"\"\"Generate sync plan comparing local files with tracking state.\n\n Args:\n local_files: Dict from scan_folder() with file info\n\n Returns:\n List of sync actions with:\n - action: SyncAction value\n - path: relative file path\n - local_info: file info from local_files\n - tracked_info: previous tracking info (if exists)\n - source_id: existing source ID for update/delete (if exists)\n \"\"\"\n plan = []\n\n # Check each local file\n for path, local_info in local_files.items():\n # Compute hash for comparison\n abs_path = Path(local_info[\"absolute_path\"])\n current_hash = self.compute_file_hash(abs_path)\n local_info[\"hash\"] = current_hash\n\n if path not in self.state.files:\n # New file - needs addition\n plan.append({\n \"action\": SyncAction.ADD.value,\n \"path\": path,\n \"local_info\": local_info,\n \"tracked_info\": None,\n \"source_id\": None,\n })\n else:\n tracked = self.state.files[path]\n\n if tracked.hash != current_hash:\n # Content changed - needs update\n if tracked.source_id:\n plan.append({\n \"action\": SyncAction.UPDATE.value,\n \"path\": path,\n \"local_info\": local_info,\n \"tracked_info\": tracked,\n \"source_id\": tracked.source_id,\n })\n else:\n # No existing source, treat as add\n plan.append({\n \"action\": SyncAction.ADD.value,\n \"path\": path,\n \"local_info\": local_info,\n \"tracked_info\": tracked,\n \"source_id\": None,\n })\n else:\n # Unchanged - skip\n plan.append({\n \"action\": SyncAction.SKIP.value,\n \"path\": path,\n \"local_info\": local_info,\n \"tracked_info\": tracked,\n \"source_id\": tracked.source_id,\n })\n\n # Check for deleted files (in tracking but not in local)\n for path in self.state.files:\n if path not in local_files:\n plan.append({\n \"action\": SyncAction.DELETE.value,\n \"path\": path,\n \"local_info\": None,\n \"tracked_info\": self.state.files[path],\n \"source_id\": self.state.files[path].source_id,\n })\n\n return plan\n\n async def execute_sync(\n self,\n notebook_id: str,\n account_index: int,\n account_email: str,\n dry_run: bool = False,\n ) -> dict:\n \"\"\"Execute full sync workflow.\n\n Args:\n notebook_id: Target NotebookLM notebook ID\n account_index: Active Google account index\n account_email: Active Google account email\n dry_run: If True, only show plan without executing\n\n Returns:\n Dict with sync results: add, update, skip, delete, errors\n \"\"\"\n from notebooklm_wrapper import NotebookLMWrapper\n\n self.load_state()\n self._warn_if_account_mismatch(account_index, account_email)\n\n local_files = self.scan_folder()\n plan = self.get_sync_plan(local_files)\n\n self._print_sync_plan(plan, dry_run)\n if dry_run:\n return self._summarize_plan(plan)\n\n async with NotebookLMWrapper() as wrapper:\n result = await self._execute_plan(wrapper, plan, notebook_id)\n\n self._update_state_after_sync(notebook_id, account_index, account_email)\n return result\n\n def _warn_if_account_mismatch(self, account_index: int, account_email: str):\n \"\"\"Warn if current account differs from tracking file account.\"\"\"\n if self.state.account_index is not None and self.state.account_index != account_index:\n print(f\"⚠️ Tracking file was created with account [{self.state.account_index}] {self.state.account_email}\")\n print(f\" Current active account: [{account_index}] {account_email}\")\n print(\" Continuing with new account (tracking file will be updated)\")\n\n def _update_state_after_sync(self, notebook_id: str, account_index: int, account_email: str):\n \"\"\"Update tracking state after successful sync.\"\"\"\n self.state.notebook_id = notebook_id\n self.state.account_index = account_index\n self.state.account_email = account_email\n self.state.last_sync_at = datetime.now(timezone.utc).isoformat()\n self.save_state()\n\n async def _execute_plan(\n self,\n wrapper,\n plan: list[dict],\n notebook_id: str,\n dry_run: bool = False,\n ) -> dict:\n \"\"\"Execute sync plan using NotebookLMWrapper.\n\n Args:\n wrapper: NotebookLMWrapper instance\n plan: List of sync actions\n notebook_id: Target notebook ID\n dry_run: If True, don't actually modify anything\n\n Returns:\n Dict with sync results\n \"\"\"\n result = {\"add\": 0, \"update\": 0, \"skip\": 0, \"delete\": 0, \"errors\": []}\n\n for item in plan:\n action = item[\"action\"]\n path = item[\"path\"]\n local_info = item[\"local_info\"]\n\n if action == SyncAction.SKIP.value:\n result[\"skip\"] += 1\n continue\n\n if dry_run:\n print(f\" [DRY-RUN] {action.upper()} {path}\")\n continue\n\n try:\n if action == SyncAction.ADD.value:\n print(f\" ➕ Adding: {path}\")\n source_id = await self._upload_file(wrapper, notebook_id, local_info)\n self._update_tracked_file(path, local_info, source_id)\n result[\"add\"] += 1\n\n elif action == SyncAction.UPDATE.value:\n print(f\" 🔄 Updating: {path}\")\n old_source_id = item[\"source_id\"]\n if old_source_id:\n await wrapper.delete_source(notebook_id, old_source_id)\n source_id = await self._upload_file(wrapper, notebook_id, local_info)\n self._update_tracked_file(path, local_info, source_id)\n result[\"update\"] += 1\n\n elif action == SyncAction.DELETE.value:\n print(f\" 🗑️ Deleting remote: {path}\")\n source_id = item[\"source_id\"]\n if source_id:\n await wrapper.delete_source(notebook_id, source_id)\n del self.state.files[path]\n result[\"delete\"] += 1\n\n except Exception as e:\n print(f\" ❌ Error {action} {path}: {e}\")\n result[\"errors\"].append({\"path\": path, \"action\": action, \"error\": str(e)})\n\n return result\n\n async def _upload_file(self, wrapper, notebook_id: str, local_info: dict) -> Optional[str]:\n \"\"\"Upload a single file to NotebookLM.\n\n Args:\n wrapper: NotebookLMWrapper instance\n notebook_id: Target notebook ID\n local_info: File info dict with absolute_path\n\n Returns:\n Source ID from upload response\n \"\"\"\n file_path = Path(local_info[\"absolute_path\"])\n upload_result = await wrapper.add_file(notebook_id, file_path)\n return upload_result.get(\"source_id\")\n\n def _update_tracked_file(self, path: str, local_info: dict, source_id: Optional[str]):\n \"\"\"Update or create a tracked file entry in state.\n\n Args:\n path: Relative file path\n local_info: File info dict with filename, hash, modified_at\n source_id: Source ID from NotebookLM\n \"\"\"\n self.state.files[path] = TrackedFile(\n filename=local_info[\"filename\"],\n hash=local_info[\"hash\"],\n modified_at=local_info[\"modified_at\"],\n source_id=source_id,\n uploaded_at=datetime.now(timezone.utc).isoformat(),\n )\n\n def _print_sync_plan(self, plan: list[dict], dry_run: bool = False):\n \"\"\"Print formatted sync plan.\n\n Args:\n plan: List of sync actions\n dry_run: If True, show dry-run indicator\n \"\"\"\n prefix = \"🔍 [DRY-RUN] \" if dry_run else \"📋 Sync Plan:\"\n print(f\"\\n{prefix}\")\n\n counts = {\"add\": 0, \"update\": 0, \"skip\": 0, \"delete\": 0}\n for item in plan:\n action = item[\"action\"]\n path = item[\"path\"]\n counts[action] += 1\n symbol = {\"add\": \"➕\", \"update\": \"🔄\", \"skip\": \"✓\", \"delete\": \"🗑️\"}[action]\n print(f\" {symbol} {path:\u003c30} [{action.upper()}]\")\n\n print(f\"\\n Total: {counts['add']} add, {counts['update']} update, {counts['skip']} skip, {counts['delete']} delete\")\n\n def _summarize_plan(self, plan: list[dict]) -> dict:\n \"\"\"Summarize plan without executing.\n\n Args:\n plan: List of sync actions\n\n Returns:\n Dict with counts by action type\n \"\"\"\n result = {\"add\": 0, \"update\": 0, \"skip\": 0, \"delete\": 0, \"errors\": []}\n for item in plan:\n result[item[\"action\"]] += 1\n return result","content_type":"text/x-python; charset=utf-8","language":"python","size":17469,"content_sha256":"87aaf0a3b7713131d1f0efcce7e6f0e3d6d2dc9ffd54d3aba2bff9c8e5917fbe"},{"filename":"scripts/zlibrary/__init__.py","content":"\"\"\"Z-Library helpers.\"\"\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":25,"content_sha256":"0971c86d80ddbb8cb81fae0aa413458f7cb7e5710bd2a07b2f7b26aeeff35824"},{"filename":"scripts/zlibrary/downloader.py","content":"#!/usr/bin/env python3\n\"\"\"\nZ-Library download automation using agent-browser.\n\"\"\"\n\nimport re\nimport threading\nimport time\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom agent_browser_client import AgentBrowserClient, AgentBrowserError\n\n\nclass ZLibraryDownloader:\n \"\"\"Download books from Z-Library using agent-browser.\"\"\"\n\n def __init__(self, client: AgentBrowserClient, downloads_dir: Optional[Path] = None):\n self.client = client\n self.downloads_dir = downloads_dir or (Path.home() / \"Downloads\")\n\n @staticmethod\n def _detect_formats(snapshot: str) -> list[str]:\n formats = set()\n for line in snapshot.splitlines():\n line_lower = line.lower()\n if \"pdf\" in line_lower:\n formats.add(\"pdf\")\n if \"epub\" in line_lower:\n formats.add(\"epub\")\n return sorted(formats)\n\n @staticmethod\n def _choose_format(formats: list[str]) -> Optional[str]:\n if \"pdf\" in formats:\n return \"pdf\"\n if \"epub\" in formats:\n return \"epub\"\n return None\n\n @staticmethod\n def _find_download_ref(snapshot: str, file_format: str) -> Optional[str]:\n if not file_format:\n return None\n for line in snapshot.splitlines():\n line_lower = line.lower()\n if file_format in line_lower and (\"link\" in line_lower or \"button\" in line_lower):\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n return None\n\n @staticmethod\n def _find_ref_by_keywords(snapshot: str, keywords: list[str]) -> Optional[str]:\n for line in snapshot.splitlines():\n line_lower = line.lower()\n if any(keyword in line_lower for keyword in keywords):\n if \"button\" in line_lower or \"link\" in line_lower:\n match = re.search(r'\\[ref=(\\w+)\\]', line)\n if match:\n return match.group(1)\n return None\n\n def _download_ref(self, ref: str, file_format: str) -> Path:\n self.downloads_dir.mkdir(parents=True, exist_ok=True)\n temp_name = f\"zlibrary_{int(time.time())}\"\n temp_path = self.downloads_dir / temp_name\n\n response = self.client._send_command(\"download\", {\n \"selector\": f\"@{ref}\",\n \"path\": str(temp_path)\n })\n\n suggested = response.get(\"suggestedFilename\")\n if suggested:\n final_path = self.downloads_dir / suggested\n if temp_path.exists() and final_path != temp_path:\n temp_path.replace(final_path)\n return final_path\n\n response_path = response.get(\"path\")\n return Path(response_path) if response_path else temp_path\n\n @staticmethod\n def _is_direct_download_url(url: str) -> bool:\n return \"/dl/\" in url\n\n def _download_direct_url(self, url: str, timeout_ms: int = 60000) -> tuple[Path, str]:\n self.downloads_dir.mkdir(parents=True, exist_ok=True)\n temp_name = f\"zlibrary_{int(time.time())}\"\n temp_path = self.downloads_dir / temp_name\n\n result = {\"response\": None, \"error\": None}\n started = threading.Event()\n\n def wait_for_download():\n sock = None\n try:\n sock = self.client._connect_socket(timeout=max(5, (timeout_ms / 1000) + 5))\n started.set()\n response = self.client._send_command_on_socket(\n sock,\n \"waitfordownload\",\n {\"path\": str(temp_path), \"timeout\": timeout_ms}\n )\n result[\"response\"] = response\n except Exception as exc:\n result[\"error\"] = exc\n finally:\n if sock:\n try:\n sock.close()\n except Exception:\n pass\n\n thread = threading.Thread(target=wait_for_download, daemon=True)\n thread.start()\n started.wait(timeout=1)\n\n navigate_error = None\n try:\n self.client.navigate(url)\n except AgentBrowserError as exc:\n navigate_error = exc\n\n thread.join(timeout=(timeout_ms / 1000) + 5)\n if thread.is_alive():\n raise RuntimeError(\"Download timed out\")\n\n if result[\"error\"] is not None:\n raise result[\"error\"]\n\n response = result[\"response\"] or {}\n if not response.get(\"success\", False):\n if navigate_error:\n raise navigate_error\n raise RuntimeError(response.get(\"error\", \"Download failed\"))\n\n data = response.get(\"data\", {})\n final_path = Path(data.get(\"path\") or temp_path)\n filename = data.get(\"filename\") or data.get(\"suggestedFilename\")\n if filename:\n suggested_path = self.downloads_dir / filename\n if final_path.exists() and suggested_path != final_path:\n final_path.replace(suggested_path)\n final_path = suggested_path\n\n file_format = final_path.suffix.lower().lstrip(\".\") or \"unknown\"\n return final_path, file_format\n\n def download(self, url: str) -> tuple[Path, str]:\n \"\"\"Download a book from Z-Library URL.\"\"\"\n if self._is_direct_download_url(url):\n return self._download_direct_url(url)\n\n self.client.navigate(url)\n time.sleep(2)\n\n snapshot = self.client.snapshot()\n formats = self._detect_formats(snapshot)\n chosen = self._choose_format(formats)\n ref = self._find_download_ref(snapshot, chosen)\n\n if not ref:\n more_ref = self._find_ref_by_keywords(snapshot, [\"more\", \"options\", \"menu\", \"dots\"])\n if more_ref:\n self.client.click(more_ref)\n time.sleep(2)\n snapshot = self.client.snapshot()\n formats = self._detect_formats(snapshot)\n chosen = self._choose_format(formats) or chosen\n ref = self._find_download_ref(snapshot, chosen)\n\n if not ref:\n ref = self._find_ref_by_keywords(snapshot, [\"download\"])\n if ref and chosen is None:\n chosen = \"unknown\"\n\n if not ref:\n raise RuntimeError(\"Download link not found\")\n\n file_path = self._download_ref(ref, chosen)\n return file_path, chosen\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6433,"content_sha256":"f22f226d16e5b396560608ae7bf813756c807f92577716f543e93a06fa1eee61"},{"filename":"scripts/zlibrary/epub_converter.py","content":"#!/usr/bin/env python3\n\"\"\"\nEPUB conversion utilities for Z-Library downloads.\nUses BeautifulSoup for HTML parsing.\n\"\"\"\n\nimport re\nfrom pathlib import Path\n\n\ndef count_words(text: str) -> int:\n \"\"\"Count combined Chinese characters and English words.\"\"\"\n chinese_chars = len(re.findall(r'[\\u4e00-\\u9fff]', text))\n english_words = len(re.findall(r'\\b[a-zA-Z]+\\b', text))\n return chinese_chars + english_words\n\n\ndef split_markdown_file(file_path: Path, max_words: int = 350000) -> list[Path]:\n \"\"\"Split a large Markdown file into smaller parts.\"\"\"\n content = file_path.read_text(encoding=\"utf-8\")\n chapters = re.split(r'\\n(?=#{1,3}\\s)', content)\n\n chunks = []\n current_chunk = \"\"\n current_words = 0\n\n for chapter in chapters:\n chapter_words = count_words(chapter)\n\n if chapter_words > max_words:\n if current_chunk:\n chunks.append(current_chunk)\n current_chunk = \"\"\n current_words = 0\n\n paragraphs = chapter.split('\\n\\n')\n temp_chunk = \"\"\n temp_words = 0\n\n for para in paragraphs:\n para_words = count_words(para)\n if temp_words + para_words > max_words and temp_chunk:\n chunks.append(temp_chunk)\n temp_chunk = para + \"\\n\\n\"\n temp_words = para_words\n else:\n temp_chunk += para + \"\\n\\n\"\n temp_words += para_words\n\n if temp_chunk:\n current_chunk = temp_chunk\n current_words = temp_words\n\n elif current_words + chapter_words > max_words:\n chunks.append(current_chunk)\n current_chunk = chapter + \"\\n\\n\"\n current_words = chapter_words\n else:\n current_chunk += chapter + \"\\n\\n\"\n current_words += chapter_words\n\n if current_chunk:\n chunks.append(current_chunk)\n\n chunk_files = []\n stem = file_path.stem\n for i, chunk in enumerate(chunks, 1):\n chunk_file = file_path.parent / f\"{stem}_part{i}.md\"\n chunk_file.write_text(chunk, encoding=\"utf-8\")\n chunk_files.append(chunk_file)\n\n return chunk_files\n\n\ndef html_to_markdown(soup) -> str:\n \"\"\"Convert BeautifulSoup object to Markdown.\"\"\"\n markdown_parts = []\n\n def process_element(element):\n if element.name is None:\n text = str(element).strip()\n return text if text else \"\"\n\n if element.name in ['script', 'style', 'nav', 'footer', 'svg']:\n return \"\"\n\n if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:\n level = int(element.name[1])\n text = element.get_text().strip()\n return f\"\\n\\n{'#' * level} {text}\\n\\n\" if text else \"\"\n\n if element.name == 'p':\n text = element.get_text().strip()\n return f\"\\n\\n{text}\\n\\n\" if text else \"\"\n\n if element.name in ['b', 'strong']:\n text = element.get_text().strip()\n return f\"**{text}**\" if text else \"\"\n\n if element.name in ['i', 'em']:\n text = element.get_text().strip()\n return f\"*{text}*\" if text else \"\"\n\n if element.name == 'code':\n text = element.get_text().strip()\n return f\"`{text}`\" if text else \"\"\n\n if element.name == 'a':\n href = element.get('href', '')\n text = element.get_text().strip()\n if href and text:\n return f\"[{text}]({href})\"\n return text\n\n if element.name == 'ul':\n items = element.find_all('li', recursive=False)\n result = \"\\n\\n\"\n for li in items:\n text = li.get_text().strip()\n if text:\n result += f\"- {text}\\n\"\n return result + \"\\n\"\n\n if element.name == 'ol':\n items = element.find_all('li', recursive=False)\n result = \"\\n\\n\"\n for i, li in enumerate(items, 1):\n text = li.get_text().strip()\n if text:\n result += f\"{i}. {text}\\n\"\n return result + \"\\n\"\n\n if element.name == 'br':\n return \"\\n\"\n\n if element.contents:\n result = \"\"\n for child in element.contents:\n result += process_element(child)\n return result\n\n return \"\"\n\n body = soup.find('body')\n markdown = process_element(body) if body else process_element(soup)\n\n markdown = re.sub(r'\\n{4,}', '\\n\\n\\n', markdown)\n markdown = re.sub(r' +', ' ', markdown)\n return markdown.strip()\n\n\ndef epub_to_markdown(epub_path: Path, output_path: Path) -> Path:\n \"\"\"Convert EPUB to Markdown file.\"\"\"\n from ebooklib import epub\n from bs4 import BeautifulSoup\n\n book = epub.read_epub(str(epub_path))\n\n title = book.get_metadata('DC', 'title')\n title_text = title[0][0] if title else \"Unknown Title\"\n author = book.get_metadata('DC', 'creator')\n author_text = author[0][0] if author else \"Unknown Author\"\n\n markdown_content = f\"# {title_text}\\n\\n\"\n markdown_content += f\"**Author:** {author_text}\\n\\n\"\n markdown_content += \"---\\n\\n\"\n\n for item in book.get_items():\n if item.get_type() == 9: # ITEM_DOCUMENT\n content = item.get_content().decode('utf-8')\n soup = BeautifulSoup(content, 'html.parser')\n chapter_md = html_to_markdown(soup)\n if len(chapter_md.strip()) > 100:\n markdown_content += chapter_md\n markdown_content += \"\\n\\n---\\n\\n\"\n\n output_path = Path(str(output_path).replace('.txt', '.md'))\n output_path.write_text(markdown_content, encoding=\"utf-8\")\n return output_path\n\n\ndef convert_epub_to_markdown(epub_path: Path, output_path: Path, max_words: int = 350000) -> list[Path]:\n \"\"\"Convert EPUB to Markdown, splitting when over max_words.\"\"\"\n markdown_path = epub_to_markdown(epub_path, output_path)\n word_count = count_words(markdown_path.read_text(encoding=\"utf-8\"))\n if word_count > max_words:\n return split_markdown_file(markdown_path, max_words=max_words)\n return [markdown_path]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6193,"content_sha256":"a107329e67446905eefb0fcb8aecebc40fc12be27e0954c44f551c83cc4e29a7"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"NotebookLM Quick Commands","type":"text"}]},{"type":"paragraph","content":[{"text":"Query Google NotebookLM for source-grounded, citation-backed answers.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Environment","type":"text"}]},{"type":"paragraph","content":[{"text":"All dependencies and authentication are handled automatically by ","type":"text"},{"text":"run.py","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"First run creates ","type":"text"},{"text":".venv","type":"text","marks":[{"type":"code_inline"}]},{"text":" and installs Python/Node.js dependencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If Google auth is missing or expired, a browser window opens automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No manual pre-flight steps required","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Usage","type":"text"}]},{"type":"paragraph","content":[{"text":"/nblm \u003ccommand> [args]","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Commands","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Notebook Management","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"login","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Authenticate with Google","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Show auth and library status","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"accounts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List all Google accounts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"accounts add","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add a new Google account","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"accounts switch \u003cid>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Switch active account (by index or email)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"accounts remove \u003cid>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Remove an account","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"local","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List notebooks in local library","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"remote","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List all notebooks from NotebookLM API","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create \u003cname>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create a new notebook","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"delete [--id ID]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delete a notebook","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rename \u003cname> [--id ID]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rename a notebook","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"summary [--id ID]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get AI-generated summary","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"describe [--id ID]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get description and suggested topics","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add \u003curl-or-id>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add notebook to local library (auto-detects URL vs notebook ID)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"activate \u003cid>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set active notebook","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Source Management","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sources [--id ID]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List sources in notebook","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload \u003cfile>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Upload a single file","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload \u003cfolder>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sync a folder of files to NotebookLM","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload-zlib \u003curl>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download from Z-Library and upload","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload-url \u003curl>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add URL as source","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload-youtube \u003curl>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add YouTube video as source","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload-text \u003ctitle> [--content TEXT]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add text as source","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-text \u003csource-id>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get full indexed text","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-guide \u003csource-id>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get AI summary and keywords","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-rename \u003csource-id> \u003cname>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rename a source","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-refresh \u003csource-id>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Re-fetch URL content","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source-delete \u003csource-id>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delete a source","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Upload options:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--use-active","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Upload to the currently active notebook","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--create-new","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Create a new notebook named after the file/folder","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--notebook-id \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Upload to a specific notebook","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Show sync plan without executing (folder sync)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--rebuild","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Force rebuild tracking file (folder sync)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Important:","type":"text","marks":[{"type":"strong"}]},{"text":" When user runs upload without specifying a target, ASK them first:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"\"Would you like to upload to the active notebook, or create a new notebook?\" Then pass the appropriate flag (","type":"text"},{"text":"--use-active","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"--create-new","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Chat & Audio/Media","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ask \u003cquestion>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Query NotebookLM","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"podcast [--instructions TEXT]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate audio podcast","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"podcast-status \u003ctask-id>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check podcast generation status","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"podcast-download [output-path]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download latest podcast","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"briefing [--instructions TEXT]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate brief audio summary","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"debate [--instructions TEXT]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate debate-style audio","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"slides [--instructions TEXT]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate slide deck","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"slides-download [output-path]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download slide deck as PDF","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"infographic [--instructions TEXT]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate infographic","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"infographic-download [output-path]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download infographic","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"media-list [--type TYPE]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List generated media (audio/video/slides/infographic)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"media-delete \u003cid>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delete a generated media item","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command Routing","type":"text"}]},{"type":"paragraph","content":[{"text":"Based on ","type":"text"},{"text":"$ARGUMENTS","type":"text","marks":[{"type":"code_inline"}]},{"text":", execute the appropriate command:","type":"text"}]},{"type":"paragraph","content":[{"type":"math_inline","content":[{"text":"IF(","type":"text"}]},{"text":"ARGUMENTS, Parse the command from: \"$ARGUMENTS\"","type":"text"}]},{"type":"paragraph","content":[{"text":"login","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py auth_manager.py setup --service google","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"accounts","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py auth_manager.py accounts list","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"accounts add","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py auth_manager.py accounts add","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"accounts switch \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py auth_manager.py accounts switch \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"accounts remove \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py auth_manager.py accounts remove \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"status","type":"text","marks":[{"type":"strong"}]},{"text":" → Run both:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"python scripts/run.py auth_manager.py status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"python scripts/run.py notebook_manager.py list","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"local","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py notebook_manager.py list","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"remote","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py notebooks","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"create \u003cname>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py create \"\u003cname>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"delete [--id ID]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py delete \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"rename \u003cname> [--id ID]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py rename \"\u003cname>\" \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"summary [--id ID]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py summary \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"describe [--id ID]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py describe \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"add \u003curl-or-id>","type":"text","marks":[{"type":"strong"}]},{"text":" → Smart add workflow (auto-detects URL vs notebook ID)","type":"text"}]},{"type":"paragraph","content":[{"text":"activate \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py notebook_manager.py activate --id \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"sources [--id ID]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py sources \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"upload \u003cfile>","type":"text","marks":[{"type":"strong"}]},{"text":" → First ASK user: \"Upload to active notebook or create new?\" Then: - Active: ","type":"text"},{"text":"python scripts/run.py source_manager.py add --file \"\u003cfile>\" --use-active","type":"text","marks":[{"type":"code_inline"}]},{"text":" - New: ","type":"text"},{"text":"python scripts/run.py source_manager.py add --file \"\u003cfile>\" --create-new","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"upload \u003cfolder>","type":"text","marks":[{"type":"strong"}]},{"text":" → Sync a folder: - First ASK user: \"Sync to active notebook, create new, or specify notebook?\" - Active: ","type":"text"},{"text":"python scripts/run.py source_manager.py sync \"\u003cfolder>\" --use-active","type":"text","marks":[{"type":"code_inline"}]},{"text":" - New: ","type":"text"},{"text":"python scripts/run.py source_manager.py sync \"\u003cfolder>\" --create-new","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Specific: ","type":"text"},{"text":"python scripts/run.py source_manager.py sync \"\u003cfolder>\" --notebook-id ID","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Dry-run: ","type":"text"},{"text":"python scripts/run.py source_manager.py sync \"\u003cfolder>\" --dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Rebuild: ","type":"text"},{"text":"python scripts/run.py source_manager.py sync \"\u003cfolder>\" --rebuild","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"upload-zlib \u003curl>","type":"text","marks":[{"type":"strong"}]},{"text":" → First ASK user: \"Upload to active notebook or create new?\" Then: - Active: ","type":"text"},{"text":"python scripts/run.py source_manager.py add --url \"\u003curl>\" --use-active","type":"text","marks":[{"type":"code_inline"}]},{"text":" - New: ","type":"text"},{"text":"python scripts/run.py source_manager.py add --url \"\u003curl>\" --create-new","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"upload-url \u003curl>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py upload-url \"\u003curl>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"upload-youtube \u003curl>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py upload-youtube \"\u003curl>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"upload-text \u003ctitle>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py upload-text \"\u003ctitle>\" \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"source-text \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py source-text \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"source-guide \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py source-guide \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"source-rename \u003cid> \u003cname>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py source-rename \"\u003cid>\" \"\u003cname>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"source-refresh \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py source-refresh \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"source-delete \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py source-delete \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"ask \u003cquestion>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py nblm_cli.py ask \"\u003cquestion>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"podcast","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py generate --format DEEP_DIVE \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"podcast-status \u003ctask-id>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py status --task-id \"\u003ctask-id>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"podcast-download [output-path]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py download \"\u003coutput-path>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"briefing","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py generate --format BRIEF \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"debate","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py generate --format DEBATE \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"slides","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py generate-slides \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"slides-download [output-path]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py download \"\u003coutput-path>\" --type slide-deck","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"infographic","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py generate-infographic \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"infographic-download [output-path]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py download \"\u003coutput-path>\" --type infographic","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"media-list [--type TYPE]","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py list \u003cargs>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"media-delete \u003cid>","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"python scripts/run.py artifact_manager.py delete \"\u003cid>\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"If command not recognized, show usage help.,","type":"text"}]},{"type":"paragraph","content":[{"text":"Show available commands with ","type":"text"},{"text":"/nblm","type":"text","marks":[{"type":"code_inline"}]},{"text":" (no arguments) )","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Podcast Options","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/nblm podcast --length DEFAULT --wait --output ./podcast.mp3\n/nblm podcast --instructions \"Focus on the key findings\"\n/nblm briefing --wait --output ./summary.mp3\n/nblm debate --instructions \"Compare the two approaches\"","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Option","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Values","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--length","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SHORT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"DEFAULT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"LONG","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--instructions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom instructions for the content","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--wait","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wait for generation to complete","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download path (requires ","type":"text"},{"text":"--wait","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Slide Deck Options","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/nblm slides --format DETAILED_DECK --wait --output ./presentation.pdf\n/nblm slides --instructions \"Focus on key diagrams\" --format PRESENTER_SLIDES","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Option","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Values","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--format","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DETAILED_DECK","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PRESENTER_SLIDES","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--length","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SHORT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"DEFAULT","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--instructions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom instructions for the content","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--wait","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wait for generation to complete","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download path (requires ","type":"text"},{"text":"--wait","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Infographic Options","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/nblm infographic --orientation LANDSCAPE --wait --output ./visual.png\n/nblm infographic --instructions \"Highlight comparison\" --detail-level DETAILED","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Option","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Values","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--orientation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LANDSCAPE","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PORTRAIT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"SQUARE","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--detail-level","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CONCISE","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"STANDARD","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"DETAILED","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--instructions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom instructions for the content","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--wait","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wait for generation to complete","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download path (requires ","type":"text"},{"text":"--wait","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Media Generation","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/nblm podcast","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deep-dive audio discussion","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MP3","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/nblm briefing","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Brief audio summary","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MP3","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/nblm debate","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Debate-style audio","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MP3","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/nblm slides","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Slide deck presentation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PDF","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/nblm infographic","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Visual infographic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PNG","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Examples","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/nblm podcast --wait --output ./deep-dive.mp3\n/nblm briefing --instructions \"Focus on chapter 3\" --wait\n/nblm debate --length LONG --wait --output ./debate.mp3\n/nblm slides --instructions \"Include key diagrams\" --format DETAILED_DECK --wait --output ./presentation.pdf\n/nblm infographic --orientation LANDSCAPE --detail-level DETAILED --wait --output ./summary.png","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Download & Manage","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/nblm podcast-download ./my-podcast.mp3\n/nblm slides-download ./presentation.pdf\n/nblm infographic-download ./visual.png\n/nblm media-list # List all generated media\n/nblm media-list --type audio # List only audio\n/nblm media-delete \u003cid> # Delete a media item","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":1},"content":[{"text":"Extended Documentation","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use This Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"Trigger when user:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mentions NotebookLM explicitly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Shares NotebookLM URL (","type":"text"},{"text":"https://notebooklm.google.com/notebook/...","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Asks to query their notebooks/documentation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wants to add documentation to NotebookLM library","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Uses phrases like \"ask my NotebookLM\", \"check my docs\", \"query my notebook\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"⚠️ CRITICAL: Add Command - Smart Discovery","type":"text"}]},{"type":"paragraph","content":[{"text":"The add command now ","type":"text"},{"text":"automatically discovers metadata","type":"text","marks":[{"type":"strong"}]},{"text":" from the notebook:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Smart Add (auto-discovers name, description, topics)\npython scripts/run.py notebook_manager.py add \u003cnotebook-id-or-url>\n\n# With optional overrides\npython scripts/run.py notebook_manager.py add \u003cid> --name \"Custom Name\" --topics \"custom,topics\"","type":"text"}]},{"type":"paragraph","content":[{"text":"What Smart Add does:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fetches notebook title from NotebookLM API","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Queries the notebook content to generate description and topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adds to local library with discovered metadata","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Supported input formats:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Notebook ID: ","type":"text"},{"text":"5fd9f36b-8000-401d-a7a0-7aa3f7832644","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full URL: ","type":"text"},{"text":"https://notebooklm.google.com/notebook/5fd9f36b-8000-401d-a7a0-7aa3f7832644","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"NEVER manually specify ","type":"text"},{"text":"--name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--description","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"--topics","type":"text","marks":[{"type":"code_inline"}]},{"text":" unless the user explicitly provides them.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Critical: Always Use run.py Wrapper","type":"text"}]},{"type":"paragraph","content":[{"text":"NEVER call scripts directly. ALWAYS use ","type":"text","marks":[{"type":"strong"}]},{"text":"python scripts/run.py [script]","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":":","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# ✅ CORRECT - Always use run.py:\npython scripts/run.py auth_manager.py status\npython scripts/run.py notebook_manager.py list\npython scripts/run.py ask_question.py --question \"...\"\n\n# ❌ WRONG - Never call directly:\npython scripts/auth_manager.py status # Fails without venv!","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"run.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" wrapper automatically:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creates ","type":"text"},{"text":".venv","type":"text","marks":[{"type":"code_inline"}]},{"text":" if needed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Installs all dependencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Activates environment","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Executes script properly","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Check Authentication Status","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py auth_manager.py status","type":"text"}]},{"type":"paragraph","content":[{"text":"If not authenticated, proceed to setup.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Authenticate (One-Time Setup)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Browser MUST be visible for manual Google login\npython scripts/run.py auth_manager.py setup","type":"text"}]},{"type":"paragraph","content":[{"text":"Important:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser is VISIBLE for authentication","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser window opens automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User must manually log in to Google","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tell user: \"A browser window will open for Google login\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Manage Notebook Library","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List all notebooks\npython scripts/run.py notebook_manager.py list\n\n# BEFORE ADDING: Ask user for metadata if unknown!\n# \"What does this notebook contain?\"\n# \"What topics should I tag it with?\"\n\n# Add notebook to library (ALL parameters are REQUIRED!)\npython scripts/run.py notebook_manager.py add \\\n --url \"https://notebooklm.google.com/notebook/...\" \\\n --name \"Descriptive Name\" \\\n --description \"What this notebook contains\" \\ # REQUIRED - ASK USER IF UNKNOWN!\n --topics \"topic1,topic2,topic3\" # REQUIRED - ASK USER IF UNKNOWN!\n\n# Search notebooks by topic\npython scripts/run.py notebook_manager.py search --query \"keyword\"\n\n# Set active notebook\npython scripts/run.py notebook_manager.py activate --id notebook-id\n\n# Remove notebook\npython scripts/run.py notebook_manager.py remove --id notebook-id","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Quick Workflow","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check library: ","type":"text"},{"text":"python scripts/run.py notebook_manager.py list","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ask question: ","type":"text"},{"text":"python scripts/run.py ask_question.py --question \"...\" --notebook-id ID","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Ask Questions","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Basic query (uses active notebook if set)\npython scripts/run.py ask_question.py --question \"Your question here\"\n\n# Query specific notebook\npython scripts/run.py ask_question.py --question \"...\" --notebook-id notebook-id\n\n# Query with notebook URL directly\npython scripts/run.py ask_question.py --question \"...\" --notebook-url \"https://...\"\n\n# Show browser for debugging\npython scripts/run.py ask_question.py --question \"...\" --show-browser","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Follow-Up Mechanism (CRITICAL)","type":"text"}]},{"type":"paragraph","content":[{"text":"Every NotebookLM answer ends with: ","type":"text"},{"text":"\"EXTREMELY IMPORTANT: Is that ALL you need to know?\"","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"Required Claude Behavior:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"STOP","type":"text","marks":[{"type":"strong"}]},{"text":" - Do not immediately respond to user","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ANALYZE","type":"text","marks":[{"type":"strong"}]},{"text":" - Compare answer to user's original request","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"IDENTIFY GAPS","type":"text","marks":[{"type":"strong"}]},{"text":" - Determine if more information needed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ASK FOLLOW-UP","type":"text","marks":[{"type":"strong"}]},{"text":" - If gaps exist, immediately ask:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py ask_question.py --question \"Follow-up with context...\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REPEAT","type":"text","marks":[{"type":"strong"}]},{"text":" - Continue until information is complete","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SYNTHESIZE","type":"text","marks":[{"type":"strong"}]},{"text":" - Combine all answers before responding to user","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Z-Library Integration","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Triggers","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User provides Z-Library URL (zlib.li, z-lib.org, zh.zlib.li)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User says \"download this book to NotebookLM\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User says \"add this book from Z-Library\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Setup (One-Time)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Authenticate with Z-Library\npython scripts/run.py auth_manager.py setup --service zlibrary","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Add book from Z-Library\npython scripts/run.py source_manager.py add --url \"https://zh.zlib.li/book/...\"\n\n# Check Z-Library auth status\npython scripts/run.py auth_manager.py status --service zlibrary","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Script Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Authentication Management (","type":"text"},{"text":"auth_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py auth_manager.py setup # Default: Google\npython scripts/run.py auth_manager.py setup --service google\npython scripts/run.py auth_manager.py setup --service zlibrary\npython scripts/run.py auth_manager.py status # Show all services\npython scripts/run.py auth_manager.py status --service zlibrary\npython scripts/run.py auth_manager.py clear --service zlibrary # Clear auth\n\n# Multi-Account Management (Google)\npython scripts/run.py auth_manager.py accounts list # List all accounts\npython scripts/run.py auth_manager.py accounts add # Add new account\npython scripts/run.py auth_manager.py accounts switch 1 # Switch by index\npython scripts/run.py auth_manager.py accounts switch [email protected] # Switch by email\npython scripts/run.py auth_manager.py accounts remove 2 # Remove account","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Notebook Management (","type":"text"},{"text":"notebook_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS\n# OR use notebook ID directly:\npython scripts/run.py notebook_manager.py add --notebook-id ID --name NAME --description DESC --topics TOPICS\npython scripts/run.py notebook_manager.py list\npython scripts/run.py notebook_manager.py search --query QUERY\npython scripts/run.py notebook_manager.py activate --id ID\npython scripts/run.py notebook_manager.py remove --id ID\npython scripts/run.py notebook_manager.py stats","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Question Interface (","type":"text"},{"text":"ask_question.py","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py ask_question.py --question \"...\" [--notebook-id ID] [--notebook-url URL] [--show-browser]","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Source Manager (","type":"text"},{"text":"source_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Upload to active notebook\npython scripts/run.py source_manager.py add --file \"/path/to/book.pdf\" --use-active\n\n# Create new notebook for upload\npython scripts/run.py source_manager.py add --file \"/path/to/book.pdf\" --create-new\n\n# Upload to specific notebook\npython scripts/run.py source_manager.py add --file \"/path/to/book.pdf\" --notebook-id NOTEBOOK_ID\n\n# Z-Library download and upload\npython scripts/run.py source_manager.py add --url \"https://zh.zlib.li/book/...\" --use-active\npython scripts/run.py source_manager.py add --url \"https://zh.zlib.li/book/...\" --create-new\n\n# Sync a folder (new!)\npython scripts/run.py source_manager.py sync \"/path/to/docs\" --use-active\npython scripts/run.py source_manager.py sync \"/path/to/docs\" --create-new\npython scripts/run.py source_manager.py sync \"/path/to/docs\" --notebook-id NOTEBOOK_ID\n\n# Sync options (new!)\npython scripts/run.py source_manager.py sync \"/path/to/docs\" --dry-run # Preview only\npython scripts/run.py source_manager.py sync \"/path/to/docs\" --rebuild # Force re-hash all files","type":"text"}]},{"type":"paragraph","content":[{"text":"Folder Sync:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scans folder for supported types: PDF, TXT, MD, DOCX, HTML, EPUB","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tracks sync state internally (no per-folder tracking file to manage)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sync strategy: add new, update modified (delete + re-upload), skip unchanged","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-account aware (tracks which Google account was used) ","type":"text"},{"text":"Note:","type":"text","marks":[{"type":"strong"}]},{"text":" One of ","type":"text"},{"text":"--use-active","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--create-new","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"--notebook-id","type":"text","marks":[{"type":"code_inline"}]},{"text":" is REQUIRED. Uploads wait for NotebookLM processing and print progress as ","type":"text"},{"text":"Ready: N/T","type":"text","marks":[{"type":"code_inline"}]},{"text":". Press Ctrl+C to stop waiting. Local file uploads use browser automation and require Google authentication. If browser automation is unavailable, set ","type":"text"},{"text":"NOTEBOOKLM_UPLOAD_MODE=text","type":"text","marks":[{"type":"code_inline"}]},{"text":" to upload extracted text instead (PDFs require ","type":"text"},{"text":"pypdf","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data Cleanup (","type":"text"},{"text":"cleanup_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py cleanup_manager.py # Preview cleanup\npython scripts/run.py cleanup_manager.py --confirm # Execute cleanup\npython scripts/run.py cleanup_manager.py --preserve-library # Keep notebooks","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Watchdog Status (","type":"text"},{"text":"auth_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/run.py auth_manager.py watchdog-status","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Environment Management","type":"text"}]},{"type":"paragraph","content":[{"text":"The virtual environment is automatically managed:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"First run creates ","type":"text"},{"text":".venv","type":"text","marks":[{"type":"code_inline"}]},{"text":" automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dependencies install automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Node.js dependencies install automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"agent-browser daemon starts on demand and keeps browser state in memory","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"daemon stops after 10 minutes of inactivity (any agent-browser command resets the timer)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"set ","type":"text"},{"text":"AGENT_BROWSER_OWNER_PID","type":"text","marks":[{"type":"code_inline"}]},{"text":" to auto-stop when the agent process exits","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/run.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" sets ","type":"text"},{"text":"AGENT_BROWSER_OWNER_PID","type":"text","marks":[{"type":"code_inline"}]},{"text":" to its parent PID by default","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Everything isolated in skill directory","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Manual setup (only if automatic fails):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python -m venv .venv\nsource .venv/bin/activate # Linux/Mac\npip install -r requirements.txt\nnpm install\nnpm run install-browsers","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Data Storage","type":"text"}]},{"type":"paragraph","content":[{"text":"All data stored in ","type":"text"},{"text":"~/.claude/skills/notebooklm/data/","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"library.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Notebook metadata (with account associations)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth/google/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Multi-account Google auth","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"index.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Account index (active account, list)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cn>-\u003cemail>.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Per-account credentials","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth/zlibrary.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Z-Library auth state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"agent_browser/session_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Current daemon session ID","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"agent_browser/last_activity.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Last activity timestamp for idle shutdown","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"agent_browser/watchdog.pid","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Idle watchdog process ID","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Security:","type":"text","marks":[{"type":"strong"}]},{"text":" Protected by ","type":"text"},{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":", never commit to git.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"Optional ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file in skill directory:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"env"},"content":[{"text":"HEADLESS=false # Browser visibility\nSHOW_BROWSER=false # Default browser display\nSTEALTH_ENABLED=true # Human-like behavior\nTYPING_WPM_MIN=160 # Typing speed\nTYPING_WPM_MAX=240\nDEFAULT_NOTEBOOK_ID= # Default notebook","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Decision Flow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"User mentions NotebookLM\n ↓\nCheck auth → python scripts/run.py auth_manager.py status\n ↓\nIf not authenticated → python scripts/run.py auth_manager.py setup\n ↓\nCheck/Add notebook → python scripts/run.py notebook_manager.py list/add (with --description)\n ↓\nActivate notebook → python scripts/run.py notebook_manager.py activate --id ID\n ↓\nAsk question → python scripts/run.py ask_question.py --question \"...\"\n ↓\nSee \"Is that ALL you need?\" → Ask follow-ups until complete\n ↓\nSynthesize and respond to user","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Troubleshooting","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Problem","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ModuleNotFoundError","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"run.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" wrapper","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Authentication fails","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Browser must be visible for setup! --show-browser","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DAEMON_UNAVAILABLE","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ensure Node.js/npm installed, run ","type":"text"},{"text":"npm install","type":"text","marks":[{"type":"code_inline"}]},{"text":", retry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AUTH_REQUIRED","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"python scripts/run.py auth_manager.py setup","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ELEMENT_NOT_FOUND","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify notebook URL and re-run with fresh page load","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate limit (50/day)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wait or add another Google account with ","type":"text"},{"text":"accounts add","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Browser crashes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"python scripts/run.py cleanup_manager.py --preserve-library","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notebook not found","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check with ","type":"text"},{"text":"notebook_manager.py list","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Best Practices","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always use run.py","type":"text","marks":[{"type":"strong"}]},{"text":" - Handles environment automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check auth first","type":"text","marks":[{"type":"strong"}]},{"text":" - Before any operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Follow-up questions","type":"text","marks":[{"type":"strong"}]},{"text":" - Don't stop at first answer","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser visible for auth","type":"text","marks":[{"type":"strong"}]},{"text":" - Required for manual login","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Include context","type":"text","marks":[{"type":"strong"}]},{"text":" - Each question is independent","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Synthesize answers","type":"text","marks":[{"type":"strong"}]},{"text":" - Combine multiple responses","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Limitations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No session persistence (each question = new browser)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rate limits on free Google accounts (50 queries/day per account; use multiple accounts to increase)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Manual upload required (user must add docs to NotebookLM)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser overhead (few seconds per question)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resources (Skill Structure)","type":"text"}]},{"type":"paragraph","content":[{"text":"Important directories and files:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - All automation scripts (ask_question.py, notebook_manager.py, etc.)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"data/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Local storage for authentication and notebook library","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Extended documentation:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"api_reference.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Detailed API documentation for all scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"troubleshooting.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Common issues and solutions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"usage_patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Best practices and workflow examples","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":".venv/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Isolated Python environment (auto-created on first run)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Protects sensitive data from being committed","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"nblm","author":"@skillopedia","source":{"stars":2012,"repo_name":"openclaw-master-skills","origin_url":"https://github.com/leoyeai/openclaw-master-skills/blob/HEAD/skills/nblm/SKILL.md","repo_owner":"leoyeai","body_sha256":"a69af7d2c2087ebb88a7c92e7f1d538fe7ab2ce3151109327d26863b1010d5bf","cluster_key":"a9c15239227243a02a6dfb2969e69119bccdeb5c2c87853c6aeea13f2d52d7f0","clean_bundle":{"format":"clean-skill-bundle-v1","source":"leoyeai/openclaw-master-skills/skills/nblm/SKILL.md","attachments":[{"id":"d54f3a1b-d40d-51d3-9fc0-25771198d897","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d54f3a1b-d40d-51d3-9fc0-25771198d897/attachment.md","path":"AUTHENTICATION.md","size":2559,"sha256":"38b0d0162c211ff5962c684f94a236a3d0b4dd9ffd2700c511a7f7de9ab9ee97","contentType":"text/markdown; charset=utf-8"},{"id":"18e2bb61-1c32-5ca6-a1be-da1919169f64","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/18e2bb61-1c32-5ca6-a1be-da1919169f64/attachment.md","path":"CHANGELOG.md","size":3968,"sha256":"6b4b0c1155bc1203aff1ecc0dc8c3f38a75abd5b663b0261bf0d111ed8b5aae0","contentType":"text/markdown; charset=utf-8"},{"id":"4c3344b1-4aa8-5bec-a9c5-2b0d9cb4d673","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4c3344b1-4aa8-5bec-a9c5-2b0d9cb4d673/attachment.md","path":"README.md","size":12891,"sha256":"f1e2378d6b9f5bdd4636134d981d473af36aa3542eb4a57d4f0f3e661bddfa2d","contentType":"text/markdown; charset=utf-8"},{"id":"5e06c970-554d-5ae4-9ad5-1632055b7abe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5e06c970-554d-5ae4-9ad5-1632055b7abe/attachment.md","path":"README.zh-CN.md","size":12097,"sha256":"71fd3ac53068ddc43dcdd00e05928d3ebc23de18f4f9f637474db7a1951c5a62","contentType":"text/markdown; charset=utf-8"},{"id":"28603e35-289f-5fce-8a6c-67b75cfe617d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28603e35-289f-5fce-8a6c-67b75cfe617d/attachment.json","path":"_meta.json","size":275,"sha256":"7ea4c8e6c4558347d3e3ad5ff085074b8edde5dfd83d1821bbddbc13c4571b86","contentType":"application/json; charset=utf-8"},{"id":"28fd13b4-1419-51b7-9fd4-7e1c4f129376","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28fd13b4-1419-51b7-9fd4-7e1c4f129376/attachment.md","path":"references/api_reference.md","size":9461,"sha256":"859ef08992590ea5b4cb78818d1f605dd46a45e85275aaff602943ead00f7590","contentType":"text/markdown; charset=utf-8"},{"id":"1f2cdc29-8ad1-5051-972c-7903a155cca5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1f2cdc29-8ad1-5051-972c-7903a155cca5/attachment.md","path":"references/troubleshooting.md","size":9277,"sha256":"ab94b2ae8ad0bc417d6c37f81b882c9e8ad8ed4908bb4b3e497bbf36e9d1cb32","contentType":"text/markdown; charset=utf-8"},{"id":"aeb94aad-2f05-5616-bd2c-69bb89ef5a82","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aeb94aad-2f05-5616-bd2c-69bb89ef5a82/attachment.md","path":"references/usage_patterns.md","size":9571,"sha256":"52972e4c3868cd17613cba4afbd4e9d8170bc738d7e4fa19fbacf46c8860dc4e","contentType":"text/markdown; charset=utf-8"},{"id":"d595d73a-882e-514b-a83f-e92cb2c6bc38","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d595d73a-882e-514b-a83f-e92cb2c6bc38/attachment.txt","path":"requirements.txt","size":374,"sha256":"9fa13b526dd6c3d83427d3ad89dad1982daf724437459cebcbb09410d43046c5","contentType":"text/plain; charset=utf-8"},{"id":"adaadca8-f71c-5973-8606-57539ddb88a2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/adaadca8-f71c-5973-8606-57539ddb88a2/attachment.py","path":"scripts/__init__.py","size":2782,"sha256":"87945c9ea7a90de72d6b7b5b7c483b2c7dbe60ee63b6363dc9ea52b9fef4cf83","contentType":"text/x-python; charset=utf-8"},{"id":"dc44e4d4-e13f-5cfa-82d3-6d47540e2f35","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc44e4d4-e13f-5cfa-82d3-6d47540e2f35/attachment.py","path":"scripts/account_manager.py","size":15105,"sha256":"176eec9a0e04d4edf24b4ad0b9991de4ba91352d7b17ce075c4ef398b18598d4","contentType":"text/x-python; charset=utf-8"},{"id":"0f43e2fd-98e0-55c2-9c6c-57c3e5dfdd7e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0f43e2fd-98e0-55c2-9c6c-57c3e5dfdd7e/attachment.py","path":"scripts/agent_browser_client.py","size":22885,"sha256":"825d4ba90cf0096d288c90e3ca79793e611e68a47bfc886feb914a9b1385e369","contentType":"text/x-python; charset=utf-8"},{"id":"f9317b1c-88a1-59af-9869-aca92cf33f8d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f9317b1c-88a1-59af-9869-aca92cf33f8d/attachment.py","path":"scripts/artifact_manager.py","size":16484,"sha256":"9003f2f36fe2d3aae5f71da928fee113c935b8d4f03ff87fe507e15d127bc04f","contentType":"text/x-python; charset=utf-8"},{"id":"3e13a3ba-859f-5f9e-9f11-f41a449b8d24","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3e13a3ba-859f-5f9e-9f11-f41a449b8d24/attachment.py","path":"scripts/ask_question.py","size":17194,"sha256":"026ec6349809dbe49969d1e5599f76a6d00554b97168862bbe137d189317ae39","contentType":"text/x-python; charset=utf-8"},{"id":"89ed1965-7a66-5431-9ca8-629fff100157","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/89ed1965-7a66-5431-9ca8-629fff100157/attachment.py","path":"scripts/auth_manager.py","size":42872,"sha256":"96fb6133400cd42e0be03a92b0a912d3e07db190570075b7414b1be9b5ca05ff","contentType":"text/x-python; charset=utf-8"},{"id":"ff23dc8f-feca-5597-8307-fd439e7382c0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ff23dc8f-feca-5597-8307-fd439e7382c0/attachment.py","path":"scripts/cleanup_manager.py","size":9592,"sha256":"cffb21eaa903267ada897c3d75e2682c9f6b2c6a1d64fd354067b9d174457b51","contentType":"text/x-python; charset=utf-8"},{"id":"d55bad2a-90e6-5d4d-ba22-813cfa5a20e0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d55bad2a-90e6-5d4d-ba22-813cfa5a20e0/attachment.py","path":"scripts/config.py","size":2096,"sha256":"7acaa8e3f246c7f07a136869cd170df042454f49532d93bcc7d5ac3996b5d805","contentType":"text/x-python; charset=utf-8"},{"id":"311f4646-6002-5ed7-969f-81c975599591","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/311f4646-6002-5ed7-969f-81c975599591/attachment.py","path":"scripts/daemon_watchdog.py","size":3026,"sha256":"e24aeeb41e8bb5a767844032d1f3a96e99d53c346e9a7a53e48e92a7a5efa128","contentType":"text/x-python; charset=utf-8"},{"id":"c01b0b1d-50b9-5a9b-adc1-a5f13155a887","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c01b0b1d-50b9-5a9b-adc1-a5f13155a887/attachment.py","path":"scripts/init_platform.py","size":12267,"sha256":"e4d02832185fabce091b1f1e60b93c2fffe16eeb66475981e7a277451e12c834","contentType":"text/x-python; charset=utf-8"},{"id":"fb6d7822-5555-56f6-9efa-009dc18a672c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fb6d7822-5555-56f6-9efa-009dc18a672c/attachment.py","path":"scripts/nblm_cli.py","size":13178,"sha256":"2fd51e2c5d568eb6a32032932dce7483d668406d00da220a64033969987a59bb","contentType":"text/x-python; charset=utf-8"},{"id":"8979b33e-3b08-55b3-b518-5a03c2c7e8e3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8979b33e-3b08-55b3-b518-5a03c2c7e8e3/attachment.py","path":"scripts/notebook_manager.py","size":23018,"sha256":"b70b42cb73d61b7b249143b76a4bb54ef4e6d595b5ab8e6ce1dee0fadf373a62","contentType":"text/x-python; charset=utf-8"},{"id":"d4294f58-20c8-590d-9083-7dbfe798effb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4294f58-20c8-590d-9083-7dbfe798effb/attachment.py","path":"scripts/notebooklm_wrapper.py","size":37158,"sha256":"af13dbb4346336cd8a6a65273b3f06c28bcee54f08be369a8d88d5e2e3b74ac9","contentType":"text/x-python; charset=utf-8"},{"id":"9b9bb4e7-e84c-5642-824e-9501ebd5dc41","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9b9bb4e7-e84c-5642-824e-9501ebd5dc41/attachment.py","path":"scripts/patchright_auth.py","size":16031,"sha256":"65e6fdd3609057c5e61abba84305380038f8c57610e68a0bc538554455803745","contentType":"text/x-python; charset=utf-8"},{"id":"58e761e9-5333-50f8-ada3-305d3f98817f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/58e761e9-5333-50f8-ada3-305d3f98817f/attachment.py","path":"scripts/run.py","size":17124,"sha256":"479e1c163757f0fb9db62f4ded6babfe7818d2f2731561ee58a03011c7cd510c","contentType":"text/x-python; charset=utf-8"},{"id":"c927e281-fc80-5e37-b8b7-b225b05cf696","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c927e281-fc80-5e37-b8b7-b225b05cf696/attachment.py","path":"scripts/setup_environment.py","size":7960,"sha256":"0718bc47cb538968b146e366c6f246b3ed7bd3981e0d10c6e8e0035870026fb4","contentType":"text/x-python; charset=utf-8"},{"id":"8ea50804-2af9-5407-bce1-8f1b240dfcda","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8ea50804-2af9-5407-bce1-8f1b240dfcda/attachment.py","path":"scripts/source_manager.py","size":16108,"sha256":"a4ad60c83758674dadf4a141dbb72e06178e6804152ac151f1e339df5046fadc","contentType":"text/x-python; charset=utf-8"},{"id":"ec4b1bd0-8bf6-5aa5-82ea-1c6936c94868","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ec4b1bd0-8bf6-5aa5-82ea-1c6936c94868/attachment.py","path":"scripts/sync_manager.py","size":17469,"sha256":"87aaf0a3b7713131d1f0efcce7e6f0e3d6d2dc9ffd54d3aba2bff9c8e5917fbe","contentType":"text/x-python; charset=utf-8"},{"id":"ce34a6c0-8718-528a-a3c0-fdaef809b6a6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ce34a6c0-8718-528a-a3c0-fdaef809b6a6/attachment.py","path":"scripts/zlibrary/__init__.py","size":25,"sha256":"0971c86d80ddbb8cb81fae0aa413458f7cb7e5710bd2a07b2f7b26aeeff35824","contentType":"text/x-python; charset=utf-8"},{"id":"7d8c994e-11e4-56a8-b19a-249548fda9e4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7d8c994e-11e4-56a8-b19a-249548fda9e4/attachment.py","path":"scripts/zlibrary/downloader.py","size":6433,"sha256":"f22f226d16e5b396560608ae7bf813756c807f92577716f543e93a06fa1eee61","contentType":"text/x-python; charset=utf-8"},{"id":"5955aa6d-1e45-5afb-a4e8-bfea69646d71","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5955aa6d-1e45-5afb-a4e8-bfea69646d71/attachment.py","path":"scripts/zlibrary/epub_converter.py","size":6193,"sha256":"a107329e67446905eefb0fcb8aecebc40fc12be27e0954c44f551c83cc4e29a7","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"e77cd7a1d357d5080b3d0a4441174e7da8bd2f651306f118c0f3944a9cb80379","attachment_count":30,"text_attachments":30,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/nblm/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"browser-automation-scraping","category_label":"Browser"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"browser-automation-scraping","import_tag":"clean-skills-v1","description":"Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced hallucinations through document-only responses."}},"renderedAt":1782979956894}

NotebookLM Quick Commands Query Google NotebookLM for source-grounded, citation-backed answers. Environment All dependencies and authentication are handled automatically by : - First run creates and installs Python/Node.js dependencies - If Google auth is missing or expired, a browser window opens automatically - No manual pre-flight steps required --- Usage Commands Notebook Management | Command | Description | |---------|-------------| | | Authenticate with Google | | | Show auth and library status | | | List all Google accounts | | | Add a new Google account | | | Switch active account (by…