Skip to content

feat(cli): add --json to source mutating + detail + stale commands (I3) - #497

Merged
teng-lin merged 1 commit into
mainfrom
cli-ux-remediation/p2-t3-source-json
May 14, 2026
Merged

teng-lin merged 1 commit into
mainfrom
cli-ux-remediation/p2-t3-source-json

Conversation

@teng-lin

@teng-lin teng-lin commented May 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

P2.T3 from the cli-ux-remediation plan (.sisyphus/plans/cli-ux-remediation/phase-2.md). Adds the standard --json flag to eight source subcommands so shell scripts and AI agents can branch on a structured JSON document instead of scraping Rich-formatted text.

What changed

Eight commands now accept --json (via @json_option from cli/options.py):

  • source delete — {action, source_id, notebook_id, success, status}
  • source delete-by-title — adds title
  • source rename — adds title (post-rename)
  • source refresh — three-state status (refreshed / no_result)
  • source clean — already_clean / dry_run / cancelled / completed, with per-deletion failure list
  • source get — mirrors the Source dataclass; carries a found boolean
  • source add-drive — full source payload plus the Drive mime_type and drive_file_id echo
  • source stale — PRESERVES the inverted exit-code semantics documented in docs/cli-exit-codes.md (stale=0, fresh=1). The JSON body carries the boolean explicitly via {"stale": <bool>, "fresh": <bool>} for callers who would prefer to branch on a field rather than the exit code.

Non-JSON output, exit codes, and confirmation prompts are unchanged. source get on not-found still exits 0 today (Phase 3 / C1 will flip that); the JSON branch already surfaces {"found": false, "source": null} so callers can branch on the field without text-scraping.

Constraints honored

  • Only cli/source.py and tests/unit/cli/test_source.py modified — strict file isolation per the phase plan (T1, T2, T4, T5 run in parallel).
  • cli/options.py untouched.
  • helpers.handle_error untouched.
  • source add and the four commands that already had inline --json (list, fulltext, guide, wait) untouched — only the eight in scope.
  • The source group docstring (P1.T3) is preserved.
  • source stale and source wait exit codes remain as documented in docs/cli-exit-codes.md.

Test plan

  • 13 new TestSourceJsonOutput smoke tests (CliRunner + mocked client).
  • Two of the new tests pin source stale --json to exit 0 when stale and exit 1 when fresh — regression guard against accidental inversion.
  • All 106 source-CLI tests pass (93 existing + 13 new).
  • uv run ruff format . — clean.
  • uv run ruff check . — clean.
  • uv run mypy src/notebooklm --ignore-missing-imports — clean.
  • uv run pytest --cov=src/notebooklm --cov-fail-under=90 — 3475 passed, coverage 92.56%, source.py at 94%.

Plan / spec

  • Phase plan: .sisyphus/plans/cli-ux-remediation/phase-2.md#p2t3----json-on-source-commands
  • Exit-code policy reference: docs/cli-exit-codes.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added --json flag to eight source subcommands (get, delete, delete-by-title, rename, refresh, add-drive, stale, clean) to emit structured JSON for automation.
    • JSON responses include action/status and relevant object fields (e.g., found, stale/fresh, drive info, deletion outcomes).
  • Documentation

    • CHANGELOG updated to document --json behavior, fields, and inverted exit semantics for stale.
  • Tests

    • Added tests validating --json output shapes, exit-code semantics, and clean/dry-run/cancel paths.

Review Change Stack

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@teng-lin

Copy link
Copy Markdown
Owner Author

@claude review

@coderabbitai

coderabbitai Bot commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c719575-d01c-497e-a66c-395f1842098d

📥 Commits

Reviewing files that changed from the base of the PR and between 00c2640 and e65eef8.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/notebooklm/cli/source.py
  • tests/unit/cli/test_source.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/cli/test_source.py
  • src/notebooklm/cli/source.py

📝 Walkthrough

Walkthrough

Adds a --json option to eight notebooklm source subcommands; each handler gains json_output and emits structured JSON on stdout (including stale/fresh and inverted exit-code behavior for source stale), plus tests and a changelog entry.

Changes

JSON output for source commands

Layer / File(s) Summary
JSON option infrastructure
src/notebooklm/cli/source.py
Import json_option decorator from .options to expose --json across commands.
source get JSON support
src/notebooklm/cli/source.py
Add @json_option and json_output; resolution receives JSON context and CLI emits found and source (or source: null + source_id) JSON.
source delete and delete-by-title JSON support
src/notebooklm/cli/source.py
Both commands add @json_option/json_output; JSON mode emits cancellation payloads on user decline and deletion-result payloads with success/status.
source rename JSON support
src/notebooklm/cli/source.py
Add @json_option/json_output; on success emit rename action JSON with source_id, notebook_id, new title, and renamed flag.
source refresh JSON support
src/notebooklm/cli/source.py
Add @json_option/json_output; suppress spinner for JSON runs and emit refresh action JSON with status (refreshed, no_result, or boolean-true handling).
source add-drive JSON support
src/notebooklm/cli/source.py
Add @json_option/json_output; JSON mode emits add-drive action and nested source object including drive_file_id and mime_type.
source stale JSON support
src/notebooklm/cli/source.py
Add @json_option/json_output; emit stale and fresh booleans in JSON and exit with 0 if stale, 1 if fresh.
source clean JSON support
src/notebooklm/cli/source.py
Add @json_option/json_output; JSON-mode returns already_clean, dry_run, or cancelled early, and on completion emits completed with deletion/failure counts and per-source failures.
Changelog documentation
CHANGELOG.md
Document --json support across the eight source subcommands, emitted fields, and preserved stale exit-code semantics.
JSON output test suite
tests/unit/cli/test_source.py
Add TestSourceJsonOutput with helper to patch auth tokens; tests invoke each command with --json, assert valid JSON stdout, verify payload shapes/fields, and pin exit-code behavior for stale/clean.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • teng-lin/notebooklm-py#450: Modifies src/notebooklm/cli/source.py flows to propagate json_output and JSON/diagnostic routing that overlaps with this PR.
  • teng-lin/notebooklm-py#261: Earlier changes to source clean that this PR extends with --json output mode.

Suggested labels

enhancement

Poem

🐰 Eight commands now whisper in JSON song,
Keys and booleans tidy and strong,
Stale flips its code, clean tells its tale,
Tests hop in to prove every trail,
Automation cheers—rabbit dances along.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding --json support to eight source CLI subcommands including mutating, detail, and stale commands.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cli-ux-remediation/p2-t3-source-json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented May 14, 2026 •

Copy link
Copy Markdown

Claude finished @teng-lin's task in 2m 22s —— View job


Code Review — PR #497: --json on source mutating + detail + stale commands

Tasks

  • Gather context and understand the request
  • Read changed files (cli/source.py, tests/unit/cli/test_source.py, CHANGELOG.md)
  • Diff against origin/main
  • Review code for correctness, style, and edge cases
  • Post comprehensive review feedback

Overall this is a clean, well-scoped PR. The inverted exit-code semantics for source stale are correctly preserved and well-documented, json_output is properly threaded to diagnostic helpers, and the spinner/status display is correctly suppressed in JSON mode. A few schema inconsistencies are worth addressing before merge.


Issues

Medium — cancelled branch omits success in source delete / source delete-by-title

The "deleted" / "unknown" branch includes "success": bool but the "cancelled" branch does not. Any caller that does data["success"] will get a KeyError when the user cancels.

src/notebooklm/cli/source.py lines ~629–638 and ~679–686:

# "cancelled" branch — missing "success"
{
    "action": "delete",
    "source_id": resolved_id,
    "notebook_id": nb_id_resolved,
    "status": "cancelled",
    # ← no "success" key
}

Fix: add "success": False to both cancelled payloads so the schema is uniform across all three outcomes (deleted / unknown / cancelled). Fix this →


Minor — source clean "already_clean" status omits "candidate_count"

Every other clean status (dry_run, cancelled, completed) includes "candidate_count": len(candidates). "already_clean" does not, even though a consistent key of 0 is trivially available.

src/notebooklm/cli/source.py lines ~1347–1358:

# already_clean branch
{
    "action": "clean",
    "notebook_id": nb_id_resolved,
    "status": "already_clean",
    "candidates": [],
    "deleted_count": 0,
    "failure_count": 0,
    # ← no "candidate_count"
}

Callers expecting a uniform schema must special-case this status or fall back to len(data["candidates"]). Fix this →


Minor — source get (found) omits "notebook_id"

Every other command's successful JSON response includes "notebook_id", but source get does not — in either the found or not-found branch.

src/notebooklm/cli/source.py lines ~570–582:

data = {
    "source": { "id": src.id, ... },
    "found": True,
    # ← no "notebook_id"
}

Minor — source add-drive omits "status" field

All other mutating commands include a "status" key (e.g., "renamed", "refreshed", "deleted"). add-drive includes only "action", "source", and "notebook_id". Adding "status": "added" would make the schema consistent. Fix this →


Minor — source refresh JSON schema differs between src being a Source vs True

When src is a Source object the response includes "title", but when src is True it does not. A consumer unconditionally accessing data["title"] will get a KeyError in the True path. Since there is no title available in that case, "title": None would make the contract explicit. Fix this →


Nit — missing test coverage for source clean --json with deletion failures

test_source_clean_json_completed only exercises the happy path (delete returns True). There's no test verifying that the failures list is populated and surfaced in JSON when one or more delete calls raise an exception. This is the most complex branch (asyncio.gather with return_exceptions=True) and would benefit from a dedicated test.


Positive observations

  • Inverted exit-code semantics for source stale --json are correctly preserved and clearly documented in both the docstring and inline comments.
  • json_output is correctly forwarded to resolve_notebook_id / resolve_source_id so "Matched…" diagnostics go to stderr rather than polluting stdout.
  • The _candidates_payload() local helper in source_clean is a clean way to avoid repeating the list comprehension across four different response paths.
  • status_id in source get JSON surfacing the raw integer alongside the string is a nice touch for callers who need to round-trip the numeric value.
  • Tests pin the exit-code behavior for both branches of source stale --json — good regression guard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/notebooklm/cli/source.py`:
- Around line 1420-1431: The JSON output and summary over-report deletions
because the loop increments the `deleted` counter for any non-exception result;
update the deletion handling so only truthy/True delete results increment
`deleted` (or compute `deleted` from a list of successful IDs) and ensure the
`deleted_count` passed to `json_output_response` (and the non-JSON summary) uses
that corrected count; look for the deletion loop that updates `deleted` and the
JSON payload construction using `deleted`, `_candidates_payload()`,
`nb_id_resolved`, `candidates`, `failures`, and change the logic to treat a
False return from the delete operation as a failure (append to `failures`) and
only count True results as successful deletions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a0c9a08-e6c6-4ff4-962a-646f053ff2c9

📥 Commits

Reviewing files that changed from the base of the PR and between 0efe4df and 00c2640.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/notebooklm/cli/source.py
  • tests/unit/cli/test_source.py

Comment on lines +1420 to +1431
if json_output:
json_output_response(
{
"action": "clean",
"notebook_id": nb_id_resolved,
"status": "completed",
"candidates": _candidates_payload(),
"candidate_count": len(candidates),
"deleted_count": deleted,
"failure_count": len(failures),
"failures": [{"id": sid, "error": err} for sid, err in failures],
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

source clean --json can over-report successful deletions.

deleted_count is derived from a loop where any non-exception result increments deleted; a False delete result is currently counted as success. This makes JSON (and non-JSON summary) inaccurate and can hide failed deletions from automation.

Suggested fix
             for i in range(0, len(delete_list), chunk_size):
                 chunk = delete_list[i : i + chunk_size]
                 delete_tasks = [client.sources.delete(nb_id_resolved, sid) for sid in chunk]
                 results = await asyncio.gather(*delete_tasks, return_exceptions=True)
                 for sid, r in zip(chunk, results, strict=True):
                     if isinstance(r, Exception):
                         failures.append((sid, str(r)))
-                    else:
+                    elif r:
                         deleted += 1
+                    else:
+                        failures.append((sid, "delete returned False"))
                 if i + chunk_size < len(delete_list):
                     await asyncio.sleep(0.5)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/notebooklm/cli/source.py` around lines 1420 - 1431, The JSON output and
