@pan-sec/notebooklm-mcp 1.6.0 → 1.8.0

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.
Files changed (134) hide show
  1. package/dist/config.d.ts +4 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +10 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/events/event-emitter.d.ts +45 -0
  6. package/dist/events/event-emitter.d.ts.map +1 -0
  7. package/dist/events/event-emitter.js +100 -0
  8. package/dist/events/event-emitter.js.map +1 -0
  9. package/dist/events/event-types.d.ts +124 -0
  10. package/dist/events/event-types.d.ts.map +1 -0
  11. package/dist/events/event-types.js +18 -0
  12. package/dist/events/event-types.js.map +1 -0
  13. package/dist/gemini/gemini-client.d.ts +45 -0
  14. package/dist/gemini/gemini-client.d.ts.map +1 -0
  15. package/dist/gemini/gemini-client.js +211 -0
  16. package/dist/gemini/gemini-client.js.map +1 -0
  17. package/dist/gemini/index.d.ts +8 -0
  18. package/dist/gemini/index.d.ts.map +1 -0
  19. package/dist/gemini/index.js +8 -0
  20. package/dist/gemini/index.js.map +1 -0
  21. package/dist/gemini/types.d.ts +136 -0
  22. package/dist/gemini/types.d.ts.map +1 -0
  23. package/dist/gemini/types.js +10 -0
  24. package/dist/gemini/types.js.map +1 -0
  25. package/dist/index.js +76 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/library/notebook-library.d.ts +25 -2
  28. package/dist/library/notebook-library.d.ts.map +1 -1
  29. package/dist/library/notebook-library.js +142 -2
  30. package/dist/library/notebook-library.js.map +1 -1
  31. package/dist/library/types.d.ts +15 -0
  32. package/dist/library/types.d.ts.map +1 -1
  33. package/dist/notebook-creation/audio-manager.d.ts +56 -0
  34. package/dist/notebook-creation/audio-manager.d.ts.map +1 -0
  35. package/dist/notebook-creation/audio-manager.js +335 -0
  36. package/dist/notebook-creation/audio-manager.js.map +1 -0
  37. package/dist/notebook-creation/discover-creation-flow.d.ts +8 -0
  38. package/dist/notebook-creation/discover-creation-flow.d.ts.map +1 -0
  39. package/dist/notebook-creation/discover-creation-flow.js +177 -0
  40. package/dist/notebook-creation/discover-creation-flow.js.map +1 -0
  41. package/dist/notebook-creation/discover-quota.d.ts +8 -0
  42. package/dist/notebook-creation/discover-quota.d.ts.map +1 -0
  43. package/dist/notebook-creation/discover-quota.js +195 -0
  44. package/dist/notebook-creation/discover-quota.js.map +1 -0
  45. package/dist/notebook-creation/discover-source-dialog.d.ts +8 -0
  46. package/dist/notebook-creation/discover-source-dialog.d.ts.map +1 -0
  47. package/dist/notebook-creation/discover-source-dialog.js +134 -0
  48. package/dist/notebook-creation/discover-source-dialog.js.map +1 -0
  49. package/dist/notebook-creation/discover-sources.d.ts +8 -0
  50. package/dist/notebook-creation/discover-sources.d.ts.map +1 -0
  51. package/dist/notebook-creation/discover-sources.js +273 -0
  52. package/dist/notebook-creation/discover-sources.js.map +1 -0
  53. package/dist/notebook-creation/discover-text-input.d.ts +7 -0
  54. package/dist/notebook-creation/discover-text-input.d.ts.map +1 -0
  55. package/dist/notebook-creation/discover-text-input.js +135 -0
  56. package/dist/notebook-creation/discover-text-input.js.map +1 -0
  57. package/dist/notebook-creation/index.d.ts +12 -0
  58. package/dist/notebook-creation/index.d.ts.map +1 -0
  59. package/dist/notebook-creation/index.js +12 -0
  60. package/dist/notebook-creation/index.js.map +1 -0
  61. package/dist/notebook-creation/notebook-creator.d.ts +95 -0
  62. package/dist/notebook-creation/notebook-creator.d.ts.map +1 -0
  63. package/dist/notebook-creation/notebook-creator.js +689 -0
  64. package/dist/notebook-creation/notebook-creator.js.map +1 -0
  65. package/dist/notebook-creation/notebook-sync.d.ts +93 -0
  66. package/dist/notebook-creation/notebook-sync.d.ts.map +1 -0
  67. package/dist/notebook-creation/notebook-sync.js +370 -0
  68. package/dist/notebook-creation/notebook-sync.js.map +1 -0
  69. package/dist/notebook-creation/run-discovery.d.ts +11 -0
  70. package/dist/notebook-creation/run-discovery.d.ts.map +1 -0
  71. package/dist/notebook-creation/run-discovery.js +151 -0
  72. package/dist/notebook-creation/run-discovery.js.map +1 -0
  73. package/dist/notebook-creation/selector-discovery.d.ts +65 -0
  74. package/dist/notebook-creation/selector-discovery.d.ts.map +1 -0
  75. package/dist/notebook-creation/selector-discovery.js +421 -0
  76. package/dist/notebook-creation/selector-discovery.js.map +1 -0
  77. package/dist/notebook-creation/selectors.d.ts +150 -0
  78. package/dist/notebook-creation/selectors.d.ts.map +1 -0
  79. package/dist/notebook-creation/selectors.js +225 -0
  80. package/dist/notebook-creation/selectors.js.map +1 -0
  81. package/dist/notebook-creation/source-manager.d.ts +73 -0
  82. package/dist/notebook-creation/source-manager.d.ts.map +1 -0
  83. package/dist/notebook-creation/source-manager.js +486 -0
  84. package/dist/notebook-creation/source-manager.js.map +1 -0
  85. package/dist/notebook-creation/test-create.d.ts +8 -0
  86. package/dist/notebook-creation/test-create.d.ts.map +1 -0
  87. package/dist/notebook-creation/test-create.js +72 -0
  88. package/dist/notebook-creation/test-create.js.map +1 -0
  89. package/dist/notebook-creation/types.d.ts +173 -0
  90. package/dist/notebook-creation/types.d.ts.map +1 -0
  91. package/dist/notebook-creation/types.js +5 -0
  92. package/dist/notebook-creation/types.js.map +1 -0
  93. package/dist/quota/index.d.ts +8 -0
  94. package/dist/quota/index.d.ts.map +1 -0
  95. package/dist/quota/index.js +8 -0
  96. package/dist/quota/index.js.map +1 -0
  97. package/dist/quota/quota-manager.d.ts +125 -0
  98. package/dist/quota/quota-manager.d.ts.map +1 -0
  99. package/dist/quota/quota-manager.js +330 -0
  100. package/dist/quota/quota-manager.js.map +1 -0
  101. package/dist/session/session-manager.d.ts +5 -0
  102. package/dist/session/session-manager.d.ts.map +1 -1
  103. package/dist/session/session-manager.js +6 -0
  104. package/dist/session/session-manager.js.map +1 -1
  105. package/dist/tools/definitions/gemini.d.ts +12 -0
  106. package/dist/tools/definitions/gemini.d.ts.map +1 -0
  107. package/dist/tools/definitions/gemini.js +135 -0
  108. package/dist/tools/definitions/gemini.js.map +1 -0
  109. package/dist/tools/definitions/notebook-management.d.ts.map +1 -1
  110. package/dist/tools/definitions/notebook-management.js +525 -0
  111. package/dist/tools/definitions/notebook-management.js.map +1 -1
  112. package/dist/tools/definitions/system.d.ts.map +1 -1
  113. package/dist/tools/definitions/system.js +158 -0
  114. package/dist/tools/definitions/system.js.map +1 -1
  115. package/dist/tools/definitions.d.ts.map +1 -1
  116. package/dist/tools/definitions.js +2 -0
  117. package/dist/tools/definitions.js.map +1 -1
  118. package/dist/tools/handlers.d.ts +257 -0
  119. package/dist/tools/handlers.d.ts.map +1 -1
  120. package/dist/tools/handlers.js +1097 -0
  121. package/dist/tools/handlers.js.map +1 -1
  122. package/dist/webhooks/index.d.ts +8 -0
  123. package/dist/webhooks/index.d.ts.map +1 -0
  124. package/dist/webhooks/index.js +8 -0
  125. package/dist/webhooks/index.js.map +1 -0
  126. package/dist/webhooks/types.d.ts +57 -0
  127. package/dist/webhooks/types.d.ts.map +1 -0
  128. package/dist/webhooks/types.js +5 -0
  129. package/dist/webhooks/types.js.map +1 -0
  130. package/dist/webhooks/webhook-dispatcher.d.ts +120 -0
  131. package/dist/webhooks/webhook-dispatcher.d.ts.map +1 -0
  132. package/dist/webhooks/webhook-dispatcher.js +519 -0
  133. package/dist/webhooks/webhook-dispatcher.js.map +1 -0
  134. package/package.json +2 -1
