Skip to content

Commit 6dcfba9

Browse files
committed
fix(cli): add explicit source delete-by-title command
1 parent 26df2c1 commit 6dcfba9

File tree

5 files changed

+308
-5
lines changed

5 files changed

+308
-5
lines changed

docs/cli-reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,11 @@ Supported source types: URLs, YouTube videos, files (PDF, text, Markdown, Word,
102102
| `rename <id> <title>` | Source ID, new title | - | `source rename src123 "New Name"` |
103103
| `refresh <id>` | Source ID | - | `source refresh src123` |
104104
| `delete <id>` | Source ID | - | `source delete src123` |
105+
| `delete-by-title <title>` | Exact source title | - | `source delete-by-title "My Source"` |
105106
| `wait <id>` | Source ID | `--timeout`, `--interval` | `source wait src123` |
106107

108+
`source delete <id>` accepts only full source IDs or unique partial-ID prefixes. To delete by exact source title, use `source delete-by-title "<title>"`.
109+
107110
### Research Commands (`notebooklm research <cmd>`)
108111

109112
| Command | Arguments | Options | Example |

docs/troubleshooting.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ If the title contains error-related text, remove the source and use the pre-fetc
210210
```bash
211211
# Remove incorrectly parsed source
212212
notebooklm source delete <source_id>
213+
# Or, if you only have the exact title:
214+
notebooklm source delete-by-title "Exact Source Title"
213215

214216
# Then re-add using the bird CLI method above
215217
```

src/notebooklm/cli/source.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
guide Get AI-generated source summary and keywords
99
stale Check if a URL/Drive source needs refresh
1010
delete Delete a source
11+
delete-by-title Delete a source by exact title
1112
rename Rename a source
1213
refresh Refresh a URL/Drive source
1314
add-drive Add a Google Drive document
1415
add-research Search web/drive and add sources from results
1516
"""
1617

1718
import asyncio
19+
import re
1820
from pathlib import Path
1921

2022
import click
@@ -33,6 +35,7 @@
3335
require_notebook,
3436
resolve_notebook_id,
3537
resolve_source_id,
38+
validate_id,
3639
with_client,
3740
)
3841

@@ -50,6 +53,7 @@ def source():
5053
guide Get AI-generated source summary and keywords
5154
stale Check if source needs refresh
5255
delete Delete a source
56+
delete-by-title Delete a source by exact title
5357
rename Rename a source
5458
refresh Refresh a URL/Drive source
5559
@@ -61,6 +65,87 @@ def source():
6165
pass
6266

6367

68+
def _build_id_ambiguity_error(source_id: str, matches) -> click.ClickException:
69+
"""Build a consistent ambiguity error for source ID prefix matches."""
70+
lines = [f"Ambiguous ID '{source_id}' matches {len(matches)} sources:"]
71+
for item in matches[:5]:
72+
title = item.title or "(untitled)"
73+
lines.append(f" {item.id[:12]}... {title}")
74+
if len(matches) > 5:
75+
lines.append(f" ... and {len(matches) - 5} more")
76+
lines.append("\nSpecify more characters to narrow down.")
77+
return click.ClickException("\n".join(lines))
78+
79+
80+
def _looks_like_full_source_id(source_id: str) -> bool:
81+
"""Return True for UUID-shaped source IDs that can skip list-based resolution."""
82+
return bool(
83+
re.fullmatch(
84+
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
85+
source_id,
86+
)
87+
)
88+
89+
90+
async def _resolve_source_for_delete(client, notebook_id: str, source_id: str):
91+
"""Resolve a source for delete, always validating against the live source list."""
92+
source_id = validate_id(source_id, "source")
93+
if _looks_like_full_source_id(source_id):
94+
return source_id
95+
96+
sources = await client.sources.list(notebook_id)
97+
matches = [item for item in sources if item.id.lower().startswith(source_id.lower())]
98+
99+
if len(matches) == 1:
100+
if matches[0].id != source_id:
101+
title = matches[0].title or "(untitled)"
102+
console.print(f"[dim]Matched: {matches[0].id[:12]}... ({title})[/dim]")
103+
return matches[0]
104+
105+
if len(matches) > 1:
106+
raise _build_id_ambiguity_error(source_id, matches)
107+
108+
title_matches = [item for item in sources if item.title == source_id]
109+
if title_matches:
110+
lines = [
111+
f"'{source_id}' matches {len(title_matches)} source title(s), not source IDs.",
112+
f"Use 'notebooklm source delete-by-title \"{source_id}\"' or delete by ID:",
113+
]
114+
for item in title_matches[:5]:
115+
lines.append(f" {item.id[:12]}... {item.title}")
116+
if len(title_matches) > 5:
117+
lines.append(f" ... and {len(title_matches) - 5} more")
118+
raise click.ClickException("\n".join(lines))
119+
120+
raise click.ClickException(
121+
f"No source found starting with '{source_id}'. "
122+
"Run 'notebooklm source list' to see available sources."
123+
)
124+
125+
126+
async def _resolve_source_by_exact_title(client, notebook_id: str, title: str):
127+
"""Resolve a source by exact title for the explicit delete-by-title flow."""
128+
title = validate_id(title, "source title")
129+
sources = await client.sources.list(notebook_id)
130+
matches = [item for item in sources if item.title == title]
131+
132+
if len(matches) == 1:
133+
return matches[0]
134+
135+
if len(matches) > 1:
136+
lines = [f"Title '{title}' matches {len(matches)} sources. Delete by ID instead:"]
137+
for item in matches[:5]:
138+
lines.append(f" {item.id[:12]}... {item.title}")
139+
if len(matches) > 5:
140+
lines.append(f" ... and {len(matches) - 5} more")
141+
raise click.ClickException("\n".join(lines))
142+
143+
raise click.ClickException(
144+
f"No source found with title '{title}'. "
145+
"Run 'notebooklm source list' to see available sources."
146+
)
147+
148+
64149
@source.command("list")
65150
@click.option(
66151
"-n",
@@ -274,8 +359,10 @@ def source_delete(ctx, source_id, notebook_id, yes, client_auth):
274359
async def _run():
275360
async with NotebookLMClient(client_auth) as client:
276361
nb_id_resolved = await resolve_notebook_id(client, nb_id)
277-
# Resolve partial ID to full ID
278-
resolved_id = await resolve_source_id(client, nb_id_resolved, source_id)
362+
resolved_source = await _resolve_source_for_delete(client, nb_id_resolved, source_id)
363+
resolved_id = (
364+
resolved_source if isinstance(resolved_source, str) else resolved_source.id
365+
)
279366

280367
if not yes and not click.confirm(f"Delete source {resolved_id}?"):
281368
return
@@ -289,6 +376,38 @@ async def _run():
289376
return _run()
290377

291378

379+
@source.command("delete-by-title")
380+
@click.argument("title")
381+
@click.option(
382+
"-n",
383+
"--notebook",
384+
"notebook_id",
385+
default=None,
386+
help="Notebook ID (uses current if not set)",
387+
)
388+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
389+
@with_client
390+
def source_delete_by_title(ctx, title, notebook_id, yes, client_auth):
391+
"""Delete a source by exact title."""
392+
nb_id = require_notebook(notebook_id)
393+
394+
async def _run():
395+
async with NotebookLMClient(client_auth) as client:
396+
nb_id_resolved = await resolve_notebook_id(client, nb_id)
397+
source = await _resolve_source_by_exact_title(client, nb_id_resolved, title)
398+
399+
if not yes and not click.confirm(f"Delete source '{source.title}' ({source.id})?"):
400+
return
401+
402+
success = await client.sources.delete(nb_id_resolved, source.id)
403+
if success:
404+
console.print(f"[green]Deleted source:[/green] {source.id}")
405+
else:
406+
console.print("[yellow]Delete may have failed[/yellow]")
407+
408+
return _run()
409+
410+
292411
@source.command("rename")
293412
@click.argument("source_id")
294413
@click.argument("new_title")

src/notebooklm/data/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ Before starting workflows, verify the CLI is ready:
129129
| Add file | `notebooklm source add ./file.pdf` |
130130
| Add YouTube | `notebooklm source add "https://youtube.com/..."` |
131131
| List sources | `notebooklm source list` |
132+
| Delete source by ID | `notebooklm source delete <source_id>` |
133+
| Delete source by exact title | `notebooklm source delete-by-title "Exact Title"` |
132134
| Wait for source processing | `notebooklm source wait <source_id>` |
133135
| Web research (fast) | `notebooklm source add-research "query"` |
134136
| Web research (deep) | `notebooklm source add-research "query" --mode deep --no-wait` |
@@ -173,7 +175,7 @@ Before starting workflows, verify the CLI is ready:
173175

174176
**Parallel safety:** Use explicit notebook IDs in parallel workflows. Commands supporting `-n` shorthand: `artifact wait`, `source wait`, `research wait/status`, `download *`. Download commands also support `-a/--artifact`. Other commands use `--notebook`. For chat, use `-c <conversation_id>` to target a specific conversation.
175177

176-
**Partial IDs:** Use first 6+ characters of UUIDs. Must be unique prefix (fails if ambiguous). Works for: `use`, `delete`, `wait` commands. For automation, prefer full UUIDs to avoid ambiguity.
178+
**Partial IDs:** Use first 6+ characters of UUIDs. Must be unique prefix (fails if ambiguous). Works for ID-based commands such as `use`, `source delete`, and `wait`. For exact source-title deletion, use `source delete-by-title "Title"`. For automation, prefer full UUIDs to avoid ambiguity.
177179

178180
## Command Output Formats
179181

0 commit comments

Comments
 (0)