Merge branch 'main' into feat/adguardhome-clean

This commit is contained in:
PYXL
2026-03-18 15:46:14 +01:00
committed by GitHub
21 changed files with 1140 additions and 2 deletions

5
.gitignore vendored
View File

@@ -111,4 +111,7 @@ assets/gen_typing_gif.py
!/docs/
/docs/*
!/docs/hub/
!/notebooklm/
/notebooklm/*
/notebooklm/.*
!/notebooklm/agent-harness/

View File

@@ -708,6 +708,8 @@ cli-anything/
├── 🧜 mermaid/agent-harness/ # Mermaid Live Editor CLI (10 tests)
├── ✨ anygen/agent-harness/ # AnyGen CLI (50 tests)
├── 🖼️ comfyui/agent-harness/ # ComfyUI CLI (70 tests)
├── 🧠 notebooklm/agent-harness/ # NotebookLM CLI (experimental, 21 tests)
├── 🖼️ comfyui/agent-harness/ # ComfyUI CLI (70 tests)
└── 🛡️ adguardhome/agent-harness/ # AdGuardHome CLI (36 tests)
```

View File

@@ -0,0 +1,29 @@
# Attribution
This harness adapts the CLI-Anything methodology for NotebookLM.
## Acknowledgements
- **CLI-Anything**
- Project: https://github.com/HKUDS/CLI-Anything
- Methodology: https://github.com/HKUDS/CLI-Anything/blob/main/cli-anything-plugin/HARNESS.md
- We follow its agent-native CLI conventions, including REPL-first design, JSON output, package layout, and test documentation patterns.
- **notebooklm-py**
- Project: https://github.com/teng-lin/notebooklm-py
- PyPI: https://pypi.org/project/notebooklm-py/
- This harness is designed to interoperate with the installed `notebooklm` CLI distributed by notebooklm-py.
- **Google NotebookLM**
- Product help: https://support.google.com/notebooklm/answer/16206563
- NotebookLM is a Google product. This harness is unofficial and not affiliated with or endorsed by Google.
## Design Boundary
This project prefers composition over copying:
- wrap the installed `notebooklm` CLI
- document upstream dependencies and policies
- avoid vendoring third-party NotebookLM implementation code into this repository
If you extend this harness, preserve these acknowledgements and keep the unofficial / experimental disclaimer intact.

View File

@@ -0,0 +1,44 @@
# NotebookLM: Project-Specific Analysis & SOP
## Architecture Summary
NotebookLM is a hosted Google research and content-generation product. Unlike the
local GUI applications that CLI-Anything usually targets, this harness wraps an
installed `notebooklm` command-line client that already manages authentication,
source ingestion, chat, artifact generation, and downloads.
This harness therefore behaves as a **service-style CLI wrapper**:
1. Resolve the local `notebooklm` executable.
2. Build explicit commands with notebook context where needed.
3. Sanitize sensitive auth-related error output.
4. Persist lightweight local session state for REPL convenience.
## Backend Strategy
- Prefer the installed `notebooklm` CLI over reimplementing NotebookLM internals.
- Keep credentials outside the repository and outside test fixtures.
- Treat this integration as experimental and unofficial.
- Require explicit confirmation for destructive or high-impact operations.
- Preserve clear attribution to CLI-Anything, notebooklm-py, and Google NotebookLM documentation.
## Contribution Boundary
This harness is designed to be safe for an upstream contribution review:
- it wraps an installed community CLI instead of vendoring third-party NotebookLM code
- it documents copyright and service-boundary concerns instead of implying official support
- it limits automated verification to non-destructive smoke coverage unless a local authenticated session is intentionally used
- it keeps end-to-end authenticated testing manual so secrets and account state stay out of CI and fixtures
## Constraints
- Depends on a valid local Google-authenticated NotebookLM session.
- Depends on behavior provided by the installed `notebooklm` CLI.
- Full authenticated end-to-end tests are manual by design.
## References
- CLI-Anything methodology: https://github.com/HKUDS/CLI-Anything
- notebooklm-py project: https://github.com/teng-lin/notebooklm-py
- Google NotebookLM help: https://support.google.com/notebooklm/answer/16206563

View File

@@ -0,0 +1,26 @@
# Third-Party Notices
## notebooklm-py
- Homepage: https://github.com/teng-lin/notebooklm-py
- PyPI: https://pypi.org/project/notebooklm-py/
- License: MIT
This harness is intended to work with the `notebooklm` CLI provided by notebooklm-py.
## Click
Used for CLI command structure.
## prompt-toolkit
Used for REPL interaction helpers.
## Important Boundary
Google NotebookLM is a third-party online service. This harness is an unofficial
integration layer and is not affiliated with Google. Users are responsible for:
- complying with Google terms and product restrictions
- protecting their own credentials and local auth state
- respecting copyright for imported sources

View File

@@ -0,0 +1,112 @@
# CLI-Anything NotebookLM Harness
Experimental NotebookLM harness for CLI-Anything.
This package wraps an installed `notebooklm` CLI and exposes a CLI-Anything-style
interface for authentication checks, notebook selection, source management, chat,
artifact generation, downloads, and sharing.
## Status
- Experimental
- Community-maintained
- Unofficial and not affiliated with Google
## Requirements
- Python 3.10+
- An installed `notebooklm` command
- A valid local NotebookLM login session
## Install
```bash
cd notebooklm/agent-harness
python3 -m pip install -e .
```
If the upstream NotebookLM CLI is not installed yet:
```bash
python3 -m pip install --user 'notebooklm-py[browser]'
python3 -m playwright install chromium
```
## Run
```bash
# Show help
cli-anything-notebooklm --help
# Check auth state (wraps `notebooklm auth check`)
cli-anything-notebooklm auth status
# List notebooks
cli-anything-notebooklm notebook list
```
## Run Tests
```bash
cd notebooklm/agent-harness
python3 -m pytest cli_anything/notebooklm/tests -q
python3 -m cli_anything.notebooklm.notebooklm_cli --help
```
## Command Groups
| Group | Purpose |
| --- | --- |
| `auth` | login helpers and authentication checks |
| `notebook` | list, create, and summarize notebooks |
| `source` | inspect sources and add URL sources |
| `chat` | ask questions and inspect history |
| `artifact` | list artifacts and generate reports |
| `download` | download generated artifacts |
| `share` | inspect sharing status |
## Common Workflows
```bash
cli-anything-notebooklm auth status
cli-anything-notebooklm notebook list
cli-anything-notebooklm source list --notebook nb_123
cli-anything-notebooklm chat ask "Summarize the current notebook"
cli-anything-notebooklm artifact generate-report --notebook nb_123
```
## For AI Agents
- Prefer explicit notebook IDs with `--notebook` instead of relying on ambient state.
- Use `--json` only on commands whose upstream `notebooklm` subcommand supports machine-readable output.
- Treat NotebookLM auth state as sensitive local data and never print cookie or storage files.
- Treat this harness as a thin wrapper around `notebooklm`, not a reimplementation of NotebookLM.
## Acknowledgements
This harness is inspired by the CLI-Anything methodology:
https://github.com/HKUDS/CLI-Anything
It is designed to work with the community-maintained `notebooklm` CLI from `notebooklm-py`:
https://github.com/teng-lin/notebooklm-py
NotebookLM is a Google product:
https://support.google.com/notebooklm/answer/16206563
This project is unofficial and not affiliated with Google.
## Safety Notes
- Do not commit local auth state into the repository.
- Do not upload sensitive content without permission.
- Respect copyright and service terms for imported sources.
- Prefer review-oriented or read-only commands first when working inside a live notebook.
- Treat sharing and artifact generation as user-impacting operations that deserve explicit intent.
## References
- CLI-Anything: https://github.com/HKUDS/CLI-Anything
- CLI-Anything HARNESS.md: https://github.com/HKUDS/CLI-Anything/blob/main/cli-anything-plugin/HARNESS.md
- notebooklm-py: https://github.com/teng-lin/notebooklm-py
- notebooklm-py on PyPI: https://pypi.org/project/notebooklm-py/
- Google NotebookLM help: https://support.google.com/notebooklm/answer/16206563

View File

@@ -0,0 +1,3 @@
"""NotebookLM harness package."""
__version__ = "0.1.0"

View File

@@ -0,0 +1,7 @@
"""Module entry point for cli-anything-notebooklm."""
from cli_anything.notebooklm.notebooklm_cli import main
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
"""Core modules for the NotebookLM harness."""

View File

@@ -0,0 +1,42 @@
"""Session persistence helpers for NotebookLM CLI context."""
from __future__ import annotations
import json
from pathlib import Path
class Session:
"""Persist the active notebook for REPL and one-shot commands."""
def __init__(self, session_file: str | Path | None = None):
if session_file is None:
session_file = Path.home() / ".cli-anything-notebooklm" / "session.json"
self.session_file = Path(session_file)
self.session_file.parent.mkdir(parents=True, exist_ok=True)
self._data = self._load()
def _load(self) -> dict:
if not self.session_file.exists():
return {"active_notebook": None}
try:
return json.loads(self.session_file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return {"active_notebook": None}
def _save(self):
self.session_file.write_text(
json.dumps(self._data, indent=2, ensure_ascii=True),
encoding="utf-8",
)
def get_active_notebook(self) -> str | None:
return self._data.get("active_notebook")
def set_active_notebook(self, notebook_id: str):
self._data["active_notebook"] = notebook_id
self._save()
def clear_active_notebook(self):
self._data["active_notebook"] = None
self._save()

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""NotebookLM CLI — Experimental NotebookLM wrapper for AI agents."""
from __future__ import annotations
import json
import sys
import click
from cli_anything.notebooklm import __version__
from cli_anything.notebooklm.core.session import Session
from cli_anything.notebooklm.utils.notebooklm_backend import run_notebooklm
_json_output = False
_session: Session | None = None
def get_session() -> Session:
global _session
if _session is None:
_session = Session()
return _session
def emit(data, message: str = ""):
if _json_output:
click.echo(json.dumps(data, indent=2, default=str))
elif message:
click.echo(message)
elif isinstance(data, str):
click.echo(data)
else:
click.echo(json.dumps(data, indent=2, default=str))
def handle_error(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as exc: # pragma: no cover - scaffold behavior
if _json_output:
click.echo(json.dumps({"error": str(exc), "type": type(exc).__name__}))
else:
click.echo(f"Error: {exc}", err=True)
sys.exit(1)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
def resolve_notebook_id(notebook_id: str | None) -> str | None:
return notebook_id or get_session().get_active_notebook()
@click.group(invoke_without_command=True)
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
@click.option("--notebook", "notebook_id", default=None, help="Active notebook ID")
@click.pass_context
def cli(ctx, use_json, notebook_id):
"""NotebookLM CLI — Experimental NotebookLM wrapper for AI agents."""
global _json_output
_json_output = use_json
if notebook_id:
get_session().set_active_notebook(notebook_id)
if ctx.invoked_subcommand is None:
ctx.invoke(repl)
@cli.command()
def repl():
"""Start a minimal REPL placeholder."""
click.echo(f"cli-anything-notebooklm v{__version__}")
click.echo("Experimental harness scaffold. Use --help to inspect command groups.")
@cli.group()
def auth():
"""Authentication and login helpers."""
@auth.command("status")
@handle_error
def auth_status():
"""Check authentication status."""
emit(run_notebooklm(["auth", "check"], json_output=_json_output))
@auth.command("login")
@handle_error
def auth_login():
"""Open the browser login flow."""
emit(run_notebooklm(["login"], json_output=_json_output))
@auth.command("check")
@handle_error
def auth_check():
"""Run a lightweight authentication check."""
emit(run_notebooklm(["auth", "check"], json_output=_json_output))
@cli.group()
def notebook():
"""Notebook management commands."""
@notebook.command("list")
@handle_error
def notebook_list():
"""List notebooks."""
emit(run_notebooklm(["list"], json_output=_json_output))
@notebook.command("create")
@click.argument("name")
@handle_error
def notebook_create(name):
"""Create a notebook."""
emit(run_notebooklm(["create", name], json_output=_json_output))
@notebook.command("summary")
@handle_error
def notebook_summary():
"""Summarize the active notebook."""
emit(
run_notebooklm(
["summary"],
notebook_id=resolve_notebook_id(None),
json_output=_json_output,
)
)
@cli.group()
def source():
"""Source ingestion and inspection commands."""
@source.command("list")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def source_list(notebook_id):
"""List sources for a notebook."""
emit(
run_notebooklm(
["source", "list"],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@source.command("add-url")
@click.argument("url")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def source_add_url(url, notebook_id):
"""Add a URL source."""
emit(
run_notebooklm(
["source", "add", url],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@cli.group()
def chat():
"""Chat and history commands."""
@chat.command("ask")
@click.argument("prompt")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def chat_ask(prompt, notebook_id):
"""Ask a question against a notebook."""
emit(
run_notebooklm(
["ask", prompt],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@chat.command("history")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def chat_history(notebook_id):
"""Show chat history."""
emit(
run_notebooklm(
["history"],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@cli.group()
def artifact():
"""Artifact generation and inspection commands."""
@artifact.command("list")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def artifact_list(notebook_id):
"""List notebook artifacts."""
emit(
run_notebooklm(
["artifact", "list"],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@artifact.command("generate-report")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def artifact_generate_report(notebook_id):
"""Generate a report artifact."""
emit(
run_notebooklm(
["generate", "report", "--wait"],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@cli.group()
def download():
"""Artifact download helpers."""
@download.command("report")
@click.argument("output_path")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def download_report(output_path, notebook_id):
"""Download the latest report artifact."""
emit(
run_notebooklm(
["download", "report", output_path],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
@cli.group()
def share():
"""Sharing and access control commands."""
@share.command("status")
@click.option("--notebook", "notebook_id", default=None, help="Notebook ID")
@handle_error
def share_status(notebook_id):
"""Inspect notebook sharing state."""
emit(
run_notebooklm(
["share", "status"],
notebook_id=resolve_notebook_id(notebook_id),
json_output=_json_output,
)
)
def main():
cli()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,78 @@
---
name: cli-anything-notebooklm
description: Experimental NotebookLM harness for listing notebooks, managing sources, asking questions, generating artifacts, and downloading outputs through an installed notebooklm CLI.
---
# cli-anything-notebooklm
Experimental NotebookLM harness for CLI-Anything.
## Installation
This package is intended to be installed from the harness directory:
```bash
cd notebooklm/agent-harness
python3 -m pip install -e .
```
Install the upstream NotebookLM CLI if needed:
```bash
python3 -m pip install --user 'notebooklm-py[browser]'
python3 -m playwright install chromium
```
## Requirements
- `notebooklm` command installed locally
- Valid local NotebookLM login session
## Usage
### Basic Commands
```bash
# Show help
cli-anything-notebooklm --help
# Start with a notebook context
cli-anything-notebooklm --notebook nb_123 source list
# Prefer JSON for agent use
cli-anything-notebooklm --json notebook list
```
## Command Groups
| Group | Purpose |
| --- | --- |
| `auth` | login and auth validation |
| `notebook` | notebook list, create, summary |
| `source` | source listing and URL add |
| `chat` | ask questions and inspect history |
| `artifact` | list and generate artifacts |
| `download` | fetch generated outputs |
| `share` | inspect sharing state |
## Agent Workflow
1. Check auth with `cli-anything-notebooklm auth status`
2. Discover notebook IDs with `cli-anything-notebooklm --json notebook list`
3. Use explicit `--notebook` for follow-up commands
4. Prefer `--json` only where the upstream `notebooklm` command supports it
## Agent Guidance
- Prefer explicit notebook IDs with `--notebook`.
- Use `--json` for machine-readable output only on commands that support it upstream.
- Treat this harness as experimental and unofficial.
- Do not expose auth files or cookies in logs.
- NotebookLM is a Google product; this harness is unofficial and not affiliated with Google.
## References
- CLI-Anything: https://github.com/HKUDS/CLI-Anything
- CLI-Anything HARNESS.md: https://github.com/HKUDS/CLI-Anything/blob/main/cli-anything-plugin/HARNESS.md
- notebooklm-py: https://github.com/teng-lin/notebooklm-py
- Google NotebookLM help: https://support.google.com/notebooklm/answer/16206563

View File

@@ -0,0 +1,89 @@
# NotebookLM Harness - Test Documentation
## Test Inventory
| File | Focus |
| --- | --- |
| `test_core.py` | backend discovery, command building, session persistence, packaging fixtures |
| `test_cli_smoke.py` | help output and command group exposure |
| `test_manual_e2e.md` | authenticated local smoke-test checklist |
## Coverage Notes
This harness has three validation layers:
- unit tests for backend helpers and session state
- smoke tests for CLI help and command registration
- manual authenticated end-to-end verification
Because NotebookLM depends on a live Google account session and an installed
community CLI, full authenticated E2E coverage is not run in public CI.
## Local Verification
Verified on 2026-03-17 in the notebooklm harness worktree.
### Commands Run
```bash
python3 -m pytest cli_anything/notebooklm/tests/test_core.py -q
python3 -m pytest cli_anything/notebooklm/tests/test_cli_smoke.py -q
python3 -m cli_anything.notebooklm.notebooklm_cli --help
```
### Results
- `test_core.py`: 9 passed
- `test_cli_smoke.py`: 5 passed
- `python3 -m cli_anything.notebooklm.notebooklm_cli --help`: exit code 0, help text rendered correctly
### Notes
- Added a regression test for module execution so `python -m cli_anything.notebooklm.notebooklm_cli --help` is covered, not just Click's in-process `CliRunner`.
- The authenticated `notebooklm` backend remains intentionally manual for end-to-end verification because it depends on a local Google session.
## PR Polish Verification
Verified on 2026-03-17 after README, SKILL, and subprocess smoke coverage upgrades.
### Commands Run
```bash
python3 -m pytest cli_anything/notebooklm/tests -q
python3 -m cli_anything.notebooklm.notebooklm_cli --help
```
### Results
- Full NotebookLM harness suite: 17 passed
- Module help command: exit code 0, rendered command groups correctly
### Notes
- Added doc-level assertions so the package README and skill file now explicitly cover installation, tests, safety boundaries, and unofficial Google attribution.
- Added `_resolve_cli`-style subprocess smoke coverage so the CLI can be exercised through a resolved command path, not only through Click's in-process test runner.
## Review Fix Verification
Verified on 2026-03-17 after addressing PR review feedback about JSON passthrough and auth command semantics.
### Commands Run
```bash
python3 -m pytest cli_anything/notebooklm/tests/test_core.py -q
python3 -m pytest cli_anything/notebooklm/tests/test_cli_smoke.py -q
python3 -m pytest cli_anything/notebooklm/tests -q
python3 -m cli_anything.notebooklm.notebooklm_cli --json auth login
```
### Results
- targeted backend tests: pass
- targeted CLI routing tests: pass
- full NotebookLM harness suite: pass
- `--json auth login`: fails fast with a structured JSON error instead of passing an invalid `--json` flag through to upstream login
### Notes
- `auth status` now wraps upstream `notebooklm auth check`, which matches authentication semantics.
- JSON passthrough is now limited to wrapper commands whose upstream `notebooklm` command has verified `--json` support.

View File

@@ -0,0 +1 @@
"""Tests for the NotebookLM harness."""

View File

@@ -0,0 +1,105 @@
"""CLI smoke tests for the NotebookLM harness scaffold."""
import os
from pathlib import Path
import shutil
import subprocess
import sys
from click.testing import CliRunner
from cli_anything.notebooklm.notebooklm_cli import cli
def _resolve_cli(name):
"""Resolve installed CLI command; fall back to python -m for local dev."""
force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1"
path = shutil.which(name)
if path:
return [path], None
if force:
raise RuntimeError(f"{name} not found in PATH. Install with: pip install -e .")
module = "cli_anything.notebooklm.notebooklm_cli"
package_root = Path(__file__).resolve().parents[3]
env = os.environ.copy()
current = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = (
f"{package_root}{os.pathsep}{current}" if current else str(package_root)
)
return [sys.executable, "-m", module], env
class TestRootHelp:
def test_root_help_shows_experimental_notebooklm(self):
result = CliRunner().invoke(cli, ["--help"])
assert result.exit_code == 0
assert "NotebookLM CLI" in result.output
assert "Experimental" in result.output
def test_root_help_lists_command_groups(self):
result = CliRunner().invoke(cli, ["--help"])
assert result.exit_code == 0
for group in ["auth", "notebook", "source", "chat", "artifact", "download", "share"]:
assert group in result.output
class TestSubcommandHelp:
def test_auth_status_help(self):
result = CliRunner().invoke(cli, ["auth", "status", "--help"])
assert result.exit_code == 0
assert "Check authentication status" in result.output
def test_notebook_list_help(self):
result = CliRunner().invoke(cli, ["notebook", "list", "--help"])
assert result.exit_code == 0
assert "List notebooks" in result.output
class TestCommandRouting:
def test_auth_status_routes_to_auth_check(self, monkeypatch):
calls = []
def fake_run(args, **kwargs):
calls.append((args, kwargs))
return {"ok": True}
monkeypatch.setattr(
"cli_anything.notebooklm.notebooklm_cli.run_notebooklm",
fake_run,
)
result = CliRunner().invoke(cli, ["auth", "status"])
assert result.exit_code == 0
assert calls == [(["auth", "check"], {"json_output": False})]
class TestModuleExecution:
def test_python_m_module_help_emits_output(self):
result = subprocess.run(
[
sys.executable,
"-m",
"cli_anything.notebooklm.notebooklm_cli",
"--help",
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0
assert "NotebookLM CLI" in result.stdout
def test_resolved_cli_help_emits_output(self):
command, env = _resolve_cli("cli-anything-notebooklm")
result = subprocess.run(
command + ["--help"],
capture_output=True,
text=True,
check=False,
env=env,
)
assert result.returncode == 0
assert "NotebookLM CLI" in result.stdout
assert "auth" in result.stdout

View File

@@ -0,0 +1,112 @@
"""Unit tests for NotebookLM harness scaffold."""
import json
from pathlib import Path
from unittest.mock import patch
import pytest
from cli_anything.notebooklm.core.session import Session
from cli_anything.notebooklm.utils.notebooklm_backend import (
build_command,
command_supports_json,
require_notebooklm,
run_notebooklm,
sanitize_error,
)
class TestBackendDiscovery:
def test_require_notebooklm_returns_path(self):
with patch("cli_anything.notebooklm.utils.notebooklm_backend.shutil.which", return_value="/usr/local/bin/notebooklm"):
assert require_notebooklm() == "/usr/local/bin/notebooklm"
def test_require_notebooklm_raises_with_install_guidance(self):
with patch("cli_anything.notebooklm.utils.notebooklm_backend.shutil.which", return_value=None):
with pytest.raises(RuntimeError, match="notebooklm command not found"):
require_notebooklm()
class TestCommandBuilder:
def test_build_command_with_notebook_id_and_json(self):
command = build_command(
["source", "list"],
notebook_id="nb_123",
json_output=True,
)
assert command == [
"notebooklm",
"source",
"list",
"-n",
"nb_123",
"--json",
]
def test_build_command_without_notebook_id(self):
command = build_command(["list"])
assert command == ["notebooklm", "list"]
def test_command_supports_json_for_verified_command(self):
assert command_supports_json(["download", "report"]) is True
def test_command_supports_json_rejects_unsupported_command(self):
assert command_supports_json(["login"]) is False
class TestRunNotebooklm:
def test_run_notebooklm_rejects_json_for_unsupported_command(self):
with patch("cli_anything.notebooklm.utils.notebooklm_backend.subprocess.run") as run_mock:
with pytest.raises(RuntimeError, match="JSON output is not supported for command: login"):
run_notebooklm(["login"], json_output=True)
run_mock.assert_not_called()
class TestErrorSanitization:
def test_sanitize_error_redacts_storage_state_path(self):
raw = "Failed to open /Users/tester/.notebooklm/storage_state.json because auth expired"
assert "storage_state.json" not in sanitize_error(raw)
assert "[redacted-auth-path]" in sanitize_error(raw)
class TestSession:
def test_session_persists_active_notebook(self, tmp_path):
session_file = tmp_path / "session.json"
session = Session(session_file=session_file)
session.set_active_notebook("nb_abc")
reloaded = Session(session_file=session_file)
assert reloaded.get_active_notebook() == "nb_abc"
def test_session_clear_active_notebook(self, tmp_path):
session_file = tmp_path / "session.json"
session = Session(session_file=session_file)
session.set_active_notebook("nb_abc")
session.clear_active_notebook()
data = json.loads(Path(session_file).read_text())
assert data["active_notebook"] is None
class TestPackagingFixtures:
def test_acknowledgements_reference_external_projects(self):
readme = Path("cli_anything/notebooklm/README.md").read_text(encoding="utf-8")
assert "CLI-Anything" in readme
assert "notebooklm-py" in readme
def test_readme_documents_install_test_and_safety_sections(self):
readme = Path("cli_anything/notebooklm/README.md").read_text(encoding="utf-8")
assert "## Install" in readme
assert "## Run Tests" in readme
assert "## Safety Notes" in readme
assert "Google NotebookLM" in readme
def test_skill_file_contains_usage_and_boundary_guidance(self):
skill = Path("cli_anything/notebooklm/skills/SKILL.md").read_text(encoding="utf-8")
assert "## Installation" in skill
assert "## Usage" in skill
assert "unofficial" in skill.lower()
assert "not affiliated with Google" in skill
def test_skill_file_exists(self):
assert Path("cli_anything/notebooklm/skills/SKILL.md").is_file()

View File

@@ -0,0 +1,11 @@
# Manual E2E Checklist
1. Run `notebooklm login`.
2. Run `cli-anything-notebooklm auth status`.
3. Run `cli-anything-notebooklm notebook list`.
4. Create a temporary notebook with `cli-anything-notebooklm notebook create "CLI Anything Smoke Test"`.
5. Add a simple text or URL source.
6. Run `cli-anything-notebooklm chat ask "Summarize the notebook."`.
7. Generate one artifact, such as a report.
8. Download the artifact to a temporary local path.
9. Delete the temporary notebook manually after verification.

View File

@@ -0,0 +1 @@
"""Utility modules for the NotebookLM harness."""

View File

@@ -0,0 +1,124 @@
"""NotebookLM backend adapter.
This module wraps an installed `notebooklm` CLI for use inside a CLI-Anything
harness. It does not implement a Google official API client.
References:
- CLI-Anything methodology: https://github.com/HKUDS/CLI-Anything
- notebooklm-py project: https://github.com/teng-lin/notebooklm-py
Security rules:
- never print credential files or cookies
- never commit auth state into the repository
- prefer explicit notebook IDs
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
JSON_SUPPORTED_COMMANDS = {
("auth", "check"),
("status",),
("list",),
("create",),
("source", "list"),
("source", "add"),
("ask",),
("history",),
("artifact", "list"),
("generate", "report"),
("download", "report"),
("share", "status"),
}
TWO_PART_COMMAND_GROUPS = {"auth", "source", "artifact", "generate", "download", "share"}
def require_notebooklm() -> str:
"""Resolve the notebooklm command from PATH."""
path = shutil.which("notebooklm")
if path:
return path
raise RuntimeError(
"notebooklm command not found. Install it with:\n"
" python3 -m pip install --user 'notebooklm-py[browser]'\n"
" python3 -m playwright install chromium"
)
def command_supports_json(args: list[str]) -> bool:
"""Return whether the wrapped notebooklm command supports --json."""
if not args:
return False
if len(args) > 1 and args[0] in TWO_PART_COMMAND_GROUPS:
key = tuple(args[:2])
else:
key = tuple(args[:1])
return key in JSON_SUPPORTED_COMMANDS
def build_command(
args: list[str],
*,
notebook_id: str | None = None,
json_output: bool = False,
) -> list[str]:
"""Build a notebooklm command with explicit notebook context."""
command = ["notebooklm", *args]
if notebook_id:
command.extend(["-n", notebook_id])
if json_output and command_supports_json(args):
command.append("--json")
return command
def sanitize_error(text: str) -> str:
"""Redact local auth file paths from stderr/stdout."""
patterns = [
r"/Users/[^/\s]+/\.notebooklm/storage_state\.json",
r"/home/[^/\s]+/\.notebooklm/storage_state\.json",
r"[A-Za-z]:\\\\Users\\\\[^\\\s]+\\\\\.notebooklm\\\\storage_state\.json",
]
sanitized = text
for pattern in patterns:
sanitized = re.sub(pattern, "[redacted-auth-path]", sanitized)
sanitized = sanitized.replace("storage_state.json", "[redacted-auth-path]")
return sanitized
def run_notebooklm(
args: list[str],
*,
notebook_id: str | None = None,
json_output: bool = False,
) -> dict | str:
"""Run notebooklm and optionally parse JSON output."""
if json_output and not command_supports_json(args):
raise RuntimeError(
f"JSON output is not supported for command: {' '.join(args)}"
)
command = build_command(
args,
notebook_id=notebook_id,
json_output=json_output,
)
command[0] = require_notebooklm()
result = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(sanitize_error(result.stderr or result.stdout))
if json_output:
return json.loads(result.stdout or "{}")
return result.stdout.strip()

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
setup.py for cli-anything-notebooklm
Install with: pip install -e .
"""
from setuptools import setup, find_namespace_packages
with open("cli_anything/notebooklm/README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="cli-anything-notebooklm",
version="0.1.0",
author="cli-anything contributors",
author_email="",
description="Experimental CLI harness for NotebookLM via an installed notebooklm CLI. Unofficial and community-maintained.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/HKUDS/CLI-Anything",
packages=find_namespace_packages(include=["cli_anything.*"]),
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
python_requires=">=3.10",
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
],
extras_require={
"dev": [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
],
},
entry_points={
"console_scripts": [
"cli-anything-notebooklm=cli_anything.notebooklm.notebooklm_cli:main",
],
},
package_data={
"cli_anything.notebooklm": ["skills/*.md"],
},
include_package_data=True,
zip_safe=False,
)

View File

@@ -2,7 +2,7 @@
"meta": {
"repo": "https://github.com/HKUDS/CLI-Anything",
"description": "CLI-Hub — Agent-native stateful CLI interfaces for softwares, codebases, and Web Services",
"updated": "2026-03-17"
"updated": "2026-03-18"
},
"clis": [
{
@@ -125,6 +125,18 @@
"skill_md": null,
"category": "diagrams"
},
{
"name": "notebooklm",
"display_name": "NotebookLM",
"version": "0.1.0",
"description": "Experimental NotebookLM harness scaffold wrapping the installed notebooklm CLI for notebook, source, chat, artifact, download, and sharing workflows",
"requires": "notebooklm CLI from notebooklm-py + valid local NotebookLM login session",
"homepage": "https://notebooklm.google.com",
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=notebooklm/agent-harness",
"entry_point": "cli-anything-notebooklm",
"skill_md": "notebooklm/agent-harness/cli_anything/notebooklm/skills/SKILL.md",
"category": "ai"
},
{
"name": "obs-studio",
"display_name": "OBS Studio",