notebooklm-node
v0.1.1
Published
Unofficial Node.js / TypeScript client and CLI for Google NotebookLM (port of notebooklm-py). Auto-refreshing auth, RPC batchexecute transport, and a commander-based CLI.
Maintainers
Readme
notebooklm-node
Unofficial Node.js / TypeScript port of notebooklm-py — a programmatic client and CLI for Google NotebookLM, built on the same undocumented batchexecute RPC protocol.
Phase 1 release. Foundation is complete: RPC transport, auth with auto-refresh, profile system,
notebooksCRUD, andchat.ask. The other domain namespaces (sources,artifacts,notes,research,sharing,settings) are exposed as typed stubs that throwNotImplementedErroron call. They're already implemented in upstreamnotebooklm-pyand are being ported in subsequent releases.
Install
As a CLI (recommended for shell use):
# global install
npm install -g notebooklm-node
# or one-off via npx (no install)
npx notebooklm-node --helpAs an SDK in your project:
npm add notebooklm-node # or
pnpm add notebooklm-node # or
yarn add notebooklm-nodeTo use notebooklm login (interactive Chromium auth flow) you also need playwright as a peer dep:
npm add -D playwright && npx playwright install chromiumCI environments without a browser can skip playwright entirely and supply auth via the NOTEBOOKLM_AUTH_JSON env var.
Requires Node ≥ 20.
CLI
After npm install -g notebooklm-node, the notebooklm binary is on your $PATH:
notebooklm [-p|--profile <name>] [--storage <path>] [--json] <command> [args]Global flags (apply to every command):
| Flag | Description |
| ----------------------- | ----------------------------------------------------------------------------- |
| -p, --profile <name> | Auth profile name. Falls back to NOTEBOOKLM_PROFILE, then default. |
| --storage <path> | Path to a storage_state.json file (overrides profile resolution). |
| --json | Emit machine-readable JSON instead of human-readable text. |
| -V, --version | Print version. |
| -h, --help | Per-command help (notebooklm <cmd> --help). |
notebooklm login
Open Chromium, sign you in, and write storage_state.json to the active profile dir.
notebooklm login
notebooklm login --headless # rare: only useful in tests
notebooklm login --timeout 600000 # raise the 5-minute timeout
notebooklm -p work login # use the "work" profileRequires playwright installed.
notebooklm use <notebookId>
Set the active notebook for subsequent commands. Stored in ~/.notebooklm/profiles/<profile>/context.json.
notebooklm use 9c0e1234-aaaa-bbbb-cccc-111122223333notebooklm status
Show resolved profile, storage path, and the active notebook.
notebooklm status
notebooklm --json status # machine-parseable outputnotebooklm clear
Clear the active notebook context (deletes context.json).
notebooklm clearnotebooklm list
List notebooks belonging to the authenticated user.
notebooklm list
notebooklm --json list # full notebook objects as JSONDefault output is tab-separated <id>\t<title>, one per line.
notebooklm create <title>
Create a notebook.
notebooklm create "Q2 Research Drop"
notebooklm --json create "Q2 Research Drop"notebooklm rename <notebookId> <newTitle>
Rename a notebook.
notebooklm rename 9c0e1234-... "Q2 Research (final)"notebooklm delete <notebookId>
Delete a notebook. Irreversible.
notebooklm delete 9c0e1234-...notebooklm ask <question...>
Ask the active notebook a question (or pass --notebook <id> to override).
# uses the active notebook
notebooklm ask what are the key themes
# explicit notebook
notebooklm ask -n 9c0e1234-... "summarise chapter 3"
# follow-up in an existing conversation
notebooklm ask -c <conversationId> "elaborate on point 2"
# JSON output (answer + conversationId + references[])
notebooklm --json ask "list the citations"--json returns:
{
"answer": "…",
"conversation_id": "…",
"turn_number": 1,
"is_follow_up": false,
"references": [
{ "sourceId": "…", "citationNumber": 1, "citedText": "…", "startChar": 12, "endChar": 87, "chunkId": "…" }
]
}Typical end-to-end session
notebooklm login # one-time per machine
notebooklm list
notebooklm use 9c0e1234-...
notebooklm ask "give me three main takeaways"
notebooklm --json ask "follow up on the second one" \
| jq -r .answerSDK
import { NotebookLMClient } from "notebooklm-node";
const client = await NotebookLMClient.fromStorage(); // ~/.notebooklm/profiles/<profile>/storage_state.json
// or:
const client = await NotebookLMClient.fromEnv(); // $NOTEBOOKLM_AUTH_JSON
await client.connect();
const notebooks = await client.notebooks.list();
const nb = await client.notebooks.create({ title: "Hello" });
const renamed = await client.notebooks.rename(nb.id, "World");
await client.notebooks.delete(nb.id);
const result = await client.chat.ask(nb.id, "what is this about?");
console.log(result.answer, result.references);
await client.close();NotebookLMClient extends EventEmitter and emits "auth:refreshed" when an
auto-refresh fires.
Auto-refresh
Every RPC call goes through a refresh-aware retry layer. If the call fails with HTTP 401/403 or an auth-shaped RPC error, the client will:
- Acquire a single in-flight refresh lock (concurrent callers join the same promise).
- Re-fetch the NotebookLM homepage and extract a fresh
SNlM0e(CSRF) andFdrFJe(session ID). - Update the in-memory cookie + auth state.
- Retry the original RPC once.
If the refresh itself fails (cookies expired), an AuthError is thrown asking
you to run notebooklm login.
Configuration
Env vars (compatible with notebooklm-py):
| Variable | Purpose |
| ----------------------- | ---------------------------------------------------------------------------- |
| NOTEBOOKLM_HOME | Base directory (default ~/.notebooklm) |
| NOTEBOOKLM_PROFILE | Active profile (default default) |
| NOTEBOOKLM_AUTH_JSON | Inline storage state JSON (CI-friendly; no file needed) |
| NOTEBOOKLM_BL | Override the build-label query param sent to the chat endpoint |
| NOTEBOOKLM_DEBUG=1 | Verbose logging (or DEBUG=notebooklm) |
The on-disk layout is identical to notebooklm-py, so you can flip back and
forth between the Python and Node clients on the same machine without
re-authenticating.
What's implemented
| Namespace | Status | Notes |
| ------------------ | ----------- | ------------------------------------------------ |
| client.notebooks | ✅ shipping | list, create, get, rename, delete, getRaw |
| client.chat | ✅ shipping | ask with conversation cache + citation parsing |
| client.sources | ⏳ stub | Available in upstream notebooklm-py |
| client.artifacts | ⏳ stub | Available in upstream notebooklm-py |
| client.notes | ⏳ stub | Available in upstream notebooklm-py |
| client.research | ⏳ stub | Available in upstream notebooklm-py |
| client.sharing | ⏳ stub | Available in upstream notebooklm-py |
| client.settings | ⏳ stub | Available in upstream notebooklm-py |
Stubs throw NotImplementedError at call time but are typed, so SDK consumers get autocomplete today.
Layout
src/
├── client.ts # NotebookLMClient (orchestrator + refresh hook)
├── auth/ # storage_state I/O, SNlM0e/FdrFJe extraction, login
├── rpc/ # batchexecute encoder/decoder + method-id table
├── core/ # HTTP wrapper + refresh-aware ClientCore + errors
├── api/
│ ├── notebooks.ts # NotebooksAPI
│ ├── chat.ts # ChatAPI.ask
│ └── stubs.ts # Phase 2 namespaces
├── cli/ # commander root + login/session/notebooks/ask
└── ...Testing
pnpm test # vitest unit + integration (80 tests)
pnpm test:coverage
pnpm typecheck
pnpm buildThe integration suite stubs globalThis.fetch via a tiny MockFetch helper
(tests/helpers/mock-fetch.ts) — no network is touched during pnpm test.
The auto-refresh integration test in tests/integration/notebooks.test.ts
asserts that:
- A 401 on the first call triggers exactly one homepage refresh.
- The retry carries the new CSRF token (
at=…) andf.sid=…. - Concurrent in-flight requests share a single refresh.
License
MIT. Unofficial: this project is not affiliated with or endorsed by Google. Use at your own risk; the underlying API is undocumented and Google can change it at any time.
