Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7054,7 +7054,7 @@
"default": false,
"envvar": null,
"has_custom_shell_complete": false,
"help": "Suppress INFO/WARN log records on stderr (only ERROR survives). Mutually exclusive with -v/-vv.",
"help": "Suppress status output and INFO/WARN log records (only errors survive). Mutually exclusive with -v/-vv.",
"is_flag": true,
"kind": "option",
"multiple": false,
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Items that need attention when upgrading from 0.4.x. Full migration prose lives
- **Shell completion + ID-aware completers.** `notebooklm completion <bash|zsh|fish>` prints a completion script; once sourced, `-n/--notebook`, `-s/--source`, and `-a/--artifact` TAB-complete live IDs from the active profile.
- **SIGINT resume hint on long-running `--wait` ops.** Ctrl-C exits 130 with `Cancelled. Resume with: notebooklm artifact poll <task_id>` (or the parallel `source wait <source_id>`) instead of dumping a `KeyboardInterrupt` traceback. Under `--json`: `{"error": true, "code": "CANCELLED", "resume_hint": "..."}`.
- **Unix `-` stdin convention on `ask`, `note create`, `source add`, and `--prompt-file`.** `echo "what is X?" | notebooklm ask -` and similar pipelines now compose without temp files.
- **`NOTEBOOKLM_NOTEBOOK` env var + global `--quiet` flag.** `NOTEBOOKLM_NOTEBOOK=<id> notebooklm ask "..."` works without `-n/--notebook` or a prior `notebooklm use`. `--quiet` raises the package logger floor to ERROR (mutually exclusive with `-v/-vv`).
- **`NOTEBOOKLM_NOTEBOOK` env var + global `--quiet` flag.** `NOTEBOOKLM_NOTEBOOK=<id> notebooklm ask "..."` works without `-n/--notebook` or a prior `notebooklm use`. `--quiet` suppresses status output, raises the package logger floor to ERROR, and remains mutually exclusive with `-v/-vv`.
- **`source add` warns when a path-shaped argument doesn't exist.** A typo like `./missin.md` previously fell through to inline-text ingestion silently; an advisory stderr warning now fires before the source is added.
- **`--follow-symlinks` opt-in on `source add`.** See **Breaking changes** above; scripts that point at symlink-resolved paths must add the flag to keep working (#476).
- **`source clean` command** (#261). Bulk-delete failed/stale sources in a notebook; pairs with `source stale` for inspection. Supports `--all`, `--latest`, `--earliest`, `--dry-run`, and `--json`.
Expand Down
4 changes: 2 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ notebooklm [-p PROFILE] [--storage PATH] [--version] [-v|--quiet] <command> [OPT
- `-p, --profile NAME` - Use a named profile (overrides `NOTEBOOKLM_PROFILE` env var)
- `--storage PATH` - Override the default storage location
- `-v, --verbose` - Increase verbosity (`-v` for INFO, `-vv` for DEBUG)
- `--quiet` - Suppress INFO/WARN log records on stderr (only ERROR survives). Mutually exclusive with `-v`/`-vv`; combining the two raises `UsageError` (exit `2`).
- `--quiet` - Suppress status output and INFO/WARN log records (only errors survive). Structured `--json` payloads are still emitted. Mutually exclusive with `-v`/`-vv`; combining the two raises `UsageError` (exit `2`).
- `--version` - Show version and exit
- `--help` - Show help message

Expand Down Expand Up @@ -1573,7 +1573,7 @@ When using this CLI programmatically:

6. **Stdin pipelines**: Four surfaces accept the canonical Unix `-` placeholder for "read from stdin": `notebooklm ask -`, `notebooklm ask --prompt-file -`, `notebooklm note create -` (or `--content -`), and `notebooklm source add -` (forces text-source path; bypasses path-shaped detection). Same convention applies to the various `generate <kind> --prompt-file -` flags.

7. **Quiet logs for CI/cron**: Pass `--quiet` (root-level) to suppress INFO/WARN log records; only ERROR survives. `--quiet` is mutually exclusive with `-v/-vv` (combining the two raises `UsageError` with exit `2`). For long-running keepalive loops, `notebooklm auth refresh --quiet` is the subcommand-scoped equivalent.
7. **Quiet output for CI/cron**: Pass `--quiet` (root-level) to suppress status output and INFO/WARN log records; errors still surface and structured `--json` payloads are still emitted. `--quiet` is mutually exclusive with `-v/-vv` (combining the two raises `UsageError` with exit `2`). For long-running keepalive loops, `notebooklm auth refresh --quiet` is the subcommand-scoped equivalent.

8. **Error handling**: Commands exit with non-zero status on failure (`1` for user/library errors, `2` for system/unexpected errors per [CLI Exit-Code Convention](cli-exit-codes.md)). With `--json`, failures surface as a typed envelope `{"error": true, "code": "<TYPED_CODE>", "message": "..."}` on stdout; without `--json`, error messages go to stderr.

Expand Down
17 changes: 8 additions & 9 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,13 @@ falsy and opts back into the soft-mode fallback.
bare `export NOTEBOOKLM_NOTEBOOK=` does not block `notebooklm use` /
`-n/--notebook` from resolving.

**The `--quiet` global flag.** `notebooklm --quiet <subcommand>` raises the
`notebooklm` package logger floor to `ERROR` for the duration of one
invocation, so cron and CI logs stay clean while real failures still surface.
It is mutually exclusive with `-v/-vv` — combining the two raises a
`UsageError` (exit `2`) since the resolved log levels conflict
(`ERROR` vs `INFO`/`DEBUG`). For per-call (rather than per-shell) silencing
of `INFO`/`WARN` the global flag is the preferred surface; `NOTEBOOKLM_LOG_LEVEL`
remains the right tool for shell-wide / always-on suppression.
**The `--quiet` global flag.** `notebooklm --quiet <subcommand>` suppresses
status prose and raises the `notebooklm` package logger floor to `ERROR` for
the duration of one invocation, so cron and CI logs stay clean while real
failures still surface. Structured `--json` payloads are still emitted. It is
mutually exclusive with `-v/-vv` — combining the two raises a `UsageError`
(exit `2`) since the resolved log levels conflict (`ERROR` vs `INFO`/`DEBUG`).
For shell-wide / always-on log suppression, use `NOTEBOOKLM_LOG_LEVEL`.

### NOTEBOOKLM_HOME

Expand Down Expand Up @@ -361,7 +360,7 @@ batchexecute response contains RPC IDs but not the one the call requested
| `--storage PATH` | Path to storage_state.json | `$NOTEBOOKLM_HOME/profiles/<profile>/storage_state.json` |
| `-p, --profile NAME` | Use a named profile | Active profile or `default` |
| `-v, --verbose` | Enable verbose output (`-v` for INFO, `-vv` for DEBUG) | - |
| `--quiet` | Suppress INFO/WARN logs on stderr (only ERROR survives). Mutually exclusive with `-v`. | - |
| `--quiet` | Suppress status output and INFO/WARN logs (only errors survive). Mutually exclusive with `-v`. | - |
| `--version` | Show version | - |
| `--help` | Show help | - |

Expand Down
14 changes: 8 additions & 6 deletions src/notebooklm/cli/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .options import json_option, list_options, notebook_option, wait_polling_options
from .rendering import (
cli_name_to_artifact_type,
cli_print,
console,
get_artifact_type_display,
json_output_response,
Expand Down Expand Up @@ -309,8 +310,8 @@ async def _run():
if json_output:
json_output_response({"id": resolved_id, "renamed": True, "new_title": new_title})
else:
console.print(f"[green]Renamed artifact:[/green] {resolved_id}")
console.print(f"[bold]New title:[/bold] {new_title}")
cli_print(f"[green]Renamed artifact:[/green] {resolved_id}", ctx=ctx)
cli_print(f"[bold]New title:[/bold] {new_title}", ctx=ctx)

return _run()

Expand Down Expand Up @@ -359,17 +360,18 @@ async def _run():
}
)
else:
console.print(f"[yellow]Cleared mind map:[/yellow] {resolved_id}")
console.print(
"[dim]Note: Mind maps are cleared, not removed. Google may garbage collect them later.[/dim]"
cli_print(f"[yellow]Cleared mind map:[/yellow] {resolved_id}", ctx=ctx)
cli_print(
"[dim]Note: Mind maps are cleared, not removed. Google may garbage collect them later.[/dim]",
ctx=ctx,
)
return

await client.artifacts.delete(nb_id_resolved, resolved_id)
if json_output:
json_output_response({"id": resolved_id, "deleted": True})
else:
console.print(f"[green]Deleted artifact:[/green] {resolved_id}")
cli_print(f"[green]Deleted artifact:[/green] {resolved_id}", ctx=ctx)

return _run()

Expand Down
16 changes: 14 additions & 2 deletions src/notebooklm/cli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,24 @@ def load_auth_from_storage(*args: Any, **kwargs: Any) -> Any:
return auth_helpers.load_auth_from_storage(*args, **kwargs)


def emit_status(msg: str, *, json_output: bool, style: str | None = None) -> None:
"""Emit a status / diagnostic line."""
def emit_status(
msg: str,
*,
json_output: bool,
style: str | None = None,
quiet: bool = False,
) -> None:
"""Emit a status / diagnostic line.

The ``quiet`` kwarg is forwarded to the renderer; when True, the line is
suppressed entirely. The renderer also honors the active root ``--quiet``
flag automatically inside a Click invocation.
"""
rendering_helpers.emit_status(
msg,
json_output=json_output,
style=style,
quiet=quiet,
stdout_console=console,
stderr_output_console=stderr_console,
)
Expand Down
16 changes: 8 additions & 8 deletions src/notebooklm/cli/note.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .error_handler import _output_error
from .input import read_stdin_text
from .options import json_option, notebook_option
from .rendering import console, json_output_response
from .rendering import cli_print, console, json_output_response
from .resolve import require_notebook, resolve_note_id, resolve_notebook_id


Expand Down Expand Up @@ -181,10 +181,10 @@ async def _run():
return

if result:
console.print("[green]Note created[/green]")
console.print(result)
cli_print("[green]Note created[/green]", ctx=ctx)
cli_print(result, ctx=ctx)
else:
console.print("[yellow]Creation may have failed[/yellow]")
cli_print("[yellow]Creation may have failed[/yellow]", ctx=ctx)

return _run()

Expand Down Expand Up @@ -286,7 +286,7 @@ async def _no_changes():
}
)
return
console.print("[yellow]Provide --title and/or --content[/yellow]")
cli_print("[yellow]Provide --title and/or --content[/yellow]", ctx=ctx)

return _no_changes()

Expand All @@ -313,7 +313,7 @@ async def _run():
json_output_response(payload)
return

console.print(f"[green]Note updated:[/green] {resolved_id}")
cli_print(f"[green]Note updated:[/green] {resolved_id}", ctx=ctx)

return _run()

Expand Down Expand Up @@ -380,7 +380,7 @@ async def _run():
)
return

console.print(f"[green]Note renamed:[/green] {new_title}")
cli_print(f"[green]Note renamed:[/green] {new_title}", ctx=ctx)

return _run()

Expand Down Expand Up @@ -445,6 +445,6 @@ async def _run():
)
return

console.print(f"[green]Deleted note:[/green] {resolved_id}")
cli_print(f"[green]Deleted note:[/green] {resolved_id}", ctx=ctx)

return _run()
21 changes: 11 additions & 10 deletions src/notebooklm/cli/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .auth_runtime import with_client
from .context import clear_context, get_current_notebook, set_current_notebook
from .options import list_options, notebook_option
from .rendering import console, json_output_response
from .rendering import cli_print, console, json_output_response
from .resolve import require_notebook, resolve_notebook_id


Expand Down Expand Up @@ -132,12 +132,13 @@ async def _run():
json_output_response(data)
return

console.print(f"[green]Created notebook:[/green] {nb.id} - {nb.title}")
cli_print(f"[green]Created notebook:[/green] {nb.id} - {nb.title}", ctx=ctx)
if switch_context:
console.print("[dim]Context set to new notebook[/dim]")
cli_print("[dim]Context set to new notebook[/dim]", ctx=ctx)
else:
console.print(
f"[dim]Tip: pass --use next time, or run 'notebooklm use {nb.id}'.[/dim]"
cli_print(
f"[dim]Tip: pass --use next time, or run 'notebooklm use {nb.id}'.[/dim]",
ctx=ctx,
)

return _run()
Expand All @@ -164,13 +165,13 @@ async def _run():

success = await client.notebooks.delete(resolved_id)
if success:
console.print(f"[green]Deleted notebook:[/green] {resolved_id}")
cli_print(f"[green]Deleted notebook:[/green] {resolved_id}", ctx=ctx)
# Clear context if we deleted the current notebook
if get_current_notebook() == resolved_id:
clear_context()
console.print("[dim]Cleared current notebook context[/dim]")
cli_print("[dim]Cleared current notebook context[/dim]", ctx=ctx)
else:
console.print("[yellow]Delete may have failed[/yellow]")
cli_print("[yellow]Delete may have failed[/yellow]", ctx=ctx)

return _run()

Expand All @@ -189,8 +190,8 @@ async def _run():
async with NotebookLMClient(client_auth) as client:
resolved_id = await resolve_notebook_id(client, notebook_id)
await client.notebooks.rename(resolved_id, new_title)
console.print(f"[green]Renamed notebook:[/green] {resolved_id}")
console.print(f"[bold]New title:[/bold] {new_title}")
cli_print(f"[green]Renamed notebook:[/green] {resolved_id}", ctx=ctx)
cli_print(f"[bold]New title:[/bold] {new_title}", ctx=ctx)

return _run()

Expand Down
99 changes: 98 additions & 1 deletion src/notebooklm/cli/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import contextlib
import json
from typing import TYPE_CHECKING, Any, NoReturn

Expand All @@ -19,6 +20,20 @@
if TYPE_CHECKING:
from ..types import Artifact


def _resolve_quiet(ctx: click.Context | None) -> bool:
"""Resolve root ``--quiet`` without importing ``cli.runtime``."""
if ctx is None:
ctx = click.get_current_context(silent=True)
if ctx is None:
return False
try:
value = ctx.find_root().params.get("quiet", False)
except (AttributeError, RuntimeError):
return False
return value if isinstance(value, bool) else False


console = Console()
# Diagnostic / status output in --json mode must go to stderr so stdout stays
# parseable JSON for automation.
Expand All @@ -30,11 +45,16 @@ def _emit_status(
*,
json_output: bool,
style: str | None = None,
quiet: bool = False,
stdout_console: Console = console,
stderr_output_console: Console = stderr_console,
) -> None:
# Shared implementation for callers that need explicit stdout/stderr
# console injection while preserving the public ``emit_status`` wrapper.
# Quiet suppresses status prose only. Errors and JSON payloads use their
# own output paths and are intentionally unaffected.
if quiet or _resolve_quiet(None):
return
target = stderr_output_console if json_output else stdout_console
if style is not None:
target.print(msg, style=style)
Expand All @@ -47,19 +67,96 @@ def emit_status(
*,
json_output: bool,
style: str | None = None,
quiet: bool = False,
stdout_console: Console = console,
stderr_output_console: Console = stderr_console,
) -> None:
"""Emit a status / diagnostic line."""
"""Emit a status / diagnostic line.

Args:
msg: The status message to print (Rich markup allowed).
json_output: When True, route the line to stderr so stdout stays
parseable JSON for automation. When False, route to stdout.
style: Optional Rich style string (e.g. ``"bold red"``).
quiet: When True, suppress entirely. The active root ``--quiet`` flag
is also honored automatically inside a Click invocation.
stdout_console: Override target for non-JSON mode (defaults to the
module-level ``console`` — patchable by callers).
stderr_output_console: Override target for JSON mode (defaults to
the module-level ``stderr_console``).
"""
_emit_status(
msg,
json_output=json_output,
style=style,
quiet=quiet,
stdout_console=stdout_console,
stderr_output_console=stderr_output_console,
)


def cli_print(
*args: Any,
ctx: click.Context | None = None,
output_console: Console | None = None,
**kwargs: Any,
) -> None:
"""Quiet-aware drop-in replacement for ``console.print(...)``.

Use this for CLI status prose that should disappear under root
``--quiet``. Outside a Click invocation, it forwards normally.

Args:
*args: Positional args forwarded to ``console.print`` (e.g. a
Rich-markup string, a ``Table``, a ``Panel``).
ctx: Optional Click context. When omitted, the helper consults
``click.get_current_context(silent=True)``; outside any Click
context (library importers, direct unit tests) the helper
forwards unconditionally so non-CLI callers are unaffected.
output_console: Optional Console override. Defaults to the module-
level ``console`` so tests that patch ``rendering.console``
(or ``helpers.console``) still see the print routed through
their patched target.
**kwargs: Forwarded to ``console.print`` (``style``, ``markup``,
``highlight``, etc.).

Note:
Errors must NOT route through this helper — ``--quiet`` silences
*status*, not *errors*. Error paths use ``_output_error`` (which
writes to stderr unconditionally) or, for JSON envelopes,
``json_error_response``.
"""
if _resolve_quiet(ctx):
return
target = output_console if output_console is not None else console
target.print(*args, **kwargs)


def cli_status(
status_message: str,
*,
ctx: click.Context | None = None,
output_console: Console | None = None,
**kwargs: Any,
):
"""Quiet-aware replacement for ``console.status(...)``.

Returns a context manager that wraps a Rich spinner. Under ``--quiet``
(resolved the same way as :func:`cli_print`), the spinner is replaced
with a no-op context manager so no animation reaches the terminal.

Args:
status_message: The status text shown next to the spinner.
ctx: Optional Click context (see :func:`cli_print`).
output_console: Optional Console override.
**kwargs: Forwarded to ``console.status`` (e.g. ``spinner=`` kwarg).
"""
if _resolve_quiet(ctx):
return contextlib.nullcontext()
target = output_console if output_console is not None else console
return target.status(status_message, **kwargs)


_CLI_ARTIFACT_ALIASES = {
"flashcard": "flashcards", # CLI uses singular, enum uses plural
}
Expand Down
Loading
Loading