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.
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.
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). |
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.
Set the active notebook for subsequent commands. Stored in ~/.notebooklm/profiles/<profile>/context.json.
notebooklm use 9c0e1234-aaaa-bbbb-cccc-111122223333Show resolved profile, storage path, and the active notebook.
notebooklm status
notebooklm --json status # machine-parseable outputClear the active notebook context (deletes context.json).
notebooklm clearList 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.
Create a notebook.
notebooklm create "Q2 Research Drop"
notebooklm --json create "Q2 Research Drop"Rename a notebook.
notebooklm rename 9c0e1234-... "Q2 Research (final)"Delete a notebook. Irreversible.
notebooklm delete 9c0e1234-...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": "…" }
]
}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 .answerimport { 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.
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.
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.
| 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.
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
└── ...
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.
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.