deepdiver 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
deepdiver/deepdive.py ADDED
@@ -0,0 +1,801 @@
1
+ """
2
+ DeepDiver CLI Module
3
+ Main command-line interface for NotebookLM Podcast Automation System
4
+
5
+ This module provides the command-line interface for DeepDiver,
6
+ enabling users to create podcasts from documents through terminal commands.
7
+
8
+ Assembly Team: Jerry ⚡, Nyro ♠️, Aureon 🌿, JamAI 🎸, Synth 🧵
9
+ """
10
+
11
+ import asyncio
12
+ import os
13
+ import sys
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ import click
19
+ from rich.console import Console
20
+ from rich.panel import Panel
21
+ from rich.text import Text
22
+
23
+ from .notebooklm_automator import (
24
+ NotebookLMAutomator,
25
+ find_chrome_executable,
26
+ check_chrome_cdp_running,
27
+ launch_chrome_cdp,
28
+ get_cdp_url
29
+ )
30
+
31
+
32
+ # Initialize Rich console for beautiful output
33
+ console = Console()
34
+
35
+
36
+ def print_assembly_header():
37
+ """Print the Assembly team header."""
38
+ header_text = Text("♠️🌿🎸🧵 DeepDiver - NotebookLM Podcast Automation", style="bold blue")
39
+ subtitle = Text("Terminal-to-Web Audio Creation Bridge", style="italic green")
40
+
41
+ console.print(Panel.fit(
42
+ f"{header_text}\n{subtitle}",
43
+ border_style="blue",
44
+ padding=(1, 2)
45
+ ))
46
+
47
+
48
+ @click.group()
49
+ @click.version_option(version="0.1.0", prog_name="DeepDiver")
50
+ def cli():
51
+ """
52
+ 🎙️ DeepDiver - NotebookLM Podcast Automation System
53
+
54
+ Create podcasts from documents using NotebookLM's Audio Overview feature
55
+ through terminal commands and browser automation.
56
+
57
+ Assembly Team: Jerry ⚡, Nyro ♠️, Aureon 🌿, JamAI 🎸, Synth 🧵
58
+ """
59
+ print_assembly_header()
60
+
61
+
62
+ @cli.command()
63
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
64
+ help='Path to configuration file')
65
+ def init(config: str):
66
+ """
67
+ Initialize DeepDiver configuration and setup.
68
+
69
+ This command:
70
+ - Validates configuration file
71
+ - Checks Chrome CDP status
72
+ - Offers to launch Chrome automatically
73
+ - Provides setup instructions for NotebookLM
74
+ """
75
+ console.print("♠️🌿🎸🧵 DeepDiver Initialization", style="bold blue")
76
+ console.print()
77
+
78
+ try:
79
+ # Check if config file exists
80
+ if not os.path.exists(config):
81
+ console.print(f"❌ Configuration file not found: {config}", style="red")
82
+ console.print("Please ensure deepdiver.yaml exists in the project directory.", style="yellow")
83
+ return
84
+
85
+ # Test configuration loading
86
+ automator = NotebookLMAutomator(config)
87
+ console.print("✅ Configuration loaded successfully", style="green")
88
+
89
+ # Get CDP URL from configuration
90
+ cdp_url = get_cdp_url(config_path=config)
91
+ console.print(f"🔗 CDP URL: {cdp_url}", style="cyan")
92
+
93
+ # ═══════════════════════════════════════════════════════════════
94
+ # CHROME CDP SETUP - Auto-launch capability
95
+ # ♠️🌿🎸🧵 G.Music Assembly - Following simexp patterns
96
+ # ═══════════════════════════════════════════════════════════════
97
+
98
+ console.print()
99
+ console.print("🚀 Chrome CDP Setup", style="bold blue")
100
+ console.print(" DeepDiver needs Chrome running with remote debugging.", style="dim")
101
+ console.print()
102
+
103
+ # Check if Chrome CDP is already running
104
+ if check_chrome_cdp_running(cdp_url):
105
+ console.print("✅ Chrome CDP is already running!", style="green")
106
+ console.print(f" Connected at: {cdp_url}", style="dim")
107
+ else:
108
+ console.print("⚠️ Chrome CDP is not running", style="yellow")
109
+
110
+ # Offer to launch Chrome automatically
111
+ chrome_cmd = find_chrome_executable()
112
+
113
+ if chrome_cmd:
114
+ console.print(f" 🔍 Found Chrome: {chrome_cmd}", style="cyan")
115
+ console.print()
116
+
117
+ # Ask user if they want to auto-launch
118
+ launch = click.confirm(" Launch Chrome automatically with CDP?", default=True)
119
+
120
+ if launch:
121
+ console.print(" 🚀 Launching Chrome...", style="blue")
122
+
123
+ if launch_chrome_cdp():
124
+ console.print(" ✅ Chrome launched successfully with CDP on port 9222", style="green")
125
+ console.print(f" 🔗 Accessible at: http://localhost:9222", style="dim")
126
+ else:
127
+ console.print(" ⚠️ Could not launch Chrome automatically", style="yellow")
128
+ console.print()
129
+ console.print(" Run manually:", style="yellow")
130
+ console.print(f" {chrome_cmd} --remote-debugging-port=9222 --user-data-dir=~/.chrome-deepdiver &", style="cyan")
131
+ else:
132
+ console.print()
133
+ console.print(" Run this command to start Chrome with CDP:", style="yellow")
134
+ console.print(f" {chrome_cmd} --remote-debugging-port=9222 --user-data-dir=~/.chrome-deepdiver &", style="cyan")
135
+ else:
136
+ console.print(" ⚠️ Could not find Chrome/Chromium on your system", style="yellow")
137
+ console.print()
138
+ console.print(" Install Chrome and run:", style="yellow")
139
+ console.print(" google-chrome --remote-debugging-port=9222 --user-data-dir=~/.chrome-deepdiver &", style="cyan")
140
+
141
+ # ═══════════════════════════════════════════════════════════════
142
+ # NOTEBOOKLM SETUP INSTRUCTIONS
143
+ # ═══════════════════════════════════════════════════════════════
144
+
145
+ console.print()
146
+ console.print("📝 NotebookLM Setup Instructions", style="bold blue")
147
+ console.print(" 1. A Chrome window has opened (or is already open)", style="dim")
148
+ console.print(" 2. Go to: https://notebooklm.google.com", style="dim")
149
+ console.print(" 3. Login with your Google account", style="dim")
150
+ console.print(" 4. Keep this Chrome window open while using DeepDiver", style="dim")
151
+ console.print()
152
+
153
+ console.print("💡 Ready to test? Run: deepdiver test", style="bold green")
154
+ console.print()
155
+ console.print("🎉 DeepDiver initialization complete!", style="bold green")
156
+
157
+ except Exception as e:
158
+ console.print(f"❌ Initialization failed: {e}", style="red")
159
+ import traceback
160
+ console.print(traceback.format_exc(), style="dim")
161
+
162
+
163
+ @cli.command()
164
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
165
+ help='Path to configuration file')
166
+ def test(config: str):
167
+ """Test NotebookLM connection and automation setup."""
168
+ console.print("🧪 Testing NotebookLM Connection...", style="blue")
169
+
170
+ async def run_test():
171
+ automator = NotebookLMAutomator(config)
172
+
173
+ try:
174
+ # Test browser connection
175
+ console.print("🔗 Testing browser connection...", style="blue")
176
+ if await automator.connect_to_browser():
177
+ console.print("✅ Browser connection successful", style="green")
178
+
179
+ # Test navigation
180
+ console.print("🌐 Testing NotebookLM navigation...", style="blue")
181
+ if await automator.navigate_to_notebooklm():
182
+ console.print("✅ NotebookLM navigation successful", style="green")
183
+
184
+ # Test authentication
185
+ console.print("🔐 Checking authentication...", style="blue")
186
+ auth_status = await automator.check_authentication()
187
+ if auth_status:
188
+ console.print("✅ User appears to be authenticated", style="green")
189
+ else:
190
+ console.print("⚠️ User may need to sign in to Google account", style="yellow")
191
+
192
+ console.print("🎉 All tests passed! DeepDiver is ready to use.", style="green")
193
+ console.print("🔗 Browser kept open for next command", style="dim")
194
+ else:
195
+ console.print("❌ NotebookLM navigation failed", style="red")
196
+ else:
197
+ console.print("❌ Browser connection failed", style="red")
198
+ console.print("Make sure Chrome is running with CDP enabled", style="yellow")
199
+
200
+ except Exception as e:
201
+ console.print(f"❌ Test failed: {e}", style="red")
202
+ # Browser stays open - no close() call
203
+
204
+ asyncio.run(run_test())
205
+
206
+
207
+ @cli.command()
208
+ @click.argument('source', type=click.Path(exists=True))
209
+ @click.option('--title', '-t', default='Generated Podcast',
210
+ help='Title for the generated podcast')
211
+ @click.option('--output', '-o', default='./output',
212
+ help='Output directory for generated audio')
213
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
214
+ help='Path to configuration file')
215
+ def podcast(source: str, title: str, output: str, config: str):
216
+ """Create a podcast from a document using NotebookLM."""
217
+ console.print(f"🎙️ Creating podcast: {title}", style="blue")
218
+ console.print(f"📄 Source: {source}", style="blue")
219
+ console.print(f"📁 Output: {output}", style="blue")
220
+
221
+ async def create_podcast():
222
+ automator = NotebookLMAutomator(config)
223
+
224
+ try:
225
+ # Connect to browser
226
+ if not await automator.connect_to_browser():
227
+ console.print("❌ Failed to connect to browser", style="red")
228
+ return
229
+
230
+ # Navigate to NotebookLM
231
+ if not await automator.navigate_to_notebooklm():
232
+ console.print("❌ Failed to navigate to NotebookLM", style="red")
233
+ return
234
+
235
+ # Check authentication
236
+ if not await automator.check_authentication():
237
+ console.print("⚠️ Please sign in to your Google account in the browser", style="yellow")
238
+ console.print("Then run the command again", style="yellow")
239
+ return
240
+
241
+ # Upload document
242
+ console.print("📤 Uploading document...", style="blue")
243
+ if not await automator.upload_document(source):
244
+ console.print("❌ Failed to upload document", style="red")
245
+ return
246
+
247
+ # Generate Audio Overview
248
+ console.print("🎵 Generating Audio Overview...", style="blue")
249
+ if not await automator.generate_audio_overview(title):
250
+ console.print("❌ Failed to generate Audio Overview", style="red")
251
+ return
252
+
253
+ # Download audio
254
+ output_path = os.path.join(output, f"{title}.mp3")
255
+ os.makedirs(output, exist_ok=True)
256
+
257
+ console.print("⬇️ Downloading audio...", style="blue")
258
+ if await automator.download_audio(output_path):
259
+ console.print(f"✅ Podcast created successfully: {output_path}", style="green")
260
+ else:
261
+ console.print("❌ Failed to download audio", style="red")
262
+
263
+ except Exception as e:
264
+ console.print(f"❌ Podcast creation failed: {e}", style="red")
265
+
266
+ finally:
267
+ await automator.close()
268
+
269
+ asyncio.run(create_podcast())
270
+
271
+
272
+ @cli.group()
273
+ def session():
274
+ """Session management commands."""
275
+ pass
276
+
277
+
278
+ @session.command()
279
+ @click.option('--ai', default='claude', help='AI assistant name')
280
+ @click.option('--issue', type=int, help='Issue number')
281
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
282
+ help='Path to configuration file')
283
+ def start(ai: str, issue: Optional[int], config: str):
284
+ """Start a new DeepDiver session."""
285
+ console.print("🔮 Starting new DeepDiver session...", style="blue")
286
+ console.print(f"🤖 AI Assistant: {ai}", style="blue")
287
+ if issue:
288
+ console.print(f"🎯 Issue: #{issue}", style="blue")
289
+
290
+ # TODO: Implement session management
291
+ console.print("⚠️ Session management not yet implemented", style="yellow")
292
+ console.print("This feature will be available in a future release", style="yellow")
293
+
294
+
295
+ @session.command()
296
+ @click.argument('message')
297
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
298
+ help='Path to configuration file')
299
+ def write(message: str, config: str):
300
+ """Write to the current session."""
301
+ console.print(f"✍️ Writing to session: {message}", style="blue")
302
+
303
+ # TODO: Implement session writing
304
+ console.print("⚠️ Session writing not yet implemented", style="yellow")
305
+ console.print("This feature will be available in a future release", style="yellow")
306
+
307
+
308
+ @session.command()
309
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
310
+ help='Path to configuration file')
311
+ def status(config: str):
312
+ """Show current session status."""
313
+ from .session_tracker import SessionTracker
314
+
315
+ tracker = SessionTracker()
316
+ tracker._load_current_session()
317
+
318
+ if not tracker.current_session:
319
+ console.print("❌ No active session", style="red")
320
+ console.print("💡 Run 'deepdiver notebook create' to start a session", style="yellow")
321
+ return
322
+
323
+ session_status = tracker.get_session_status()
324
+
325
+ console.print("📊 Session Status", style="bold blue")
326
+ console.print()
327
+ console.print(f"🔮 Session ID: {session_status['session_id'][:16]}...", style="cyan")
328
+ console.print(f"🤖 AI Assistant: {session_status['ai_assistant']}", style="cyan")
329
+ console.print(f"📓 Notebooks: {session_status['notebooks_count']}", style="cyan")
330
+ if session_status['active_notebook_id']:
331
+ console.print(f"🟢 Active Notebook: {session_status['active_notebook_id']}", style="green")
332
+ console.print(f"📝 Notes: {session_status['notes_count']}", style="dim")
333
+ console.print(f"📅 Created: {session_status['created_at']}", style="dim")
334
+
335
+
336
+ @session.command(name='close')
337
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
338
+ help='Path to configuration file')
339
+ def close_session(config: str):
340
+ """Close browser and cleanup session resources."""
341
+ console.print("🔒 Closing browser session...", style="blue")
342
+
343
+ async def run_close():
344
+ from .notebooklm_automator import NotebookLMAutomator
345
+
346
+ automator = NotebookLMAutomator(config)
347
+
348
+ try:
349
+ # Connect to browser (if it's still running)
350
+ if await automator.connect_to_browser():
351
+ console.print("✅ Connected to browser", style="green")
352
+ # Close the browser
353
+ await automator.close()
354
+ console.print("✅ Browser closed successfully", style="green")
355
+ else:
356
+ console.print("⚠️ Browser not running", style="yellow")
357
+ except Exception as e:
358
+ console.print(f"⚠️ Error closing browser: {e}", style="yellow")
359
+ console.print("Browser may have already been closed", style="dim")
360
+
361
+ asyncio.run(run_close())
362
+ console.print("💡 Session data preserved in ./sessions/", style="cyan")
363
+ console.print("💡 Run 'deepdiver notebook create' to start a new session", style="cyan")
364
+
365
+
366
+ @cli.command()
367
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
368
+ help='Path to configuration file')
369
+ def status(config: str):
370
+ """Show DeepDiver system status."""
371
+ console.print("📊 DeepDiver System Status", style="blue")
372
+
373
+ try:
374
+ # Check configuration
375
+ automator = NotebookLMAutomator(config)
376
+ console.print("✅ Configuration loaded", style="green")
377
+
378
+ # Check Chrome browser
379
+ console.print("🔍 Checking Chrome browser...", style="blue")
380
+ console.print("Make sure Chrome is running with CDP enabled", style="yellow")
381
+
382
+ console.print("🎯 System Status: Ready for automation", style="green")
383
+
384
+ except Exception as e:
385
+ console.print(f"❌ System status check failed: {e}", style="red")
386
+
387
+
388
+ @cli.command(name='get-html')
389
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
390
+ help='Path to configuration file')
391
+ def get_html(config: str):
392
+ """Get the HTML content of the NotebookLM page."""
393
+ console.print("📄 Getting HTML content of NotebookLM page...", style="blue")
394
+
395
+ async def run_get_html():
396
+ from .notebooklm_automator import NotebookLMAutomator
397
+ automator = NotebookLMAutomator(config)
398
+
399
+ try:
400
+ if await automator.connect_to_browser():
401
+ if await automator.navigate_to_notebooklm():
402
+ content = await automator.get_page_content()
403
+ if content:
404
+ console.print(Text(content))
405
+ except Exception as e:
406
+ console.print(f"❌ Failed to get HTML: {e}", style="red")
407
+ # Removed finally block to keep browser open
408
+
409
+ asyncio.run(run_get_html())
410
+
411
+
412
+ @cli.group()
413
+ def notebook():
414
+ """Notebook management commands."""
415
+ pass
416
+
417
+
418
+ @notebook.command(name='create')
419
+ @click.option('--source', '-s', help='Add a source to the notebook (URL or file path)')
420
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
421
+ help='Path to configuration file')
422
+ def notebook_create(source: str, config: str):
423
+ """Create a new notebook in NotebookLM with optional initial source.
424
+
425
+ The source can be:
426
+ - SimExp session URL: https://app.simplenote.com/p/[NOTE_ID]
427
+ - Web article URL: https://example.com/article
428
+ - YouTube URL: https://youtube.com/watch?v=...
429
+ - Local file path: ./document.pdf
430
+
431
+ Examples:
432
+ deepdiver notebook create --source "https://app.simplenote.com/p/abc123"
433
+ deepdiver notebook create --source "https://example.com/research"
434
+ deepdiver notebook create --source "./notes.pdf"
435
+ deepdiver notebook create # Create empty notebook
436
+ """
437
+ if source:
438
+ console.print(f"📓 Creating notebook with source: {source}", style="blue")
439
+ else:
440
+ console.print("📓 Creating a new NotebookLM notebook...", style="blue")
441
+
442
+ async def run_create_notebook():
443
+ from .notebooklm_automator import NotebookLMAutomator
444
+ from .session_tracker import SessionTracker
445
+
446
+ automator = NotebookLMAutomator(config)
447
+ tracker = SessionTracker()
448
+
449
+ try:
450
+ # Load or start session
451
+ tracker._load_current_session()
452
+ if not tracker.current_session:
453
+ result = tracker.start_session(ai_assistant='claude')
454
+ console.print(f"🔮 New session started: {result['session_id'][:8]}...", style="cyan")
455
+
456
+ if await automator.connect_to_browser():
457
+ if await automator.navigate_to_notebooklm():
458
+ notebook_data = await automator.create_notebook()
459
+
460
+ if notebook_data:
461
+ console.print("✅ Notebook created successfully!", style="green")
462
+ console.print(f"📋 Notebook ID: {notebook_data['id']}", style="cyan")
463
+ console.print(f"🔗 Notebook URL:", style="cyan")
464
+ console.print(f" {notebook_data['url']}", style="bold blue")
465
+
466
+ # Add to session
467
+ tracker.add_notebook(notebook_data)
468
+ console.print(f"💾 Notebook saved to session", style="green")
469
+
470
+ # Add source if provided
471
+ if source:
472
+ console.print(f"\n🔗 Adding source to notebook...", style="blue")
473
+ result = await automator.add_source(source, notebook_id=notebook_data['id'])
474
+ if result:
475
+ console.print(f"✅ Source added successfully!", style="green")
476
+ # Update session tracker with source info
477
+ tracker.add_source_to_notebook(notebook_data['id'], {
478
+ 'source': source,
479
+ 'type': 'url' if source.startswith(('http://', 'https://')) else 'file'
480
+ })
481
+ else:
482
+ console.print(f"❌ Failed to add source", style="red")
483
+ console.print(f"💡 Tip: You can add sources later with 'deepdiver notebook add-source'", style="yellow")
484
+
485
+ console.print(f"\n🔗 Browser kept open for next command", style="dim")
486
+ else:
487
+ console.print("❌ Failed to create notebook", style="red")
488
+ except Exception as e:
489
+ console.print(f"❌ Failed to create notebook: {e}", style="red")
490
+ # Browser stays open - no close() call
491
+
492
+ asyncio.run(run_create_notebook())
493
+
494
+
495
+ @notebook.command(name='url')
496
+ @click.option('--notebook-id', '-n', help='Notebook ID (uses active notebook if not specified)')
497
+ @click.option('--format', '-f', type=click.Choice(['url', 'markdown', 'json']), default='url',
498
+ help='Output format')
499
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
500
+ help='Path to configuration file')
501
+ def notebook_url(notebook_id: Optional[str], format: str, config: str):
502
+ """Get notebook URL in various formats."""
503
+ from .session_tracker import SessionTracker
504
+
505
+ tracker = SessionTracker()
506
+ tracker._load_current_session()
507
+
508
+ if not tracker.current_session:
509
+ console.print("❌ No active session found", style="red")
510
+ console.print("💡 Create a notebook first with: deepdiver notebook create", style="yellow")
511
+ return
512
+
513
+ # Get notebook
514
+ if notebook_id:
515
+ notebook = tracker.get_notebook_by_id(notebook_id)
516
+ else:
517
+ notebook = tracker.get_active_notebook()
518
+
519
+ if not notebook:
520
+ if notebook_id:
521
+ console.print(f"❌ Notebook not found: {notebook_id}", style="red")
522
+ else:
523
+ console.print("❌ No active notebook found", style="red")
524
+ console.print("💡 Available notebooks:", style="yellow")
525
+ notebooks = tracker.list_notebooks()
526
+ for nb in notebooks:
527
+ console.print(f" • {nb['id']}: {nb['url']}", style="cyan")
528
+ return
529
+
530
+ # Format output
531
+ if format == 'url':
532
+ console.print(f"🔗 Notebook URL:", style="bold green")
533
+ console.print(f"{notebook['url']}", style="bold blue")
534
+
535
+ elif format == 'markdown':
536
+ console.print("📝 Markdown format:", style="bold green")
537
+ title = notebook.get('title', 'NotebookLM Notebook')
538
+ console.print(f"[{title}]({notebook['url']})", style="cyan")
539
+
540
+ elif format == 'json':
541
+ import json
542
+ console.print("📊 JSON format:", style="bold green")
543
+ output = {
544
+ 'id': notebook['id'],
545
+ 'url': notebook['url'],
546
+ 'title': notebook.get('title', 'Untitled Notebook'),
547
+ 'created_at': notebook.get('created_at'),
548
+ 'sources': notebook.get('sources', [])
549
+ }
550
+ console.print(json.dumps(output, indent=2), style="cyan")
551
+
552
+
553
+ @notebook.command(name='share')
554
+ @click.argument('email')
555
+ @click.option('--notebook-id', '-n', help='Notebook ID to share (uses active notebook if not specified)')
556
+ @click.option('--role', '-r', type=click.Choice(['editor', 'viewer']), default='editor',
557
+ help='Role to grant (editor or viewer)')
558
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
559
+ help='Path to configuration file')
560
+ def notebook_share(email: str, notebook_id: Optional[str], role: str, config: str):
561
+ """Share notebook with a collaborator by email."""
562
+ console.print(f"👥 Sharing notebook with: {email}", style="blue")
563
+ console.print(f"📝 Role: {role}", style="cyan")
564
+
565
+ async def run_share_notebook():
566
+ from .notebooklm_automator import NotebookLMAutomator
567
+ from .session_tracker import SessionTracker
568
+
569
+ automator = NotebookLMAutomator(config)
570
+ tracker = SessionTracker()
571
+ tracker._load_current_session()
572
+
573
+ try:
574
+ # Get notebook to share
575
+ if notebook_id:
576
+ notebook = tracker.get_notebook_by_id(notebook_id)
577
+ else:
578
+ notebook = tracker.get_active_notebook()
579
+
580
+ if not notebook:
581
+ console.print("❌ No notebook found to share", style="red")
582
+ return
583
+
584
+ console.print(f"📓 Sharing notebook: {notebook['id']}", style="cyan")
585
+
586
+ # Connect and navigate to notebook
587
+ if await automator.connect_to_browser():
588
+ if await automator.navigate_to_notebook(notebook_id=notebook['id']):
589
+ # Share the notebook
590
+ if await automator.share_notebook(email=email, role=role):
591
+ console.print(f"✅ Notebook shared successfully with {email}!", style="green")
592
+ console.print(f"📧 {email} will receive an invitation email", style="cyan")
593
+
594
+ # Track collaboration in session
595
+ if tracker.current_session:
596
+ collaborators = notebook.get('collaborators', [])
597
+ collaborators.append({'email': email, 'role': role, 'added_at': datetime.now().isoformat()})
598
+ tracker.update_notebook(notebook['id'], {'collaborators': collaborators})
599
+ else:
600
+ console.print(f"❌ Failed to share notebook", style="red")
601
+ console.print("💡 Make sure the notebook is open and you have permission to share", style="yellow")
602
+ else:
603
+ console.print("❌ Failed to navigate to notebook", style="red")
604
+ except Exception as e:
605
+ console.print(f"❌ Failed to share notebook: {e}", style="red")
606
+ # Browser stays open - no close() call
607
+
608
+ asyncio.run(run_share_notebook())
609
+
610
+
611
+ @notebook.command(name='list')
612
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
613
+ help='Path to configuration file')
614
+ def notebook_list(config: str):
615
+ """List all notebooks in the current session."""
616
+ from .session_tracker import SessionTracker
617
+
618
+ tracker = SessionTracker()
619
+ tracker._load_current_session()
620
+
621
+ if not tracker.current_session:
622
+ console.print("❌ No active session found", style="red")
623
+ return
624
+
625
+ notebooks = tracker.list_notebooks()
626
+
627
+ if not notebooks:
628
+ console.print("📭 No notebooks in this session", style="yellow")
629
+ console.print("💡 Create one with: deepdiver notebook create", style="cyan")
630
+ return
631
+
632
+ console.print(f"📚 Notebooks in session ({len(notebooks)} total):", style="bold green")
633
+ active_id = tracker.current_session.get('active_notebook_id')
634
+
635
+ for nb in notebooks:
636
+ active_marker = "🟢" if nb['id'] == active_id else "⚪"
637
+ title = nb.get('title', 'Untitled')
638
+ sources_count = len(nb.get('sources', []))
639
+ console.print(f"\n{active_marker} {title}", style="bold cyan")
640
+ console.print(f" ID: {nb['id']}", style="dim")
641
+ console.print(f" URL: {nb['url']}", style="blue")
642
+ console.print(f" Sources: {sources_count}", style="dim")
643
+ console.print(f" Created: {nb.get('created_at', 'Unknown')}", style="dim")
644
+
645
+
646
+ @notebook.command(name='open')
647
+ @click.argument('notebook_id')
648
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
649
+ help='Path to configuration file')
650
+ def notebook_open(notebook_id: str, config: str):
651
+ """Navigate to an existing notebook."""
652
+ console.print(f"🔄 Opening notebook: {notebook_id}", style="blue")
653
+
654
+ async def run_open_notebook():
655
+ from .notebooklm_automator import NotebookLMAutomator
656
+ from .session_tracker import SessionTracker
657
+
658
+ automator = NotebookLMAutomator(config)
659
+ tracker = SessionTracker()
660
+ tracker._load_current_session()
661
+
662
+ try:
663
+ if await automator.connect_to_browser():
664
+ if await automator.navigate_to_notebook(notebook_id=notebook_id):
665
+ console.print("✅ Successfully navigated to notebook", style="green")
666
+
667
+ # Set as active in session
668
+ if tracker.current_session:
669
+ tracker.set_active_notebook(notebook_id)
670
+ console.print("💾 Set as active notebook in session", style="cyan")
671
+ console.print("🔗 Browser kept open for next command", style="dim")
672
+ else:
673
+ console.print("❌ Failed to navigate to notebook", style="red")
674
+ except Exception as e:
675
+ console.print(f"❌ Failed to open notebook: {e}", style="red")
676
+ # Browser stays open - no close() call
677
+
678
+ asyncio.run(run_open_notebook())
679
+
680
+
681
+ @notebook.command(name='add-source')
682
+ @click.argument('notebook_id')
683
+ @click.argument('source')
684
+ @click.option('--name', '-n', help='Custom name for the source')
685
+ @click.option('--config', '-c', default='deepdiver/deepdiver.yaml',
686
+ help='Path to configuration file')
687
+ def notebook_add_source(notebook_id: str, source: str, name: Optional[str], config: str):
688
+ """Add a source to an existing notebook.
689
+
690
+ SOURCE can be:
691
+ - SimExp URL: https://app.simplenote.com/p/[NOTE_ID]
692
+ - Web URL: https://example.com/article
693
+ - YouTube URL: https://youtube.com/watch?v=...
694
+ - Local file: ./document.pdf
695
+
696
+ Examples:
697
+ deepdiver notebook add-source abc-123 "https://app.simplenote.com/p/xyz"
698
+ deepdiver notebook add-source abc-123 "https://youtube.com/watch?v=xyz"
699
+ deepdiver notebook add-source abc-123 ./research.pdf
700
+ """
701
+ console.print(f"📄 Adding source to notebook: {notebook_id}", style="blue")
702
+
703
+ # Detect source type for better messaging
704
+ if source.startswith(('http://', 'https://')):
705
+ console.print(f"🔗 Source URL: {source}", style="cyan")
706
+ else:
707
+ console.print(f"📎 Source file: {source}", style="cyan")
708
+
709
+ if name:
710
+ console.print(f"🏷️ Custom name: {name}", style="cyan")
711
+
712
+ async def run_add_source():
713
+ from .notebooklm_automator import NotebookLMAutomator
714
+ from .session_tracker import SessionTracker
715
+
716
+ automator = NotebookLMAutomator(config)
717
+ tracker = SessionTracker()
718
+ tracker._load_current_session()
719
+
720
+ try:
721
+ # Verify notebook exists in session
722
+ notebook = None
723
+ if tracker.current_session:
724
+ notebook = tracker.get_notebook_by_id(notebook_id)
725
+ if not notebook:
726
+ console.print(f"⚠️ Notebook {notebook_id} not found in session", style="yellow")
727
+ console.print("💡 The notebook will still be added to if it exists in NotebookLM", style="dim")
728
+
729
+ # Connect to browser
730
+ if not await automator.connect_to_browser():
731
+ console.print("❌ Failed to connect to browser", style="red")
732
+ console.print("💡 Make sure Chrome is running with: deepdiver init", style="yellow")
733
+ return
734
+
735
+ # Add source to the specified notebook (handles both URLs and files)
736
+ console.print(f"📤 Adding source to notebook...", style="blue")
737
+ result_notebook_id = await automator.add_source(source, notebook_id=notebook_id)
738
+
739
+ if result_notebook_id:
740
+ console.print("✅ Source added successfully!", style="green")
741
+ console.print(f"📋 Notebook ID: {result_notebook_id}", style="cyan")
742
+
743
+ # Track source in session
744
+ if tracker.current_session:
745
+ # Determine source type and create metadata
746
+ if source.startswith(('http://', 'https://')):
747
+ # URL source
748
+ source_data = {
749
+ 'filename': name or source,
750
+ 'path': source,
751
+ 'type': 'url',
752
+ 'size': 0 # Unknown for URLs
753
+ }
754
+ else:
755
+ # File source
756
+ from pathlib import Path
757
+ source_path = Path(source)
758
+ source_data = {
759
+ 'filename': name or source_path.name,
760
+ 'path': source,
761
+ 'type': source_path.suffix[1:] if source_path.suffix else 'unknown',
762
+ 'size': source_path.stat().st_size if source_path.exists() else 0
763
+ }
764
+
765
+ # Add source to notebook in session
766
+ if tracker.add_source_to_notebook(result_notebook_id, source_data):
767
+ console.print(f"💾 Source tracked in session", style="green")
768
+
769
+ # Display updated source count
770
+ sources = tracker.list_notebook_sources(result_notebook_id)
771
+ console.print(f"📚 Total sources in notebook: {len(sources)}", style="cyan")
772
+ else:
773
+ console.print("⚠️ Could not track source in session", style="yellow")
774
+
775
+ console.print(f"🔗 Browser kept open for next command", style="dim")
776
+ else:
777
+ console.print("❌ Failed to add source to notebook", style="red")
778
+ console.print("💡 Make sure the notebook ID is correct and you have permission to edit", style="yellow")
779
+
780
+ except Exception as e:
781
+ console.print(f"❌ Failed to add source: {e}", style="red")
782
+ import traceback
783
+ console.print(traceback.format_exc(), style="dim")
784
+ # Browser stays open - no close() call
785
+
786
+ asyncio.run(run_add_source())
787
+
788
+
789
+ def main():
790
+ """Main entry point for DeepDiver CLI."""
791
+ try:
792
+ cli()
793
+ except KeyboardInterrupt:
794
+ console.print("\n👋 DeepDiver session interrupted", style="yellow")
795
+ except Exception as e:
796
+ console.print(f"❌ DeepDiver error: {e}", style="red")
797
+ sys.exit(1)
798
+
799
+
800
+ if __name__ == "__main__":
801
+ main()