Aller au contenu
Hermès Skills
← Retour au catalogue

notebooklm

Query Google NotebookLM notebooks directly from Hermes Agent via Camofox browser automation. Uses the existing Google session from Genspark for zero-additional-auth access. Pure SKILL.md + lightweight Python library helper — no Patchright, no Playwright, no .venv.

Quand l'utiliser (Trigger)

Déclenchement standard selon le contexte de l'écosystème Hermès.

Mode d'emploi (Usage)

Mode d'emploi standard via l'agent Hermès.

NotebookLM Skill — Hermes Agent

Query Google NotebookLM notebooks via Camofox, reusing the existing Google session from Genspark. Each query opens the notebook, asks the question, polls for the Gemini answer (source-grounded from your docs), and returns it.

When to Use

  • User shares a NotebookLM URL (https://notebooklm.google.com/notebook/...)
  • User asks to query their notebooks/documentation
  • User mentions NotebookLM explicitly
  • You need source-grounded answers from specific uploaded documents
  • “query my notebook”, “check my docs”, “ask NotebookLM”

Architecture

~/.hermes/skills/notebooklm/
├── SKILL.md                    # Agent instructions (this file)
├── library.py                  # Pure-Python JSON CRUD, no browser deps
├── scripts/
│   └── library.py              # Notebook metadata management (stdlib only)
└── data/
    └── library.json            # Notebook metadata, created on first add

No .venv, no requirements.txt, no Patchright, no Playwright. The browser automation uses Camofox via Hermes’ native browser_* tools. The Python helper (library.py) uses stdlib only (argparse, json, pathlib).

Why Not the Upstream Scripts

The original skill (github.com/PleasePrompto/notebooklm-skill) uses Playwright via patchright for browser automation. Hermes Agent already has:

  • Camofox — Firefox with C++ fingerprint spoofing (stronger anti-detection)
  • managed_persistence: true — session persists across restarts
  • browser_ tools* — navigate, snapshot, click, type, press, console

Running Patchright alongside Camofox means two stealth browsers on one VPS, unable to share state, with separate auth flows. This skill solves everything with a single SKILL.md + a tiny Python library helper.

Auth — Session Persistence Reality

Google OAuth session cookies do NOT persist across Camofox restarts. This is a Google security feature, not a Camofox bug. Google explicitly marks OAuth tokens as non-persistable.

What works:

  • Within a single Camofox process: ✅ session stays active
  • After soft Camofox restart (same temp profile survives): ✅ sometimes works
  • After gateway restart / VPS reboot: ❌ Google login required again
  • Cookie backup/restore (see references/session-persistence.md): ✅ reduces 2FA to first restart only

To verify session: browser_navigate("https://notebooklm.google.com/") → if you see the notebook list (not accounts.google.com), auth is valid.

First-Time Auth Flow (recovery after restart)

Use the procedure in references/session-persistence.md:

  1. Navigate to notebooklm.google.com
  2. Fill email/password via browser_* tools
  3. User approves 2FA push notification on phone (15s effort)
  4. Checkbox “Don’t ask again” is pre-checked ✅
  5. Run scripts/backup-cookies.sh to save cookies for next restart
  6. Future restarts only need password re-entry (no 2FA)

Notebook List Snapshot (when authenticated)

The NotebookLM homepage shows a rich table with columns:

  • Titre — notebook name (may include emoji prefixes like ⚖️🔬🎮)
  • Sources — count of source documents
  • Création — creation date
  • Visibilité — public/private indicator
  • Rôle — Owner, Reader, etc.
  • Actions — dropdown menu (button ref like @e14, @e15, etc.)

The snapshot is paginated. You may see a truncated line [... X more lines truncated...] at the end. Use browser_snapshot() again or browser_scroll() to reveal more rows.

“Vous avez atteint le nombre maximal de notebooks” — this appears when the free tier limit is hit. It does NOT block querying existing notebooks.

To find a specific notebook: Look for its name in the snapshot rows under heading "Notebooks récents" [level=2] table. Each row has a button "Menu d'actions du projet" (ref varies, e.g. @e14-@e26).

To open a notebook: Click on the title cell or use:

browser_navigate("https://notebooklm.google.com/notebook/<UUID>")

Pre-Workflow: Verify Session (each session)

browser_navigate("https://notebooklm.google.com/")
browser_snapshot()

If redirected to accounts.google.com → use recovery in references/session-persistence.md.

Workflow

Step 1: Check Library

terminal(command="python3 ~/.hermes/skills/notebooklm/library.py list")

If the library is empty, ask the user for notebook details or accept a URL directly.

Step 2: Resolve Notebook URL

Two ways to get the URL:

  • From library: terminal(command="python3 ~/.hermes/skills/notebooklm/library.py get-url --id <id>")
  • From user directly: use the —url they provided

Step 3: Navigate to Notebook

browser_navigate("https://notebooklm.google.com/notebook/<UUID>")

Wait for the page to load. If redirected to accounts.google.com, surface the auth issue and run session recovery procedure.

Step 4: Detect and Interact with Query Input

⚠️ CRITICAL: NotebookLM’s chat textarea is a React-controlled element that DOES NOT appear in the accessibility tree. Never look for it in the browser_snapshot() output — always use browser_console JavaScript.

There are always TWO <textarea> elements in the DOM:

  • textarea[0]: Source search input (placeholder: “Rechercher de nouvelles sources sur le Web”) — in the left panel
  • textarea[1]: Chat query input (placeholder: “Posez une question ou créez quelque chose”) — THE ONE YOU NEED

Verifying which is which:

document.querySelectorAll('textarea')[1].placeholder
// → "Posez une question ou créez quelque chose"

Filling the input — MUST use execCommand for React to detect the change:

The native value setter (Object.getOwnPropertyDescriptor(...).set) and .textContent assignment both FAIL to trigger React’s onChange handler. Only execCommand('insertText') works reliably:

const ta = document.querySelectorAll('textarea')[1];
ta.focus();
document.execCommand('insertText', false, "Your question here");

This is the only method that activates the submit button (disabled → false).

Submit — click the form’s submit button (Enter does NOT work):

NotebookLM’s React handler ignores keyboard events entirely. You must find and click the submit button programmatically:

const ta = document.querySelectorAll('textarea')[1];
const form = ta.closest('form');
const submitBtn = form.querySelector('button[type="submit"]');
if (!submitBtn.disabled) {
  submitBtn.click();
}

Combined command (fill + submit in one shot):

(function(){
  const ta = document.querySelectorAll('textarea')[1];
  ta.focus();
  document.execCommand('insertText', false, "Your question here");
  const btn = ta.closest('form').querySelector('button[type="submit"]');
  if (btn.disabled) { return "button still disabled — execCommand didn't trigger React"; }
  btn.click();
  return "submitted";
})()

Verify the question was submitted:

document.body.innerText.includes("Your question here")

Step 5: Wait for Response — PATIENCE IS MANDATORY

NotebookLM generates answers via Gemini, scanning ALL uploaded sources. With 30+ large legal documents, the full cycle takes 45-120 seconds.

🚨 CRITICAL BEHAVIOR RULE — do NOT poll frantically:

  • Poll ONCE every 15 seconds, maximum.
  • Each poll is a full page snapshot that consumes tokens.
  • Between polls, truly wait — use terminal(command="sleep 15").
  • The user will tell you if they want an update. Do not send progress messages.
  • If the user says “be patient like a human” — listen immediately.
  • A human would glance at the screen, note “still loading”, and go back to work. Do that.

Status indicators during generation (progression order):

StatusPhase
“Retrieving details…”Looking up
“Parsing the data…”Processing
“Checking your uploads…”Scanning docs
“Assessing relevance…”Matching sources
“Consulting your sources…”Cross-referencing
“Reading through pages…”Long docs
“Reading your inputs…”Thorough review
“Looking at sources…”Active research
“Réflexion…”Final generation

Polling approach:

  1. Execute combined fill+submit command
  2. terminal(command="sleep 15") — actually wait
  3. browser_snapshot() — check for answer text or status indicator
  4. If still generating → terminal(command="sleep 15") again
  5. Max timeout: 120 seconds (8 polls)

Indicators of complete response:

  • Full text visible in the response area
  • Source citation buttons appear (e.g. “1: 02_Charte_canadienne_droits_libertes.txt”)
  • Input field becomes editable again (placeholder: “Posez une question…”)
  • Status indicator text (“Réflexion…”, “Reading…”) is GONE

Extracting the answer:

document.body.innerText

The answer text is between the question heading and the “Studio” section. Include source citations — they link back to the uploaded documents.

Step 6: Return Result

Format the answer for the user. Include sources/citations if present.

If the answer is “I could not find…” or NotebookLM couldn’t answer, tell the user honestly rather than fabricating.

IMPORTANT: After returning the answer, ask the user if they need more.

library.py — Helper Script

A ~80-line Python script with stdlib-only dependencies. It manages a JSON library of notebooks with metadata. Invoke via terminal.

# List all notebooks
python3 ~/.hermes/skills/notebooklm/library.py list

# Add a notebook
python3 ~/.hermes/skills/notebooklm/library.py add \
  --url "https://notebooklm.google.com/notebook/..." \
  --name "My Docs" \
  --description "Project documentation" \
  --topics "docs,api,guide"

# Get URL for a notebook (for navigation)
python3 ~/.hermes/skills/notebooklm/library.py get-url --id <id>

# Search notebooks
python3 ~/.hermes/skills/notebooklm/library.py search --query "keyword"

# Set active notebook
python3 ~/.hermes/skills/notebooklm/library.py activate --id <id>

# Remove notebook
python3 ~/.hermes/skills/notebooklm/library.py remove --id <id>

Always use get-url to resolve the URL before navigation. The library stores the full URL — no need for the user to re-copy it.

Follow-up Questions

Each question opens a new browser session. To ask a follow-up:

# Step 4 combined command with context from previous answer
browser_console(expression="(function(){...fill+submit...})()")
# Wait patiently with sleep 15
# Extract with document.body.innerText

IMPORTANT: Each question must be self-contained — include enough context from prior answers so the new response doesn’t repeat work. NotebookLM’s session history is not preserved across browser navigations.

SAVE the notebook URL after the first interaction so follow-ups don’t require re-navigating from the homepage.

Pitfalls

⚠️ Session lost after gateway restart (known limitation)

managed_persistence: true does NOT persist Google OAuth cookies across gateway restarts. See references/session-persistence.md.

Quick recovery:

  1. browser_navigate("https://notebooklm.google.com/")
  2. Fill email/password
  3. Tell user “Approve the 2FA notification on your phone”
  4. After login, run bash ~/scripts/backup-cookies.sh

⚠️ Chat textarea not in accessibility tree

The React textarea NEVER appears in browser_snapshot(). Always detect and fill it via browser_console JavaScript.

⚠️ Enter key and nativeSetter both fail

  • browser_press(key="Enter") — ignored by React
  • Object.getOwnPropertyDescriptor(...).set — does NOT trigger onChange
  • Only document.execCommand('insertText', false, text) works

⚠️ Long generation time — BE PATIENT

31 large sources = 45-120s generation. Poll every 15s max. Never poll every 3 seconds — this is annoying, burns tokens, and looks impatient. Use terminal(command="sleep 15") between polls.

⚠️ Submit button may remain disabled

If execCommand didn’t trigger React, the submit button stays disabled. Verify with: ta.closest('form').querySelector('button[type="submit"]').disabled If still disabled, try focusing the textarea first before execCommand.

⚠️ Notebook doesn’t load (empty page)

Some NotebookLM notebooks require certain Google permissions.

⚠️ Rate limits

Google accounts have daily rate limits for NotebookLM queries (~50/day on free tier).

⚠️ Cookies backup required after login

After successful login, ALWAYS run bash ~/scripts/backup-cookies.sh.

Best Practices

  1. Use the library — save notebook URLs for reuse
  2. Verify session each time — navigate to notebooklm.google.com first
  3. Backup cookies after loginbash ~/scripts/backup-cookies.sh
  4. Detect textarea via JS, not snapshot — always use browser_console
  5. BE PATIENT — 45-120s for large notebooks, poll every 15s max, use sleep
  6. Use execCommand, not nativeSetter — only execCommand triggers React
  7. Submit via form button, not Enter — React ignores keyboard events
  8. Don’t send progress messages — wait silently, deliver the answer
  9. Self-contained questions — include context from prior responses
  10. Check for “Sources” — NotebookLM cites source documents, include them

References

  • references/session-persistence.md — Google OAuth session persistence
  • references/notebooklm-selectors.md — DOM selectors and interaction patterns
  • scripts/backup-cookies.sh — Backup Google cookies to ~/.camofox/cookies/
  • scripts/restore-cookies.sh — Restore cookies after Camofox restart