@pan-sec/notebooklm-mcp 2026.2.11 → 2026.3.1

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 (305) hide show
  1. package/README.md +62 -19
  2. package/SECURITY.md +31 -61
  3. package/dist/auth/auth-manager.d.ts +2 -1
  4. package/dist/auth/auth-manager.d.ts.map +1 -1
  5. package/dist/auth/auth-manager.js +117 -44
  6. package/dist/auth/auth-manager.js.map +1 -1
  7. package/dist/auth/mcp-auth.d.ts +24 -4
  8. package/dist/auth/mcp-auth.d.ts.map +1 -1
  9. package/dist/auth/mcp-auth.js +149 -19
  10. package/dist/auth/mcp-auth.js.map +1 -1
  11. package/dist/compliance/alert-manager.d.ts.map +1 -1
  12. package/dist/compliance/alert-manager.js +7 -4
  13. package/dist/compliance/alert-manager.js.map +1 -1
  14. package/dist/compliance/breach-detection.d.ts.map +1 -1
  15. package/dist/compliance/breach-detection.js +14 -7
  16. package/dist/compliance/breach-detection.js.map +1 -1
  17. package/dist/compliance/change-log.d.ts.map +1 -1
  18. package/dist/compliance/change-log.js +7 -4
  19. package/dist/compliance/change-log.js.map +1 -1
  20. package/dist/compliance/compliance-logger.d.ts.map +1 -1
  21. package/dist/compliance/compliance-logger.js +11 -6
  22. package/dist/compliance/compliance-logger.js.map +1 -1
  23. package/dist/compliance/consent-manager.d.ts.map +1 -1
  24. package/dist/compliance/consent-manager.js +5 -3
  25. package/dist/compliance/consent-manager.js.map +1 -1
  26. package/dist/compliance/data-erasure.d.ts +1 -1
  27. package/dist/compliance/data-erasure.d.ts.map +1 -1
  28. package/dist/compliance/data-erasure.js +142 -83
  29. package/dist/compliance/data-erasure.js.map +1 -1
  30. package/dist/compliance/data-export.d.ts.map +1 -1
  31. package/dist/compliance/data-export.js +23 -12
  32. package/dist/compliance/data-export.js.map +1 -1
  33. package/dist/compliance/data-inventory.d.ts.map +1 -1
  34. package/dist/compliance/data-inventory.js +7 -6
  35. package/dist/compliance/data-inventory.js.map +1 -1
  36. package/dist/compliance/dsar-handler.d.ts +7 -1
  37. package/dist/compliance/dsar-handler.d.ts.map +1 -1
  38. package/dist/compliance/dsar-handler.js +74 -61
  39. package/dist/compliance/dsar-handler.js.map +1 -1
  40. package/dist/compliance/evidence-collector.d.ts.map +1 -1
  41. package/dist/compliance/evidence-collector.js +10 -6
  42. package/dist/compliance/evidence-collector.js.map +1 -1
  43. package/dist/compliance/health-monitor.d.ts.map +1 -1
  44. package/dist/compliance/health-monitor.js +15 -9
  45. package/dist/compliance/health-monitor.js.map +1 -1
  46. package/dist/compliance/incident-manager.d.ts.map +1 -1
  47. package/dist/compliance/incident-manager.js +5 -3
  48. package/dist/compliance/incident-manager.js.map +1 -1
  49. package/dist/compliance/policy-docs.d.ts.map +1 -1
  50. package/dist/compliance/policy-docs.js +14 -11
  51. package/dist/compliance/policy-docs.js.map +1 -1
  52. package/dist/compliance/privacy-notice-text.d.ts.map +1 -1
  53. package/dist/compliance/privacy-notice-text.js +3 -4
  54. package/dist/compliance/privacy-notice-text.js.map +1 -1
  55. package/dist/compliance/privacy-notice.d.ts.map +1 -1
  56. package/dist/compliance/privacy-notice.js +5 -3
  57. package/dist/compliance/privacy-notice.js.map +1 -1
  58. package/dist/compliance/report-generator.d.ts.map +1 -1
  59. package/dist/compliance/report-generator.js +5 -3
  60. package/dist/compliance/report-generator.js.map +1 -1
  61. package/dist/compliance/retention-engine.d.ts.map +1 -1
  62. package/dist/compliance/retention-engine.js +24 -10
  63. package/dist/compliance/retention-engine.js.map +1 -1
  64. package/dist/compliance/siem-exporter.d.ts.map +1 -1
  65. package/dist/compliance/siem-exporter.js +40 -16
  66. package/dist/compliance/siem-exporter.js.map +1 -1
  67. package/dist/config.d.ts +8 -31
  68. package/dist/config.d.ts.map +1 -1
  69. package/dist/config.js +26 -64
  70. package/dist/config.js.map +1 -1
  71. package/dist/errors.d.ts +22 -2
  72. package/dist/errors.d.ts.map +1 -1
  73. package/dist/errors.js +55 -4
  74. package/dist/errors.js.map +1 -1
  75. package/dist/gemini/gemini-client.d.ts +1 -0
  76. package/dist/gemini/gemini-client.d.ts.map +1 -1
  77. package/dist/gemini/gemini-client.js +50 -49
  78. package/dist/gemini/gemini-client.js.map +1 -1
  79. package/dist/gemini/types.d.ts +3 -1
  80. package/dist/gemini/types.d.ts.map +1 -1
  81. package/dist/gemini/types.js.map +1 -1
  82. package/dist/index.d.ts +52 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +412 -89
  85. package/dist/index.js.map +1 -1
  86. package/dist/library/notebook-library.d.ts.map +1 -1
  87. package/dist/library/notebook-library.js +2 -1
  88. package/dist/library/notebook-library.js.map +1 -1
  89. package/dist/logging/query-logger.d.ts +13 -1
  90. package/dist/logging/query-logger.d.ts.map +1 -1
  91. package/dist/logging/query-logger.js +62 -10
  92. package/dist/logging/query-logger.js.map +1 -1
  93. package/dist/notebook-creation/audio-manager.d.ts.map +1 -1
  94. package/dist/notebook-creation/audio-manager.js +19 -24
  95. package/dist/notebook-creation/audio-manager.js.map +1 -1
  96. package/dist/notebook-creation/browser-options.d.ts +28 -0
  97. package/dist/notebook-creation/browser-options.d.ts.map +1 -0
  98. package/dist/notebook-creation/browser-options.js +75 -0
  99. package/dist/notebook-creation/browser-options.js.map +1 -0
  100. package/dist/notebook-creation/data-table-manager.d.ts.map +1 -1
  101. package/dist/notebook-creation/data-table-manager.js +20 -21
  102. package/dist/notebook-creation/data-table-manager.js.map +1 -1
  103. package/dist/notebook-creation/discover-creation-flow.d.ts +0 -6
  104. package/dist/notebook-creation/discover-creation-flow.d.ts.map +1 -1
  105. package/dist/notebook-creation/discover-creation-flow.js +10 -10
  106. package/dist/notebook-creation/discover-creation-flow.js.map +1 -1
  107. package/dist/notebook-creation/discover-quota.d.ts +0 -6
  108. package/dist/notebook-creation/discover-quota.d.ts.map +1 -1
  109. package/dist/notebook-creation/discover-quota.js +12 -13
  110. package/dist/notebook-creation/discover-quota.js.map +1 -1
  111. package/dist/notebook-creation/discover-sources.js +15 -16
  112. package/dist/notebook-creation/discover-sources.js.map +1 -1
  113. package/dist/notebook-creation/dom-scripts.d.ts +10 -0
  114. package/dist/notebook-creation/dom-scripts.d.ts.map +1 -0
  115. package/dist/notebook-creation/dom-scripts.js +58 -0
  116. package/dist/notebook-creation/dom-scripts.js.map +1 -0
  117. package/dist/notebook-creation/errors.d.ts +18 -0
  118. package/dist/notebook-creation/errors.d.ts.map +1 -0
  119. package/dist/notebook-creation/errors.js +20 -0
  120. package/dist/notebook-creation/errors.js.map +1 -0
  121. package/dist/notebook-creation/index.d.ts +2 -1
  122. package/dist/notebook-creation/index.d.ts.map +1 -1
  123. package/dist/notebook-creation/index.js +2 -1
  124. package/dist/notebook-creation/index.js.map +1 -1
  125. package/dist/notebook-creation/notebook-creator.d.ts +6 -82
  126. package/dist/notebook-creation/notebook-creator.d.ts.map +1 -1
  127. package/dist/notebook-creation/notebook-creator.js +49 -835
  128. package/dist/notebook-creation/notebook-creator.js.map +1 -1
  129. package/dist/notebook-creation/notebook-nav.d.ts +19 -0
  130. package/dist/notebook-creation/notebook-nav.d.ts.map +1 -0
  131. package/dist/notebook-creation/notebook-nav.js +240 -0
  132. package/dist/notebook-creation/notebook-nav.js.map +1 -0
  133. package/dist/notebook-creation/notebook-sync.d.ts.map +1 -1
  134. package/dist/notebook-creation/notebook-sync.js +36 -38
  135. package/dist/notebook-creation/notebook-sync.js.map +1 -1
  136. package/dist/notebook-creation/selector-discovery.d.ts.map +1 -1
  137. package/dist/notebook-creation/selector-discovery.js +17 -24
  138. package/dist/notebook-creation/selector-discovery.js.map +1 -1
  139. package/dist/notebook-creation/selectors.d.ts +23 -37
  140. package/dist/notebook-creation/selectors.d.ts.map +1 -1
  141. package/dist/notebook-creation/selectors.js +56 -60
  142. package/dist/notebook-creation/selectors.js.map +1 -1
  143. package/dist/notebook-creation/source-manager.d.ts +25 -0
  144. package/dist/notebook-creation/source-manager.d.ts.map +1 -1
  145. package/dist/notebook-creation/source-manager.js +689 -50
  146. package/dist/notebook-creation/source-manager.js.map +1 -1
  147. package/dist/notebook-creation/types.d.ts +4 -0
  148. package/dist/notebook-creation/types.d.ts.map +1 -1
  149. package/dist/notebook-creation/video-manager.d.ts.map +1 -1
  150. package/dist/notebook-creation/video-manager.js +33 -35
  151. package/dist/notebook-creation/video-manager.js.map +1 -1
  152. package/dist/observability/metrics.d.ts +19 -0
  153. package/dist/observability/metrics.d.ts.map +1 -0
  154. package/dist/observability/metrics.js +35 -0
  155. package/dist/observability/metrics.js.map +1 -0
  156. package/dist/quota/quota-manager.d.ts +11 -3
  157. package/dist/quota/quota-manager.d.ts.map +1 -1
  158. package/dist/quota/quota-manager.js +139 -47
  159. package/dist/quota/quota-manager.js.map +1 -1
  160. package/dist/resources/resource-handlers.d.ts.map +1 -1
  161. package/dist/resources/resource-handlers.js +39 -17
  162. package/dist/resources/resource-handlers.js.map +1 -1
  163. package/dist/session/browser-session.d.ts.map +1 -1
  164. package/dist/session/browser-session.js +22 -22
  165. package/dist/session/browser-session.js.map +1 -1
  166. package/dist/session/session-timeout.d.ts.map +1 -1
  167. package/dist/session/session-timeout.js +4 -2
  168. package/dist/session/session-timeout.js.map +1 -1
  169. package/dist/session/shared-context-manager.d.ts.map +1 -1
  170. package/dist/session/shared-context-manager.js +31 -30
  171. package/dist/session/shared-context-manager.js.map +1 -1
  172. package/dist/tools/annotations.d.ts.map +1 -1
  173. package/dist/tools/annotations.js +9 -56
  174. package/dist/tools/annotations.js.map +1 -1
  175. package/dist/tools/definitions/ask-question.d.ts.map +1 -1
  176. package/dist/tools/definitions/ask-question.js +35 -100
  177. package/dist/tools/definitions/ask-question.js.map +1 -1
  178. package/dist/tools/definitions/chat-history.d.ts +47 -1
  179. package/dist/tools/definitions/chat-history.d.ts.map +1 -1
  180. package/dist/tools/definitions/chat-history.js +10 -1
  181. package/dist/tools/definitions/chat-history.js.map +1 -1
  182. package/dist/tools/definitions/data-tables.d.ts.map +1 -1
  183. package/dist/tools/definitions/data-tables.js +2 -0
  184. package/dist/tools/definitions/data-tables.js.map +1 -1
  185. package/dist/tools/definitions/gemini.d.ts.map +1 -1
  186. package/dist/tools/definitions/gemini.js +54 -11
  187. package/dist/tools/definitions/gemini.js.map +1 -1
  188. package/dist/tools/definitions/notebook-management.d.ts.map +1 -1
  189. package/dist/tools/definitions/notebook-management.js +100 -70
  190. package/dist/tools/definitions/notebook-management.js.map +1 -1
  191. package/dist/tools/definitions/query-history.d.ts +47 -1
  192. package/dist/tools/definitions/query-history.d.ts.map +1 -1
  193. package/dist/tools/definitions/query-history.js +7 -0
  194. package/dist/tools/definitions/query-history.js.map +1 -1
  195. package/dist/tools/definitions/session-management.d.ts.map +1 -1
  196. package/dist/tools/definitions/session-management.js +5 -0
  197. package/dist/tools/definitions/session-management.js.map +1 -1
  198. package/dist/tools/definitions/system.d.ts.map +1 -1
  199. package/dist/tools/definitions/system.js +71 -100
  200. package/dist/tools/definitions/system.js.map +1 -1
  201. package/dist/tools/definitions/video.d.ts.map +1 -1
  202. package/dist/tools/definitions/video.js +4 -1
  203. package/dist/tools/definitions/video.js.map +1 -1
  204. package/dist/tools/definitions.d.ts.map +1 -1
  205. package/dist/tools/definitions.js +4 -0
  206. package/dist/tools/definitions.js.map +1 -1
  207. package/dist/tools/handlers/ask-question.d.ts +1 -1
  208. package/dist/tools/handlers/ask-question.d.ts.map +1 -1
  209. package/dist/tools/handlers/ask-question.js +57 -13
  210. package/dist/tools/handlers/ask-question.js.map +1 -1
  211. package/dist/tools/handlers/audio-video.d.ts.map +1 -1
  212. package/dist/tools/handlers/audio-video.js +22 -161
  213. package/dist/tools/handlers/audio-video.js.map +1 -1
  214. package/dist/tools/handlers/auth.d.ts +14 -19
  215. package/dist/tools/handlers/auth.d.ts.map +1 -1
  216. package/dist/tools/handlers/auth.js +77 -121
  217. package/dist/tools/handlers/auth.js.map +1 -1
  218. package/dist/tools/handlers/error-utils.d.ts +16 -0
  219. package/dist/tools/handlers/error-utils.d.ts.map +1 -0
  220. package/dist/tools/handlers/error-utils.js +39 -0
  221. package/dist/tools/handlers/error-utils.js.map +1 -0
  222. package/dist/tools/handlers/gemini.d.ts +2 -0
  223. package/dist/tools/handlers/gemini.d.ts.map +1 -1
  224. package/dist/tools/handlers/gemini.js +88 -51
  225. package/dist/tools/handlers/gemini.js.map +1 -1
  226. package/dist/tools/handlers/index.d.ts +39 -47
  227. package/dist/tools/handlers/index.d.ts.map +1 -1
  228. package/dist/tools/handlers/index.js +15 -4
  229. package/dist/tools/handlers/index.js.map +1 -1
  230. package/dist/tools/handlers/notebook-creation.d.ts.map +1 -1
  231. package/dist/tools/handlers/notebook-creation.js +102 -86
  232. package/dist/tools/handlers/notebook-creation.js.map +1 -1
  233. package/dist/tools/handlers/notebook-management.d.ts +8 -8
  234. package/dist/tools/handlers/notebook-management.d.ts.map +1 -1
  235. package/dist/tools/handlers/notebook-management.js +34 -80
  236. package/dist/tools/handlers/notebook-management.js.map +1 -1
  237. package/dist/tools/handlers/session-management.d.ts +8 -10
  238. package/dist/tools/handlers/session-management.d.ts.map +1 -1
  239. package/dist/tools/handlers/session-management.js +34 -63
  240. package/dist/tools/handlers/session-management.js.map +1 -1
  241. package/dist/tools/handlers/system.d.ts.map +1 -1
  242. package/dist/tools/handlers/system.js +45 -10
  243. package/dist/tools/handlers/system.js.map +1 -1
  244. package/dist/tools/handlers/types.d.ts +1 -1
  245. package/dist/tools/handlers/types.d.ts.map +1 -1
  246. package/dist/tools/handlers/webhooks.d.ts.map +1 -1
  247. package/dist/tools/handlers/webhooks.js +15 -13
  248. package/dist/tools/handlers/webhooks.js.map +1 -1
  249. package/dist/types.d.ts +7 -17
  250. package/dist/types.d.ts.map +1 -1
  251. package/dist/utils/audit-logger.d.ts +19 -1
  252. package/dist/utils/audit-logger.d.ts.map +1 -1
  253. package/dist/utils/audit-logger.js +198 -30
  254. package/dist/utils/audit-logger.js.map +1 -1
  255. package/dist/utils/cleanup-manager.d.ts.map +1 -1
  256. package/dist/utils/cleanup-manager.js +6 -3
  257. package/dist/utils/cleanup-manager.js.map +1 -1
  258. package/dist/utils/crypto.d.ts +4 -1
  259. package/dist/utils/crypto.d.ts.map +1 -1
  260. package/dist/utils/crypto.js +32 -21
  261. package/dist/utils/crypto.js.map +1 -1
  262. package/dist/utils/file-lock.d.ts.map +1 -1
  263. package/dist/utils/file-lock.js +87 -16
  264. package/dist/utils/file-lock.js.map +1 -1
  265. package/dist/utils/file-permissions.d.ts +2 -0
  266. package/dist/utils/file-permissions.d.ts.map +1 -1
  267. package/dist/utils/file-permissions.js +2 -1
  268. package/dist/utils/file-permissions.js.map +1 -1
  269. package/dist/utils/logger.d.ts +4 -0
  270. package/dist/utils/logger.d.ts.map +1 -1
  271. package/dist/utils/logger.js +16 -0
  272. package/dist/utils/logger.js.map +1 -1
  273. package/dist/utils/page-utils.d.ts +13 -0
  274. package/dist/utils/page-utils.d.ts.map +1 -1
  275. package/dist/utils/page-utils.js +61 -39
  276. package/dist/utils/page-utils.js.map +1 -1
  277. package/dist/utils/response-validator.d.ts.map +1 -1
  278. package/dist/utils/response-validator.js +27 -22
  279. package/dist/utils/response-validator.js.map +1 -1
  280. package/dist/utils/secrets-scanner.d.ts +11 -0
  281. package/dist/utils/secrets-scanner.d.ts.map +1 -1
  282. package/dist/utils/secrets-scanner.js +65 -17
  283. package/dist/utils/secrets-scanner.js.map +1 -1
  284. package/dist/utils/secure-memory.d.ts +9 -31
  285. package/dist/utils/secure-memory.d.ts.map +1 -1
  286. package/dist/utils/secure-memory.js +17 -102
  287. package/dist/utils/secure-memory.js.map +1 -1
  288. package/dist/utils/security.d.ts +4 -3
  289. package/dist/utils/security.d.ts.map +1 -1
  290. package/dist/utils/security.js +43 -13
  291. package/dist/utils/security.js.map +1 -1
  292. package/dist/utils/stealth-utils.d.ts.map +1 -1
  293. package/dist/utils/stealth-utils.js +4 -4
  294. package/dist/utils/stealth-utils.js.map +1 -1
  295. package/dist/webhooks/types.d.ts +4 -0
  296. package/dist/webhooks/types.d.ts.map +1 -1
  297. package/dist/webhooks/webhook-dispatcher.d.ts +80 -12
  298. package/dist/webhooks/webhook-dispatcher.d.ts.map +1 -1
  299. package/dist/webhooks/webhook-dispatcher.js +497 -74
  300. package/dist/webhooks/webhook-dispatcher.js.map +1 -1
  301. package/docs/archive/ISSUES-legacy-2026-04-24.md +644 -0
  302. package/docs/dependency-risk.md +25 -0
  303. package/docs/testing-runbook.md +166 -0
  304. package/docs/usage-guide.md +2 -1
  305. package/package.json +34 -16
@@ -1,897 +1,111 @@
1
1
  /**
2
2
  * NotebookLM Notebook Creator
3
3
  *
4
- * Creates notebooks programmatically via browser automation.
5
- * Supports URL, text, and file sources.
4
+ * Thin orchestration layer for browser navigation and source addition.
6
5
  */
7
- import { findElement, waitForElement, getSelectors } from "./selectors.js";
8
6
  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/";
7
+ import { randomDelay } from "../utils/stealth-utils.js";
8
+ import { NotebookNavigation } from "./notebook-nav.js";
9
+ import { NotebookCreationSourceManager } from "./source-manager.js";
14
10
  /**
15
11
  * Creates NotebookLM notebooks with sources
16
12
  */
17
13
  export class NotebookCreator {
18
- authManager;
19
- contextManager;
20
- page = null;
14
+ operationQueue = Promise.resolve();
15
+ navigation;
16
+ sourceManager;
21
17
  constructor(authManager, contextManager) {
22
- this.authManager = authManager;
23
- this.contextManager = contextManager;
18
+ this.navigation = new NotebookNavigation(authManager, contextManager);
19
+ this.sourceManager = new NotebookCreationSourceManager(() => this.navigation.getCurrentPage());
20
+ }
21
+ async withOperationLock(fn) {
22
+ let release;
23
+ const acquired = new Promise((resolve) => { release = resolve; });
24
+ const previous = this.operationQueue;
25
+ this.operationQueue = acquired;
26
+ await previous;
27
+ try {
28
+ return await fn();
29
+ }
30
+ finally {
31
+ release();
32
+ }
24
33
  }
25
- /**
26
- * Create a new notebook with sources
27
- */
28
34
  async createNotebook(options) {
35
+ return this.withOperationLock(() => this.runCreateNotebook(options));
36
+ }
37
+ async runCreateNotebook(options) {
29
38
  const { name, sources, sendProgress } = options;
30
- const totalSteps = 3 + sources.length; // Init + Create + Sources + Finalize
39
+ const totalSteps = 3 + sources.length;
31
40
  let currentStep = 0;
32
41
  const failedSources = [];
33
42
  let successCount = 0;
34
43
  try {
35
- // Step 1: Initialize browser and navigate
36
44
  currentStep++;
37
45
  await sendProgress?.("Initializing browser...", currentStep, totalSteps);
38
- await this.initialize(options.browserOptions?.headless);
39
- // Step 2: Create new notebook
46
+ await this.navigation.initialize(options.browserOptions?.headless);
40
47
  currentStep++;
41
48
  await sendProgress?.("Creating new notebook...", currentStep, totalSteps);
42
- await this.clickNewNotebook();
43
- // Wait for notebook to fully load and stabilize
49
+ await this.navigation.clickNewNotebook();
44
50
  await randomDelay(3000, 4000);
45
- // Verify we're on a notebook page
46
- const createdNotebookUrl = this.page.url();
51
+ const page = this.navigation.getCurrentPage();
52
+ if (!page) {
53
+ throw new Error("Notebook creation page not available after clicking new notebook");
54
+ }
55
+ const createdNotebookUrl = page.url();
47
56
  log.info(`📍 Notebook URL after creation: ${createdNotebookUrl}`);
48
57
  if (!createdNotebookUrl.includes("/notebook/")) {
49
- throw new Error(`Failed to create notebook - unexpected URL: ${createdNotebookUrl}`);
58
+ throw new Error("Failed to create notebook - unexpected URL received (check logs for details)");
50
59
  }
51
- // Store the notebook ID for verification later
52
60
  const notebookId = createdNotebookUrl.split("/notebook/")[1]?.split("?")[0];
53
61
  log.info(`📓 Created notebook ID: ${notebookId}`);
54
- await this.setNotebookName(name);
55
- // Step 3+: Add each source
62
+ await this.navigation.setNotebookName(name);
56
63
  for (const source of sources) {
57
64
  currentStep++;
58
- const sourceDesc = this.getSourceDescription(source);
65
+ const sourceDesc = this.sourceManager.getSourceDescription(source);
59
66
  await sendProgress?.(`Adding source: ${sourceDesc}...`, currentStep, totalSteps);
60
67
  try {
61
- await this.addSource(source);
68
+ await this.navigation.validateCurrentAuth();
69
+ await this.sourceManager.addSource(source);
62
70
  successCount++;
63
71
  log.success(`✅ Added source: ${sourceDesc}`);
64
72
  }
65
73
  catch (error) {
66
74
  const errorMsg = error instanceof Error ? error.message : String(error);
67
75
  log.error(`❌ Failed to add source: ${sourceDesc} - ${errorMsg}`);
68
- failedSources.push({ source, error: errorMsg });
76
+ failedSources.push({
77
+ source,
78
+ error: errorMsg,
79
+ ...(process.env.DEBUG === "true" && error instanceof Error && error.stack
80
+ ? { stack: error.stack }
81
+ : {}),
82
+ });
69
83
  }
70
- // Delay between sources
71
84
  await randomDelay(1000, 2000);
72
85
  }
73
- // Step N: Finalize and get URL
74
86
  currentStep++;
75
87
  await sendProgress?.("Finalizing notebook...", currentStep, totalSteps);
76
- const notebookUrl = await this.finalizeAndGetUrl();
88
+ const notebookUrl = await this.navigation.finalizeAndGetUrl();
77
89
  log.success(`✅ Notebook created: ${notebookUrl}`);
78
90
  return {
79
91
  url: notebookUrl,
80
92
  name,
81
93
  sourceCount: successCount,
82
94
  createdAt: new Date().toISOString(),
95
+ partial: failedSources.length > 0,
83
96
  failedSources: failedSources.length > 0 ? failedSources : undefined,
84
97
  };
85
98
  }
86
99
  catch (error) {
87
- const errorMsg = error instanceof Error ? error.message : String(error);
88
- log.error(`❌ Notebook creation failed: ${errorMsg}`);
89
100
  throw error;
90
101
  }
91
102
  finally {
92
- await this.cleanup();
93
- }
94
- }
95
- /**
96
- * Initialize browser and navigate to NotebookLM
97
- */
98
- async initialize(headless) {
99
- log.info("🌐 Initializing browser for notebook creation...");
100
- // Get browser context
101
- // Note: getOrCreateContext(true) = show browser, getOrCreateContext(false) = headless
102
- // When browserOptions.headless === false, user wants visible browser, so pass true
103
- const context = await this.contextManager.getOrCreateContext(headless === false ? true : undefined);
104
- // Check authentication
105
- const isAuthenticated = await this.authManager.validateWithRetry(context);
106
- if (!isAuthenticated) {
107
- throw new Error("Not authenticated to NotebookLM. Please run setup_auth first.");
108
- }
109
- // Create new page
110
- this.page = await context.newPage();
111
- // Navigate to NotebookLM
112
- await this.page.goto(NOTEBOOKLM_URL, {
113
- waitUntil: "domcontentloaded",
114
- timeout: CONFIG.browserTimeout,
115
- });
116
- await randomDelay(2000, 3000);
117
- // Wait for page to be ready
118
- await this.page.waitForLoadState("networkidle").catch(() => { });
119
- log.success("✅ Browser initialized and navigated to NotebookLM");
120
- }
121
- /**
122
- * Click the "New notebook" button
123
- */
124
- async clickNewNotebook() {
125
- if (!this.page)
126
- throw new Error("Page not initialized");
127
- log.info("📝 Clicking 'New notebook' button...");
128
- // Try to find and click the new notebook button
129
- const selectors = getSelectors("newNotebookButton");
130
- for (const selector of selectors) {
131
- try {
132
- const element = await this.page.$(selector);
133
- if (element && await element.isVisible()) {
134
- await realisticClick(this.page, selector, true);
135
- await randomDelay(1000, 2000);
136
- log.success("✅ Clicked 'New notebook' button");
137
- return;
138
- }
139
- }
140
- catch {
141
- continue;
142
- }
143
- }
144
- // Try text-based selectors as fallback via evaluate (since :has-text() isn't supported)
145
- const textPatterns = ["New notebook", "Create notebook", "Create new", "New"];
146
- for (const pattern of textPatterns) {
147
- try {
148
- const clicked = await this.page.evaluate((searchText) => {
149
- // @ts-expect-error - DOM types
150
- const elements = document.querySelectorAll('button, a, [role="button"]');
151
- for (const el of elements) {
152
- const elText = el.textContent?.toLowerCase() || "";
153
- const ariaLabel = el.getAttribute("aria-label")?.toLowerCase() || "";
154
- if (elText.includes(searchText.toLowerCase()) || ariaLabel.includes(searchText.toLowerCase())) {
155
- el.click();
156
- return true;
157
- }
158
- }
159
- return false;
160
- }, pattern);
161
- if (clicked) {
162
- await randomDelay(1000, 2000);
163
- log.success("✅ Clicked 'New notebook' button (text match)");
164
- return;
165
- }
166
- }
167
- catch {
168
- continue;
169
- }
170
- }
171
- throw new Error("Could not find 'New notebook' button");
172
- }
173
- /**
174
- * Set the notebook name
175
- */
176
- async setNotebookName(name) {
177
- if (!this.page)
178
- throw new Error("Page not initialized");
179
- log.info(`📝 Setting notebook name: ${name}`);
180
- // Wait for and find the name input
181
- const element = await waitForElement(this.page, "notebookNameInput", {
182
- timeout: 10000,
183
- });
184
- if (!element) {
185
- // NotebookLM might auto-generate a name - check if we're on the notebook page
186
- log.warning("⚠️ Name input not found - notebook may have been created with default name");
187
- return;
188
- }
189
- // Type the name
190
- const selectors = getSelectors("notebookNameInput");
191
- for (const selector of selectors) {
192
- try {
193
- const input = await this.page.$(selector);
194
- if (input && await input.isVisible()) {
195
- await humanType(this.page, selector, name, { withTypos: false });
196
- await randomDelay(500, 1000);
197
- log.success(`✅ Set notebook name: ${name}`);
198
- return;
199
- }
200
- }
201
- catch {
202
- continue;
203
- }
204
- }
205
- }
206
- /**
207
- * Add a source to the notebook
208
- */
209
- async addSource(source) {
210
- if (!this.page)
211
- throw new Error("Page not initialized");
212
- // CRITICAL: Track the notebook URL to detect accidental navigation
213
- const expectedNotebookUrl = this.page.url();
214
- log.info(`📍 Current notebook URL: ${expectedNotebookUrl}`);
215
- // Check if source dialog is already open (happens for new notebooks)
216
- const dialogAlreadyOpen = await this.isSourceDialogOpen();
217
- log.info(`📋 Source dialog already open: ${dialogAlreadyOpen}`);
218
- if (!dialogAlreadyOpen) {
219
- // Click "Add source" button only if dialog isn't already open
220
- await this.clickAddSource();
221
- // Verify we didn't accidentally navigate away
222
- const currentUrl = this.page.url();
223
- if (!currentUrl.includes("/notebook/") ||
224
- (expectedNotebookUrl.includes("/notebook/") &&
225
- !currentUrl.includes(expectedNotebookUrl.split("/notebook/")[1]?.split("?")[0] || ""))) {
226
- log.error(`❌ URL changed unexpectedly! Expected: ${expectedNotebookUrl}, Got: ${currentUrl}`);
227
- throw new Error(`Navigation error: accidentally navigated away from notebook. This may indicate clicking wrong button.`);
228
- }
229
- }
230
- else {
231
- log.info("📋 Source dialog already open - skipping clickAddSource");
232
- }
233
- // Handle based on source type
234
- switch (source.type) {
235
- case "url":
236
- await this.addUrlSource(source.value);
237
- break;
238
- case "text":
239
- await this.addTextSource(source.value, source.title);
240
- break;
241
- case "file":
242
- await this.addFileSource(source.value);
243
- break;
244
- default:
245
- throw new Error(`Unknown source type: ${source.type}`);
246
- }
247
- // Verify we're still on the same notebook after adding source
248
- const finalUrl = this.page.url();
249
- log.info(`📍 URL after adding source: ${finalUrl}`);
250
- }
251
- /**
252
- * Check if the source dialog is already open
253
- */
254
- async isSourceDialogOpen() {
255
- if (!this.page)
256
- return false;
257
- // Check for source dialog indicators
258
- const dialogIndicators = await this.page.evaluate(() => {
259
- // Method 0: Structural check — any visible mat-dialog-container means a dialog is open
260
- // Locale-independent (class name, not translated text)
261
- // @ts-expect-error - DOM types
262
- const dialogs = document.querySelectorAll("mat-dialog-container");
263
- for (const d of dialogs) {
264
- if (d.offsetParent !== null && d.textContent?.trim()) {
265
- return { open: true, reason: "dialog_container_visible" };
266
- }
267
- }
268
- // Method 1: Check for source type chip group (locale-independent structural check)
269
- // @ts-expect-error - DOM types
270
- const chipGroup = document.querySelector("mat-chip-listbox, mat-chip-group, mat-chip-set");
271
- if (chipGroup && chipGroup.offsetParent !== null) {
272
- return { open: true, reason: "chip_group_visible" };
273
- }
274
- // Method 2: Check for file upload dropzone (initial notebook state, locale-independent)
275
- // @ts-expect-error - DOM types
276
- const dropzones = document.querySelectorAll('.dropzone, [class*="dropzone"]');
277
- if (dropzones.length > 0) {
278
- for (const dz of dropzones) {
279
- if (dz.offsetParent !== null) {
280
- return { open: true, reason: "dropzone_visible" };
281
- }
282
- }
283
- }
284
- // Method 3: Check for file upload button (locale-independent: class/type, not text)
285
- // @ts-expect-error - DOM types
286
- const uploadBtn = document.querySelector('button[class*="upload"], input[type="file"]');
287
- if (uploadBtn && uploadBtn.offsetParent !== null) {
288
- return { open: true, reason: "upload_button_visible" };
289
- }
290
- return { open: false };
291
- });
292
- log.info(`📋 isSourceDialogOpen check: ${JSON.stringify(dialogIndicators)}`);
293
- return dialogIndicators.open;
294
- }
295
- /**
296
- * Click the "Add source" button
297
- */
298
- async clickAddSource() {
299
- if (!this.page)
300
- throw new Error("Page not initialized");
301
- log.info("📎 Clicking 'Add source' button...");
302
- // DEBUG: Log current URL to see if we're on the notebook page
303
- const currentUrl = this.page.url();
304
- log.info(` Current URL: ${currentUrl}`);
305
- // Wait for page to settle and for any animations/updates to complete
306
- await randomDelay(2000, 3000);
307
- // DEBUG: Log all buttons found on the page
308
- const buttonsInfo = await this.page.evaluate(() => {
309
- // @ts-expect-error - DOM types
310
- const buttons = document.querySelectorAll('button, [role="button"]');
311
- const info = [];
312
- for (const btn of buttons) {
313
- const text = btn.textContent?.trim().substring(0, 50) || "";
314
- const aria = btn.getAttribute("aria-label") || "";
315
- const cls = btn.className?.substring(0, 50) || "";
316
- const visible = btn.offsetParent !== null;
317
- // Only include buttons with relevant content
318
- if (aria.toLowerCase().includes("add") || aria.toLowerCase().includes("create") ||
319
- text.toLowerCase().includes("add") || text.toLowerCase().includes("create") ||
320
- cls.toLowerCase().includes("add") || cls.toLowerCase().includes("create")) {
321
- info.push({ text, aria, class: cls, visible });
322
- }
323
- }
324
- return info;
325
- });
326
- log.info(` Buttons found: ${JSON.stringify(buttonsInfo, null, 2)}`);
327
- // Method 1: Use Playwright locator with aria-label (try both singular and plural)
328
- try {
329
- // Try singular first
330
- let addSourceLocator = this.page.locator('button[aria-label="Add source"]');
331
- let count = await addSourceLocator.count();
332
- log.info(` Method 1a: Found ${count} button(s) with aria-label="Add source"`);
333
- // Try plural if singular not found
334
- if (count === 0) {
335
- addSourceLocator = this.page.locator('button[aria-label="Add sources"]');
336
- count = await addSourceLocator.count();
337
- log.info(` Method 1b: Found ${count} button(s) with aria-label="Add sources"`);
338
- }
339
- if (count > 0) {
340
- const isVisible = await addSourceLocator.first().isVisible();
341
- log.info(` Method 1: First button visible: ${isVisible}`);
342
- if (isVisible) {
343
- await addSourceLocator.first().click();
344
- await randomDelay(800, 1500);
345
- log.success("✅ Clicked 'Add source' button (locator)");
346
- return;
347
- }
348
- }
349
- }
350
- catch (e) {
351
- log.info(` Locator approach failed: ${e}`);
352
- }
353
- // Method 2: Use class selector
354
- try {
355
- const classLocator = this.page.locator('button.add-source-button');
356
- const count = await classLocator.count();
357
- log.info(` Method 2: Found ${count} button(s) with class add-source-button`);
358
- if (count > 0) {
359
- const isVisible = await classLocator.first().isVisible();
360
- log.info(` Method 2: First button visible: ${isVisible}`);
361
- if (isVisible) {
362
- await classLocator.first().click();
363
- await randomDelay(800, 1500);
364
- log.success("✅ Clicked 'Add source' button (class)");
365
- return;
366
- }
367
- }
368
- }
369
- catch (e) {
370
- log.info(` Class selector failed: ${e}`);
371
- }
372
- // Method 3: Fallback using page.evaluate with JavaScript click
373
- try {
374
- const clicked = await this.page.evaluate(() => {
375
- // @ts-expect-error - DOM types
376
- const elements = document.querySelectorAll('button, [role="button"]');
377
- for (const el of elements) {
378
- const elText = el.textContent?.trim().toLowerCase() || "";
379
- const ariaLabel = el.getAttribute("aria-label")?.toLowerCase() || "";
380
- const className = el.className?.toLowerCase() || "";
381
- // Skip if this is a "Create notebook" or "Add note" button
382
- // Check BOTH aria-label AND text content for "create" to avoid clicking wrong button
383
- if (ariaLabel.includes("create") || className.includes("create-notebook") ||
384
- elText.includes("create") || elText.includes("add note") ||
385
- className.includes("add-note")) {
386
- continue;
387
- }
388
- // Match "Add source" or "Add sources" specifically
389
- if (ariaLabel === "add source" || ariaLabel.includes("add source") ||
390
- elText.includes("add source") || className.includes("add-source")) {
391
- el.click();
392
- return { clicked: true, aria: ariaLabel, text: elText.substring(0, 30) };
393
- }
394
- }
395
- return { clicked: false };
396
- });
397
- if (clicked.clicked) {
398
- await randomDelay(800, 1500);
399
- log.success(`✅ Clicked 'Add source' button (JS fallback) - aria: ${clicked.aria}, text: ${clicked.text}`);
400
- return;
401
- }
402
- }
403
- catch {
404
- // Continue to error
405
- }
406
- // If we get here, button wasn't found. Try waiting and retrying once more.
407
- log.warning("⚠️ Add source button not found, waiting and retrying...");
408
- await randomDelay(3000, 4000);
409
- // Final retry with Method 1 (try both singular and plural)
410
- try {
411
- let addSourceLocator = this.page.locator('button[aria-label="Add source"]');
412
- let count = await addSourceLocator.count();
413
- log.info(` Retry: Found ${count} button(s) with aria-label="Add source"`);
414
- if (count === 0) {
415
- addSourceLocator = this.page.locator('button[aria-label="Add sources"]');
416
- count = await addSourceLocator.count();
417
- log.info(` Retry: Found ${count} button(s) with aria-label="Add sources"`);
418
- }
419
- if (count > 0 && await addSourceLocator.first().isVisible()) {
420
- await addSourceLocator.first().click();
421
- await randomDelay(800, 1500);
422
- log.success("✅ Clicked 'Add source' button (retry)");
423
- return;
424
- }
425
- }
426
- catch (e) {
427
- log.info(` Retry failed: ${e}`);
428
- }
429
- throw new Error("Could not find 'Add source' button after retry");
430
- }
431
- /**
432
- * Add a URL source
433
- */
434
- async addUrlSource(url) {
435
- if (!this.page)
436
- throw new Error("Page not initialized");
437
- log.info(`🔗 Adding URL source: ${url}`);
438
- // Click "Website" option - discovered as span with "Website" text
439
- await this.clickSourceTypeByText(["Website", "webWebsite", "Link", "Discover sources"]);
440
- // Find and fill URL input
441
- await randomDelay(500, 1000);
442
- const selectors = getSelectors("urlInput");
443
- for (const selector of selectors) {
444
- try {
445
- const input = await this.page.$(selector);
446
- if (input && await input.isVisible()) {
447
- await humanType(this.page, selector, url, { withTypos: false });
448
- await randomDelay(500, 1000);
449
- // Submit
450
- await this.clickSubmitButton();
451
- await this.waitForSourceProcessing();
452
- return;
453
- }
454
- }
455
- catch {
456
- continue;
457
- }
458
- }
459
- throw new Error("Could not find URL input field");
460
- }
461
- /**
462
- * Add a text source
463
- */
464
- async addTextSource(text, title) {
465
- if (!this.page)
466
- throw new Error("Page not initialized");
467
- log.info(`📝 Adding text source${title ? `: ${title}` : ""}`);
468
- // Click "Copied text" source type chip.
469
- // NOTE: This chip label is locale-dependent ("Texte copié" in French etc.).
470
- // We try locale-independent data attributes first, then fall back to English text.
471
- const textOptionClicked = await this.page.evaluate(() => {
472
- // Primary: data attribute (locale-independent, if NotebookLM uses stable data-type values)
473
- // @ts-expect-error - DOM types
474
- const byData = document.querySelector('[data-source-type="text"], [data-type="text"], mat-chip[value="text"]');
475
- if (byData) {
476
- byData.click();
477
- return { clicked: true, method: "data-attr", text: byData.textContent?.substring(0, 30) };
478
- }
479
- // Fallback: text match (English only — may miss French/other locales)
480
- // @ts-expect-error - DOM types
481
- const chips = document.querySelectorAll('mat-chip, mat-chip-option, [mat-chip-option]');
482
- for (const chip of chips) {
483
- const text = chip.textContent?.trim() || "";
484
- if (text.includes("Copied text")) {
485
- chip.click();
486
- return { clicked: true, method: "mat-chip", text: text.substring(0, 30) };
487
- }
488
- }
489
- // Fallback: find span with exact text and click its closest clickable ancestor
490
- // @ts-expect-error - DOM types
491
- const spans = document.querySelectorAll('span');
492
- for (const span of spans) {
493
- const text = span.textContent?.trim() || "";
494
- if (text === "Copied text") {
495
- // Try to find clickable parent (mat-chip, button, or div with click handler)
496
- let target = span;
497
- for (let i = 0; i < 5; i++) {
498
- if (target.parentElement) {
499
- target = target.parentElement;
500
- const tagName = target.tagName?.toLowerCase();
501
- if (tagName === "mat-chip" || tagName === "mat-chip-option" || tagName === "button") {
502
- target.click();
503
- return { clicked: true, method: "parent-" + tagName };
504
- }
505
- }
506
- }
507
- // If no good parent, just click the span
508
- span.click();
509
- return { clicked: true, method: "span-direct" };
510
- }
511
- }
512
- return { clicked: false };
513
- });
514
- if (!textOptionClicked.clicked) {
515
- log.warning("⚠️ Could not click 'Copied text' option");
516
- }
517
- // Wait for text area to appear
518
- await randomDelay(2000, 2500);
519
- // Find the text area - discovered as textarea.text-area
520
- const textarea = await this.page.$('textarea.text-area') ||
521
- await this.page.$('textarea[class*="text-area"]') ||
522
- await this.page.$('textarea.mat-mdc-form-field-textarea-control');
523
- if (textarea) {
524
- const isVisible = await textarea.isVisible().catch(() => false);
525
- if (!isVisible) {
526
- // Try waiting a bit more
527
- await randomDelay(1000, 1500);
528
- }
529
- // Click to focus
530
- await textarea.click();
531
- await randomDelay(200, 400);
532
- // For large text, use clipboard paste instead of typing
533
- if (text.length > 500) {
534
- await this.page.evaluate((t) => {
535
- // @ts-expect-error - DOM types available in browser context
536
- navigator.clipboard.writeText(t);
537
- }, text);
538
- await this.page.keyboard.press("Control+V");
539
- }
540
- else {
541
- // Type the text
542
- await textarea.fill(text);
543
- }
544
- await randomDelay(500, 1000);
545
- // Click "Insert" button
546
- await this.clickInsertButton();
547
- // Wait for processing but be lenient with errors
548
- await this.waitForSourceProcessingLenient();
549
- return;
550
- }
551
- throw new Error("Could not find text input area");
552
- }
553
- /**
554
- * Add a file source
555
- * December 2025: NotebookLM creates a hidden input[type="file"] AFTER clicking
556
- * the "choose file" button. CRITICAL: Must use Playwright's real click (not JS click)
557
- * because Angular blocks programmatic JavaScript clicks on the upload trigger.
558
- */
559
- async addFileSource(filePath) {
560
- if (!this.page)
561
- throw new Error("Page not initialized");
562
- // Validate file exists
563
- const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
564
- if (!fs.existsSync(absolutePath)) {
565
- throw new Error(`File not found: ${absolutePath}`);
566
- }
567
- log.info(`📁 Adding file source: ${path.basename(absolutePath)}`);
568
- await randomDelay(500, 1000);
569
- // Method 1 (PRIMARY): Use Playwright's real click on "choose file" button
570
- // CRITICAL: Angular blocks JavaScript element.click() - must use Playwright's click()
571
- log.info(" Using Playwright click on 'choose file' button...");
572
- try {
573
- // Try clicking the "choose file" span using Playwright's real click
574
- const chooseFileLocator = this.page.locator('span.dropzone__file-dialog-button');
575
- if (await chooseFileLocator.count() > 0 && await chooseFileLocator.first().isVisible()) {
576
- await chooseFileLocator.first().click();
577
- log.info(" Clicked 'choose file' span with Playwright");
578
- // Wait for the file input to appear (it's created dynamically after real click)
579
- // Note: The input has display:none, so use state:'attached' not 'visible'
580
- await this.page.waitForSelector('input[type="file"]', { timeout: 5000, state: 'attached' });
581
- await randomDelay(200, 400);
582
- // Now set the file on the newly created input
583
- const fileInputLocator = this.page.locator('input[type="file"]');
584
- await fileInputLocator.first().setInputFiles(absolutePath);
585
- log.success(" ✅ File uploaded via Playwright click + setInputFiles");
586
- await randomDelay(1000, 2000);
587
- // Use lenient processing check for file uploads (avoids false positive error detection)
588
- await this.waitForSourceProcessingLenient();
589
- return;
590
- }
591
- // Try the xapscotty trigger button
592
- const xapscottyLocator = this.page.locator('[xapscottyuploadertrigger]');
593
- if (await xapscottyLocator.count() > 0 && await xapscottyLocator.first().isVisible()) {
594
- await xapscottyLocator.first().click();
595
- log.info(" Clicked xapscotty trigger with Playwright");
596
- await this.page.waitForSelector('input[type="file"]', { timeout: 5000, state: 'attached' });
597
- await randomDelay(200, 400);
598
- const fileInputLocator = this.page.locator('input[type="file"]');
599
- await fileInputLocator.first().setInputFiles(absolutePath);
600
- log.success(" ✅ File uploaded via xapscotty click + setInputFiles");
601
- await randomDelay(1000, 2000);
602
- await this.waitForSourceProcessingLenient();
603
- return;
604
- }
605
- // Try the upload icon button (class-based first for locale-independence, then English aria-label)
606
- const uploadBtnLocator = this.page.locator('button[class*="upload"], button[aria-label="Upload sources from your computer"]');
607
- if (await uploadBtnLocator.count() > 0 && await uploadBtnLocator.first().isVisible()) {
608
- await uploadBtnLocator.first().click();
609
- log.info(" Clicked upload button with Playwright");
610
- await this.page.waitForSelector('input[type="file"]', { timeout: 5000, state: 'attached' });
611
- await randomDelay(200, 400);
612
- const fileInputLocator = this.page.locator('input[type="file"]');
613
- await fileInputLocator.first().setInputFiles(absolutePath);
614
- log.success(" ✅ File uploaded via upload button click + setInputFiles");
615
- await randomDelay(1000, 2000);
616
- await this.waitForSourceProcessingLenient();
617
- return;
618
- }
619
- }
620
- catch (e) {
621
- log.info(` Playwright click approach: ${e}`);
622
- }
623
- // Method 2: Try filechooser event with Playwright click
624
- log.info(" Trying filechooser event approach...");
625
- try {
626
- // Set up file chooser listener BEFORE clicking
627
- const fileChooserPromise = this.page.waitForEvent('filechooser', { timeout: 5000 });
628
- // Click using Playwright's real click
629
- const chooseFileLocator = this.page.locator('span.dropzone__file-dialog-button');
630
- if (await chooseFileLocator.count() > 0) {
631
- await chooseFileLocator.first().click();
632
- }
633
- const fileChooser = await fileChooserPromise;
634
- await fileChooser.setFiles(absolutePath);
635
- log.success(" ✅ File uploaded via filechooser event");
636
- await randomDelay(1000, 2000);
637
- await this.waitForSourceProcessingLenient();
638
- return;
639
- }
640
- catch (e) {
641
- log.info(` Filechooser approach: ${e}`);
642
- }
643
- // Method 3: Try existing input[type="file"] directly (in case it already exists)
644
- try {
645
- const fileInputLocator = this.page.locator('input[type="file"]');
646
- const count = await fileInputLocator.count();
647
- if (count > 0) {
648
- await fileInputLocator.first().setInputFiles(absolutePath);
649
- log.success(" ✅ File uploaded via existing locator");
650
- await randomDelay(1000, 2000);
651
- await this.waitForSourceProcessingLenient();
652
- return;
653
- }
654
- }
655
- catch (e) {
656
- log.info(` Existing locator attempt: ${e}`);
657
- }
658
- throw new Error("Could not upload file - all methods failed. NotebookLM may be using an unsupported upload method.");
659
- }
660
- /**
661
- * Click a source type by text content (for the new dialog structure)
662
- */
663
- async clickSourceTypeByText(textPatterns) {
664
- if (!this.page)
665
- throw new Error("Page not initialized");
666
- for (const pattern of textPatterns) {
667
- try {
668
- const clicked = await this.page.evaluate((searchText) => {
669
- // @ts-expect-error - DOM types
670
- const elements = document.querySelectorAll('span, button, [role="button"], div');
671
- for (const el of elements) {
672
- const text = el.textContent?.trim() || "";
673
- // Match exact text or text that contains the pattern
674
- if (text === searchText || text.toLowerCase().includes(searchText.toLowerCase())) {
675
- // Make sure it's visible
676
- if (el.offsetParent !== null) {
677
- el.click();
678
- return true;
679
- }
680
- }
681
- }
682
- return false;
683
- }, pattern);
684
- if (clicked) {
685
- log.success(`✅ Clicked source type: ${pattern}`);
686
- await randomDelay(800, 1200);
687
- return;
688
- }
689
- }
690
- catch {
691
- continue;
692
- }
693
- }
694
- log.warning(`⚠️ Could not find source type: ${textPatterns.join(", ")}`);
695
- }
696
- /**
697
- * Click the submit/add button
698
- */
699
- async clickSubmitButton() {
700
- if (!this.page)
701
- throw new Error("Page not initialized");
702
- const selectors = getSelectors("submitButton");
703
- for (const selector of selectors) {
704
- try {
705
- const element = await this.page.$(selector);
706
- if (element && await element.isVisible()) {
707
- await element.click();
708
- return;
709
- }
710
- }
711
- catch {
712
- continue;
713
- }
714
- }
715
- // Try pressing Enter as fallback
716
- await this.page.keyboard.press("Enter");
717
- }
718
- /**
719
- * Click the "Insert" button (for text sources)
720
- */
721
- async clickInsertButton() {
722
- if (!this.page)
723
- throw new Error("Page not initialized");
724
- // Find and click the "Insert" button — prefer locale-independent selectors
725
- const clicked = await this.page.evaluate(() => {
726
- // Primary: type=submit (locale-independent)
727
- // @ts-expect-error - DOM types
728
- const submitBtn = document.querySelector("button[type='submit']:not([disabled])");
729
- if (submitBtn && submitBtn.offsetParent !== null) {
730
- submitBtn.click();
731
- return true;
732
- }
733
- // Secondary: primary color class (locale-independent NotebookLM convention)
734
- // @ts-expect-error - DOM types
735
- const primaryBtn = document.querySelector("button.button-color--primary:not([disabled])");
736
- if (primaryBtn && primaryBtn.offsetParent !== null) {
737
- primaryBtn.click();
738
- return true;
739
- }
740
- // Fallback: text match (English only)
741
- // @ts-expect-error - DOM types
742
- const buttons = document.querySelectorAll("button");
743
- for (const btn of buttons) {
744
- const text = btn.textContent?.trim() || "";
745
- if (text === "Insert" || text.toLowerCase() === "insert") {
746
- btn.click();
747
- return true;
748
- }
749
- }
750
- return false;
751
- });
752
- if (clicked) {
753
- log.success("✅ Clicked 'Insert' button");
754
- return;
755
- }
756
- // Fallback: try the general submit button
757
- log.warning("⚠️ 'Insert' button not found, trying submit button");
758
- await this.clickSubmitButton();
759
- }
760
- /**
761
- * Wait for source processing to complete
762
- */
763
- async waitForSourceProcessing() {
764
- if (!this.page)
765
- throw new Error("Page not initialized");
766
- log.info("⏳ Waiting for source processing...");
767
- const timeout = 60000; // 1 minute timeout
768
- const startTime = Date.now();
769
- while (Date.now() - startTime < timeout) {
770
- // Check for success indicator
771
- const successElement = await findElement(this.page, "successIndicator");
772
- if (successElement) {
773
- log.success("✅ Source processed successfully");
774
- return;
775
- }
776
- // Check for error
777
- const errorElement = await findElement(this.page, "errorMessage");
778
- if (errorElement) {
779
- // @ts-expect-error - innerText exists on element
780
- const errorText = await errorElement.innerText?.() || "Unknown error";
781
- throw new Error(`Source processing failed: ${errorText}`);
782
- }
783
- // Check if processing indicator is gone
784
- const processingElement = await findElement(this.page, "processingIndicator");
785
- if (!processingElement) {
786
- // No processing indicator and no error - assume success
787
- await randomDelay(1000, 1500);
788
- return;
789
- }
790
- await this.page.waitForTimeout(1000);
791
- }
792
- log.warning("⚠️ Source processing timeout - continuing anyway");
793
- }
794
- /**
795
- * Lenient version of waitForSourceProcessing that ignores false positive errors
796
- */
797
- async waitForSourceProcessingLenient() {
798
- if (!this.page)
799
- throw new Error("Page not initialized");
800
- log.info("⏳ Waiting for source processing...");
801
- // Simple approach: wait a fixed time and check if dialog closed
802
- await randomDelay(3000, 4000);
803
- // Check if we're back to the main notebook view (no source dialog)
804
- const dialogStillOpen = await this.isSourceDialogOpen();
805
- if (!dialogStillOpen) {
806
- log.success("✅ Source dialog closed - assuming success");
807
- return;
808
- }
809
- // Check for actual error indicators (be specific)
810
- const hasError = await this.page.evaluate(() => {
811
- // @ts-expect-error - DOM types
812
- const alerts = document.querySelectorAll('[role="alert"]');
813
- for (const alert of alerts) {
814
- const text = alert.textContent?.toLowerCase() || "";
815
- // Only treat as error if it contains error-related words
816
- if (text.includes("error") || text.includes("failed") || text.includes("invalid") || text.includes("unable")) {
817
- return text.substring(0, 100);
818
- }
819
- }
820
- return null;
821
- });
822
- if (hasError) {
823
- throw new Error(`Source processing failed: ${hasError}`);
824
- }
825
- // Wait a bit more for processing
826
- await randomDelay(2000, 3000);
827
- log.success("✅ Source processing appears complete");
828
- }
829
- /**
830
- * Finalize notebook creation and get the URL
831
- */
832
- async finalizeAndGetUrl() {
833
- if (!this.page)
834
- throw new Error("Page not initialized");
835
- log.info("🔗 Getting notebook URL...");
836
- // The URL should already be the notebook URL after creation
837
- await randomDelay(1000, 2000);
838
- const currentUrl = this.page.url();
839
- // Check if we're on a notebook page
840
- if (currentUrl.includes("/notebook/")) {
841
- return currentUrl;
842
- }
843
- // Try to find the notebook URL in the page
844
- const notebookLinks = await this.page.$$('a[href*="/notebook/"]');
845
- if (notebookLinks.length > 0) {
846
- const href = await notebookLinks[0].getAttribute("href");
847
- if (href) {
848
- return href.startsWith("http") ? href : `https://notebooklm.google.com${href}`;
849
- }
850
- }
851
- // Return current URL as fallback
852
- return currentUrl;
853
- }
854
- /**
855
- * Get a human-readable description of a source
856
- */
857
- getSourceDescription(source) {
858
- switch (source.type) {
859
- case "url":
860
- try {
861
- const url = new URL(source.value);
862
- return `URL: ${url.hostname}`;
863
- }
864
- catch {
865
- return `URL: ${source.value.slice(0, 50)}`;
866
- }
867
- case "text":
868
- return source.title || `Text: ${source.value.slice(0, 30)}...`;
869
- case "file":
870
- return `File: ${path.basename(source.value)}`;
871
- default:
872
- return "Unknown source";
873
- }
874
- }
875
- /**
876
- * Cleanup resources
877
- */
878
- async cleanup() {
879
- if (this.page) {
880
- try {
881
- await this.page.close();
882
- }
883
- catch {
884
- // Ignore cleanup errors
885
- }
886
- this.page = null;
103
+ await this.navigation.cleanup();
887
104
  }
888
105
  }
889
106
  }
890
- /**
891
- * Create a notebook with the given options
892
- */
893
107
  export async function createNotebook(authManager, contextManager, options) {
894
108
  const creator = new NotebookCreator(authManager, contextManager);
895
- return await creator.createNotebook(options);
109
+ return creator.createNotebook(options);
896
110
  }
897
111
  //# sourceMappingURL=notebook-creator.js.map