Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# nlm - NotebookLM CLI Tool 📚
# nlm - NotebookLM CLI Tool 📚

`nlm` is a command-line interface for Google's NotebookLM, allowing you to manage notebooks, sources, and audio overviews from your terminal.

Expand Down
146 changes: 146 additions & 0 deletions docs/WSL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# WSL Setup Guide for nlm

This guide explains how to set up `nlm` (NotebookLM CLI) on Windows Subsystem for Linux (WSL).

## The Problem

Chrome/Chromium on Linux encrypts cookies using the system keyring. When `nlm` copies the browser profile to a temporary directory, the encryption keys are lost and authentication fails with "redirected to authentication page - not logged in".

## Solution

Use the `NLM_USE_ORIGINAL_PROFILE=1` environment variable to make `nlm` use the original profile directory instead of copying it.

## Prerequisites

- WSL2 installed on Windows
- Go 1.21+ installed in WSL

## Installation Steps

### 1. Install Chromium in WSL

```bash
# Ubuntu/Debian
sudo apt update
sudo apt install chromium-browser

# Or via snap (if apt version is not available)
sudo snap install chromium
```

### 2. Create Profile Symlink

`nlm` looks for Chrome profiles in `~/.config/google-chrome`. Since we're using Chromium, create a symlink:

```bash
# For apt-installed Chromium
ln -sf ~/.config/chromium ~/.config/google-chrome

# For snap-installed Chromium
ln -sf ~/snap/chromium/common/chromium ~/.config/google-chrome
```

### 3. Initial Browser Setup

Launch Chromium with basic password storage (to avoid keyring prompts):

```bash
chromium --password-store=basic
```

Then:
1. Navigate to https://notebooklm.google.com
2. Sign in with your Google account
3. Close the browser

### 4. Install nlm

```bash
go install github.com/tmc/nlm/cmd/nlm@latest
```

### 5. Authenticate nlm

Run the authentication with the original profile flag:

```bash
NLM_USE_ORIGINAL_PROFILE=1 ~/go/bin/nlm auth -debug
```

You should see:
```
Using original profile directory: /home/username/.config/google-chrome
...
Authentication successful!
```

### 6. Verify Installation

```bash
# List your notebooks
~/go/bin/nlm list

# List sources in a notebook
~/go/bin/nlm sources <notebook-id>
```

## Usage

Always use `NLM_USE_ORIGINAL_PROFILE=1` when running `nlm` commands in WSL:

```bash
export NLM_USE_ORIGINAL_PROFILE=1
nlm list
nlm generate-chat <notebook-id> "Your question here"
```

Or add to your `.bashrc`:

```bash
echo 'export NLM_USE_ORIGINAL_PROFILE=1' >> ~/.bashrc
source ~/.bashrc
```

## Troubleshooting

### "no valid profiles found"

Make sure the symlink exists:
```bash
ls -la ~/.config/google-chrome
```

Should point to your Chromium profile directory.

### "redirected to authentication page"

1. Make sure you're using `NLM_USE_ORIGINAL_PROFILE=1`
2. Re-login to NotebookLM in Chromium manually
3. Run `nlm auth -debug` again

### Chrome process conflicts

If authentication fails, close any running Chromium processes:
```bash
pkill -f chromium
```

Then try again.

## How It Works

The `NLM_USE_ORIGINAL_PROFILE` environment variable tells `nlm` to:

1. **Without flag (default)**: Copy browser profile to a temp directory, losing encryption keys
2. **With flag=1**: Use the original profile directory directly, preserving cookie encryption

This is implemented in `internal/auth/auth.go` in both `tryMultipleProfiles()` and `GetAuth()` functions.

## Security Note

When using `NLM_USE_ORIGINAL_PROFILE=1`, `nlm` has access to your actual browser profile. This is necessary for authentication but means:

- Close other Chromium windows before running `nlm` to avoid profile lock conflicts
- The browser automation has access to your real cookies and session data

This is the same level of access as your regular browser session.
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,44 @@ package method

import (
notebooklmv1alpha1 "github.com/tmc/nlm/gen/notebooklm/v1alpha1"
"github.com/tmc/nlm/internal/rpc/argbuilder"
)

// GENERATION_BEHAVIOR: append