@@ -0,0 +1,689 @@
1
+ /**
2
+ * NotebookLM Notebook Creator
3
+ *
4
+ * Creates notebooks programmatically via browser automation.
5
+ * Supports URL, text, and file sources.
6
+ */
7
+ import { findElement, waitForElement, getSelectors } from "./selectors.js";
8
+ import { log } from "../utils/logger.js";
9
+ import { randomDelay, humanType, realisticClick } from "../utils/stealth-utils.js";
10
+ import { CONFIG } from "../config.js";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ const NOTEBOOKLM_URL = "https://notebooklm.google.com/";
14
+ /**
15
+ * Creates NotebookLM notebooks with sources
16
+ */
17
+ export class NotebookCreator {
18
+ authManager;
19
+ contextManager;
20
+ page = null;
21
+ constructor(authManager, contextManager) {
22
+ this.authManager = authManager;
23
+ this.contextManager = contextManager;
24
+ }
25
+ /**
26
+ * Create a new notebook with sources
27
+ */
28
+ async createNotebook(options) {
29
+ const { name, sources, sendProgress } = options;
30
+ const totalSteps = 3 + sources.length; // Init + Create + Sources + Finalize
31
+ let currentStep = 0;
32
+ const failedSources = [];
33
+ let successCount = 0;
34
+ try {
35
+ // Step 1: Initialize browser and navigate
36
+ currentStep++;
37
+ await sendProgress?.("Initializing browser...", currentStep, totalSteps);
38
+ await this.initialize(options.browserOptions?.headless);
39
+ // Step 2: Create new notebook
40
+ currentStep++;
41
+ await sendProgress?.("Creating new notebook...", currentStep, totalSteps);
42
+ await this.clickNewNotebook();
43
+ await this.setNotebookName(name);
44
+ // Step 3+: Add each source
45
+ for (const source of sources) {
46
+ currentStep++;
47
+ const sourceDesc = this.getSourceDescription(source);
48
+ await sendProgress?.(`Adding source: ${sourceDesc}...`, currentStep, totalSteps);
49
+ try {
50
+ await this.addSource(source);
51
+ successCount++;
52
+ log.success(`✅ Added source: ${sourceDesc}`);
53
+ }
54
+ catch (error) {
55
+ const errorMsg = error instanceof Error ? error.message : String(error);
56
+ log.error(`❌ Failed to add source: ${sourceDesc} - ${errorMsg}`);
57
+ failedSources.push({ source, error: errorMsg });
58
+ }
59
+ // Delay between sources
60
+ await randomDelay(1000, 2000);
61
+ }
62
+ // Step N: Finalize and get URL
63
+ currentStep++;
64
+ await sendProgress?.("Finalizing notebook...", currentStep, totalSteps);
65
+ const notebookUrl = await this.finalizeAndGetUrl();
66
+ log.success(`✅ Notebook created: ${notebookUrl}`);
67
+ return {
68
+ url: notebookUrl,
69
+ name,
70
+ sourceCount: successCount,
71
+ createdAt: new Date().toISOString(),
72
+ failedSources: failedSources.length > 0 ? failedSources : undefined,
73
+ };
74
+ }
75
+ catch (error) {
76
+ const errorMsg = error instanceof Error ? error.message : String(error);
77
+ log.error(`❌ Notebook creation failed: ${errorMsg}`);
78
+ throw error;
79
+ }
80
+ finally {
81
+ await this.cleanup();
82
+ }
83
+ }
84
+ /**
85
+ * Initialize browser and navigate to NotebookLM
86
+ */
87
+ async initialize(headless) {
88
+ log.info("🌐 Initializing browser for notebook creation...");
89
+ // Get browser context
90
+ // Note: getOrCreateContext(true) = show browser, getOrCreateContext(false) = headless
91
+ // When browserOptions.headless === false, user wants visible browser, so pass true
92
+ const context = await this.contextManager.getOrCreateContext(headless === false ? true : undefined);
93
+ // Check authentication
94
+ const isAuthenticated = await this.authManager.validateCookiesExpiry(context);
95
+ if (!isAuthenticated) {
96
+ throw new Error("Not authenticated to NotebookLM. Please run setup_auth first.");
97
+ }
98
+ // Create new page
99
+ this.page = await context.newPage();
100
+ // Navigate to NotebookLM
101
+ await this.page.goto(NOTEBOOKLM_URL, {
102
+ waitUntil: "domcontentloaded",
103
+ timeout: CONFIG.browserTimeout,
104
+ });
105
+ await randomDelay(2000, 3000);
106
+ // Wait for page to be ready
107
+ await this.page.waitForLoadState("networkidle").catch(() => { });
108
+ log.success("✅ Browser initialized and navigated to NotebookLM");
109
+ }
110
+ /**
111
+ * Click the "New notebook" button
112
+ */
113
+ async clickNewNotebook() {
114
+ if (!this.page)
115
+ throw new Error("Page not initialized");
116
+ log.info("📝 Clicking 'New notebook' button...");
117
+ // Try to find and click the new notebook button
118
+ const selectors = getSelectors("newNotebookButton");
119
+ for (const selector of selectors) {
120
+ try {
121
+ const element = await this.page.$(selector);
122
+ if (element && await element.isVisible()) {
123
+ await realisticClick(this.page, selector, true);
124
+ await randomDelay(1000, 2000);
125
+ log.success("✅ Clicked 'New notebook' button");
126
+ return;
127
+ }
128
+ }
129
+ catch {
130
+ continue;
131
+ }
132
+ }
133
+ // Try text-based selectors as fallback via evaluate (since :has-text() isn't supported)
134
+ const textPatterns = ["New notebook", "Create notebook", "Create new", "New"];
135
+ for (const pattern of textPatterns) {
136
+ try {
137
+ const clicked = await this.page.evaluate((searchText) => {
138
+ // @ts-expect-error - DOM types
139
+ const elements = document.querySelectorAll('button, a, [role="button"]');
140
+ for (const el of elements) {
141
+ const elText = el.textContent?.toLowerCase() || "";
142
+ const ariaLabel = el.getAttribute("aria-label")?.toLowerCase() || "";
143
+ if (elText.includes(searchText.toLowerCase()) || ariaLabel.includes(searchText.toLowerCase())) {
144
+ el.click();
145
+ return true;
146
+ }
147
+ }
148
+ return false;
149
+ }, pattern);
150
+ if (clicked) {
151
+ await randomDelay(1000, 2000);
152
+ log.success("✅ Clicked 'New notebook' button (text match)");
153
+ return;
154
+ }
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ }
160
+ throw new Error("Could not find 'New notebook' button");
161
+ }
162
+ /**
163
+ * Set the notebook name
164
+ */
165
+ async setNotebookName(name) {
166
+ if (!this.page)
167
+ throw new Error("Page not initialized");
168
+ log.info(`📝 Setting notebook name: ${name}`);
169
+ // Wait for and find the name input
170
+ const element = await waitForElement(this.page, "notebookNameInput", {
171
+ timeout: 10000,
172
+ });
173
+ if (!element) {
174
+ // NotebookLM might auto-generate a name - check if we're on the notebook page
175
+ log.warning("⚠️ Name input not found - notebook may have been created with default name");
176
+ return;
177
+ }
178
+ // Type the name
179
+ const selectors = getSelectors("notebookNameInput");
180
+ for (const selector of selectors) {
181
+ try {
182
+ const input = await this.page.$(selector);
183
+ if (input && await input.isVisible()) {
184
+ await humanType(this.page, selector, name, { withTypos: false });
185
+ await randomDelay(500, 1000);
186
+ log.success(`✅ Set notebook name: ${name}`);
187
+ return;
188
+ }
189
+ }
190
+ catch {
191
+ continue;
192
+ }
193
+ }
194
+ }
195
+ /**
196
+ * Add a source to the notebook
197
+ */
198
+ async addSource(source) {
199
+ if (!this.page)
200
+ throw new Error("Page not initialized");
201
+ // Check if source dialog is already open (happens for new notebooks)
202
+ const dialogAlreadyOpen = await this.isSourceDialogOpen();
203
+ if (!dialogAlreadyOpen) {
204
+ // Click "Add source" button only if dialog isn't already open
205
+ await this.clickAddSource();
206
+ }
207
+ else {
208
+ log.info("📋 Source dialog already open");
209
+ }
210
+ // Handle based on source type
211
+ switch (source.type) {
212
+ case "url":
213
+ await this.addUrlSource(source.value);
214
+ break;
215
+ case "text":
216
+ await this.addTextSource(source.value, source.title);
217
+ break;
218
+ case "file":
219
+ await this.addFileSource(source.value);
220
+ break;
221
+ default:
222
+ throw new Error(`Unknown source type: ${source.type}`);
223
+ }
224
+ }
225
+ /**
226
+ * Check if the source dialog is already open
227
+ */
228
+ async isSourceDialogOpen() {
229
+ if (!this.page)
230
+ return false;
231
+ // Check for source dialog indicators
232
+ const dialogIndicators = await this.page.evaluate(() => {
233
+ // @ts-expect-error - DOM types
234
+ const spans = document.querySelectorAll('span');
235
+ for (const span of spans) {
236
+ const text = span.textContent?.trim() || "";
237
+ // These texts only appear when the source dialog is open
238
+ if (text === "Copied text" || text === "Website" || text === "Discover sources") {
239
+ return true;
240
+ }
241
+ }
242
+ return false;
243
+ });
244
+ return dialogIndicators;
245
+ }
246
+ /**
247
+ * Click the "Add source" button
248
+ */
249
+ async clickAddSource() {
250
+ if (!this.page)
251
+ throw new Error("Page not initialized");
252
+ log.info("📎 Clicking 'Add source' button...");
253
+ const selectors = getSelectors("addSourceButton");
254
+ for (const selector of selectors) {
255
+ try {
256
+ const element = await this.page.$(selector);
257
+ if (element && await element.isVisible()) {
258
+ await realisticClick(this.page, selector, true);
259
+ await randomDelay(800, 1500);
260
+ log.success("✅ Clicked 'Add source' button");
261
+ return;
262
+ }
263
+ }
264
+ catch {
265
+ continue;
266
+ }
267
+ }
268
+ // Fallback: look for any "add" button via evaluate (since :has-text() isn't supported)
269
+ const addPatterns = ["Add source", "Add", "Upload", "+"];
270
+ for (const pattern of addPatterns) {
271
+ try {
272
+ const clicked = await this.page.evaluate((searchText) => {
273
+ // @ts-expect-error - DOM types
274
+ const elements = document.querySelectorAll('button, [role="button"]');
275
+ for (const el of elements) {
276
+ const elText = el.textContent?.trim() || "";
277
+ const ariaLabel = el.getAttribute("aria-label")?.toLowerCase() || "";
278
+ // For "+" we need exact match, for others partial match
279
+ if (searchText === "+") {
280
+ if (elText === "+" || ariaLabel.includes("add")) {
281
+ el.click();
282
+ return true;
283
+ }
284
+ }
285
+ else if (elText.toLowerCase().includes(searchText.toLowerCase()) || ariaLabel.includes(searchText.toLowerCase())) {
286
+ el.click();
287
+ return true;
288
+ }
289
+ }
290
+ return false;
291
+ }, pattern);
292
+ if (clicked) {
293
+ await randomDelay(800, 1500);
294
+ log.success("✅ Clicked 'Add source' button (fallback)");
295
+ return;
296
+ }
297
+ }
298
+ catch {
299
+ continue;
300
+ }
301
+ }
302
+ throw new Error("Could not find 'Add source' button");
303
+ }
304
+ /**
305
+ * Add a URL source
306
+ */
307
+ async addUrlSource(url) {
308
+ if (!this.page)
309
+ throw new Error("Page not initialized");
310
+ log.info(`🔗 Adding URL source: ${url}`);
311
+ // Click "Website" option - discovered as span with "Website" text
312
+ await this.clickSourceTypeByText(["Website", "webWebsite", "Link", "Discover sources"]);
313
+ // Find and fill URL input
314
+ await randomDelay(500, 1000);
315
+ const selectors = getSelectors("urlInput");
316
+ for (const selector of selectors) {
317
+ try {
318
+ const input = await this.page.$(selector);
319
+ if (input && await input.isVisible()) {
320
+ await humanType(this.page, selector, url, { withTypos: false });
321
+ await randomDelay(500, 1000);
322
+ // Submit
323
+ await this.clickSubmitButton();
324
+ await this.waitForSourceProcessing();
325
+ return;
326
+ }
327
+ }
328
+ catch {
329
+ continue;
330
+ }
331
+ }
332
+ throw new Error("Could not find URL input field");
333
+ }
334
+ /**
335
+ * Add a text source
336
+ */
337
+ async addTextSource(text, title) {
338
+ if (!this.page)
339
+ throw new Error("Page not initialized");
340
+ log.info(`📝 Adding text source${title ? `: ${title}` : ""}`);
341
+ // Click "Copied text" option - look for mat-chip or span with exact text
342
+ const textOptionClicked = await this.page.evaluate(() => {
343
+ // First, try to find mat-chip elements (Angular Material chips)
344
+ // @ts-expect-error - DOM types
345
+ const chips = document.querySelectorAll('mat-chip, mat-chip-option, [mat-chip-option]');
346
+ for (const chip of chips) {
347
+ const text = chip.textContent?.trim() || "";
348
+ if (text.includes("Copied text")) {
349
+ chip.click();
350
+ return { clicked: true, method: "mat-chip", text: text.substring(0, 30) };
351
+ }
352
+ }
353
+ // Fallback: find span with exact text and click its closest clickable ancestor
354
+ // @ts-expect-error - DOM types
355
+ const spans = document.querySelectorAll('span');
356
+ for (const span of spans) {
357
+ const text = span.textContent?.trim() || "";
358
+ if (text === "Copied text") {
359
+ // Try to find clickable parent (mat-chip, button, or div with click handler)
360
+ let target = span;
361
+ for (let i = 0; i < 5; i++) {
362
+ if (target.parentElement) {
363
+ target = target.parentElement;
364
+ const tagName = target.tagName?.toLowerCase();
365
+ if (tagName === "mat-chip" || tagName === "mat-chip-option" || tagName === "button") {
366
+ target.click();
367
+ return { clicked: true, method: "parent-" + tagName };
368
+ }
369
+ }
370
+ }
371
+ // If no good parent, just click the span
372
+ span.click();
373
+ return { clicked: true, method: "span-direct" };
374
+ }
375
+ }
376
+ return { clicked: false };
377
+ });
378
+ if (!textOptionClicked.clicked) {
379
+ log.warning("⚠️ Could not click 'Copied text' option");
380
+ }
381
+ // Wait for text area to appear
382
+ await randomDelay(2000, 2500);
383
+ // Find the text area - discovered as textarea.text-area
384
+ const textarea = await this.page.$('textarea.text-area') ||
385
+ await this.page.$('textarea[class*="text-area"]') ||
386
+ await this.page.$('textarea.mat-mdc-form-field-textarea-control');
387
+ if (textarea) {
388
+ const isVisible = await textarea.isVisible().catch(() => false);
389
+ if (!isVisible) {
390
+ // Try waiting a bit more
391
+ await randomDelay(1000, 1500);
392
+ }
393
+ // Click to focus
394
+ await textarea.click();
395
+ await randomDelay(200, 400);
396
+ // For large text, use clipboard paste instead of typing
397
+ if (text.length > 500) {
398
+ await this.page.evaluate((t) => {
399
+ // @ts-expect-error - DOM types available in browser context
400
+ navigator.clipboard.writeText(t);
401
+ }, text);
402
+ await this.page.keyboard.press("Control+V");
403
+ }
404
+ else {
405
+ // Type the text
406
+ await textarea.fill(text);
407
+ }
408
+ await randomDelay(500, 1000);
409
+ // Click "Insert" button
410
+ await this.clickInsertButton();
411
+ // Wait for processing but be lenient with errors
412
+ await this.waitForSourceProcessingLenient();
413
+ return;
414
+ }
415
+ throw new Error("Could not find text input area");
416
+ }
417
+ /**
418
+ * Add a file source
419
+ */
420
+ async addFileSource(filePath) {
421
+ if (!this.page)
422
+ throw new Error("Page not initialized");
423
+ // Validate file exists
424
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
425
+ if (!fs.existsSync(absolutePath)) {
426
+ throw new Error(`File not found: ${absolutePath}`);
427
+ }
428
+ log.info(`📁 Adding file source: ${path.basename(absolutePath)}`);
429
+ await randomDelay(500, 1000);
430
+ // First try to find file input directly
431
+ let fileInput = await this.page.$('input[type="file"]');
432
+ if (fileInput) {
433
+ await fileInput.setInputFiles(absolutePath);
434
+ await randomDelay(1000, 2000);
435
+ await this.waitForSourceProcessing();
436
+ return;
437
+ }
438
+ // If not found, click on upload option first
439
+ log.info(" Looking for upload option...");
440
+ const uploadClicked = await this.page.evaluate(() => {
441
+ // @ts-expect-error - DOM types
442
+ const elements = document.querySelectorAll("button, [role='button'], span, div[role='button']");
443
+ for (const el of elements) {
444
+ const text = el.textContent?.toLowerCase() || "";
445
+ const aria = el.getAttribute("aria-label")?.toLowerCase() || "";
446
+ if ((text.includes("upload") || text.includes("file") || text.includes("computer") ||
447
+ aria.includes("upload") || aria.includes("file")) &&
448
+ el.offsetParent !== null) {
449
+ el.click();
450
+ return true;
451
+ }
452
+ }
453
+ return false;
454
+ });
455
+ if (uploadClicked) {
456
+ await randomDelay(1000, 1500);
457
+ fileInput = await this.page.$('input[type="file"]');
458
+ if (fileInput) {
459
+ await fileInput.setInputFiles(absolutePath);
460
+ await randomDelay(1000, 2000);
461
+ await this.waitForSourceProcessing();
462
+ return;
463
+ }
464
+ }
465
+ throw new Error("Could not find file upload input");
466
+ }
467
+ /**
468
+ * Click a source type by text content (for the new dialog structure)
469
+ */
470
+ async clickSourceTypeByText(textPatterns) {
471
+ if (!this.page)
472
+ throw new Error("Page not initialized");
473
+ for (const pattern of textPatterns) {
474
+ try {
475
+ const clicked = await this.page.evaluate((searchText) => {
476
+ // @ts-expect-error - DOM types
477
+ const elements = document.querySelectorAll('span, button, [role="button"], div');
478
+ for (const el of elements) {
479
+ const text = el.textContent?.trim() || "";
480
+ // Match exact text or text that contains the pattern
481
+ if (text === searchText || text.toLowerCase().includes(searchText.toLowerCase())) {
482
+ // Make sure it's visible
483
+ if (el.offsetParent !== null) {
484
+ el.click();
485
+ return true;
486
+ }
487
+ }
488
+ }
489
+ return false;
490
+ }, pattern);
491
+ if (clicked) {
492
+ log.success(`✅ Clicked source type: ${pattern}`);
493
+ await randomDelay(800, 1200);
494
+ return;
495
+ }
496
+ }
497
+ catch {
498
+ continue;
499
+ }
500
+ }
501
+ log.warning(`⚠️ Could not find source type: ${textPatterns.join(", ")}`);
502
+ }
503
+ /**
504
+ * Click the submit/add button
505
+ */
506
+ async clickSubmitButton() {
507
+ if (!this.page)
508
+ throw new Error("Page not initialized");
509
+ const selectors = getSelectors("submitButton");
510
+ for (const selector of selectors) {
511
+ try {
512
+ const element = await this.page.$(selector);
513
+ if (element && await element.isVisible()) {
514
+ await element.click();
515
+ return;
516
+ }
517
+ }
518
+ catch {
519
+ continue;
520
+ }
521
+ }
522
+ // Try pressing Enter as fallback
523
+ await this.page.keyboard.press("Enter");
524
+ }
525
+ /**
526
+ * Click the "Insert" button (for text sources)
527
+ */
528
+ async clickInsertButton() {
529
+ if (!this.page)
530
+ throw new Error("Page not initialized");
531
+ // Find and click the "Insert" button by text
532
+ const clicked = await this.page.evaluate(() => {
533
+ // @ts-expect-error - DOM types
534
+ const buttons = document.querySelectorAll('button');
535
+ for (const btn of buttons) {
536
+ const text = btn.textContent?.trim() || "";
537
+ if (text === "Insert" || text.toLowerCase() === "insert") {
538
+ btn.click();
539
+ return true;
540
+ }
541
+ }
542
+ return false;
543
+ });
544
+ if (clicked) {
545
+ log.success("✅ Clicked 'Insert' button");
546
+ return;
547
+ }
548
+ // Fallback: try the general submit button
549
+ log.warning("⚠️ 'Insert' button not found, trying submit button");
550
+ await this.clickSubmitButton();
551
+ }
552
+ /**
553
+ * Wait for source processing to complete
554
+ */
555
+ async waitForSourceProcessing() {
556
+ if (!this.page)
557
+ throw new Error("Page not initialized");
558
+ log.info("⏳ Waiting for source processing...");
559
+ const timeout = 60000; // 1 minute timeout
560
+ const startTime = Date.now();
561
+ while (Date.now() - startTime < timeout) {
562
+ // Check for success indicator
563
+ const successElement = await findElement(this.page, "successIndicator");
564
+ if (successElement) {
565
+ log.success("✅ Source processed successfully");
566
+ return;
567
+ }
568
+ // Check for error
569
+ const errorElement = await findElement(this.page, "errorMessage");
570
+ if (errorElement) {
571
+ // @ts-expect-error - innerText exists on element
572
+ const errorText = await errorElement.innerText?.() || "Unknown error";
573
+ throw new Error(`Source processing failed: ${errorText}`);
574
+ }
575
+ // Check if processing indicator is gone
576
+ const processingElement = await findElement(this.page, "processingIndicator");
577
+ if (!processingElement) {
578
+ // No processing indicator and no error - assume success
579
+ await randomDelay(1000, 1500);
580
+ return;
581
+ }
582
+ await this.page.waitForTimeout(1000);
583
+ }
584
+ log.warning("⚠️ Source processing timeout - continuing anyway");
585
+ }
586
+ /**
587
+ * Lenient version of waitForSourceProcessing that ignores false positive errors
588
+ */
589
+ async waitForSourceProcessingLenient() {
590
+ if (!this.page)
591
+ throw new Error("Page not initialized");
592
+ log.info("⏳ Waiting for source processing...");
593
+ // Simple approach: wait a fixed time and check if dialog closed
594
+ await randomDelay(3000, 4000);
595
+ // Check if we're back to the main notebook view (no source dialog)
596
+ const dialogStillOpen = await this.isSourceDialogOpen();
597
+ if (!dialogStillOpen) {
598
+ log.success("✅ Source dialog closed - assuming success");
599
+ return;
600
+ }
601
+ // Check for actual error indicators (be specific)
602
+ const hasError = await this.page.evaluate(() => {
603
+ // @ts-expect-error - DOM types
604
+ const alerts = document.querySelectorAll('[role="alert"]');
605
+ for (const alert of alerts) {
606
+ const text = alert.textContent?.toLowerCase() || "";
607
+ // Only treat as error if it contains error-related words
608
+ if (text.includes("error") || text.includes("failed") || text.includes("invalid") || text.includes("unable")) {
609
+ return text.substring(0, 100);
610
+ }
611
+ }
612
+ return null;
613
+ });
614
+ if (hasError) {
615
+ throw new Error(`Source processing failed: ${hasError}`);
616
+ }
617
+ // Wait a bit more for processing
618
+ await randomDelay(2000, 3000);
619
+ log.success("✅ Source processing appears complete");
620
+ }
621
+ /**
622
+ * Finalize notebook creation and get the URL
623
+ */
624
+ async finalizeAndGetUrl() {
625
+ if (!this.page)
626
+ throw new Error("Page not initialized");
627
+ log.info("🔗 Getting notebook URL...");
628
+ // The URL should already be the notebook URL after creation
629
+ await randomDelay(1000, 2000);
630
+ const currentUrl = this.page.url();
631
+ // Check if we're on a notebook page
632
+ if (currentUrl.includes("/notebook/")) {
633
+ return currentUrl;
634
+ }
635
+ // Try to find the notebook URL in the page
636
+ const notebookLinks = await this.page.$$('a[href*="/notebook/"]');
637
+ if (notebookLinks.length > 0) {
638
+ const href = await notebookLinks[0].getAttribute("href");
639
+ if (href) {
640
+ return href.startsWith("http") ? href : `https://notebooklm.google.com${href}`;
641
+ }
642
+ }
643
+ // Return current URL as fallback
644
+ return currentUrl;
645
+ }
646
+ /**
647
+ * Get a human-readable description of a source
648
+ */
649
+ getSourceDescription(source) {
650
+ switch (source.type) {
651
+ case "url":
652
+ try {
653
+ const url = new URL(source.value);
654
+ return `URL: ${url.hostname}`;
655
+ }
656
+ catch {
657
+ return `URL: ${source.value.slice(0, 50)}`;
658
+ }
659
+ case "text":
660
+ return source.title || `Text: ${source.value.slice(0, 30)}...`;
661
+ case "file":
662
+ return `File: ${path.basename(source.value)}`;
663
+ default:
664
+ return "Unknown source";
665
+ }
666
+ }
667
+ /**
668
+ * Cleanup resources
669
+ */
670
+ async cleanup() {
671
+ if (this.page) {
672
+ try {
673
+ await this.page.close();
674
+ }
675
+ catch {
676
+ // Ignore cleanup errors
677
+ }
678
+ this.page = null;
679
+ }
680
+ }
681
+ }
682
+ /**
683
+ * Create a notebook with the given options
684
+ */
685
+ export async function createNotebook(authManager, contextManager, options) {
686
+ const creator = new NotebookCreator(authManager, contextManager);
687
+ return await creator.createNotebook(options);
688
+ }
689
+ //# sourceMappingURL=notebook-creator.js.map