mirror of
https://fastgit.cc/github.com/HKUDS/CLI-Anything
synced 2026-04-30 22:02:01 +08:00
Merge branch 'main' into feat/adguardhome-clean
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -111,4 +111,7 @@ assets/gen_typing_gif.py
|
||||
!/docs/
|
||||
/docs/*
|
||||
!/docs/hub/
|
||||
|
||||
!/notebooklm/
|
||||
/notebooklm/*
|
||||
/notebooklm/.*
|
||||
!/notebooklm/agent-harness/
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
29
notebooklm/agent-harness/ATTRIBUTION.md
Normal file
29
notebooklm/agent-harness/ATTRIBUTION.md
Normal 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.
|
||||
44
notebooklm/agent-harness/NOTEBOOKLM.md
Normal file
44
notebooklm/agent-harness/NOTEBOOKLM.md
Normal 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
|
||||
26
notebooklm/agent-harness/THIRD_PARTY_NOTICES.md
Normal file
26
notebooklm/agent-harness/THIRD_PARTY_NOTICES.md
Normal 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
|
||||
112
notebooklm/agent-harness/cli_anything/notebooklm/README.md
Normal file
112
notebooklm/agent-harness/cli_anything/notebooklm/README.md
Normal 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
|
||||
@@ -0,0 +1,3 @@
|
||||
"""NotebookLM harness package."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Module entry point for cli-anything-notebooklm."""
|
||||
|
||||
from cli_anything.notebooklm.notebooklm_cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
"""Core modules for the NotebookLM harness."""
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for the NotebookLM harness."""
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
"""Utility modules for the NotebookLM harness."""
|
||||
@@ -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()
|
||||
54
notebooklm/agent-harness/setup.py
Normal file
54
notebooklm/agent-harness/setup.py
Normal 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,
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user