// EncodeActOnSourcesArgs encodes arguments for LabsTailwindOrchestrationService.ActOnSources
// RPC ID: yyryJe
// Argument format: [%project_id%, %action%, %source_ids%]
// Updated format based on actual browser API calls (November 2025)
// Format: [[[[source_ids]]],null,null,null,null,[action,[[context]],extra],null,[2,null,[1]]]
func EncodeActOnSourcesArgs(req *notebooklmv1alpha1.ActOnSourcesRequest) []interface{} {
// Using generalized argument encoder
args, err := argbuilder.EncodeRPCArgs(req, "[%project_id%, %action%, %source_ids%]")
if err != nil {
// Log error and return empty args as fallback
// In production, this should be handled better
return []interface{}{}
// Build nested source IDs array
// Browser format in f.req args: [[[source_id]]] (3 levels in position 0)
// Because the whole args array adds 1 level, position 0 needs 3 levels
var sourceIDsInner []interface{}
for _, sid := range req.GetSourceIds() {
sourceIDsInner = append(sourceIDsInner, sid)
}
// sourceIDsInner = [sid] - 1 level
// Wrap 2 more times: [[[sid]]]
sourceIDsNested := []interface{}{[]interface{}{sourceIDsInner}}

// Build action info: [action, [["[CONTEXT]", ""]], ""]
actionInfo := []interface{}{
req.GetAction(),
[]interface{}{[]interface{}{"[CONTEXT]", ""}},
"",
}

// Build the full argument array
args := []interface{}{
sourceIDsNested, // Position 0: [[[[source_ids]]]]
nil, // Position 1
nil, // Position 2
nil, // Position 3
nil, // Position 4
actionInfo, // Position 5: [action, [[context]], extra]
nil, // Position 6
[]interface{}{2, nil, []interface{}{1}}, // Position 7: metadata
}

return args
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,35 @@ package method

import (
notebooklmv1alpha1 "github.com/tmc/nlm/gen/notebooklm/v1alpha1"
"github.com/tmc/nlm/internal/rpc/argbuilder"
)

// GENERATION_BEHAVIOR: append

// EncodeGenerateFreeFormStreamedArgs encodes arguments for LabsTailwindOrchestrationService.GenerateFreeFormStreamed
// RPC ID: BD
// Argument format: [[%all_sources%], %prompt%, null, [2]] when sources present
// Fallback format: [%project_id%, %prompt%] when no sources
// Updated format based on browser API calls pattern (November 2025)
// Format similar to ActOnSources: [[[source_ids]],prompt,null,null,null,null,null,[2,null,[1]]]
func EncodeGenerateFreeFormStreamedArgs(req *notebooklmv1alpha1.GenerateFreeFormStreamedRequest) []interface{} {
// If sources are provided, use the gRPC format with sources
if len(req.SourceIds) > 0 {
// Build source array
sourceArray := make([]interface{}, len(req.SourceIds))
for i, sourceId := range req.SourceIds {
sourceArray[i] = []interface{}{sourceId}
}

// Use gRPC format: [[%all_sources%], %prompt%, null, [2]]
return []interface{}{
[]interface{}{sourceArray},
req.Prompt,
nil,
[]interface{}{2},
}
// Build source IDs array with 2 levels of nesting
// Final format in f.req: [[[source_id]]] (3 levels after JSON serialization adds 1)
var sourceIDsInner []interface{}
for _, sid := range req.GetSourceIds() {
sourceIDsInner = append(sourceIDsInner, sid)
}
// Wrap 1 time: [[sid]]
sourceIDsNested := []interface{}{sourceIDsInner}

// Fallback to old format without sources
args, err := argbuilder.EncodeRPCArgs(req, "[%project_id%, %prompt%]")
if err != nil {
// Log error and return empty args as fallback
// In production, this should be handled better
return []interface{}{}
// Build the full argument array
args := []interface{}{
sourceIDsNested, // Position 0: [[source_ids]]
req.GetPrompt(), // Position 1: prompt text
nil, // Position 2
nil, // Position 3
nil, // Position 4
nil, // Position 5
nil, // Position 6
[]interface{}{2, nil, []interface{}{1}}, // Position 7: metadata
}

return args
}
55 changes: 42 additions & 13 deletions gen/service/LabsTailwindOrchestrationService_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading