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