summary over-report deletions because the loop increments the `deleted` counter
for any non-exception result; update the deletion handling so only truthy/True
delete results increment `deleted` (or compute `deleted` from a list of
successful IDs) and ensure the `deleted_count` passed to `json_output_response`
(and the non-JSON summary) uses that corrected count; look for the deletion loop
that updates `deleted` and the JSON payload construction using `deleted`,
`_candidates_payload()`, `nb_id_resolved`, `candidates`, `failures`, and change
the logic to treat a False return from the delete operation as a failure (append
to `failures`) and only count True results as successful deletions.

Closes P2.T3 from the cli-ux-remediation plan. Eight ``source``
subcommands now accept the standard ``--json`` flag from
``cli/options.py:json_option`` and emit a structured JSON document on
stdout for parseable automation:

- ``source delete``        — {action, source_id, notebook_id, success, status}
- ``source delete-by-title`` — adds ``title``
- ``source rename``        — adds ``title`` (post-rename)
- ``source refresh``       — three-state status (refreshed/no_result)
- ``source clean``         — already_clean / dry_run / cancelled / completed
                             with per-deletion failure list
- ``source get``           — mirrors ``Source`` dataclass; ``found`` flag
- ``source add-drive``     — {action, source{id,title,type,url,
                              drive_file_id, mime_type}, notebook_id}
- ``source stale``         — PRESERVES the inverted exit-code semantics
                             documented in docs/cli-exit-codes.md
                             (stale=0, fresh=1) — JSON body carries the
                             boolean explicitly via {stale, fresh}

Non-JSON output, exit codes, and confirmation prompts are unchanged.
``source get`` not-found still exits 0 (Phase 3 / C1 will flip this);
the JSON branch surfaces ``{found: false, source: null}`` so callers
can already branch on the field today.

Adds 13 JSON-output smoke tests (``TestSourceJsonOutput``) using
``CliRunner`` with a mocked client. Two of the tests pin the inverted
``source stale --json`` exit codes so future refactors can't silently
flip them.

CHANGELOG entry under [Unreleased] / Added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@teng-lin
teng-lin force-pushed the cli-ux-remediation/p2-t3-source-json branch from 00c2640 to e65eef8 Compare May 14, 2026 19:49
@teng-lin
teng-lin merged commit bfa3ec3 into main May 14, 2026
19 checks passed
@teng-lin
teng-lin deleted the cli-ux-remediation/p2-t3-source-json branch June 4, 2026 09:49
zeekay pushed a commit to Dream-AI-4444/notebooklm-py that referenced this pull request Sep 9, 2026
…3) (teng-lin#497)

Closes P2.T3 from the cli-ux-remediation plan. Eight ``source``
subcommands now accept the standard ``--json`` flag from
``cli/options.py:json_option`` and emit a structured JSON document on
stdout for parseable automation:

- ``source delete``        — {action, source_id, notebook_id, success, status}
- ``source delete-by-title`` — adds ``title``
- ``source rename``        — adds ``title`` (post-rename)
- ``source refresh``       — three-state status (refreshed/no_result)
- ``source clean``         — already_clean / dry_run / cancelled / completed
                             with per-deletion failure list
- ``source get``           — mirrors ``Source`` dataclass; ``found`` flag
- ``source add-drive``     — {action, source{id,title,type,url,
                              drive_file_id, mime_type}, notebook_id}
- ``source stale``         — PRESERVES the inverted exit-code semantics
                             documented in docs/cli-exit-codes.md
                             (stale=0, fresh=1) — JSON body carries the
                             boolean explicitly via {stale, fresh}

Non-JSON output, exit codes, and confirmation prompts are unchanged.
``source get`` not-found still exits 0 (Phase 3 / C1 will flip this);
the JSON branch surfaces ``{found: false, source: null}`` so callers
can already branch on the field today.

Adds 13 JSON-output smoke tests (``TestSourceJsonOutput``) using
``CliRunner`` with a mocked client. Two of the tests pin the inverted
``source stale --json`` exit codes so future refactors can't silently
flip them.

CHANGELOG entry under [Unreleased] / Added.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant