@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.
- package/README.md +62 -19
- package/SECURITY.md +31 -61
- package/dist/auth/auth-manager.d.ts +2 -1
- package/dist/auth/auth-manager.d.ts.map +1 -1
- package/dist/auth/auth-manager.js +117 -44
- package/dist/auth/auth-manager.js.map +1 -1
- package/dist/auth/mcp-auth.d.ts +24 -4
- package/dist/auth/mcp-auth.d.ts.map +1 -1
- package/dist/auth/mcp-auth.js +149 -19
- package/dist/auth/mcp-auth.js.map +1 -1
- package/dist/compliance/alert-manager.d.ts.map +1 -1
- package/dist/compliance/alert-manager.js +7 -4
- package/dist/compliance/alert-manager.js.map +1 -1
- package/dist/compliance/breach-detection.d.ts.map +1 -1
- package/dist/compliance/breach-detection.js +14 -7
- package/dist/compliance/breach-detection.js.map +1 -1
- package/dist/compliance/change-log.d.ts.map +1 -1
- package/dist/compliance/change-log.js +7 -4
- package/dist/compliance/change-log.js.map +1 -1
- package/dist/compliance/compliance-logger.d.ts.map +1 -1
- package/dist/compliance/compliance-logger.js +11 -6
- package/dist/compliance/compliance-logger.js.map +1 -1
- package/dist/compliance/consent-manager.d.ts.map +1 -1
- package/dist/compliance/consent-manager.js +5 -3
- package/dist/compliance/consent-manager.js.map +1 -1
- package/dist/compliance/data-erasure.d.ts +1 -1
- package/dist/compliance/data-erasure.d.ts.map +1 -1
- package/dist/compliance/data-erasure.js +142 -83
- package/dist/compliance/data-erasure.js.map +1 -1
- package/dist/compliance/data-export.d.ts.map +1 -1
- package/dist/compliance/data-export.js +23 -12
- package/dist/compliance/data-export.js.map +1 -1
- package/dist/compliance/data-inventory.d.ts.map +1 -1
- package/dist/compliance/data-inventory.js +7 -6
- package/dist/compliance/data-inventory.js.map +1 -1
- package/dist/compliance/dsar-handler.d.ts +7 -1
- package/dist/compliance/dsar-handler.d.ts.map +1 -1
- package/dist/compliance/dsar-handler.js +74 -61
- package/dist/compliance/dsar-handler.js.map +1 -1
- package/dist/compliance/evidence-collector.d.ts.map +1 -1
- package/dist/compliance/evidence-collector.js +10 -6
- package/dist/compliance/evidence-collector.js.map +1 -1
- package/dist/compliance/health-monitor.d.ts.map +1 -1
- package/dist/compliance/health-monitor.js +15 -9
- package/dist/compliance/health-monitor.js.map +1 -1
- package/dist/compliance/incident-manager.d.ts.map +1 -1
- package/dist/compliance/incident-manager.js +5 -3
- package/dist/compliance/incident-manager.js.map +1 -1
- package/dist/compliance/policy-docs.d.ts.map +1 -1
- package/dist/compliance/policy-docs.js +14 -11
- package/dist/compliance/policy-docs.js.map +1 -1
- package/dist/compliance/privacy-notice-text.d.ts.map +1 -1
- package/dist/compliance/privacy-notice-text.js +3 -4
- package/dist/compliance/privacy-notice-text.js.map +1 -1
- package/dist/compliance/privacy-notice.d.ts.map +1 -1
- package/dist/compliance/privacy-notice.js +5 -3
- package/dist/compliance/privacy-notice.js.map +1 -1
- package/dist/compliance/report-generator.d.ts.map +1 -1
- package/dist/compliance/report-generator.js +5 -3
- package/dist/compliance/report-generator.js.map +1 -1
- package/dist/compliance/retention-engine.d.ts.map +1 -1
- package/dist/compliance/retention-engine.js +24 -10
- package/dist/compliance/retention-engine.js.map +1 -1
- package/dist/compliance/siem-exporter.d.ts.map +1 -1
- package/dist/compliance/siem-exporter.js +40 -16
- package/dist/compliance/siem-exporter.js.map +1 -1
- package/dist/config.d.ts +8 -31
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -64
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +22 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +55 -4
- package/dist/errors.js.map +1 -1
- package/dist/gemini/gemini-client.d.ts +1 -0
- package/dist/gemini/gemini-client.d.ts.map +1 -1
- package/dist/gemini/gemini-client.js +50 -49
- package/dist/gemini/gemini-client.js.map +1 -1
- package/dist/gemini/types.d.ts +3 -1
- package/dist/gemini/types.d.ts.map +1 -1
- package/dist/gemini/types.js.map +1 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +412 -89
- package/dist/index.js.map +1 -1
- package/dist/library/notebook-library.d.ts.map +1 -1
- package/dist/library/notebook-library.js +2 -1
- package/dist/library/notebook-library.js.map +1 -1
- package/dist/logging/query-logger.d.ts +13 -1
- package/dist/logging/query-logger.d.ts.map +1 -1
- package/dist/logging/query-logger.js +62 -10
- package/dist/logging/query-logger.js.map +1 -1
- package/dist/notebook-creation/audio-manager.d.ts.map +1 -1
- package/dist/notebook-creation/audio-manager.js +19 -24
- package/dist/notebook-creation/audio-manager.js.map +1 -1
- package/dist/notebook-creation/browser-options.d.ts +28 -0
- package/dist/notebook-creation/browser-options.d.ts.map +1 -0
- package/dist/notebook-creation/browser-options.js +75 -0
- package/dist/notebook-creation/browser-options.js.map +1 -0
- package/dist/notebook-creation/data-table-manager.d.ts.map +1 -1
- package/dist/notebook-creation/data-table-manager.js +20 -21
- package/dist/notebook-creation/data-table-manager.js.map +1 -1
- package/dist/notebook-creation/discover-creation-flow.d.ts +0 -6
- package/dist/notebook-creation/discover-creation-flow.d.ts.map +1 -1
- package/dist/notebook-creation/discover-creation-flow.js +10 -10
- package/dist/notebook-creation/discover-creation-flow.js.map +1 -1
- package/dist/notebook-creation/discover-quota.d.ts +0 -6
- package/dist/notebook-creation/discover-quota.d.ts.map +1 -1
- package/dist/notebook-creation/discover-quota.js +12 -13
- package/dist/notebook-creation/discover-quota.js.map +1 -1
- package/dist/notebook-creation/discover-sources.js +15 -16
- package/dist/notebook-creation/discover-sources.js.map +1 -1
- package/dist/notebook-creation/dom-scripts.d.ts +10 -0
- package/dist/notebook-creation/dom-scripts.d.ts.map +1 -0
- package/dist/notebook-creation/dom-scripts.js +58 -0
- package/dist/notebook-creation/dom-scripts.js.map +1 -0
- package/dist/notebook-creation/errors.d.ts +18 -0
- package/dist/notebook-creation/errors.d.ts.map +1 -0
- package/dist/notebook-creation/errors.js +20 -0
- package/dist/notebook-creation/errors.js.map +1 -0
- package/dist/notebook-creation/index.d.ts +2 -1
- package/dist/notebook-creation/index.d.ts.map +1 -1
- package/dist/notebook-creation/index.js +2 -1
- package/dist/notebook-creation/index.js.map +1 -1
- package/dist/notebook-creation/notebook-creator.d.ts +6 -82
- package/dist/notebook-creation/notebook-creator.d.ts.map +1 -1
- package/dist/notebook-creation/notebook-creator.js +49 -835
- package/dist/notebook-creation/notebook-creator.js.map +1 -1
- package/dist/notebook-creation/notebook-nav.d.ts +19 -0
- package/dist/notebook-creation/notebook-nav.d.ts.map +1 -0
- package/dist/notebook-creation/notebook-nav.js +240 -0
- package/dist/notebook-creation/notebook-nav.js.map +1 -0
- package/dist/notebook-creation/notebook-sync.d.ts.map +1 -1
- package/dist/notebook-creation/notebook-sync.js +36 -38
- package/dist/notebook-creation/notebook-sync.js.map +1 -1
- package/dist/notebook-creation/selector-discovery.d.ts.map +1 -1
- package/dist/notebook-creation/selector-discovery.js +17 -24
- package/dist/notebook-creation/selector-discovery.js.map +1 -1
- package/dist/notebook-creation/selectors.d.ts +23 -37
- package/dist/notebook-creation/selectors.d.ts.map +1 -1
- package/dist/notebook-creation/selectors.js +56 -60
- package/dist/notebook-creation/selectors.js.map +1 -1
- package/dist/notebook-creation/source-manager.d.ts +25 -0
- package/dist/notebook-creation/source-manager.d.ts.map +1 -1
- package/dist/notebook-creation/source-manager.js +689 -50
- package/dist/notebook-creation/source-manager.js.map +1 -1
- package/dist/notebook-creation/types.d.ts +4 -0
- package/dist/notebook-creation/types.d.ts.map +1 -1
- package/dist/notebook-creation/video-manager.d.ts.map +1 -1
- package/dist/notebook-creation/video-manager.js +33 -35
- package/dist/notebook-creation/video-manager.js.map +1 -1
- package/dist/observability/metrics.d.ts +19 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +35 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/quota/quota-manager.d.ts +11 -3
- package/dist/quota/quota-manager.d.ts.map +1 -1
- package/dist/quota/quota-manager.js +139 -47
- package/dist/quota/quota-manager.js.map +1 -1
- package/dist/resources/resource-handlers.d.ts.map +1 -1
- package/dist/resources/resource-handlers.js +39 -17
- package/dist/resources/resource-handlers.js.map +1 -1
- package/dist/session/browser-session.d.ts.map +1 -1
- package/dist/session/browser-session.js +22 -22
- package/dist/session/browser-session.js.map +1 -1
- package/dist/session/session-timeout.d.ts.map +1 -1
- package/dist/session/session-timeout.js +4 -2
- package/dist/session/session-timeout.js.map +1 -1
- package/dist/session/shared-context-manager.d.ts.map +1 -1
- package/dist/session/shared-context-manager.js +31 -30
- package/dist/session/shared-context-manager.js.map +1 -1
- package/dist/tools/annotations.d.ts.map +1 -1
- package/dist/tools/annotations.js +9 -56
- package/dist/tools/annotations.js.map +1 -1
- package/dist/tools/definitions/ask-question.d.ts.map +1 -1
- package/dist/tools/definitions/ask-question.js +35 -100
- package/dist/tools/definitions/ask-question.js.map +1 -1
- package/dist/tools/definitions/chat-history.d.ts +47 -1
- package/dist/tools/definitions/chat-history.d.ts.map +1 -1
- package/dist/tools/definitions/chat-history.js +10 -1
- package/dist/tools/definitions/chat-history.js.map +1 -1
- package/dist/tools/definitions/data-tables.d.ts.map +1 -1
- package/dist/tools/definitions/data-tables.js +2 -0
- package/dist/tools/definitions/data-tables.js.map +1 -1
- package/dist/tools/definitions/gemini.d.ts.map +1 -1
- package/dist/tools/definitions/gemini.js +54 -11
- package/dist/tools/definitions/gemini.js.map +1 -1
- package/dist/tools/definitions/notebook-management.d.ts.map +1 -1
- package/dist/tools/definitions/notebook-management.js +100 -70
- package/dist/tools/definitions/notebook-management.js.map +1 -1
- package/dist/tools/definitions/query-history.d.ts +47 -1
- package/dist/tools/definitions/query-history.d.ts.map +1 -1
- package/dist/tools/definitions/query-history.js +7 -0
- package/dist/tools/definitions/query-history.js.map +1 -1
- package/dist/tools/definitions/session-management.d.ts.map +1 -1
- package/dist/tools/definitions/session-management.js +5 -0
- package/dist/tools/definitions/session-management.js.map +1 -1
- package/dist/tools/definitions/system.d.ts.map +1 -1
- package/dist/tools/definitions/system.js +71 -100
- package/dist/tools/definitions/system.js.map +1 -1
- package/dist/tools/definitions/video.d.ts.map +1 -1
- package/dist/tools/definitions/video.js +4 -1
- package/dist/tools/definitions/video.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +4 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers/ask-question.d.ts +1 -1
- package/dist/tools/handlers/ask-question.d.ts.map +1 -1
- package/dist/tools/handlers/ask-question.js +57 -13
- package/dist/tools/handlers/ask-question.js.map +1 -1
- package/dist/tools/handlers/audio-video.d.ts.map +1 -1
- package/dist/tools/handlers/audio-video.js +22 -161
- package/dist/tools/handlers/audio-video.js.map +1 -1
- package/dist/tools/handlers/auth.d.ts +14 -19
- package/dist/tools/handlers/auth.d.ts.map +1 -1
- package/dist/tools/handlers/auth.js +77 -121
- package/dist/tools/handlers/auth.js.map +1 -1
- package/dist/tools/handlers/error-utils.d.ts +16 -0
- package/dist/tools/handlers/error-utils.d.ts.map +1 -0
- package/dist/tools/handlers/error-utils.js +39 -0
- package/dist/tools/handlers/error-utils.js.map +1 -0
- package/dist/tools/handlers/gemini.d.ts +2 -0
- package/dist/tools/handlers/gemini.d.ts.map +1 -1
- package/dist/tools/handlers/gemini.js +88 -51
- package/dist/tools/handlers/gemini.js.map +1 -1
- package/dist/tools/handlers/index.d.ts +39 -47
- package/dist/tools/handlers/index.d.ts.map +1 -1
- package/dist/tools/handlers/index.js +15 -4
- package/dist/tools/handlers/index.js.map +1 -1
- package/dist/tools/handlers/notebook-creation.d.ts.map +1 -1
- package/dist/tools/handlers/notebook-creation.js +102 -86
- package/dist/tools/handlers/notebook-creation.js.map +1 -1
- package/dist/tools/handlers/notebook-management.d.ts +8 -8
- package/dist/tools/handlers/notebook-management.d.ts.map +1 -1
- package/dist/tools/handlers/notebook-management.js +34 -80
- package/dist/tools/handlers/notebook-management.js.map +1 -1
- package/dist/tools/handlers/session-management.d.ts +8 -10
- package/dist/tools/handlers/session-management.d.ts.map +1 -1
- package/dist/tools/handlers/session-management.js +34 -63
- package/dist/tools/handlers/session-management.js.map +1 -1
- package/dist/tools/handlers/system.d.ts.map +1 -1
- package/dist/tools/handlers/system.js +45 -10
- package/dist/tools/handlers/system.js.map +1 -1
- package/dist/tools/handlers/types.d.ts +1 -1
- package/dist/tools/handlers/types.d.ts.map +1 -1
- package/dist/tools/handlers/webhooks.d.ts.map +1 -1
- package/dist/tools/handlers/webhooks.js +15 -13
- package/dist/tools/handlers/webhooks.js.map +1 -1
- package/dist/types.d.ts +7 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/audit-logger.d.ts +19 -1
- package/dist/utils/audit-logger.d.ts.map +1 -1
- package/dist/utils/audit-logger.js +198 -30
- package/dist/utils/audit-logger.js.map +1 -1
- package/dist/utils/cleanup-manager.d.ts.map +1 -1
- package/dist/utils/cleanup-manager.js +6 -3
- package/dist/utils/cleanup-manager.js.map +1 -1
- package/dist/utils/crypto.d.ts +4 -1
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +32 -21
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +87 -16
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/file-permissions.d.ts +2 -0
- package/dist/utils/file-permissions.d.ts.map +1 -1
- package/dist/utils/file-permissions.js +2 -1
- package/dist/utils/file-permissions.js.map +1 -1
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/page-utils.d.ts +13 -0
- package/dist/utils/page-utils.d.ts.map +1 -1
- package/dist/utils/page-utils.js +61 -39
- package/dist/utils/page-utils.js.map +1 -1
- package/dist/utils/response-validator.d.ts.map +1 -1
- package/dist/utils/response-validator.js +27 -22
- package/dist/utils/response-validator.js.map +1 -1
- package/dist/utils/secrets-scanner.d.ts +11 -0
- package/dist/utils/secrets-scanner.d.ts.map +1 -1
- package/dist/utils/secrets-scanner.js +65 -17
- package/dist/utils/secrets-scanner.js.map +1 -1
- package/dist/utils/secure-memory.d.ts +9 -31
- package/dist/utils/secure-memory.d.ts.map +1 -1
- package/dist/utils/secure-memory.js +17 -102
- package/dist/utils/secure-memory.js.map +1 -1
- package/dist/utils/security.d.ts +4 -3
- package/dist/utils/security.d.ts.map +1 -1
- package/dist/utils/security.js +43 -13
- package/dist/utils/security.js.map +1 -1
- package/dist/utils/stealth-utils.d.ts.map +1 -1
- package/dist/utils/stealth-utils.js +4 -4
- package/dist/utils/stealth-utils.js.map +1 -1
- package/dist/webhooks/types.d.ts +4 -0
- package/dist/webhooks/types.d.ts.map +1 -1
- package/dist/webhooks/webhook-dispatcher.d.ts +80 -12
- package/dist/webhooks/webhook-dispatcher.d.ts.map +1 -1
- package/dist/webhooks/webhook-dispatcher.js +497 -74
- package/dist/webhooks/webhook-dispatcher.js.map +1 -1
- package/docs/archive/ISSUES-legacy-2026-04-24.md +644 -0
- package/docs/dependency-risk.md +25 -0
- package/docs/testing-runbook.md +166 -0
- package/docs/usage-guide.md +2 -1
- package/package.json +34 -16
|
@@ -1,897 +1,111 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NotebookLM Notebook Creator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
operationQueue = Promise.resolve();
|
|
15
|
+
navigation;
|
|
16
|
+
sourceManager;
|
|
21
17
|
constructor(authManager, contextManager) {
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
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;
|
|
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
|
-
|
|
46
|
-
|
|
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(
|
|
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.
|
|
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({
|
|
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
|
|
109
|
+
return creator.createNotebook(options);
|
|
896
110
|
}
|
|
897
111
|
//# sourceMappingURL=notebook-creator.js.map
|