@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
|
@@ -7,7 +7,10 @@ import * as fs from "fs";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { log } from "../utils/logger.js";
|
|
9
9
|
import { randomDelay, humanType } from "../utils/stealth-utils.js";
|
|
10
|
-
import {
|
|
10
|
+
import { getSelectors } from "./selectors.js";
|
|
11
|
+
import { findElement } from "../utils/page-utils.js";
|
|
12
|
+
const STANDARD_SOURCE_PROCESSING_TIMEOUT_MS = 30000;
|
|
13
|
+
const LENIENT_SOURCE_PROCESSING_TIMEOUT_MS = 60000;
|
|
11
14
|
export class SourceManager {
|
|
12
15
|
authManager;
|
|
13
16
|
contextManager;
|
|
@@ -42,6 +45,7 @@ export class SourceManager {
|
|
|
42
45
|
await page.waitForTimeout(2000);
|
|
43
46
|
// Extract source information from the page
|
|
44
47
|
const sources = await page.evaluate(() => {
|
|
48
|
+
const browser = globalThis;
|
|
45
49
|
const results = [];
|
|
46
50
|
// Look for source items in the sidebar/sources panel
|
|
47
51
|
// Common patterns in NotebookLM:
|
|
@@ -56,8 +60,7 @@ export class SourceManager {
|
|
|
56
60
|
'.sources-list > *',
|
|
57
61
|
];
|
|
58
62
|
for (const selector of sourceSelectors) {
|
|
59
|
-
|
|
60
|
-
const items = document.querySelectorAll(selector);
|
|
63
|
+
const items = Array.from(browser.document.querySelectorAll(selector));
|
|
61
64
|
for (let i = 0; i < items.length; i++) {
|
|
62
65
|
const item = items[i];
|
|
63
66
|
const text = item.textContent?.trim() || "";
|
|
@@ -125,27 +128,50 @@ export class SourceManager {
|
|
|
125
128
|
try {
|
|
126
129
|
await page.waitForLoadState('networkidle', { timeout: 15000 });
|
|
127
130
|
}
|
|
128
|
-
catch {
|
|
131
|
+
catch (err) {
|
|
132
|
+
log.debug(`source-manager: waiting for network idle after page load: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
133
|
log.warning(" Network idle timeout, continuing...");
|
|
130
134
|
}
|
|
131
135
|
await randomDelay(3000, 4000);
|
|
132
136
|
// Check page state
|
|
133
137
|
const pageState = await page.evaluate(() => {
|
|
134
|
-
|
|
135
|
-
const addSourceBtn = document.querySelector('button[aria-label="Add source"]');
|
|
136
|
-
|
|
137
|
-
return { hasAddSourceBtn: !!addSourceBtn, windowWidth: window.innerWidth };
|
|
138
|
+
const browser = globalThis;
|
|
139
|
+
const addSourceBtn = browser.document.querySelector('button[aria-label="Add source"]');
|
|
140
|
+
return { hasAddSourceBtn: !!addSourceBtn, windowWidth: browser.window.innerWidth };
|
|
138
141
|
});
|
|
139
142
|
log.dim(` Page state: width=${pageState.windowWidth}, addSourceBtn=${pageState.hasAddSourceBtn}`);
|
|
140
143
|
// Check if source dialog is already open (new/empty notebooks may auto-open it)
|
|
141
144
|
const dialogAlreadyOpen = await page.evaluate(() => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const dropzone = document.querySelector('.dropzone__file-dialog-button, span[xapscottyuploadertrigger]');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const sourceOptions = document.querySelector('[aria-label="Upload sources from your computer"]');
|
|
148
|
-
|
|
145
|
+
const browser = globalThis;
|
|
146
|
+
const asElements = (selector) => Array.from(browser.document.querySelectorAll(selector));
|
|
147
|
+
const dropzone = browser.document.querySelector('.dropzone__file-dialog-button, span[xapscottyuploadertrigger]');
|
|
148
|
+
if (dropzone && dropzone.offsetParent !== null)
|
|
149
|
+
return true;
|
|
150
|
+
const sourceOptions = browser.document.querySelector('[aria-label="Upload sources from your computer"]');
|
|
151
|
+
if (sourceOptions && sourceOptions.offsetParent !== null)
|
|
152
|
+
return true;
|
|
153
|
+
const textArea = browser.document.querySelector('textarea.text-area, textarea[class*="text-area"], textarea.mat-mdc-form-field-textarea-control');
|
|
154
|
+
if (textArea && textArea.offsetParent !== null)
|
|
155
|
+
return true;
|
|
156
|
+
const chips = asElements("mat-chip, mat-chip-option, [mat-chip-option], button, span");
|
|
157
|
+
for (const chip of chips) {
|
|
158
|
+
if (chip.offsetParent === null)
|
|
159
|
+
continue;
|
|
160
|
+
const text = chip.textContent?.trim() || "";
|
|
161
|
+
if (text.includes("Copied text") || text.includes("Website") || text.includes("Upload")) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const dialogs = asElements("mat-dialog-container, [role=\"dialog\"], .cdk-overlay-pane");
|
|
166
|
+
for (const dialog of dialogs) {
|
|
167
|
+
if (dialog.offsetParent === null)
|
|
168
|
+
continue;
|
|
169
|
+
const text = dialog.textContent?.trim() || "";
|
|
170
|
+
if (text.includes("Copied text") || text.includes("Website") || text.includes("Upload")) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
149
175
|
});
|
|
150
176
|
if (dialogAlreadyOpen) {
|
|
151
177
|
log.info(" 📋 Source dialog already open");
|
|
@@ -184,10 +210,9 @@ export class SourceManager {
|
|
|
184
210
|
await randomDelay(1500, 2000);
|
|
185
211
|
// Verify dialog opened
|
|
186
212
|
const dialogOpened = await page.evaluate(() => {
|
|
187
|
-
|
|
188
|
-
const dropzone = document.querySelector('.dropzone__file-dialog-button, span[xapscottyuploadertrigger]');
|
|
189
|
-
|
|
190
|
-
const sourceOptions = document.querySelector('[aria-label="Upload sources from your computer"]');
|
|
213
|
+
const browser = globalThis;
|
|
214
|
+
const dropzone = browser.document.querySelector('.dropzone__file-dialog-button, span[xapscottyuploadertrigger]');
|
|
215
|
+
const sourceOptions = browser.document.querySelector('[aria-label="Upload sources from your computer"]');
|
|
191
216
|
return !!(dropzone || sourceOptions);
|
|
192
217
|
});
|
|
193
218
|
if (!dialogOpened) {
|
|
@@ -248,14 +273,14 @@ export class SourceManager {
|
|
|
248
273
|
const sourceIndex = parseInt(indexMatch[1], 10);
|
|
249
274
|
// Find and click on the source to select it
|
|
250
275
|
const clicked = await page.evaluate((index) => {
|
|
276
|
+
const browser = globalThis;
|
|
251
277
|
const sourceSelectors = [
|
|
252
278
|
'mat-list-item',
|
|
253
279
|
'[class*="source-item"]',
|
|
254
280
|
'[role="listitem"]',
|
|
255
281
|
];
|
|
256
282
|
for (const selector of sourceSelectors) {
|
|
257
|
-
|
|
258
|
-
const items = document.querySelectorAll(selector);
|
|
283
|
+
const items = Array.from(browser.document.querySelectorAll(selector));
|
|
259
284
|
if (items.length > index) {
|
|
260
285
|
// Look for delete button within the item or select it
|
|
261
286
|
const item = items[index];
|
|
@@ -279,6 +304,7 @@ export class SourceManager {
|
|
|
279
304
|
// Look for delete button in toolbar or context menu
|
|
280
305
|
await randomDelay(500, 800);
|
|
281
306
|
const deleted = await page.evaluate(() => {
|
|
307
|
+
const browser = globalThis;
|
|
282
308
|
// Look for delete button that appeared after selection
|
|
283
309
|
const deleteSelectors = [
|
|
284
310
|
'button[aria-label*="delete" i]',
|
|
@@ -287,8 +313,7 @@ export class SourceManager {
|
|
|
287
313
|
'[class*="trash"]',
|
|
288
314
|
];
|
|
289
315
|
for (const selector of deleteSelectors) {
|
|
290
|
-
|
|
291
|
-
const btn = document.querySelector(selector);
|
|
316
|
+
const btn = browser.document.querySelector(selector);
|
|
292
317
|
if (btn && btn.offsetParent !== null) {
|
|
293
318
|
btn.click();
|
|
294
319
|
return true;
|
|
@@ -303,9 +328,9 @@ export class SourceManager {
|
|
|
303
328
|
// Confirm deletion if dialog appears
|
|
304
329
|
await randomDelay(500, 800);
|
|
305
330
|
await page.evaluate(() => {
|
|
331
|
+
const browser = globalThis;
|
|
306
332
|
// Look for confirm button in dialog
|
|
307
|
-
|
|
308
|
-
const buttons = document.querySelectorAll("button");
|
|
333
|
+
const buttons = Array.from(browser.document.querySelectorAll("button"));
|
|
309
334
|
for (const btn of buttons) {
|
|
310
335
|
const text = btn.textContent?.toLowerCase() || "";
|
|
311
336
|
if (text.includes("delete") || text.includes("remove") || text.includes("confirm")) {
|
|
@@ -340,16 +365,15 @@ export class SourceManager {
|
|
|
340
365
|
async addUrlSourceInternal(page, url) {
|
|
341
366
|
// Click URL/Website source type option — prefer locale-independent signals
|
|
342
367
|
const urlOptionClicked = await page.evaluate(() => {
|
|
368
|
+
const browser = globalThis;
|
|
343
369
|
// Primary: data/value attribute (locale-independent)
|
|
344
|
-
|
|
345
|
-
const byData = document.querySelector('[data-source-type="url"], [value="url"], mat-chip[value="url"]');
|
|
370
|
+
const byData = browser.document.querySelector('[data-source-type="url"], [value="url"], mat-chip[value="url"]');
|
|
346
371
|
if (byData) {
|
|
347
372
|
byData.click();
|
|
348
373
|
return true;
|
|
349
374
|
}
|
|
350
375
|
// Fallback: text/aria match ("URL" is the same word in most languages)
|
|
351
|
-
|
|
352
|
-
const buttons = document.querySelectorAll("button, [role='button'], mat-chip");
|
|
376
|
+
const buttons = Array.from(browser.document.querySelectorAll("button, [role='button'], mat-chip"));
|
|
353
377
|
for (const btn of buttons) {
|
|
354
378
|
const text = btn.textContent?.toLowerCase() || "";
|
|
355
379
|
const aria = btn.getAttribute("aria-label")?.toLowerCase() || "";
|
|
@@ -381,13 +405,17 @@ export class SourceManager {
|
|
|
381
405
|
async addTextSourceInternal(page, text, _title) {
|
|
382
406
|
// Click text/paste option
|
|
383
407
|
const textOptionClicked = await page.evaluate(() => {
|
|
384
|
-
|
|
385
|
-
const buttons = document.querySelectorAll("button, [role='button'], mat-chip");
|
|
408
|
+
const browser = globalThis;
|
|
409
|
+
const buttons = Array.from(browser.document.querySelectorAll("button, [role='button'], mat-chip, mat-chip-option, [mat-chip-option]"));
|
|
386
410
|
for (const btn of buttons) {
|
|
387
411
|
const btnText = btn.textContent?.toLowerCase() || "";
|
|
388
412
|
const aria = btn.getAttribute("aria-label")?.toLowerCase() || "";
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
const combined = `${btnText} ${aria}`;
|
|
414
|
+
if (combined.includes("search the web") || combined.includes("discover sources")) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (btnText.includes("copied text") || btnText.includes("paste as text") ||
|
|
418
|
+
aria.includes("copied") || aria.includes("paste as text")) {
|
|
391
419
|
btn.click();
|
|
392
420
|
return true;
|
|
393
421
|
}
|
|
@@ -398,16 +426,19 @@ export class SourceManager {
|
|
|
398
426
|
throw new Error("Could not find text/paste source option");
|
|
399
427
|
}
|
|
400
428
|
await randomDelay(800, 1200);
|
|
401
|
-
|
|
402
|
-
|
|
429
|
+
const activeSelector = await this.findValidTextInputSelectorOnPage(page);
|
|
430
|
+
if (!activeSelector) {
|
|
431
|
+
throw new Error("Could not find text input area");
|
|
432
|
+
}
|
|
433
|
+
const textArea = await page.$(activeSelector);
|
|
403
434
|
if (!textArea) {
|
|
404
435
|
throw new Error("Could not find text input area");
|
|
405
436
|
}
|
|
406
437
|
// Use clipboard for large text
|
|
407
438
|
if (text.length > 500) {
|
|
408
439
|
await page.evaluate((t) => {
|
|
409
|
-
|
|
410
|
-
navigator.clipboard.writeText(t);
|
|
440
|
+
const browser = globalThis;
|
|
441
|
+
browser.navigator.clipboard.writeText(t);
|
|
411
442
|
}, text);
|
|
412
443
|
await textArea.focus();
|
|
413
444
|
await page.keyboard.down("Control");
|
|
@@ -415,28 +446,26 @@ export class SourceManager {
|
|
|
415
446
|
await page.keyboard.up("Control");
|
|
416
447
|
}
|
|
417
448
|
else {
|
|
418
|
-
await humanType(page,
|
|
449
|
+
await humanType(page, activeSelector, text);
|
|
419
450
|
}
|
|
420
451
|
await randomDelay(500, 800);
|
|
421
452
|
// Click Insert button — prefer locale-independent selectors
|
|
422
453
|
const insertClicked = await page.evaluate(() => {
|
|
454
|
+
const browser = globalThis;
|
|
423
455
|
// Primary: type=submit (locale-independent)
|
|
424
|
-
|
|
425
|
-
const submitBtn = document.querySelector("button[type='submit']:not([disabled])");
|
|
456
|
+
const submitBtn = browser.document.querySelector("button[type='submit']:not([disabled])");
|
|
426
457
|
if (submitBtn && submitBtn.offsetParent !== null) {
|
|
427
458
|
submitBtn.click();
|
|
428
459
|
return true;
|
|
429
460
|
}
|
|
430
461
|
// Secondary: primary color class (locale-independent NotebookLM convention)
|
|
431
|
-
|
|
432
|
-
const primaryBtn = document.querySelector("button.button-color--primary:not([disabled])");
|
|
462
|
+
const primaryBtn = browser.document.querySelector("button.button-color--primary:not([disabled])");
|
|
433
463
|
if (primaryBtn && primaryBtn.offsetParent !== null) {
|
|
434
464
|
primaryBtn.click();
|
|
435
465
|
return true;
|
|
436
466
|
}
|
|
437
467
|
// Fallback: text match (English only)
|
|
438
|
-
|
|
439
|
-
const buttons = document.querySelectorAll("button");
|
|
468
|
+
const buttons = Array.from(browser.document.querySelectorAll("button"));
|
|
440
469
|
for (const btn of buttons) {
|
|
441
470
|
const text = btn.textContent?.toLowerCase() || "";
|
|
442
471
|
if (text.includes("insert") || text.includes("add") || text.includes("submit")) {
|
|
@@ -450,6 +479,35 @@ export class SourceManager {
|
|
|
450
479
|
throw new Error("Could not find Insert button");
|
|
451
480
|
}
|
|
452
481
|
}
|
|
482
|
+
async findValidTextInputSelectorOnPage(page) {
|
|
483
|
+
for (const selector of getSelectors("textInput")) {
|
|
484
|
+
try {
|
|
485
|
+
const state = await page.evaluate((targetSelector) => {
|
|
486
|
+
const browser = globalThis;
|
|
487
|
+
const textarea = browser.document.querySelector(targetSelector);
|
|
488
|
+
if (!textarea || textarea.offsetParent === null) {
|
|
489
|
+
return { matches: false };
|
|
490
|
+
}
|
|
491
|
+
const aria = textarea.getAttribute("aria-label")?.toLowerCase() || "";
|
|
492
|
+
const placeholder = textarea.getAttribute("placeholder")?.toLowerCase() || "";
|
|
493
|
+
const className = textarea.className?.toLowerCase() || "";
|
|
494
|
+
const haystack = `${aria} ${placeholder} ${className}`;
|
|
495
|
+
return {
|
|
496
|
+
matches: !haystack.includes("discover sources") &&
|
|
497
|
+
!haystack.includes("search the web") &&
|
|
498
|
+
!haystack.includes("query-box"),
|
|
499
|
+
};
|
|
500
|
+
}, selector);
|
|
501
|
+
if (state.matches) {
|
|
502
|
+
return selector;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
log.debug(`source-manager: validating standalone text source input selector: ${err instanceof Error ? err.message : String(err)}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
453
511
|
/**
|
|
454
512
|
* Internal: Add file source
|
|
455
513
|
* December 2025: NotebookLM creates a hidden input[type="file"] AFTER clicking
|
|
@@ -550,15 +608,14 @@ export class SourceManager {
|
|
|
550
608
|
/**
|
|
551
609
|
* Wait for source processing to complete
|
|
552
610
|
*/
|
|
553
|
-
async waitForSourceProcessing(page, timeoutMs =
|
|
611
|
+
async waitForSourceProcessing(page, timeoutMs = STANDARD_SOURCE_PROCESSING_TIMEOUT_MS) {
|
|
554
612
|
const startTime = Date.now();
|
|
555
613
|
while (Date.now() - startTime < timeoutMs) {
|
|
556
614
|
const isProcessing = await page.evaluate(() => {
|
|
615
|
+
const browser = globalThis;
|
|
557
616
|
// Look for processing indicators
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
// @ts-expect-error - DOM types
|
|
561
|
-
const spinner = document.querySelector('[class*="spinner"], [class*="loading"]');
|
|
617
|
+
const progressBar = browser.document.querySelector('[role="progressbar"]');
|
|
618
|
+
const spinner = browser.document.querySelector('[class*="spinner"], [class*="loading"]');
|
|
562
619
|
return !!(progressBar || spinner);
|
|
563
620
|
});
|
|
564
621
|
if (!isProcessing) {
|
|
@@ -576,11 +633,593 @@ export class SourceManager {
|
|
|
576
633
|
try {
|
|
577
634
|
await this.page.close();
|
|
578
635
|
}
|
|
579
|
-
catch {
|
|
636
|
+
catch (err) {
|
|
637
|
+
log.debug(`source-manager: closing page: ${err instanceof Error ? err.message : String(err)}`);
|
|
580
638
|
// Ignore close errors
|
|
581
639
|
}
|
|
582
640
|
this.page = null;
|
|
583
641
|
}
|
|
584
642
|
}
|
|
585
643
|
}
|
|
644
|
+
export class NotebookCreationSourceManager {
|
|
645
|
+
getPage;
|
|
646
|
+
constructor(getPage) {
|
|
647
|
+
this.getPage = getPage;
|
|
648
|
+
}
|
|
649
|
+
async addSource(source) {
|
|
650
|
+
const page = this.requirePage();
|
|
651
|
+
const expectedNotebookUrl = page.url();
|
|
652
|
+
log.info(`📍 Current notebook URL: ${expectedNotebookUrl}`);
|
|
653
|
+
const dialogAlreadyOpen = await this.isSourceDialogOpen();
|
|
654
|
+
log.info(`📋 Source dialog already open: ${dialogAlreadyOpen}`);
|
|
655
|
+
if (!dialogAlreadyOpen) {
|
|
656
|
+
await this.clickAddSource();
|
|
657
|
+
const currentUrl = page.url();
|
|
658
|
+
if (!currentUrl.includes("/notebook/") ||
|
|
659
|
+
(expectedNotebookUrl.includes("/notebook/") &&
|
|
660
|
+
!currentUrl.includes(expectedNotebookUrl.split("/notebook/")[1]?.split("?")[0] || ""))) {
|
|
661
|
+
log.error(`❌ URL changed unexpectedly! Expected: ${expectedNotebookUrl}, Got: ${currentUrl}`);
|
|
662
|
+
throw new Error("Navigation error: accidentally navigated away from notebook. This may indicate clicking wrong button.");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
log.info("📋 Source dialog already open - skipping clickAddSource");
|
|
667
|
+
}
|
|
668
|
+
switch (source.type) {
|
|
669
|
+
case "url":
|
|
670
|
+
await this.addUrlSource(source.value);
|
|
671
|
+
break;
|
|
672
|
+
case "text":
|
|
673
|
+
await this.addTextSource(source.value, source.title);
|
|
674
|
+
break;
|
|
675
|
+
case "file":
|
|
676
|
+
await this.addFileSource(source.value);
|
|
677
|
+
break;
|
|
678
|
+
default:
|
|
679
|
+
throw new Error(`Unknown source type: ${source.type}`);
|
|
680
|
+
}
|
|
681
|
+
log.info(`📍 URL after adding source: ${page.url()}`);
|
|
682
|
+
}
|
|
683
|
+
getSourceDescription(source) {
|
|
684
|
+
switch (source.type) {
|
|
685
|
+
case "url":
|
|
686
|
+
try {
|
|
687
|
+
const url = new URL(source.value);
|
|
688
|
+
return `URL: ${url.hostname}`;
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
log.debug(`source-manager: parsing source URL in getSourceDescription: ${err instanceof Error ? err.message : String(err)}`);
|
|
692
|
+
return `URL: ${source.value.slice(0, 50)}`;
|
|
693
|
+
}
|
|
694
|
+
case "text":
|
|
695
|
+
return source.title || `Text: ${source.value.slice(0, 30)}...`;
|
|
696
|
+
case "file":
|
|
697
|
+
return `File: ${path.basename(source.value)}`;
|
|
698
|
+
default:
|
|
699
|
+
return "Unknown source";
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
requirePage() {
|
|
703
|
+
const page = this.getPage();
|
|
704
|
+
if (!page)
|
|
705
|
+
throw new Error("Page not initialized");
|
|
706
|
+
return page;
|
|
707
|
+
}
|
|
708
|
+
async isSourceDialogOpen() {
|
|
709
|
+
const page = this.getPage();
|
|
710
|
+
if (!page)
|
|
711
|
+
return false;
|
|
712
|
+
const dialogIndicators = await page.evaluate(() => {
|
|
713
|
+
const browser = globalThis;
|
|
714
|
+
const dialogs = Array.from(browser.document.querySelectorAll("mat-dialog-container"));
|
|
715
|
+
for (const d of dialogs) {
|
|
716
|
+
if (d.offsetParent !== null && d.textContent?.trim()) {
|
|
717
|
+
return { open: true, reason: "dialog_container_visible" };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const chipGroup = browser.document.querySelector("mat-chip-listbox, mat-chip-group, mat-chip-set");
|
|
721
|
+
if (chipGroup && chipGroup.offsetParent !== null) {
|
|
722
|
+
return { open: true, reason: "chip_group_visible" };
|
|
723
|
+
}
|
|
724
|
+
const dropzones = Array.from(browser.document.querySelectorAll('.dropzone, [class*="dropzone"]'));
|
|
725
|
+
if (dropzones.length > 0) {
|
|
726
|
+
for (const dz of dropzones) {
|
|
727
|
+
if (dz.offsetParent !== null) {
|
|
728
|
+
return { open: true, reason: "dropzone_visible" };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const uploadBtn = browser.document.querySelector('button[class*="upload"], input[type="file"]');
|
|
733
|
+
if (uploadBtn && uploadBtn.offsetParent !== null) {
|
|
734
|
+
return { open: true, reason: "upload_button_visible" };
|
|
735
|
+
}
|
|
736
|
+
return { open: false };
|
|
737
|
+
});
|
|
738
|
+
log.info(`📋 isSourceDialogOpen check: ${JSON.stringify(dialogIndicators)}`);
|
|
739
|
+
return dialogIndicators.open;
|
|
740
|
+
}
|
|
741
|
+
async tryAddSourceByAria(page) {
|
|
742
|
+
try {
|
|
743
|
+
let locator = page.locator('button[aria-label="Add source"]');
|
|
744
|
+
let count = await locator.count();
|
|
745
|
+
log.info(` aria "Add source": ${count} found`);
|
|
746
|
+
if (count === 0) {
|
|
747
|
+
locator = page.locator('button[aria-label="Add sources"]');
|
|
748
|
+
count = await locator.count();
|
|
749
|
+
log.info(` aria "Add sources": ${count} found`);
|
|
750
|
+
}
|
|
751
|
+
if (count > 0 && await locator.first().isVisible()) {
|
|
752
|
+
await locator.first().click();
|
|
753
|
+
await randomDelay(800, 1500);
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (e) {
|
|
758
|
+
log.info(` aria approach: ${e}`);
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
async tryAddSourceByClass(page) {
|
|
763
|
+
try {
|
|
764
|
+
const locator = page.locator('button.add-source-button');
|
|
765
|
+
const count = await locator.count();
|
|
766
|
+
log.info(` class add-source-button: ${count} found`);
|
|
767
|
+
if (count > 0 && await locator.first().isVisible()) {
|
|
768
|
+
await locator.first().click();
|
|
769
|
+
await randomDelay(800, 1500);
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
catch (e) {
|
|
774
|
+
log.info(` class approach: ${e}`);
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
async tryAddSourceByJs(page) {
|
|
779
|
+
try {
|
|
780
|
+
const clicked = await page.evaluate(() => {
|
|
781
|
+
const browser = globalThis;
|
|
782
|
+
const elements = Array.from(browser.document.querySelectorAll('button, [role="button"]'));
|
|
783
|
+
for (const el of elements) {
|
|
784
|
+
const elText = el.textContent?.trim().toLowerCase() ?? "";
|
|
785
|
+
const ariaLabel = el.getAttribute("aria-label")?.toLowerCase() ?? "";
|
|
786
|
+
const className = el.className?.toLowerCase() ?? "";
|
|
787
|
+
if (ariaLabel.includes("create") || className.includes("create-notebook") ||
|
|
788
|
+
elText.includes("create") || elText.includes("add note") ||
|
|
789
|
+
className.includes("add-note"))
|
|
790
|
+
continue;
|
|
791
|
+
if (ariaLabel === "add source" || ariaLabel.includes("add source") ||
|
|
792
|
+
elText.includes("add source") || className.includes("add-source")) {
|
|
793
|
+
el.click();
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return false;
|
|
798
|
+
});
|
|
799
|
+
if (clicked) {
|
|
800
|
+
await randomDelay(800, 1500);
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
log.debug(`source-manager: clicking 'Add source' via JS: ${err instanceof Error ? err.message : String(err)}`);
|
|
806
|
+
}
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
async clickAddSource() {
|
|
810
|
+
const page = this.requirePage();
|
|
811
|
+
log.info("📎 Clicking 'Add source' button...");
|
|
812
|
+
const debugEnabled = process.env.DEBUG === "true";
|
|
813
|
+
if (debugEnabled) {
|
|
814
|
+
log.dim(` Current URL: ${page.url()}`);
|
|
815
|
+
}
|
|
816
|
+
await randomDelay(2000, 3000);
|
|
817
|
+
if (debugEnabled) {
|
|
818
|
+
const buttonsInfo = await page.evaluate(() => {
|
|
819
|
+
const browser = globalThis;
|
|
820
|
+
return Array.from(browser.document.querySelectorAll('button, [role="button"]'))
|
|
821
|
+
.filter((btn) => {
|
|
822
|
+
const aria = btn.getAttribute("aria-label")?.toLowerCase() ?? "";
|
|
823
|
+
const text = btn.textContent?.trim().toLowerCase() ?? "";
|
|
824
|
+
const cls = btn.className?.toLowerCase() ?? "";
|
|
825
|
+
return aria.includes("add") || aria.includes("create") || text.includes("add") ||
|
|
826
|
+
text.includes("create") || cls.includes("add") || cls.includes("create");
|
|
827
|
+
})
|
|
828
|
+
.map((btn) => ({
|
|
829
|
+
text: btn.textContent?.trim().substring(0, 50) ?? "",
|
|
830
|
+
aria: btn.getAttribute("aria-label") ?? "",
|
|
831
|
+
class: btn.className?.substring(0, 50) ?? "",
|
|
832
|
+
visible: btn.offsetParent !== null,
|
|
833
|
+
}));
|
|
834
|
+
});
|
|
835
|
+
log.dim(` Buttons found: ${JSON.stringify(buttonsInfo, null, 2)}`);
|
|
836
|
+
}
|
|
837
|
+
if (await this.tryAddSourceByAria(page))
|
|
838
|
+
return;
|
|
839
|
+
if (await this.tryAddSourceByClass(page))
|
|
840
|
+
return;
|
|
841
|
+
if (await this.tryAddSourceByJs(page))
|
|
842
|
+
return;
|
|
843
|
+
log.warning("⚠️ Add source button not found, waiting and retrying...");
|
|
844
|
+
await randomDelay(3000, 4000);
|
|
845
|
+
if (await this.tryAddSourceByAria(page))
|
|
846
|
+
return;
|
|
847
|
+
throw new Error("Could not find 'Add source' button after retry");
|
|
848
|
+
}
|
|
849
|
+
async addUrlSource(url) {
|
|
850
|
+
const page = this.requirePage();
|
|
851
|
+
log.info(`🔗 Adding URL source: ${url}`);
|
|
852
|
+
await this.clickSourceTypeByText(["Website", "webWebsite", "Link", "Discover sources"]);
|
|
853
|
+
await randomDelay(500, 1000);
|
|
854
|
+
const selectors = getSelectors("urlInput");
|
|
855
|
+
for (const selector of selectors) {
|
|
856
|
+
try {
|
|
857
|
+
const input = await page.$(selector);
|
|
858
|
+
if (input && await input.isVisible()) {
|
|
859
|
+
await humanType(page, selector, url, { withTypos: false });
|
|
860
|
+
await randomDelay(500, 1000);
|
|
861
|
+
await this.clickDialogSubmit();
|
|
862
|
+
await this.waitForSourceProcessing({ mode: "strict" });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
catch (err) {
|
|
867
|
+
log.debug(`source-manager: entering URL into source input selector: ${err instanceof Error ? err.message : String(err)}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
throw new Error("Could not find URL input field");
|
|
871
|
+
}
|
|
872
|
+
async addTextSource(text, title) {
|
|
873
|
+
const page = this.requirePage();
|
|
874
|
+
log.info(`📝 Adding text source${title ? `: ${title}` : ""}`);
|
|
875
|
+
let clickedBySelector = false;
|
|
876
|
+
for (const selector of getSelectors("textSourceOption")) {
|
|
877
|
+
try {
|
|
878
|
+
const option = await page.$(selector);
|
|
879
|
+
if (option && await option.isVisible()) {
|
|
880
|
+
await option.click();
|
|
881
|
+
clickedBySelector = true;
|
|
882
|
+
log.success(`✅ Clicked text source option via selector: ${selector}`);
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
log.debug(`source-manager: clicking text source option selector: ${err instanceof Error ? err.message : String(err)}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (!clickedBySelector) {
|
|
891
|
+
await this.clickSourceTypeByText(["Copied text", "Paste as text", "Paste"]);
|
|
892
|
+
}
|
|
893
|
+
await randomDelay(2000, 2500);
|
|
894
|
+
const activeSelector = await this.findValidTextInputSelector(page);
|
|
895
|
+
const textarea = activeSelector ? await page.$(activeSelector) : null;
|
|
896
|
+
if (!textarea || !activeSelector) {
|
|
897
|
+
const visibleOptions = await this.getVisibleSourceOptions(page);
|
|
898
|
+
log.warning(`⚠️ Visible source options after text-source click: ${JSON.stringify(visibleOptions)}`);
|
|
899
|
+
const visibleTextareas = await this.getVisibleTextareas(page);
|
|
900
|
+
log.warning(`⚠️ Visible textareas after text-source click: ${JSON.stringify(visibleTextareas)}`);
|
|
901
|
+
throw new Error("Could not find text input area");
|
|
902
|
+
}
|
|
903
|
+
const isVisible = await textarea.isVisible().catch(() => false);
|
|
904
|
+
if (!isVisible) {
|
|
905
|
+
await randomDelay(1000, 1500);
|
|
906
|
+
}
|
|
907
|
+
await textarea.click();
|
|
908
|
+
await randomDelay(200, 400);
|
|
909
|
+
if (text.length > 500) {
|
|
910
|
+
await page.evaluate((clipboardText) => {
|
|
911
|
+
const browser = globalThis;
|
|
912
|
+
browser.navigator.clipboard.writeText(clipboardText);
|
|
913
|
+
}, text);
|
|
914
|
+
await page.keyboard.press("Control+V");
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
await humanType(page, activeSelector, text, { withTypos: false });
|
|
918
|
+
}
|
|
919
|
+
await page.evaluate(({ selector, value }) => {
|
|
920
|
+
const browser = globalThis;
|
|
921
|
+
const textarea = browser.document.querySelector(selector);
|
|
922
|
+
if (!textarea)
|
|
923
|
+
return false;
|
|
924
|
+
textarea.focus();
|
|
925
|
+
if (typeof textarea.value === "string") {
|
|
926
|
+
textarea.value = value;
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
textarea.textContent = value;
|
|
930
|
+
}
|
|
931
|
+
textarea.dispatchEvent(new browser.Event("input", { bubbles: true }));
|
|
932
|
+
textarea.dispatchEvent(new browser.Event("change", { bubbles: true }));
|
|
933
|
+
return true;
|
|
934
|
+
}, { selector: activeSelector, value: text });
|
|
935
|
+
await randomDelay(500, 1000);
|
|
936
|
+
const textState = await page.evaluate(({ selector }) => {
|
|
937
|
+
const browser = globalThis;
|
|
938
|
+
const textarea = browser.document.querySelector(selector);
|
|
939
|
+
const buttons = Array.from(browser.document.querySelectorAll("button")).map((button) => ({
|
|
940
|
+
text: button.textContent?.trim() || "",
|
|
941
|
+
disabled: button.getAttribute("disabled") !== null || button.getAttribute("aria-disabled") === "true",
|
|
942
|
+
}));
|
|
943
|
+
return {
|
|
944
|
+
textLength: typeof textarea?.value === "string"
|
|
945
|
+
? textarea.value.length
|
|
946
|
+
: (textarea?.textContent?.length || 0),
|
|
947
|
+
textareaClass: textarea?.className || "",
|
|
948
|
+
textareaAria: textarea?.getAttribute?.("aria-label") || "",
|
|
949
|
+
textareaPlaceholder: textarea?.getAttribute?.("placeholder") || "",
|
|
950
|
+
visibleDialogs: Array.from(browser.document.querySelectorAll("[role='dialog'], mat-dialog-container, .cdk-overlay-pane"))
|
|
951
|
+
.filter((dialog) => dialog.offsetParent !== null)
|
|
952
|
+
.map((dialog) => (dialog.textContent || "").trim().slice(0, 200))
|
|
953
|
+
.filter((text) => text.length > 0)
|
|
954
|
+
.slice(0, 4),
|
|
955
|
+
buttons: buttons.filter((button) => button.text.length > 0).slice(0, 12),
|
|
956
|
+
};
|
|
957
|
+
}, { selector: activeSelector });
|
|
958
|
+
log.info(`🧪 Text source state before insert: ${JSON.stringify(textState)}`);
|
|
959
|
+
await this.clickDialogSubmit();
|
|
960
|
+
await this.waitForSourceProcessing({ mode: "lenient" });
|
|
961
|
+
}
|
|
962
|
+
async tryFileUploadViaTrigger(page, locatorStr, absolutePath) {
|
|
963
|
+
const locator = page.locator(locatorStr);
|
|
964
|
+
if (await locator.count() > 0 && await locator.first().isVisible()) {
|
|
965
|
+
await locator.first().click();
|
|
966
|
+
await page.waitForSelector('input[type="file"]', { timeout: 5000, state: 'attached' });
|
|
967
|
+
await randomDelay(200, 400);
|
|
968
|
+
await page.locator('input[type="file"]').first().setInputFiles(absolutePath);
|
|
969
|
+
await randomDelay(1000, 2000);
|
|
970
|
+
await this.waitForSourceProcessing({ mode: "lenient" });
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
async addFileSource(filePath) {
|
|
976
|
+
const page = this.requirePage();
|
|
977
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
978
|
+
if (!fs.existsSync(absolutePath)) {
|
|
979
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
980
|
+
}
|
|
981
|
+
log.info(`📁 Adding file source: ${path.basename(absolutePath)}`);
|
|
982
|
+
await randomDelay(500, 1000);
|
|
983
|
+
try {
|
|
984
|
+
if (await this.tryFileUploadViaTrigger(page, 'span.dropzone__file-dialog-button', absolutePath))
|
|
985
|
+
return;
|
|
986
|
+
if (await this.tryFileUploadViaTrigger(page, '[xapscottyuploadertrigger]', absolutePath))
|
|
987
|
+
return;
|
|
988
|
+
if (await this.tryFileUploadViaTrigger(page, 'button[class*="upload"], button[aria-label="Upload sources from your computer"]', absolutePath))
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
catch (e) {
|
|
992
|
+
log.info(` Playwright click approach: ${e}`);
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
const fileChooserPromise = page.waitForEvent('filechooser', { timeout: 5000 });
|
|
996
|
+
const chooseFileLocator = page.locator('span.dropzone__file-dialog-button');
|
|
997
|
+
if (await chooseFileLocator.count() > 0) {
|
|
998
|
+
await chooseFileLocator.first().click();
|
|
999
|
+
}
|
|
1000
|
+
const fileChooser = await fileChooserPromise;
|
|
1001
|
+
await fileChooser.setFiles(absolutePath);
|
|
1002
|
+
log.success(" ✅ File uploaded via filechooser event");
|
|
1003
|
+
await randomDelay(1000, 2000);
|
|
1004
|
+
await this.waitForSourceProcessing({ mode: "lenient" });
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
catch (e) {
|
|
1008
|
+
log.info(` Filechooser approach: ${e}`);
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
const fileInputLocator = page.locator('input[type="file"]');
|
|
1012
|
+
if (await fileInputLocator.count() > 0) {
|
|
1013
|
+
await fileInputLocator.first().setInputFiles(absolutePath);
|
|
1014
|
+
log.success(" ✅ File uploaded via existing locator");
|
|
1015
|
+
await randomDelay(1000, 2000);
|
|
1016
|
+
await this.waitForSourceProcessing({ mode: "lenient" });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
catch (e) {
|
|
1021
|
+
log.info(` Existing locator attempt: ${e}`);
|
|
1022
|
+
}
|
|
1023
|
+
throw new Error("Could not upload file - all methods failed. NotebookLM may be using an unsupported upload method.");
|
|
1024
|
+
}
|
|
1025
|
+
async clickSourceTypeByText(textPatterns) {
|
|
1026
|
+
const page = this.requirePage();
|
|
1027
|
+
for (const pattern of textPatterns) {
|
|
1028
|
+
try {
|
|
1029
|
+
const clicked = await page.evaluate((searchText) => {
|
|
1030
|
+
const browser = globalThis;
|
|
1031
|
+
const elements = Array.from(browser.document.querySelectorAll('button, [role="button"], mat-chip, mat-chip-option, [mat-chip-option]'));
|
|
1032
|
+
for (const el of elements) {
|
|
1033
|
+
const text = el.textContent?.trim() || "";
|
|
1034
|
+
const aria = el.getAttribute("aria-label")?.trim() || "";
|
|
1035
|
+
const haystack = `${text} ${aria}`.toLowerCase();
|
|
1036
|
+
if (haystack.includes("search the web") || haystack.includes("discover sources")) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if ((text === searchText || haystack.includes(searchText.toLowerCase())) &&
|
|
1040
|
+
el.offsetParent !== null) {
|
|
1041
|
+
el.click();
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return false;
|
|
1046
|
+
}, pattern);
|
|
1047
|
+
if (clicked) {
|
|
1048
|
+
log.success(`✅ Clicked source type: ${pattern}`);
|
|
1049
|
+
await randomDelay(800, 1200);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
catch (err) {
|
|
1054
|
+
log.debug(`source-manager: clicking source type tab via text pattern: ${err instanceof Error ? err.message : String(err)}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const visibleOptions = await this.getVisibleSourceOptions(this.requirePage());
|
|
1058
|
+
log.warning(`⚠️ Could not find source type: ${textPatterns.join(", ")} — visible: ${JSON.stringify(visibleOptions)}`);
|
|
1059
|
+
}
|
|
1060
|
+
async findValidTextInputSelector(page) {
|
|
1061
|
+
for (const selector of getSelectors("textInput")) {
|
|
1062
|
+
try {
|
|
1063
|
+
const state = await page.evaluate((targetSelector) => {
|
|
1064
|
+
const browser = globalThis;
|
|
1065
|
+
const textarea = browser.document.querySelector(targetSelector);
|
|
1066
|
+
if (!textarea || textarea.offsetParent === null) {
|
|
1067
|
+
return { matches: false, rejected: "missing" };
|
|
1068
|
+
}
|
|
1069
|
+
const aria = textarea.getAttribute("aria-label")?.toLowerCase() || "";
|
|
1070
|
+
const placeholder = textarea.getAttribute("placeholder")?.toLowerCase() || "";
|
|
1071
|
+
const className = textarea.className?.toLowerCase() || "";
|
|
1072
|
+
const haystack = `${aria} ${placeholder} ${className}`;
|
|
1073
|
+
if (haystack.includes("discover sources") ||
|
|
1074
|
+
haystack.includes("search the web") ||
|
|
1075
|
+
haystack.includes("query-box")) {
|
|
1076
|
+
return { matches: false, rejected: haystack };
|
|
1077
|
+
}
|
|
1078
|
+
return { matches: true, rejected: "" };
|
|
1079
|
+
}, selector);
|
|
1080
|
+
if (state.matches) {
|
|
1081
|
+
return selector;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
log.debug(`source-manager: validating text source input selector: ${err instanceof Error ? err.message : String(err)}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
async getVisibleSourceOptions(page) {
|
|
1091
|
+
try {
|
|
1092
|
+
return await page.evaluate(() => {
|
|
1093
|
+
const browser = globalThis;
|
|
1094
|
+
return Array.from(browser.document.querySelectorAll('button, [role="button"], mat-chip, mat-chip-option, [mat-chip-option]'))
|
|
1095
|
+
.map((el) => el)
|
|
1096
|
+
.filter((el) => el.offsetParent !== null)
|
|
1097
|
+
.map((el) => `${el.textContent?.trim() || ""} ${el.getAttribute("aria-label") || ""}`.trim())
|
|
1098
|
+
.filter((text) => text.length > 0)
|
|
1099
|
+
.slice(0, 20);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
catch (err) {
|
|
1103
|
+
log.debug(`source-manager: collecting visible source options: ${err instanceof Error ? err.message : String(err)}`);
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async getVisibleTextareas(page) {
|
|
1108
|
+
try {
|
|
1109
|
+
return await page.evaluate(() => {
|
|
1110
|
+
const browser = globalThis;
|
|
1111
|
+
return Array.from(browser.document.querySelectorAll("textarea"))
|
|
1112
|
+
.map((el) => el)
|
|
1113
|
+
.filter((el) => el.offsetParent !== null)
|
|
1114
|
+
.map((el) => ({
|
|
1115
|
+
selector: `textarea.${el.className?.replace(/\s+/g, ".")}` || "textarea",
|
|
1116
|
+
aria: el.getAttribute("aria-label") || "",
|
|
1117
|
+
placeholder: el.getAttribute("placeholder") || "",
|
|
1118
|
+
classes: el.className || "",
|
|
1119
|
+
}));
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
catch (err) {
|
|
1123
|
+
log.debug(`source-manager: collecting visible textareas: ${err instanceof Error ? err.message : String(err)}`);
|
|
1124
|
+
return [];
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async clickDialogSubmit() {
|
|
1128
|
+
const page = this.requirePage();
|
|
1129
|
+
// Strategy 1: JS-based click (works when elements are partially obscured)
|
|
1130
|
+
const clicked = await page.evaluate(() => {
|
|
1131
|
+
const browser = globalThis;
|
|
1132
|
+
const submitBtn = browser.document.querySelector("button[type='submit']:not([disabled])");
|
|
1133
|
+
if (submitBtn && submitBtn.offsetParent !== null) {
|
|
1134
|
+
submitBtn.click();
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
const primaryBtn = browser.document.querySelector("button.button-color--primary:not([disabled])");
|
|
1138
|
+
if (primaryBtn && primaryBtn.offsetParent !== null) {
|
|
1139
|
+
primaryBtn.click();
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
const buttons = Array.from(browser.document.querySelectorAll("button"));
|
|
1143
|
+
for (const btn of buttons) {
|
|
1144
|
+
const text = btn.textContent?.trim() ?? "";
|
|
1145
|
+
if (text === "Insert" || text.toLowerCase() === "insert") {
|
|
1146
|
+
btn.click();
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return false;
|
|
1151
|
+
});
|
|
1152
|
+
if (clicked)
|
|
1153
|
+
return;
|
|
1154
|
+
// Strategy 2: Patchright selector-based click
|
|
1155
|
+
for (const selector of getSelectors("submitButton")) {
|
|
1156
|
+
try {
|
|
1157
|
+
const element = await page.$(selector);
|
|
1158
|
+
if (element && await element.isVisible()) {
|
|
1159
|
+
await element.click();
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
catch (err) {
|
|
1164
|
+
log.debug(`source-manager: clicking submit button selector: ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// Final fallback
|
|
1168
|
+
await page.keyboard.press("Enter");
|
|
1169
|
+
}
|
|
1170
|
+
async waitForSourceProcessing(options = { mode: "strict" }) {
|
|
1171
|
+
const page = this.requirePage();
|
|
1172
|
+
log.info("⏳ Waiting for source processing...");
|
|
1173
|
+
if (options.mode === "lenient") {
|
|
1174
|
+
await randomDelay(3000, 4000);
|
|
1175
|
+
const dialogStillOpen = await this.isSourceDialogOpen();
|
|
1176
|
+
if (!dialogStillOpen) {
|
|
1177
|
+
log.success("✅ Source dialog closed - assuming success");
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
await this.throwIfSourceErrorAlert(page);
|
|
1181
|
+
await randomDelay(2000, 3000);
|
|
1182
|
+
log.success("✅ Source processing appears complete");
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const timeout = LENIENT_SOURCE_PROCESSING_TIMEOUT_MS;
|
|
1186
|
+
const startTime = Date.now();
|
|
1187
|
+
while (Date.now() - startTime < timeout) {
|
|
1188
|
+
const successElement = await findElement(page, "successIndicator");
|
|
1189
|
+
if (successElement) {
|
|
1190
|
+
log.success("✅ Source processed successfully");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const errorElement = await findElement(page, "errorMessage");
|
|
1194
|
+
if (errorElement) {
|
|
1195
|
+
const errorText = (await errorElement.innerText?.()) ||
|
|
1196
|
+
"Unknown error";
|
|
1197
|
+
throw new Error(`Source processing failed: ${errorText}`);
|
|
1198
|
+
}
|
|
1199
|
+
const processingElement = await findElement(page, "processingIndicator");
|
|
1200
|
+
if (!processingElement) {
|
|
1201
|
+
await randomDelay(1000, 1500);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
await page.waitForTimeout(1000);
|
|
1205
|
+
}
|
|
1206
|
+
log.warning("⚠️ Source processing timeout - continuing anyway");
|
|
1207
|
+
}
|
|
1208
|
+
async throwIfSourceErrorAlert(page) {
|
|
1209
|
+
const hasError = await page.evaluate(() => {
|
|
1210
|
+
const browser = globalThis;
|
|
1211
|
+
const alerts = Array.from(browser.document.querySelectorAll('[role="alert"]'));
|
|
1212
|
+
for (const alert of alerts) {
|
|
1213
|
+
const text = alert.textContent?.toLowerCase() || "";
|
|
1214
|
+
if (text.includes("error") || text.includes("failed") || text.includes("invalid") || text.includes("unable")) {
|
|
1215
|
+
return text.substring(0, 100);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return null;
|
|
1219
|
+
});
|
|
1220
|
+
if (hasError) {
|
|
1221
|
+
throw new Error(`Source processing failed: ${hasError}`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
586
1225
|
//# sourceMappingURL=source-manager.js.map
|