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/__init__.py +38 -0
- deepdiver/content_processor.py +343 -0
- deepdiver/deepdive.py +801 -0
- deepdiver/deepdiver.yaml +79 -0
- deepdiver/notebooklm_automator.py +1441 -0
- deepdiver/podcast_manager.py +402 -0
- deepdiver/session_tracker.py +723 -0
- deepdiver-0.1.0.dist-info/METADATA +455 -0
- deepdiver-0.1.0.dist-info/RECORD +13 -0
- deepdiver-0.1.0.dist-info/WHEEL +5 -0
- deepdiver-0.1.0.dist-info/entry_points.txt +2 -0
- deepdiver-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepdiver-0.1.0.dist-info/top_level.txt +1 -0
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()
|