Skip to content

Commit ba714ee

Browse files
teng-linclaude
andcommitted
fix(skill): use package data for skill install + docs updates
Version 0.1.1 Fixes: - skill install now works when installed via pip by using importlib.resources to read SKILL.md from package data - Moved skills/notebooklm/SKILL.md to src/notebooklm/data/SKILL.md Documentation updates: - Added missing source wait and artifact wait commands to CLI reference - Updated export methods to show ExportType enum instead of integers - Fixed slide-deck download docs (PDF, not PNGs) - Fixed incorrect --wait usage pattern in getting-started.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4da535c commit ba714ee

File tree

9 files changed

+63
-49
lines changed

9 files changed

+63
-49
lines changed

docs/cli-reference.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ See [Configuration](configuration.md) for details on environment variables and C
7575
| `rename <id> <title>` | Source ID, new title | - | `source rename src123 "New Name"` |
7676
| `refresh <id>` | Source ID | - | `source refresh src123` |
7777
| `delete <id>` | Source ID | - | `source delete src123` |
78+
| `wait <id>` | Source ID | `--timeout`, `--interval` | `source wait src123` |
7879

7980
### Research Commands (`notebooklm research <cmd>`)
8081

@@ -105,8 +106,9 @@ See [Configuration](configuration.md) for details on environment variables and C
105106
| `get <id>` | Artifact ID | - | `artifact get art123` |
106107
| `rename <id> <title>` | Artifact ID, title | - | `artifact rename art123 "Title"` |
107108
| `delete <id>` | Artifact ID | - | `artifact delete art123` |
108-
| `export <id>` | Artifact ID | - | `artifact export art123` |
109+
| `export <id>` | Artifact ID | `--type [docs|sheets]`, `--title` | `artifact export art123 --type sheets` |
109110
| `poll <task_id>` | Task ID | - | `artifact poll task123` |
111+
| `wait <id>` | Artifact ID | `--timeout`, `--interval` | `artifact wait art123` |
110112
| `suggestions` | - | - | `artifact suggestions` |
111113

112114
### Download Commands (`notebooklm download <type>`)
@@ -307,14 +309,14 @@ notebooklm generate audio [description] [OPTIONS]
307309

308310
**Examples:**
309311
```bash
310-
# Basic podcast
312+
# Basic podcast (starts async, returns immediately)
311313
notebooklm generate audio
312314

313315
# Debate format with custom instructions
314316
notebooklm generate audio "Compare the two main viewpoints" --format debate
315317

316-
# Wait for completion
317-
notebooklm generate audio --wait
318+
# Generate and wait for completion
319+
notebooklm generate audio "Focus on key points" --wait
318320
```
319321

320322
### Generate: `video`

docs/getting-started.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,10 @@ notebooklm ask "What are the key themes in this article?"
118118
### Step 5: Generate a Podcast
119119

120120
```bash
121-
notebooklm generate audio "Focus on the history and future predictions"
121+
notebooklm generate audio "Focus on the history and future predictions" --wait
122122
```
123123

124-
This starts an async generation job. Wait for it:
125-
126-
```bash
127-
notebooklm generate audio --wait
128-
```
124+
This generates a podcast and waits for completion (takes 2-5 minutes).
129125

130126
### Step 6: Download the Result
131127

docs/python-api.md

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ await client.sources.refresh(nb_id, src.id) # Re-fetch URL content
284284
| `download_audio(notebook_id, output_path, artifact_id=None)` | `str, str, str` | `str` | Download audio to file (MP4/MP3) |
285285
| `download_video(notebook_id, output_path, artifact_id=None)` | `str, str, str` | `str` | Download video to file (MP4) |
286286
| `download_infographic(notebook_id, output_path, artifact_id=None)` | `str, str, str` | `str` | Download infographic to file (PNG) |
287-
| `download_slide_deck(notebook_id, output_dir, artifact_id=None)` | `str, str, str` | `list[str]` | Download slides to directory (PNGs) |
287+
| `download_slide_deck(notebook_id, output_path, artifact_id=None)` | `str, str, str` | `str` | Download slide deck as PDF |
288288

289289
**Download Methods:**
290290

@@ -301,15 +301,14 @@ path = await client.artifacts.download_video(nb_id, "video.mp4")
301301
# Download infographic
302302
path = await client.artifacts.download_infographic(nb_id, "infographic.png")
303303

304-
# Download slide deck (creates multiple files)
305-
slide_paths = await client.artifacts.download_slide_deck(nb_id, "./slides/")
306-
# Returns: ["./slides/slide_001.png", "./slides/slide_002.png", ...]
304+
# Download slide deck as PDF
305+
path = await client.artifacts.download_slide_deck(nb_id, "./slides.pdf")
306+
# Returns: "./slides.pdf"
307307
```
308308

309309
**Notes:**
310310
- If `artifact_id` is not specified, downloads the first completed artifact of that type
311311
- Raises `ValueError` if no completed artifact is found
312-
- `download_slide_deck` creates the output directory if it doesn't exist
313312
- Some URLs require browser-based download (handled automatically)
314313

315314
#### Export Methods
@@ -318,20 +317,23 @@ Export artifacts to Google Docs or Google Sheets.
318317

319318
| Method | Parameters | Returns | Description |
320319
|--------|------------|---------|-------------|
321-
| `export_report(notebook_id, artifact_id, title="Export", export_type=1)` | `str, str, str, int` | `Any` | Export report to Google Docs |
322-
| `export_data_table(notebook_id, artifact_id, title="Export")` | `str, str, str` | `Any` | Export data table to Google Sheets |
323-
| `export(notebook_id, artifact_id=None, content=None, title="Export", export_type=1)` | `str, str, str, str, int` | `Any` | Generic export to Docs/Sheets |
320+
| `export_report(notebook_id, artifact_id, title, export_type)` | `str, str, str, ExportType` | `Any` | Export report to Google Docs/Sheets |
321+
| `export_data_table(notebook_id, artifact_id, title)` | `str, str, str` | `Any` | Export data table to Google Sheets |
322+
| `export(notebook_id, artifact_id, content, title, export_type)` | `str, str, str, str, ExportType` | `Any` | Generic export to Docs/Sheets |
324323

325-
**Export Types:**
326-
- `export_type=1`: Export to Google Docs
327-
- `export_type=2`: Export to Google Sheets
324+
**Export Types (ExportType enum):**
325+
- `ExportType.DOCS` (1): Export to Google Docs
326+
- `ExportType.SHEETS` (2): Export to Google Sheets
328327

329328
```python
329+
from notebooklm import ExportType
330+
330331
# Export a report to Google Docs
331332
result = await client.artifacts.export_report(
332333
nb_id,
333334
artifact_id="report_123",
334-
title="My Briefing Doc"
335+
title="My Briefing Doc",
336+
export_type=ExportType.DOCS
335337
)
336338
# result contains the Google Docs URL
337339

@@ -343,12 +345,12 @@ result = await client.artifacts.export_data_table(
343345
)
344346
# result contains the Google Sheets URL
345347

346-
# Generic export (e.g., export any artifact to Docs)
348+
# Generic export (e.g., export any artifact to Sheets)
347349
result = await client.artifacts.export(
348350
nb_id,
349351
artifact_id="artifact_789",
350352
title="Exported Content",
351-
export_type=1 # 1=Docs, 2=Sheets
353+
export_type=ExportType.SHEETS
352354
)
353355
```
354356

@@ -694,6 +696,14 @@ class SlideDeckLength(Enum):
694696
SHORT = 2
695697
```
696698

699+
### Export
700+
701+
```python
702+
class ExportType(Enum):
703+
DOCS = 1 # Export to Google Docs
704+
SHEETS = 2 # Export to Google Sheets
705+
```
706+
697707
### Chat Configuration
698708

699709
```python

docs/stability.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ SourceError, SourceProcessingError, SourceTimeoutError, SourceNotFoundError
6767
RPCError
6868

6969
# Enums
70-
AudioFormat, VideoFormat, StudioContentType, etc.
70+
AudioFormat, VideoFormat, StudioContentType, ExportType, etc.
7171

7272
# Auth
7373
AuthTokens, DEFAULT_STORAGE_PATH

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "notebooklm-py"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "Unofficial Python client for Google NotebookLM API"
55
readme = "README.md"
66
requires-python = ">=3.9"

src/notebooklm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
See docs/troubleshooting.md for guidance on handling API changes.
1414
"""
1515

16-
__version__ = "0.1.0"
16+
__version__ = "0.1.1"
1717

1818
# Public API: Authentication
1919
from .auth import AuthTokens, DEFAULT_STORAGE_PATH

src/notebooklm/cli/skill.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,29 @@
44
"""
55

66
import re
7+
from importlib import resources
78
from pathlib import Path
9+
from typing import Optional
810

911
import click
1012

1113
from .helpers import console
1214

1315

1416
# Skill paths
15-
SKILL_SOURCE = Path(__file__).parent.parent.parent.parent / "skills" / "notebooklm" / "SKILL.md"
1617
SKILL_DEST_DIR = Path.home() / ".claude" / "skills" / "notebooklm"
1718
SKILL_DEST = SKILL_DEST_DIR / "SKILL.md"
1819

1920

21+
def get_skill_source_content() -> Optional[str]:
22+
"""Read the skill source file from package data."""
23+
try:
24+
# Python 3.9+ way to read package data
25+
return resources.files("notebooklm").joinpath("data", "SKILL.md").read_text()
26+
except (FileNotFoundError, TypeError):
27+
return None
28+
29+
2030
def get_package_version() -> str:
2131
"""Get the current package version."""
2232
try:
@@ -51,19 +61,17 @@ def install():
5161
Copies the skill file to ~/.claude/skills/notebooklm/SKILL.md
5262
and embeds the current package version for tracking.
5363
"""
54-
# Check if source exists
55-
if not SKILL_SOURCE.exists():
56-
console.print(f"[red]Error:[/red] Skill source not found at {SKILL_SOURCE}")
57-
console.print("This may indicate an incomplete installation.")
64+
# Read skill content from package data
65+
content = get_skill_source_content()
66+
if content is None:
67+
console.print("[red]Error:[/red] Skill source not found in package data.")
68+
console.print("This may indicate an incomplete or corrupted installation.")
69+
console.print("Try reinstalling: pip install --force-reinstall notebooklm-py")
5870
raise SystemExit(1)
5971

6072
# Create destination directory
6173
SKILL_DEST_DIR.mkdir(parents=True, exist_ok=True)
6274

63-
# Read skill content
64-
with open(SKILL_SOURCE) as f:
65-
content = f.read()
66-
6775
# Embed version in skill file (after frontmatter)
6876
version = get_package_version()
6977
version_comment = f"<!-- notebooklm-py v{version} -->\n"
File renamed without changes.

tests/unit/cli/test_skill.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Tests for skill CLI commands."""
22

33
import pytest
4-
from pathlib import Path
5-
from unittest.mock import patch, mock_open
4+
from unittest.mock import patch
65
from click.testing import CliRunner
76

87
from notebooklm.notebooklm_cli import cli
@@ -19,26 +18,25 @@ class TestSkillInstall:
1918
def test_skill_install_creates_directory_and_file(self, runner, tmp_path):
2019
"""Test that install creates the skill file."""
2120
skill_dest = tmp_path / "skills" / "notebooklm" / "SKILL.md"
21+
mock_source_content = "---\nname: notebooklm\n---\n# Test"
2222

2323
with patch("notebooklm.cli.skill.SKILL_DEST", skill_dest), \
2424
patch("notebooklm.cli.skill.SKILL_DEST_DIR", skill_dest.parent), \
25-
patch("notebooklm.cli.skill.SKILL_SOURCE") as mock_source:
25+
patch("notebooklm.cli.skill.get_skill_source_content", return_value=mock_source_content):
2626

27-
# Create a mock source file
28-
mock_source.exists.return_value = True
29-
mock_source_content = "---\nname: notebooklm\n---\n# Test"
30-
31-
with patch("builtins.open", mock_open(read_data=mock_source_content)):
32-
result = runner.invoke(cli, ["skill", "install"])
27+
result = runner.invoke(cli, ["skill", "install"])
3328

34-
# Check command succeeded (may fail due to mock complexity, but structure is right)
35-
assert "install" in result.output.lower() or result.exit_code == 0
29+
assert result.exit_code == 0
30+
assert "installed" in result.output.lower()
31+
assert skill_dest.exists()
3632

3733
def test_skill_install_source_not_found(self, runner, tmp_path):
3834
"""Test error when source file doesn't exist."""
39-
skill_source = tmp_path / "nonexistent" / "SKILL.md"
35+
skill_dest = tmp_path / "skills" / "notebooklm" / "SKILL.md"
4036

41-
with patch("notebooklm.cli.skill.SKILL_SOURCE", skill_source):
37+
with patch("notebooklm.cli.skill.SKILL_DEST", skill_dest), \
38+
patch("notebooklm.cli.skill.SKILL_DEST_DIR", skill_dest.parent), \
39+
patch("notebooklm.cli.skill.get_skill_source_content", return_value=None):
4240
result = runner.invoke(cli, ["skill", "install"])
4341

4442
assert result.exit_code == 1

0 commit comments

Comments
 (0)