sourceloop 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +401 -0
  2. package/dist/commands/attach.d.ts +2 -0
  3. package/dist/commands/attach.js +103 -0
  4. package/dist/commands/attach.js.map +1 -0
  5. package/dist/commands/auth.d.ts +2 -0
  6. package/dist/commands/auth.js +33 -0
  7. package/dist/commands/auth.js.map +1 -0
  8. package/dist/commands/chrome.d.ts +2 -0
  9. package/dist/commands/chrome.js +30 -0
  10. package/dist/commands/chrome.js.map +1 -0
  11. package/dist/commands/compose.d.ts +2 -0
  12. package/dist/commands/compose.js +14 -0
  13. package/dist/commands/compose.js.map +1 -0
  14. package/dist/commands/doctor.d.ts +2 -0
  15. package/dist/commands/doctor.js +15 -0
  16. package/dist/commands/doctor.js.map +1 -0
  17. package/dist/commands/import-latest.d.ts +2 -0
  18. package/dist/commands/import-latest.js +42 -0
  19. package/dist/commands/import-latest.js.map +1 -0
  20. package/dist/commands/ingest.d.ts +2 -0
  21. package/dist/commands/ingest.js +14 -0
  22. package/dist/commands/ingest.js.map +1 -0
  23. package/dist/commands/init.d.ts +2 -0
  24. package/dist/commands/init.js +24 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/notebook-bind.d.ts +2 -0
  27. package/dist/commands/notebook-bind.js +39 -0
  28. package/dist/commands/notebook-bind.js.map +1 -0
  29. package/dist/commands/notebook-create.d.ts +2 -0
  30. package/dist/commands/notebook-create.js +39 -0
  31. package/dist/commands/notebook-create.js.map +1 -0
  32. package/dist/commands/notebook-import.d.ts +2 -0
  33. package/dist/commands/notebook-import.js +39 -0
  34. package/dist/commands/notebook-import.js.map +1 -0
  35. package/dist/commands/notebook-source.d.ts +2 -0
  36. package/dist/commands/notebook-source.js +71 -0
  37. package/dist/commands/notebook-source.js.map +1 -0
  38. package/dist/commands/plan.d.ts +9 -0
  39. package/dist/commands/plan.js +59 -0
  40. package/dist/commands/plan.js.map +1 -0
  41. package/dist/commands/run.d.ts +2 -0
  42. package/dist/commands/run.js +76 -0
  43. package/dist/commands/run.js.map +1 -0
  44. package/dist/commands/status.d.ts +2 -0
  45. package/dist/commands/status.js +15 -0
  46. package/dist/commands/status.js.map +1 -0
  47. package/dist/commands/topic.d.ts +2 -0
  48. package/dist/commands/topic.js +53 -0
  49. package/dist/commands/topic.js.map +1 -0
  50. package/dist/core/attach/launch-managed-chrome.d.ts +27 -0
  51. package/dist/core/attach/launch-managed-chrome.js +136 -0
  52. package/dist/core/attach/launch-managed-chrome.js.map +1 -0
  53. package/dist/core/attach/manage-targets.d.ts +49 -0
  54. package/dist/core/attach/manage-targets.js +179 -0
  55. package/dist/core/attach/manage-targets.js.map +1 -0
  56. package/dist/core/ingest/frontmatter.d.ts +4 -0
  57. package/dist/core/ingest/frontmatter.js +30 -0
  58. package/dist/core/ingest/frontmatter.js.map +1 -0
  59. package/dist/core/ingest/html-to-markdown.d.ts +5 -0
  60. package/dist/core/ingest/html-to-markdown.js +53 -0
  61. package/dist/core/ingest/html-to-markdown.js.map +1 -0
  62. package/dist/core/ingest/ingest-source.d.ts +11 -0
  63. package/dist/core/ingest/ingest-source.js +115 -0
  64. package/dist/core/ingest/ingest-source.js.map +1 -0
  65. package/dist/core/notebooklm/adapter.d.ts +17 -0
  66. package/dist/core/notebooklm/adapter.js +2 -0
  67. package/dist/core/notebooklm/adapter.js.map +1 -0
  68. package/dist/core/notebooklm/auth.d.ts +30 -0
  69. package/dist/core/notebooklm/auth.js +105 -0
  70. package/dist/core/notebooklm/auth.js.map +1 -0
  71. package/dist/core/notebooklm/browser-agent-adapter.d.ts +21 -0
  72. package/dist/core/notebooklm/browser-agent-adapter.js +37 -0
  73. package/dist/core/notebooklm/browser-agent-adapter.js.map +1 -0
  74. package/dist/core/notebooklm/browser-agent.d.ts +121 -0
  75. package/dist/core/notebooklm/browser-agent.js +1604 -0
  76. package/dist/core/notebooklm/browser-agent.js.map +1 -0
  77. package/dist/core/notebooklm/config.d.ts +20 -0
  78. package/dist/core/notebooklm/config.js +133 -0
  79. package/dist/core/notebooklm/config.js.map +1 -0
  80. package/dist/core/notebooklm/fixture-adapter.d.ts +13 -0
  81. package/dist/core/notebooklm/fixture-adapter.js +32 -0
  82. package/dist/core/notebooklm/fixture-adapter.js.map +1 -0
  83. package/dist/core/notebooklm/response-extraction.d.ts +23 -0
  84. package/dist/core/notebooklm/response-extraction.js +348 -0
  85. package/dist/core/notebooklm/response-extraction.js.map +1 -0
  86. package/dist/core/notebooks/bind-notebook.d.ts +21 -0
  87. package/dist/core/notebooks/bind-notebook.js +95 -0
  88. package/dist/core/notebooks/bind-notebook.js.map +1 -0
  89. package/dist/core/notebooks/manage-managed-notebooks.d.ts +70 -0
  90. package/dist/core/notebooks/manage-managed-notebooks.js +491 -0
  91. package/dist/core/notebooks/manage-managed-notebooks.js.map +1 -0
  92. package/dist/core/notebooks/manage-notebook-source-manifests.d.ts +25 -0
  93. package/dist/core/notebooks/manage-notebook-source-manifests.js +127 -0
  94. package/dist/core/notebooks/manage-notebook-source-manifests.js.map +1 -0
  95. package/dist/core/operator/workspace-operator.d.ts +82 -0
  96. package/dist/core/operator/workspace-operator.js +610 -0
  97. package/dist/core/operator/workspace-operator.js.map +1 -0
  98. package/dist/core/outputs/compose-run.d.ts +11 -0
  99. package/dist/core/outputs/compose-run.js +98 -0
  100. package/dist/core/outputs/compose-run.js.map +1 -0
  101. package/dist/core/runs/load-artifacts.d.ts +14 -0
  102. package/dist/core/runs/load-artifacts.js +51 -0
  103. package/dist/core/runs/load-artifacts.js.map +1 -0
  104. package/dist/core/runs/question-planner.d.ts +20 -0
  105. package/dist/core/runs/question-planner.js +276 -0
  106. package/dist/core/runs/question-planner.js.map +1 -0
  107. package/dist/core/runs/render-run-note.d.ts +13 -0
  108. package/dist/core/runs/render-run-note.js +111 -0
  109. package/dist/core/runs/render-run-note.js.map +1 -0
  110. package/dist/core/runs/run-qa.d.ts +28 -0
  111. package/dist/core/runs/run-qa.js +393 -0
  112. package/dist/core/runs/run-qa.js.map +1 -0
  113. package/dist/core/topics/manage-topics.d.ts +27 -0
  114. package/dist/core/topics/manage-topics.js +314 -0
  115. package/dist/core/topics/manage-topics.js.map +1 -0
  116. package/dist/core/vault/notes.d.ts +29 -0
  117. package/dist/core/vault/notes.js +147 -0
  118. package/dist/core/vault/notes.js.map +1 -0
  119. package/dist/core/vault/paths.d.ts +31 -0
  120. package/dist/core/vault/paths.js +44 -0
  121. package/dist/core/vault/paths.js.map +1 -0
  122. package/dist/core/workspace/bootstrap.d.ts +16 -0
  123. package/dist/core/workspace/bootstrap.js +443 -0
  124. package/dist/core/workspace/bootstrap.js.map +1 -0
  125. package/dist/core/workspace/constants.d.ts +3 -0
  126. package/dist/core/workspace/constants.js +16 -0
  127. package/dist/core/workspace/constants.js.map +1 -0
  128. package/dist/core/workspace/init-workspace.d.ts +15 -0
  129. package/dist/core/workspace/init-workspace.js +86 -0
  130. package/dist/core/workspace/init-workspace.js.map +1 -0
  131. package/dist/core/workspace/load-workspace.d.ts +6 -0
  132. package/dist/core/workspace/load-workspace.js +51 -0
  133. package/dist/core/workspace/load-workspace.js.map +1 -0
  134. package/dist/core/workspace/schema.d.ts +19 -0
  135. package/dist/core/workspace/schema.js +19 -0
  136. package/dist/core/workspace/schema.js.map +1 -0
  137. package/dist/index.d.ts +2 -0
  138. package/dist/index.js +41 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/lib/cli-output.d.ts +2 -0
  141. package/dist/lib/cli-output.js +7 -0
  142. package/dist/lib/cli-output.js.map +1 -0
  143. package/dist/lib/obsidian.d.ts +4 -0
  144. package/dist/lib/obsidian.js +23 -0
  145. package/dist/lib/obsidian.js.map +1 -0
  146. package/dist/lib/slugify.d.ts +1 -0
  147. package/dist/lib/slugify.js +10 -0
  148. package/dist/lib/slugify.js.map +1 -0
  149. package/dist/lib/write-json.d.ts +1 -0
  150. package/dist/lib/write-json.js +5 -0
  151. package/dist/lib/write-json.js.map +1 -0
  152. package/dist/schemas/attach.d.ts +118 -0
  153. package/dist/schemas/attach.js +33 -0
  154. package/dist/schemas/attach.js.map +1 -0
  155. package/dist/schemas/managed-notebook.d.ts +47 -0
  156. package/dist/schemas/managed-notebook.js +30 -0
  157. package/dist/schemas/managed-notebook.js.map +1 -0
  158. package/dist/schemas/notebook-source.d.ts +31 -0
  159. package/dist/schemas/notebook-source.js +23 -0
  160. package/dist/schemas/notebook-source.js.map +1 -0
  161. package/dist/schemas/notebook.d.ts +26 -0
  162. package/dist/schemas/notebook.js +18 -0
  163. package/dist/schemas/notebook.js.map +1 -0
  164. package/dist/schemas/run.d.ts +169 -0
  165. package/dist/schemas/run.js +80 -0
  166. package/dist/schemas/run.js.map +1 -0
  167. package/dist/schemas/source.d.ts +18 -0
  168. package/dist/schemas/source.js +13 -0
  169. package/dist/schemas/source.js.map +1 -0
  170. package/dist/schemas/topic.d.ts +37 -0
  171. package/dist/schemas/topic.js +25 -0
  172. package/dist/schemas/topic.js.map +1 -0
  173. package/package.json +44 -0
@@ -0,0 +1,1604 @@
1
+ import { access } from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
3
+ import net from "node:net";
4
+ import { chromium } from "playwright";
5
+ import { NOTEBOOKLM_ADD_SOURCE_SELECTORS, NOTEBOOKLM_ANSWER_BODY_SELECTORS, NOTEBOOKLM_CITATION_SELECTORS, NOTEBOOKLM_CITATION_OVERFLOW_SELECTORS, NOTEBOOKLM_CITATION_POPOVER_SELECTORS, NOTEBOOKLM_CREATE_NOTEBOOK_SELECTORS, NOTEBOOKLM_DEFAULT_URL, NOTEBOOKLM_IMPORT_ERROR_SELECTORS, NOTEBOOKLM_IMPORT_FILE_INPUT_SELECTORS, NOTEBOOKLM_IMPORT_FILE_OPTION_SELECTORS, NOTEBOOKLM_INITIAL_SOURCE_INTAKE_SELECTORS, NOTEBOOKLM_IMPORT_SUCCESS_CANDIDATE_SELECTORS, NOTEBOOKLM_IMPORT_SUBMIT_SELECTORS, NOTEBOOKLM_IMPORT_URL_INPUT_SELECTORS, NOTEBOOKLM_IMPORT_URL_OPTION_SELECTORS, NOTEBOOKLM_NOTEBOOK_TITLE_INPUT_SELECTORS, NOTEBOOKLM_QUERY_INPUT_SELECTORS, NOTEBOOKLM_RESPONSE_SELECTORS, NOTEBOOKLM_SUBMIT_SELECTORS, NOTEBOOKLM_THINKING_SELECTOR } from "./config.js";
6
+ import { extractCitationReferencesFromSnapshot, extractNormalizedAnswerFromSnapshot } from "./response-extraction.js";
7
+ const NOTEBOOKLM_CITATION_OVERFLOW_PATTERNS = [
8
+ /^\.{3,}$/,
9
+ /^…$/,
10
+ /^more_horiz$/i,
11
+ /\bshow more\b/i,
12
+ /\bmore citations?\b/i,
13
+ /\bexpand\b/i,
14
+ /더보기/,
15
+ /펼치기/,
16
+ /추가/
17
+ ];
18
+ const NOTEBOOKLM_NON_OVERFLOW_CONTROL_PATTERNS = [
19
+ /\bcopy\b/i,
20
+ /\bshare\b/i,
21
+ /\bretry\b/i,
22
+ /\brefresh\b/i,
23
+ /\bthumb/i,
24
+ /좋아요/,
25
+ /싫어요/,
26
+ /복사/,
27
+ /공유/
28
+ ];
29
+ export function isLikelyCitationOverflowControl(candidate) {
30
+ const values = [
31
+ candidate.text,
32
+ candidate.ariaLabel,
33
+ candidate.title,
34
+ candidate.className,
35
+ candidate.dataTestId
36
+ ]
37
+ .map((value) => normalizeControlText(value))
38
+ .filter(Boolean);
39
+ if (values.length === 0) {
40
+ return false;
41
+ }
42
+ const combined = values.join(" | ");
43
+ if (NOTEBOOKLM_NON_OVERFLOW_CONTROL_PATTERNS.some((pattern) => pattern.test(combined))) {
44
+ return false;
45
+ }
46
+ return NOTEBOOKLM_CITATION_OVERFLOW_PATTERNS.some((pattern) => values.some((value) => pattern.test(value)));
47
+ }
48
+ export function shouldExpandCitationOverflowControl(candidate) {
49
+ return Boolean(candidate.citationAdjacent) && isLikelyCitationOverflowControl(candidate);
50
+ }
51
+ export function isLikelyNotebookLMCitationMarker(candidate) {
52
+ const text = normalizeControlText(candidate.text);
53
+ const ariaLabel = normalizeControlText(candidate.ariaLabel);
54
+ const dialogLabel = normalizeControlText(candidate.dialogLabel);
55
+ const triggerDescription = normalizeControlText(candidate.triggerDescription);
56
+ if (/^\d+$/.test(text)) {
57
+ return true;
58
+ }
59
+ if (/^\d+\s*:/.test(ariaLabel)) {
60
+ return true;
61
+ }
62
+ const combined = `${dialogLabel} ${triggerDescription}`.trim();
63
+ if (/citation|인용/i.test(combined)) {
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ export function hasNotebookLMCitationSnippet(note) {
69
+ const normalized = normalizeControlText(note);
70
+ if (!normalized) {
71
+ return false;
72
+ }
73
+ return normalized.split(" | ").length >= 3;
74
+ }
75
+ export class ChromeAttachValidationError extends Error {
76
+ code;
77
+ constructor(code, message) {
78
+ super(message);
79
+ this.code = code;
80
+ this.name = "ChromeAttachValidationError";
81
+ }
82
+ }
83
+ export async function disposeNotebookBrowserSessionResources(input) {
84
+ await input.closePage().catch(() => undefined);
85
+ await input.closeBrowserConnection().catch(() => undefined);
86
+ if (input.ownsBrowserProcess) {
87
+ await input.killSpawnedProcess?.();
88
+ }
89
+ }
90
+ export async function validateChromeAttachTarget(input) {
91
+ const sessionFactory = input.sessionFactory ?? defaultNotebookBrowserSessionFactory;
92
+ const session = await sessionFactory.createSession({
93
+ target: input.target,
94
+ ...(input.showBrowser !== undefined ? { showBrowser: input.showBrowser } : {})
95
+ });
96
+ try {
97
+ await session.preflight(input.notebookUrl);
98
+ return { ok: true };
99
+ }
100
+ catch (error) {
101
+ if (error instanceof ChromeAttachValidationError) {
102
+ return {
103
+ ok: false,
104
+ code: error.code,
105
+ message: error.message
106
+ };
107
+ }
108
+ throw error;
109
+ }
110
+ finally {
111
+ await session.close();
112
+ }
113
+ }
114
+ export const defaultNotebookBrowserSessionFactory = {
115
+ async createSession(input) {
116
+ return createPlaywrightNotebookBrowserSession(input);
117
+ }
118
+ };
119
+ async function createPlaywrightNotebookBrowserSession(input) {
120
+ const state = await connectToAttachedChrome(input.target, input.showBrowser ?? false);
121
+ const context = getDefaultContext(state.browser);
122
+ let page;
123
+ let ownsPage = false;
124
+ const ensurePage = async (notebookUrl) => {
125
+ if (page) {
126
+ return page;
127
+ }
128
+ if (input.reuseExistingNotebookPage && notebookUrl) {
129
+ const existingPage = await findReusableNotebookPage(context, notebookUrl);
130
+ if (existingPage) {
131
+ page = existingPage;
132
+ ownsPage = false;
133
+ return page;
134
+ }
135
+ }
136
+ page = await context.newPage();
137
+ ownsPage = true;
138
+ return page;
139
+ };
140
+ return {
141
+ async preflight(notebookUrl) {
142
+ const targetUrl = notebookUrl ?? NOTEBOOKLM_DEFAULT_URL;
143
+ const activePage = await ensurePage(targetUrl);
144
+ if (notebookUrl && input.reuseExistingNotebookPage && isNotebookPageMatch(activePage.url(), notebookUrl)) {
145
+ await activePage.bringToFront().catch(() => undefined);
146
+ }
147
+ else {
148
+ await openNotebookPage(activePage, targetUrl);
149
+ }
150
+ if (notebookUrl) {
151
+ await ensureNotebookAccessible(activePage);
152
+ await waitForNotebookSettled(activePage);
153
+ return;
154
+ }
155
+ await ensureNotebookHomeAccessible(activePage);
156
+ },
157
+ async askQuestion(question) {
158
+ const activePage = await ensurePage();
159
+ const inputSelector = await waitForFirstVisibleSelector(activePage, NOTEBOOKLM_QUERY_INPUT_SELECTORS, 10_000, "Could not find a visible NotebookLM query input.");
160
+ await waitForNotebookSettled(activePage, inputSelector);
161
+ const previousAnswer = await snapshotLatestResponse(activePage);
162
+ await clearQueryInput(activePage, inputSelector);
163
+ await setQueryInputText(activePage, inputSelector, question.prompt);
164
+ await submitQuery(activePage, inputSelector);
165
+ const latestElement = await waitForStableLatestResponse(activePage, previousAnswer);
166
+ await waitForStableCitationMetadata(latestElement);
167
+ let responseSnapshot = await collectResponseSnapshot(activePage, latestElement);
168
+ const answer = extractNormalizedAnswerFromSnapshot(responseSnapshot);
169
+ if (!answer) {
170
+ throw new Error(`NotebookLM returned an empty answer for question ${question.id}`);
171
+ }
172
+ let citations = extractCitationReferencesFromSnapshot(responseSnapshot.citationCandidates);
173
+ if (shouldRetryNotebookLMCitationCapture(citations)) {
174
+ const refreshedSnapshot = await recaptureLatestResponseSnapshot(activePage);
175
+ if (refreshedSnapshot) {
176
+ const refreshedCitations = extractCitationReferencesFromSnapshot(refreshedSnapshot.citationCandidates);
177
+ if (countRichNotebookLMCitations(refreshedCitations) > countRichNotebookLMCitations(citations)) {
178
+ responseSnapshot = refreshedSnapshot;
179
+ citations = refreshedCitations;
180
+ }
181
+ }
182
+ }
183
+ return {
184
+ answer,
185
+ citations
186
+ };
187
+ },
188
+ async captureLatestAnswer() {
189
+ const activePage = await ensurePage();
190
+ await waitForNotebookSettled(activePage);
191
+ const latestElement = await waitForLatestVisibleResponse(activePage, 30_000);
192
+ if (!latestElement) {
193
+ throw new Error("Could not find a latest NotebookLM response to import.");
194
+ }
195
+ await waitForStableCitationMetadata(latestElement);
196
+ let responseSnapshot = await collectResponseSnapshot(activePage, latestElement);
197
+ const answer = extractNormalizedAnswerFromSnapshot(responseSnapshot);
198
+ if (!answer) {
199
+ throw new Error("NotebookLM did not expose a readable latest answer.");
200
+ }
201
+ let citations = extractCitationReferencesFromSnapshot(responseSnapshot.citationCandidates);
202
+ if (shouldRetryNotebookLMCitationCapture(citations)) {
203
+ const refreshedSnapshot = await recaptureLatestResponseSnapshot(activePage);
204
+ if (refreshedSnapshot) {
205
+ const refreshedCitations = extractCitationReferencesFromSnapshot(refreshedSnapshot.citationCandidates);
206
+ if (countRichNotebookLMCitations(refreshedCitations) > countRichNotebookLMCitations(citations)) {
207
+ responseSnapshot = refreshedSnapshot;
208
+ citations = refreshedCitations;
209
+ }
210
+ }
211
+ }
212
+ return {
213
+ answer,
214
+ citations
215
+ };
216
+ },
217
+ async createNotebook(title) {
218
+ const activePage = await ensurePage();
219
+ await openNotebookPage(activePage, NOTEBOOKLM_DEFAULT_URL);
220
+ await ensureNotebookHomeAccessible(activePage);
221
+ await clickFirstVisibleLocator(activePage, NOTEBOOKLM_CREATE_NOTEBOOK_SELECTORS, "Could not find a NotebookLM create notebook control.");
222
+ await waitForNotebookUrl(activePage, 30_000);
223
+ await waitForNotebookSettled(activePage).catch(() => undefined);
224
+ await bestEffortFillNotebookTitle(activePage, title);
225
+ return {
226
+ notebookUrl: canonicalizeNotebookUrl(activePage.url())
227
+ };
228
+ },
229
+ async importSource(input) {
230
+ const activePage = await ensurePage(input.notebookUrl);
231
+ if (input.notebookUrl) {
232
+ if (isNotebookPageMatch(activePage.url(), input.notebookUrl)) {
233
+ await activePage.bringToFront().catch(() => undefined);
234
+ }
235
+ else {
236
+ await openNotebookPage(activePage, input.notebookUrl);
237
+ }
238
+ }
239
+ await ensureNotebookPageAccessible(activePage);
240
+ if (input.notebookUrl) {
241
+ ensureNotebookTargetMatch(activePage.url(), input.notebookUrl);
242
+ }
243
+ const importSurface = await waitForNotebookImportSurface(activePage);
244
+ const baselineCandidates = await captureImportSuccessCandidates(activePage).catch(() => []);
245
+ const baselineSourceCount = await captureVisibleSourceCount(activePage).catch(() => undefined);
246
+ try {
247
+ if (importSurface === "add_source") {
248
+ const clickedAddSource = await bestEffortClickAny(activePage, NOTEBOOKLM_ADD_SOURCE_SELECTORS);
249
+ if (!clickedAddSource && !(await hasVisibleSelector(activePage, NOTEBOOKLM_INITIAL_SOURCE_INTAKE_SELECTORS))) {
250
+ throw new Error("Could not find a NotebookLM add source control.");
251
+ }
252
+ await waitForImportChoiceSurface(activePage, 5_000);
253
+ }
254
+ if (input.importKind === "file_upload") {
255
+ if (!(await hasVisibleSelector(activePage, NOTEBOOKLM_IMPORT_FILE_INPUT_SELECTORS))) {
256
+ await bestEffortClickAny(activePage, NOTEBOOKLM_IMPORT_FILE_OPTION_SELECTORS);
257
+ }
258
+ const fileSelector = await waitForFirstExistingSelector(activePage, NOTEBOOKLM_IMPORT_FILE_INPUT_SELECTORS, 10_000);
259
+ await activePage.locator(fileSelector).first().setInputFiles(input.filePath);
260
+ }
261
+ else {
262
+ if (!(await hasVisibleSelector(activePage, NOTEBOOKLM_IMPORT_URL_INPUT_SELECTORS))) {
263
+ await bestEffortClickAny(activePage, NOTEBOOKLM_IMPORT_URL_OPTION_SELECTORS);
264
+ }
265
+ const urlSelector = await waitForFirstVisibleSelector(activePage, NOTEBOOKLM_IMPORT_URL_INPUT_SELECTORS, 10_000, "Could not find a visible NotebookLM URL input control.");
266
+ await clearInputLike(activePage, urlSelector);
267
+ await fillInputLike(activePage, urlSelector, input.url);
268
+ await clickFirstVisibleLocator(activePage, NOTEBOOKLM_IMPORT_SUBMIT_SELECTORS, "Could not find a NotebookLM import submit control.");
269
+ }
270
+ return await waitForImportOutcome(activePage, baselineCandidates, baselineSourceCount, input, importSurface, 12_000);
271
+ }
272
+ catch (error) {
273
+ return {
274
+ status: "failed",
275
+ failureReason: formatError(error)
276
+ };
277
+ }
278
+ },
279
+ async close() {
280
+ const spawnedProcess = state.spawnedProcess;
281
+ const killSpawnedProcess = spawnedProcess
282
+ ? async () => {
283
+ await terminateSpawnedChromeProcess(spawnedProcess);
284
+ }
285
+ : undefined;
286
+ await disposeNotebookBrowserSessionResources({
287
+ closePage: () => (page && ownsPage ? page.close() : Promise.resolve()),
288
+ closeBrowserConnection: () => state.browser.close(),
289
+ ownsBrowserProcess: state.ownsBrowser,
290
+ ...(killSpawnedProcess ? { killSpawnedProcess } : {})
291
+ });
292
+ }
293
+ };
294
+ }
295
+ async function findReusableNotebookPage(context, notebookUrl) {
296
+ const targetPath = normalizeNotebookPath(notebookUrl);
297
+ if (!targetPath) {
298
+ return undefined;
299
+ }
300
+ return context.pages().find((candidatePage) => isNotebookPageMatch(candidatePage.url(), notebookUrl));
301
+ }
302
+ export function isNotebookPageMatch(currentUrl, notebookUrl) {
303
+ return normalizeNotebookPath(currentUrl) === normalizeNotebookPath(notebookUrl);
304
+ }
305
+ export function canonicalizeNotebookUrl(url) {
306
+ try {
307
+ const parsed = new URL(url);
308
+ parsed.searchParams.delete("addSource");
309
+ parsed.hash = "";
310
+ const search = parsed.searchParams.toString();
311
+ return `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ""}`.replace(/\/+$/, "");
312
+ }
313
+ catch {
314
+ return url;
315
+ }
316
+ }
317
+ export function extractNotebookResourceId(url) {
318
+ try {
319
+ const parsed = new URL(canonicalizeNotebookUrl(url));
320
+ const segments = parsed.pathname.split("/").filter(Boolean);
321
+ const notebookIndex = segments.findIndex((segment) => segment === "notebook");
322
+ if (notebookIndex === -1) {
323
+ return undefined;
324
+ }
325
+ const resourceId = segments[notebookIndex + 1];
326
+ return resourceId?.trim() ? resourceId : undefined;
327
+ }
328
+ catch {
329
+ return undefined;
330
+ }
331
+ }
332
+ function normalizeNotebookPath(url) {
333
+ try {
334
+ return canonicalizeNotebookUrl(url).replace(/\?.*$/, "");
335
+ }
336
+ catch {
337
+ return undefined;
338
+ }
339
+ }
340
+ export function ensureNotebookTargetMatch(currentUrl, notebookUrl) {
341
+ if (isNotebookPageMatch(currentUrl, notebookUrl)) {
342
+ return;
343
+ }
344
+ throw new Error(`NotebookLM did not open the requested notebook. Expected ${canonicalizeNotebookUrl(notebookUrl)}, but landed on ${canonicalizeNotebookUrl(currentUrl)}.`);
345
+ }
346
+ async function connectToAttachedChrome(target, showBrowser) {
347
+ if (target.targetType === "remote_debugging_endpoint") {
348
+ try {
349
+ const browser = await chromium.connectOverCDP(target.endpoint);
350
+ return { browser, ownsBrowser: false };
351
+ }
352
+ catch (error) {
353
+ throw new ChromeAttachValidationError("chrome_unreachable", `Could not reach Chrome remote debugging endpoint ${target.endpoint}: ${formatError(error)}`);
354
+ }
355
+ }
356
+ return launchProfileChrome(target, showBrowser);
357
+ }
358
+ async function launchProfileChrome(target, showBrowser) {
359
+ const port = target.remoteDebuggingPort ?? (await allocateFreePort());
360
+ const endpoint = `http://127.0.0.1:${port}`;
361
+ try {
362
+ const browser = await chromium.connectOverCDP(endpoint);
363
+ return { browser, ownsBrowser: false };
364
+ }
365
+ catch {
366
+ // no running endpoint for this profile target; fall back to launching Chrome
367
+ }
368
+ const executablePath = await resolveChromeExecutablePath(target.chromeExecutablePath);
369
+ const launchArgs = [
370
+ `--user-data-dir=${target.profileDirPath}`,
371
+ `--remote-debugging-port=${port}`,
372
+ "--no-first-run",
373
+ "--no-default-browser-check",
374
+ ...(showBrowser ? [] : ["--headless=new"]),
375
+ ...target.launchArgs,
376
+ NOTEBOOKLM_DEFAULT_URL
377
+ ];
378
+ const spawnedProcess = spawn(executablePath, launchArgs, {
379
+ stdio: "ignore"
380
+ });
381
+ try {
382
+ await waitForRemoteDebuggingEndpoint(endpoint, 15_000);
383
+ const browser = await chromium.connectOverCDP(endpoint);
384
+ return { browser, spawnedProcess, ownsBrowser: true };
385
+ }
386
+ catch (error) {
387
+ spawnedProcess.kill("SIGTERM");
388
+ throw new ChromeAttachValidationError("chrome_unreachable", `Could not launch Chrome from profile ${target.profileDirPath}: ${formatError(error)}`);
389
+ }
390
+ }
391
+ const GRACEFUL_CHROME_SHUTDOWN_TIMEOUT_MS = 5_000;
392
+ const FORCEFUL_CHROME_SHUTDOWN_TIMEOUT_MS = 2_000;
393
+ async function terminateSpawnedChromeProcess(spawnedProcess) {
394
+ if (spawnedProcess.exitCode !== null || spawnedProcess.signalCode !== null) {
395
+ return;
396
+ }
397
+ const exitPromise = waitForChildProcessExit(spawnedProcess);
398
+ const terminatedGracefully = requestChildProcessSignal(spawnedProcess, "SIGTERM");
399
+ if (!terminatedGracefully && spawnedProcess.exitCode === null && spawnedProcess.signalCode === null) {
400
+ return;
401
+ }
402
+ if (await waitForChildProcessExitWithin(exitPromise, GRACEFUL_CHROME_SHUTDOWN_TIMEOUT_MS)) {
403
+ return;
404
+ }
405
+ requestChildProcessSignal(spawnedProcess, "SIGKILL");
406
+ await waitForChildProcessExitWithin(exitPromise, FORCEFUL_CHROME_SHUTDOWN_TIMEOUT_MS);
407
+ }
408
+ function requestChildProcessSignal(spawnedProcess, signal) {
409
+ try {
410
+ return spawnedProcess.kill(signal);
411
+ }
412
+ catch {
413
+ return false;
414
+ }
415
+ }
416
+ function waitForChildProcessExit(spawnedProcess) {
417
+ if (spawnedProcess.exitCode !== null || spawnedProcess.signalCode !== null) {
418
+ return Promise.resolve();
419
+ }
420
+ return new Promise((resolve) => {
421
+ const finalize = () => {
422
+ spawnedProcess.off("close", finalize);
423
+ spawnedProcess.off("exit", finalize);
424
+ spawnedProcess.off("error", finalize);
425
+ resolve();
426
+ };
427
+ spawnedProcess.once("close", finalize);
428
+ spawnedProcess.once("exit", finalize);
429
+ spawnedProcess.once("error", finalize);
430
+ });
431
+ }
432
+ async function waitForChildProcessExitWithin(exitPromise, timeoutMs) {
433
+ return Promise.race([
434
+ exitPromise.then(() => true),
435
+ sleep(timeoutMs).then(() => false)
436
+ ]);
437
+ }
438
+ export async function resolveChromeExecutablePath(customPath) {
439
+ const candidates = customPath
440
+ ? [customPath]
441
+ : process.platform === "darwin"
442
+ ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]
443
+ : process.platform === "win32"
444
+ ? [
445
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
446
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
447
+ ]
448
+ : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
449
+ for (const candidate of candidates) {
450
+ try {
451
+ await access(candidate);
452
+ return candidate;
453
+ }
454
+ catch { }
455
+ }
456
+ throw new Error("Could not resolve a Google Chrome executable path. Provide --chrome-path when registering the attach target.");
457
+ }
458
+ export async function allocateFreePort() {
459
+ return new Promise((resolve, reject) => {
460
+ const server = net.createServer();
461
+ server.on("error", reject);
462
+ server.listen(0, "127.0.0.1", () => {
463
+ const address = server.address();
464
+ if (!address || typeof address === "string") {
465
+ reject(new Error("Could not allocate a free TCP port."));
466
+ return;
467
+ }
468
+ const port = address.port;
469
+ server.close((error) => {
470
+ if (error) {
471
+ reject(error);
472
+ return;
473
+ }
474
+ resolve(port);
475
+ });
476
+ });
477
+ });
478
+ }
479
+ export async function waitForRemoteDebuggingEndpoint(endpoint, timeoutMs) {
480
+ const deadline = Date.now() + timeoutMs;
481
+ while (Date.now() < deadline) {
482
+ try {
483
+ const response = await fetch(`${endpoint}/json/version`);
484
+ if (response.ok) {
485
+ return;
486
+ }
487
+ }
488
+ catch { }
489
+ await sleep(250);
490
+ }
491
+ throw new Error(`Timed out waiting for Chrome remote debugging endpoint at ${endpoint}`);
492
+ }
493
+ function getDefaultContext(browser) {
494
+ const [defaultContext] = browser.contexts();
495
+ if (!defaultContext) {
496
+ throw new ChromeAttachValidationError("chrome_unreachable", "Chrome attached successfully but did not expose a default browser context.");
497
+ }
498
+ return defaultContext;
499
+ }
500
+ async function openNotebookPage(page, url) {
501
+ try {
502
+ await page.goto(url, { waitUntil: "domcontentloaded" });
503
+ }
504
+ catch (error) {
505
+ if (!(error instanceof Error) || !error.message.includes("ERR_INVALID_ARGUMENT")) {
506
+ throw error;
507
+ }
508
+ await page.goto(url, { waitUntil: "domcontentloaded" });
509
+ }
510
+ }
511
+ async function ensureNotebookAccessible(page) {
512
+ await ensureNotebookPageAccessible(page);
513
+ try {
514
+ await waitForFirstVisibleSelector(page, NOTEBOOKLM_QUERY_INPUT_SELECTORS, 10_000, "Could not find a visible NotebookLM query input.");
515
+ }
516
+ catch (error) {
517
+ if (await looksLikeSignInPage(page)) {
518
+ throw new ChromeAttachValidationError("notebooklm_sign_in_required", "Chrome is reachable, but NotebookLM is not ready because the session is not signed in.");
519
+ }
520
+ throw new ChromeAttachValidationError("notebooklm_preflight_failed", `Chrome is reachable, but NotebookLM is not usable yet: ${formatError(error)}`);
521
+ }
522
+ }
523
+ async function ensureNotebookHomeAccessible(page) {
524
+ await page.waitForLoadState("domcontentloaded");
525
+ await sleep(750);
526
+ const currentUrl = page.url();
527
+ if (!currentUrl.startsWith("https://notebooklm.google.com/")) {
528
+ if (currentUrl.includes("accounts.google.com")) {
529
+ throw new ChromeAttachValidationError("notebooklm_sign_in_required", "Chrome is reachable, but NotebookLM redirected to Google sign-in. Sign in to NotebookLM in this Chrome target first.");
530
+ }
531
+ throw new ChromeAttachValidationError("notebooklm_preflight_failed", `Chrome is reachable, but NotebookLM home did not open successfully: ${currentUrl}`);
532
+ }
533
+ }
534
+ async function ensureNotebookPageAccessible(page) {
535
+ await page.waitForLoadState("domcontentloaded");
536
+ await sleep(750);
537
+ const currentUrl = page.url();
538
+ if (!currentUrl.startsWith("https://notebooklm.google.com/")) {
539
+ if (currentUrl.includes("accounts.google.com")) {
540
+ throw new ChromeAttachValidationError("notebooklm_sign_in_required", "Chrome is reachable, but NotebookLM redirected to Google sign-in. Sign in to NotebookLM in this Chrome target first.");
541
+ }
542
+ throw new ChromeAttachValidationError("notebooklm_preflight_failed", `Chrome is reachable, but NotebookLM did not open successfully: ${currentUrl}`);
543
+ }
544
+ }
545
+ async function waitForNotebookSettled(page, knownInputSelector) {
546
+ await page.waitForLoadState("domcontentloaded");
547
+ await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
548
+ const inputSelector = knownInputSelector ??
549
+ (await waitForFirstVisibleSelector(page, NOTEBOOKLM_QUERY_INPUT_SELECTORS, 10_000, "Could not find a visible NotebookLM query input."));
550
+ await waitForUsableQueryInput(page, inputSelector, 10_000);
551
+ await scrollNotebookToLatest(page);
552
+ await sleep(1_000);
553
+ }
554
+ async function looksLikeSignInPage(page) {
555
+ const text = (await page.textContent("body").catch(() => null))?.toLowerCase() ?? "";
556
+ return text.includes("sign in") || text.includes("로그인") || text.includes("continue to");
557
+ }
558
+ async function waitForFirstVisibleSelector(page, selectors, timeout = 10_000, errorMessage = "Could not find a visible NotebookLM control.") {
559
+ const deadline = Date.now() + timeout;
560
+ while (Date.now() < deadline) {
561
+ for (const selector of selectors) {
562
+ try {
563
+ const element = await page.$(selector);
564
+ if (element && (await element.isVisible())) {
565
+ return selector;
566
+ }
567
+ }
568
+ catch { }
569
+ }
570
+ await sleep(250);
571
+ }
572
+ throw new Error(errorMessage);
573
+ }
574
+ async function waitForNotebookImportSurface(page, timeout = 10_000) {
575
+ const deadline = Date.now() + timeout;
576
+ const initialIntakeAssumptionThreshold = Date.now() + 2_000;
577
+ while (Date.now() < deadline) {
578
+ if (await looksLikeNotebookHomePage(page)) {
579
+ throw new Error("NotebookLM rendered the home notebook list instead of the requested notebook detail view.");
580
+ }
581
+ if (page.url().includes("addSource=true") ||
582
+ (await hasVisibleSelector(page, NOTEBOOKLM_INITIAL_SOURCE_INTAKE_SELECTORS))) {
583
+ return "initial_source_intake";
584
+ }
585
+ if (await hasVisibleSelector(page, NOTEBOOKLM_ADD_SOURCE_SELECTORS)) {
586
+ return "add_source";
587
+ }
588
+ if (Date.now() >= initialIntakeAssumptionThreshold &&
589
+ (await looksLikeEmptyNotebookDetail(page))) {
590
+ return "initial_source_intake";
591
+ }
592
+ if (await looksLikeSignInPage(page)) {
593
+ throw new ChromeAttachValidationError("notebooklm_sign_in_required", "Chrome is reachable, but NotebookLM is not ready because the session is not signed in.");
594
+ }
595
+ await sleep(250);
596
+ }
597
+ throw new Error("Could not find a NotebookLM source import control.");
598
+ }
599
+ async function waitForImportChoiceSurface(page, timeout = 5_000) {
600
+ const deadline = Date.now() + timeout;
601
+ while (Date.now() < deadline) {
602
+ if ((await hasVisibleSelector(page, NOTEBOOKLM_IMPORT_URL_OPTION_SELECTORS)) ||
603
+ (await hasVisibleSelector(page, NOTEBOOKLM_IMPORT_URL_INPUT_SELECTORS)) ||
604
+ (await hasVisibleSelector(page, NOTEBOOKLM_IMPORT_FILE_OPTION_SELECTORS)) ||
605
+ (await hasVisibleSelector(page, NOTEBOOKLM_IMPORT_FILE_INPUT_SELECTORS))) {
606
+ return;
607
+ }
608
+ await sleep(100);
609
+ }
610
+ }
611
+ async function looksLikeNotebookHomePage(page) {
612
+ const normalizedPath = normalizeNotebookPath(page.url()) ?? "";
613
+ if (normalizedPath.includes("/notebook/")) {
614
+ return false;
615
+ }
616
+ return hasVisibleSelector(page, NOTEBOOKLM_CREATE_NOTEBOOK_SELECTORS);
617
+ }
618
+ async function looksLikeEmptyNotebookDetail(page) {
619
+ const normalizedPath = normalizeNotebookPath(page.url()) ?? "";
620
+ if (!normalizedPath.includes("/notebook/")) {
621
+ return false;
622
+ }
623
+ if (await hasVisibleSelector(page, NOTEBOOKLM_QUERY_INPUT_SELECTORS)) {
624
+ return false;
625
+ }
626
+ if (await hasVisibleSelector(page, NOTEBOOKLM_ADD_SOURCE_SELECTORS)) {
627
+ return false;
628
+ }
629
+ return true;
630
+ }
631
+ async function hasVisibleSelector(page, selectors) {
632
+ for (const selector of selectors) {
633
+ try {
634
+ const locator = page.locator(selector).first();
635
+ if ((await locator.count()) > 0 && (await locator.isVisible())) {
636
+ return true;
637
+ }
638
+ }
639
+ catch { }
640
+ }
641
+ return false;
642
+ }
643
+ async function clickFirstVisibleLocator(page, selectors, errorMessage) {
644
+ for (const selector of selectors) {
645
+ const locator = page.locator(selector).first();
646
+ try {
647
+ if ((await locator.count()) > 0 && (await locator.isVisible()) && (await isActionableLocator(locator))) {
648
+ await locator.click();
649
+ return;
650
+ }
651
+ }
652
+ catch { }
653
+ }
654
+ throw new Error(errorMessage);
655
+ }
656
+ async function waitForFirstExistingSelector(page, selectors, timeout = 10_000) {
657
+ const deadline = Date.now() + timeout;
658
+ while (Date.now() < deadline) {
659
+ for (const selector of selectors) {
660
+ try {
661
+ if ((await page.locator(selector).count()) > 0) {
662
+ return selector;
663
+ }
664
+ }
665
+ catch { }
666
+ }
667
+ await sleep(250);
668
+ }
669
+ throw new Error("Could not find a matching NotebookLM control.");
670
+ }
671
+ async function bestEffortClickAny(page, selectors) {
672
+ for (const selector of selectors) {
673
+ const locator = page.locator(selector).first();
674
+ try {
675
+ if ((await locator.count()) > 0 && (await locator.isVisible()) && (await isActionableLocator(locator))) {
676
+ await locator.click();
677
+ return true;
678
+ }
679
+ }
680
+ catch { }
681
+ }
682
+ return false;
683
+ }
684
+ async function isActionableLocator(locator) {
685
+ try {
686
+ return await locator.evaluate((element) => {
687
+ const candidate = element;
688
+ if (candidate.disabled) {
689
+ return false;
690
+ }
691
+ const ariaDisabled = candidate.getAttribute("aria-disabled");
692
+ return ariaDisabled !== "true";
693
+ });
694
+ }
695
+ catch {
696
+ return false;
697
+ }
698
+ }
699
+ async function waitForUsableQueryInput(page, selector, timeout = 10_000) {
700
+ const deadline = Date.now() + timeout;
701
+ while (Date.now() < deadline) {
702
+ try {
703
+ const handle = await page.$(selector);
704
+ if (handle && (await handle.isVisible())) {
705
+ const isUsable = await handle.evaluate((node) => {
706
+ const textarea = node;
707
+ return !textarea.disabled && !textarea.readOnly;
708
+ });
709
+ if (isUsable) {
710
+ return;
711
+ }
712
+ }
713
+ }
714
+ catch { }
715
+ await sleep(250);
716
+ }
717
+ throw new Error("NotebookLM query input did not become usable in time.");
718
+ }
719
+ async function waitForNotebookUrl(page, timeoutMs) {
720
+ const deadline = Date.now() + timeoutMs;
721
+ while (Date.now() < deadline) {
722
+ const currentUrl = page.url();
723
+ if (normalizeNotebookPath(currentUrl)?.includes("/notebook/")) {
724
+ return;
725
+ }
726
+ await sleep(250);
727
+ }
728
+ throw new Error("Timed out waiting for NotebookLM to open a notebook page.");
729
+ }
730
+ async function bestEffortFillNotebookTitle(page, title) {
731
+ for (const selector of NOTEBOOKLM_NOTEBOOK_TITLE_INPUT_SELECTORS) {
732
+ const locator = page.locator(selector).first();
733
+ try {
734
+ if ((await locator.count()) > 0 && (await locator.isVisible())) {
735
+ await clearInputLike(page, selector);
736
+ await fillInputLike(page, selector, title);
737
+ await page.keyboard.press("Enter").catch(() => undefined);
738
+ return;
739
+ }
740
+ }
741
+ catch { }
742
+ }
743
+ }
744
+ async function snapshotLatestResponse(page) {
745
+ const latestElement = await getLatestResponseElement(page);
746
+ if (latestElement) {
747
+ const text = extractNormalizedAnswerFromSnapshot(await collectResponseTextSnapshot(latestElement));
748
+ if (text) {
749
+ return text;
750
+ }
751
+ }
752
+ return null;
753
+ }
754
+ async function getLatestResponseElement(page) {
755
+ for (const selector of NOTEBOOKLM_RESPONSE_SELECTORS) {
756
+ const elements = await page.$$(selector);
757
+ const latestElement = elements[elements.length - 1];
758
+ if (latestElement) {
759
+ return latestElement;
760
+ }
761
+ }
762
+ return null;
763
+ }
764
+ async function waitForLatestVisibleResponse(page, timeout) {
765
+ const deadline = Date.now() + timeout;
766
+ while (Date.now() < deadline) {
767
+ await scrollNotebookToLatest(page);
768
+ const latestElement = await getLatestResponseElement(page);
769
+ if (latestElement) {
770
+ try {
771
+ if (await latestElement.isVisible()) {
772
+ const text = extractNormalizedAnswerFromSnapshot(await collectResponseTextSnapshot(latestElement));
773
+ if (text) {
774
+ return latestElement;
775
+ }
776
+ }
777
+ }
778
+ catch { }
779
+ }
780
+ await sleep(250);
781
+ }
782
+ return null;
783
+ }
784
+ async function scrollNotebookToLatest(page) {
785
+ await page
786
+ .evaluate(() => {
787
+ const g = globalThis;
788
+ const scrollingElement = g.document.scrollingElement ?? g.document.documentElement ?? g.document.body;
789
+ scrollingElement.scrollTo?.({ top: scrollingElement.scrollHeight, behavior: "instant" });
790
+ g.scrollTo({ top: g.document.body.scrollHeight, behavior: "instant" });
791
+ })
792
+ .catch(() => undefined);
793
+ }
794
+ async function waitForStableLatestResponse(page, previousAnswer) {
795
+ const deadline = Date.now() + 120_000;
796
+ let stableCount = 0;
797
+ let latestText = null;
798
+ let latestElement = null;
799
+ while (Date.now() < deadline) {
800
+ const thinkingElement = await page.$(NOTEBOOKLM_THINKING_SELECTOR);
801
+ if (thinkingElement && (await thinkingElement.isVisible())) {
802
+ await sleep(500);
803
+ continue;
804
+ }
805
+ for (const selector of NOTEBOOKLM_RESPONSE_SELECTORS) {
806
+ const elements = await page.$$(selector);
807
+ const candidate = elements[elements.length - 1];
808
+ if (!candidate) {
809
+ continue;
810
+ }
811
+ const candidateText = extractNormalizedAnswerFromSnapshot(await collectResponseTextSnapshot(candidate));
812
+ if (!candidateText || candidateText === previousAnswer) {
813
+ continue;
814
+ }
815
+ if (candidateText === latestText) {
816
+ stableCount += 1;
817
+ }
818
+ else {
819
+ latestText = candidateText;
820
+ latestElement = candidate;
821
+ stableCount = 1;
822
+ }
823
+ if (stableCount >= 3 && latestElement) {
824
+ await scrollNotebookToLatest(page);
825
+ return (await getLatestResponseElement(page)) ?? latestElement;
826
+ }
827
+ }
828
+ await sleep(500);
829
+ }
830
+ throw new Error("Timed out waiting for NotebookLM to return a stable answer.");
831
+ }
832
+ const EMPTY_CITATION_METADATA_SETTLE_POLLS = 6;
833
+ export function shouldTreatCitationMetadataAsSettled(input) {
834
+ const signature = normalizeControlText(input.signature);
835
+ const latestSignature = normalizeControlText(input.latestSignature);
836
+ if (!signature) {
837
+ const emptyStableCount = latestSignature ? 1 : input.emptyStableCount + 1;
838
+ return {
839
+ stableCount: 0,
840
+ emptyStableCount,
841
+ settled: emptyStableCount >= EMPTY_CITATION_METADATA_SETTLE_POLLS
842
+ };
843
+ }
844
+ const stableCount = signature === latestSignature ? input.stableCount + 1 : 1;
845
+ return {
846
+ stableCount,
847
+ emptyStableCount: 0,
848
+ settled: stableCount >= 3
849
+ };
850
+ }
851
+ async function waitForStableCitationMetadata(element, timeoutMs = 6_000) {
852
+ const deadline = Date.now() + timeoutMs;
853
+ let stableCount = 0;
854
+ let emptyStableCount = 0;
855
+ let latestSignature = "";
856
+ while (Date.now() < deadline) {
857
+ const signature = await element
858
+ .evaluate((root) => {
859
+ const normalize = (value) => (value ?? "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
860
+ const markers = [...root.querySelectorAll(".citation-marker")].map((node) => {
861
+ const elementNode = node;
862
+ const text = normalize(elementNode.innerText || elementNode.textContent || "");
863
+ const ariaNode = elementNode.querySelector?.("[aria-label]");
864
+ const ariaLabel = normalize(elementNode.getAttribute?.("aria-label") || ariaNode?.getAttribute?.("aria-label") || "");
865
+ const dialogLabel = normalize(elementNode.getAttribute?.("dialoglabel"));
866
+ const triggerDescription = normalize(elementNode.getAttribute?.("triggerdescription"));
867
+ const real = /^\d+$/.test(text) || /^\d+\s*:/.test(ariaLabel) || /citation|인용/i.test(`${dialogLabel} ${triggerDescription}`.trim());
868
+ if (!real) {
869
+ return "";
870
+ }
871
+ return `${text}|${ariaLabel}|${dialogLabel}|${triggerDescription}`;
872
+ });
873
+ return markers.filter(Boolean).join("||");
874
+ })
875
+ .catch(() => "");
876
+ const state = shouldTreatCitationMetadataAsSettled({
877
+ signature,
878
+ latestSignature,
879
+ stableCount,
880
+ emptyStableCount
881
+ });
882
+ stableCount = state.stableCount;
883
+ emptyStableCount = state.emptyStableCount;
884
+ latestSignature = normalizeControlText(signature);
885
+ if (state.settled) {
886
+ return;
887
+ }
888
+ await sleep(250);
889
+ }
890
+ }
891
+ export function countRichNotebookLMCitations(citations) {
892
+ return citations.filter((citation) => hasNotebookLMCitationSnippet(citation.note)).length;
893
+ }
894
+ export function shouldRetryNotebookLMCitationCapture(citations) {
895
+ if (citations.length < 3) {
896
+ return false;
897
+ }
898
+ return countRichNotebookLMCitations(citations) < Math.min(3, citations.length);
899
+ }
900
+ async function recaptureLatestResponseSnapshot(page) {
901
+ const notebookUrl = page.url();
902
+ if (!normalizeNotebookPath(notebookUrl)?.includes("/notebook/")) {
903
+ return undefined;
904
+ }
905
+ await openNotebookPage(page, notebookUrl);
906
+ await ensureNotebookAccessible(page);
907
+ await waitForNotebookSettled(page);
908
+ await sleep(3_000);
909
+ const latestElement = await waitForLatestVisibleResponse(page, 30_000);
910
+ if (!latestElement) {
911
+ return undefined;
912
+ }
913
+ await waitForStableCitationMetadata(latestElement, 4_000);
914
+ return collectResponseSnapshot(page, latestElement);
915
+ }
916
+ async function collectResponseTextSnapshot(element) {
917
+ return element.evaluate((root, selectors) => {
918
+ const normalize = (value) => (value ?? "").replace(/\u00a0/g, " ").trim();
919
+ const dedupe = (items, key) => {
920
+ const seen = new Set();
921
+ return items.filter((item) => {
922
+ const itemKey = key(item);
923
+ if (!itemKey || seen.has(itemKey)) {
924
+ return false;
925
+ }
926
+ seen.add(itemKey);
927
+ return true;
928
+ });
929
+ };
930
+ const queryAll = (scope, selector) => scope.querySelectorAll ? [...scope.querySelectorAll(selector)] : [];
931
+ const extractMarkerLabels = (scope) => {
932
+ const markerNodes = queryAll(scope, ".citation-marker, .citation-marker [aria-label]");
933
+ const labels = markerNodes
934
+ .map((node) => {
935
+ const elementNode = node;
936
+ const ariaLabel = normalize(elementNode.getAttribute?.("aria-label"));
937
+ const text = normalize(elementNode.innerText || elementNode.textContent || "");
938
+ const numbered = ariaLabel.match(/^(\d+)\s*:/);
939
+ if (numbered?.[1]) {
940
+ return numbered[1];
941
+ }
942
+ if (/^\d+$/.test(text)) {
943
+ return text;
944
+ }
945
+ return "";
946
+ })
947
+ .filter((value, index, array) => value && array.indexOf(value) === index);
948
+ return labels;
949
+ };
950
+ const extractMarkerLabel = (scope) => {
951
+ const ariaLabel = normalize(scope.getAttribute?.("aria-label"));
952
+ const text = normalize(scope.innerText || scope.textContent || "");
953
+ const numbered = ariaLabel.match(/^(\d+)\s*:/);
954
+ if (numbered?.[1]) {
955
+ return numbered[1];
956
+ }
957
+ if (/^\d+$/.test(text)) {
958
+ return text;
959
+ }
960
+ return "";
961
+ };
962
+ const serializeBodyNode = (scope) => {
963
+ if (scope.nodeType === 3) {
964
+ return scope.textContent ?? "";
965
+ }
966
+ if (scope.matches?.(".citation-marker, .citation-marker [aria-label]")) {
967
+ const label = extractMarkerLabel(scope);
968
+ return label ? `[${label}]` : "";
969
+ }
970
+ const childNodes = scope.childNodes ? [...scope.childNodes] : [];
971
+ if (childNodes.length === 0) {
972
+ return scope.textContent ?? "";
973
+ }
974
+ return childNodes.map((child) => serializeBodyNode(child)).join("");
975
+ };
976
+ const bodyTexts = dedupe(selectors.answerBodySelectors.flatMap((selector) => queryAll(root, selector).map((node) => {
977
+ const elementNode = node;
978
+ const text = normalize(serializeBodyNode(elementNode));
979
+ if (!text) {
980
+ return "";
981
+ }
982
+ return text;
983
+ })), (item) => item);
984
+ const rootNode = root;
985
+ return {
986
+ responseText: normalize(rootNode.innerText || rootNode.textContent || ""),
987
+ bodyTexts,
988
+ citationCandidates: []
989
+ };
990
+ }, {
991
+ answerBodySelectors: NOTEBOOKLM_ANSWER_BODY_SELECTORS
992
+ });
993
+ }
994
+ async function collectResponseSnapshot(page, element) {
995
+ const markerAttribute = "data-sourceloop-citation-marker";
996
+ const overflowAttribute = "data-sourceloop-citation-overflow";
997
+ await expandCitationOverflowControls(page, element, overflowAttribute);
998
+ const snapshotElement = (await getLatestResponseElement(page)) ?? element;
999
+ const snapshot = await snapshotElement.evaluate((root, selectors) => {
1000
+ const normalize = (value) => (value ?? "").replace(/\u00a0/g, " ").trim();
1001
+ const dedupe = (items, key) => {
1002
+ const seen = new Set();
1003
+ return items.filter((item) => {
1004
+ const itemKey = key(item);
1005
+ if (!itemKey || seen.has(itemKey)) {
1006
+ return false;
1007
+ }
1008
+ seen.add(itemKey);
1009
+ return true;
1010
+ });
1011
+ };
1012
+ const queryAll = (scope, selector) => scope.querySelectorAll ? [...scope.querySelectorAll(selector)] : [];
1013
+ const extractMarkerLabels = (scope) => {
1014
+ const markerNodes = queryAll(scope, ".citation-marker, .citation-marker [aria-label]");
1015
+ const labels = markerNodes
1016
+ .map((node) => {
1017
+ const elementNode = node;
1018
+ const ariaLabel = normalize(elementNode.getAttribute?.("aria-label"));
1019
+ const text = normalize(elementNode.innerText || elementNode.textContent || "");
1020
+ const numbered = ariaLabel.match(/^(\d+)\s*:/);
1021
+ if (numbered?.[1]) {
1022
+ return numbered[1];
1023
+ }
1024
+ if (/^\d+$/.test(text)) {
1025
+ return text;
1026
+ }
1027
+ return "";
1028
+ })
1029
+ .filter((value, index, array) => value && array.indexOf(value) === index);
1030
+ return labels;
1031
+ };
1032
+ const extractMarkerLabel = (scope) => {
1033
+ const ariaLabel = normalize(scope.getAttribute?.("aria-label"));
1034
+ const text = normalize(scope.innerText || scope.textContent || "");
1035
+ const numbered = ariaLabel.match(/^(\d+)\s*:/);
1036
+ if (numbered?.[1]) {
1037
+ return numbered[1];
1038
+ }
1039
+ if (/^\d+$/.test(text)) {
1040
+ return text;
1041
+ }
1042
+ return "";
1043
+ };
1044
+ const serializeBodyNode = (scope) => {
1045
+ if (scope.nodeType === 3) {
1046
+ return scope.textContent ?? "";
1047
+ }
1048
+ if (scope.matches?.(".citation-marker, .citation-marker [aria-label]")) {
1049
+ const label = extractMarkerLabel(scope);
1050
+ return label ? `[${label}]` : "";
1051
+ }
1052
+ const childNodes = scope.childNodes ? [...scope.childNodes] : [];
1053
+ if (childNodes.length === 0) {
1054
+ return scope.textContent ?? "";
1055
+ }
1056
+ return childNodes.map((child) => serializeBodyNode(child)).join("");
1057
+ };
1058
+ const getFirstMeaningfulDescendantAriaLabel = (scope) => {
1059
+ const ariaNodes = queryAll(scope, "[aria-label]");
1060
+ for (const node of ariaNodes) {
1061
+ const elementNode = node;
1062
+ const ariaLabel = normalize(elementNode.getAttribute?.("aria-label"));
1063
+ if (ariaLabel) {
1064
+ return ariaLabel;
1065
+ }
1066
+ }
1067
+ return "";
1068
+ };
1069
+ const bodyTexts = dedupe(selectors.answerBodySelectors.flatMap((selector) => queryAll(root, selector).map((node) => {
1070
+ const elementNode = node;
1071
+ const text = normalize(serializeBodyNode(elementNode));
1072
+ if (!text) {
1073
+ return "";
1074
+ }
1075
+ return text;
1076
+ })), (item) => item);
1077
+ let markerCounter = 0;
1078
+ const citationCandidates = dedupe(queryAll(root, ".citation-marker")
1079
+ .map((node) => {
1080
+ const markerNode = node;
1081
+ const text = normalize(markerNode.innerText || markerNode.textContent || "");
1082
+ const ariaLabel = normalize(markerNode.getAttribute?.("aria-label")) || getFirstMeaningfulDescendantAriaLabel(markerNode);
1083
+ const dialogLabel = normalize(markerNode.getAttribute?.("dialoglabel"));
1084
+ const triggerDescription = normalize(markerNode.getAttribute?.("triggerdescription"));
1085
+ if (!selectors.citationSelectors.some((selector) => markerNode.matches?.(selector) || queryAll(markerNode, selector).length > 0) ||
1086
+ !(/^\d+$/.test(text) ||
1087
+ /^\d+\s*:/.test(ariaLabel) ||
1088
+ /citation|인용/i.test(`${dialogLabel} ${triggerDescription}`.trim()))) {
1089
+ return undefined;
1090
+ }
1091
+ let markerId = normalize(markerNode.getAttribute?.(selectors.markerAttribute));
1092
+ if (!markerId) {
1093
+ markerCounter += 1;
1094
+ markerId = `marker-${markerCounter}`;
1095
+ markerNode.setAttribute?.(selectors.markerAttribute, markerId);
1096
+ }
1097
+ return {
1098
+ text,
1099
+ ariaLabel,
1100
+ title: normalize(markerNode.getAttribute?.("title") ?? markerNode.title),
1101
+ href: normalize(markerNode.getAttribute?.("href") ?? markerNode.href),
1102
+ markerId,
1103
+ selector: ".citation-marker",
1104
+ dialogLabel,
1105
+ triggerDescription,
1106
+ dataTestId: normalize(markerNode.dataset?.testid),
1107
+ role: normalize(markerNode.getAttribute?.("role")),
1108
+ className: normalize(markerNode.className)
1109
+ };
1110
+ })
1111
+ .filter((candidate) => Boolean(candidate)), (item) => JSON.stringify(item));
1112
+ const rootNode = root;
1113
+ return {
1114
+ responseText: normalize(rootNode.innerText || rootNode.textContent || ""),
1115
+ bodyTexts,
1116
+ citationCandidates
1117
+ };
1118
+ }, {
1119
+ answerBodySelectors: NOTEBOOKLM_ANSWER_BODY_SELECTORS,
1120
+ citationSelectors: NOTEBOOKLM_CITATION_SELECTORS,
1121
+ markerAttribute
1122
+ });
1123
+ try {
1124
+ const popoverTextsByMarkerId = await collectCitationPopoverTexts(page, snapshot.citationCandidates, markerAttribute);
1125
+ return {
1126
+ ...snapshot,
1127
+ citationCandidates: snapshot.citationCandidates.map((candidate) => {
1128
+ const popoverText = candidate.markerId ? popoverTextsByMarkerId.get(candidate.markerId) : undefined;
1129
+ return popoverText
1130
+ ? {
1131
+ ...candidate,
1132
+ popoverText
1133
+ }
1134
+ : candidate;
1135
+ })
1136
+ };
1137
+ }
1138
+ finally {
1139
+ await clearTemporaryCitationMarkers(page, markerAttribute);
1140
+ await clearTemporaryCitationMarkers(page, overflowAttribute);
1141
+ }
1142
+ }
1143
+ async function expandCitationOverflowControls(page, element, overflowAttribute) {
1144
+ const candidates = await element.evaluate((root, selectors) => {
1145
+ const normalize = (value) => (value ?? "").replace(/\u00a0/g, " ").trim();
1146
+ const queryAll = (scope, selector) => scope.querySelectorAll ? [...scope.querySelectorAll(selector)] : [];
1147
+ const collectScopes = (start) => {
1148
+ const scopes = [start];
1149
+ let current = start.parentElement ?? null;
1150
+ let depth = 0;
1151
+ while (current && depth < 3) {
1152
+ scopes.push(current);
1153
+ current = current.parentElement ?? null;
1154
+ depth += 1;
1155
+ }
1156
+ return scopes;
1157
+ };
1158
+ const isCitationAdjacent = (node) => {
1159
+ if (node.closest?.(".citation-marker")) {
1160
+ return true;
1161
+ }
1162
+ let current = node.parentElement ?? null;
1163
+ let depth = 0;
1164
+ while (current && depth < 3) {
1165
+ const nearbyMarkers = queryAll(current, ".citation-marker");
1166
+ if (nearbyMarkers.length > 0) {
1167
+ return true;
1168
+ }
1169
+ current = current.parentElement ?? null;
1170
+ depth += 1;
1171
+ }
1172
+ return false;
1173
+ };
1174
+ const scopes = collectScopes(root);
1175
+ let counter = 0;
1176
+ const seen = new Set();
1177
+ const results = [];
1178
+ for (const scope of scopes) {
1179
+ for (const selector of selectors.overflowSelectors) {
1180
+ for (const node of queryAll(scope, selector)) {
1181
+ const elementNode = node;
1182
+ const overflowId = `overflow-${++counter}`;
1183
+ elementNode.setAttribute?.(selectors.overflowAttribute, overflowId);
1184
+ const candidate = {
1185
+ overflowId,
1186
+ text: normalize(elementNode.innerText || elementNode.textContent || ""),
1187
+ ariaLabel: normalize(elementNode.getAttribute?.("aria-label")),
1188
+ title: normalize(elementNode.getAttribute?.("title") ?? elementNode.title),
1189
+ className: normalize(elementNode.className),
1190
+ dataTestId: normalize(elementNode.dataset?.testid),
1191
+ selector,
1192
+ citationAdjacent: isCitationAdjacent(elementNode)
1193
+ };
1194
+ const key = JSON.stringify(candidate);
1195
+ if (seen.has(key)) {
1196
+ continue;
1197
+ }
1198
+ seen.add(key);
1199
+ results.push(candidate);
1200
+ }
1201
+ }
1202
+ }
1203
+ return results;
1204
+ }, {
1205
+ overflowSelectors: NOTEBOOKLM_CITATION_OVERFLOW_SELECTORS,
1206
+ overflowAttribute
1207
+ });
1208
+ for (const candidate of candidates.filter((item) => shouldExpandCitationOverflowControl(item))) {
1209
+ const locator = page.locator(`[${overflowAttribute}="${escapeAttributeValue(candidate.overflowId)}"]`).first();
1210
+ try {
1211
+ if ((await locator.count()) === 0 || !(await locator.isVisible())) {
1212
+ continue;
1213
+ }
1214
+ await locator.click({ timeout: 1_000 });
1215
+ await sleep(250);
1216
+ }
1217
+ catch { }
1218
+ }
1219
+ }
1220
+ async function collectCitationPopoverTexts(page, candidates, markerAttribute) {
1221
+ const markerIds = [...new Set(candidates.map((candidate) => candidate.markerId).filter((value) => Boolean(value)))];
1222
+ const popoverTextsByMarkerId = new Map();
1223
+ for (const markerId of markerIds) {
1224
+ const markerLocator = page.locator(`[${markerAttribute}="${escapeAttributeValue(markerId)}"]`).first();
1225
+ if ((await markerLocator.count()) === 0) {
1226
+ continue;
1227
+ }
1228
+ try {
1229
+ await markerLocator.scrollIntoViewIfNeeded().catch(() => undefined);
1230
+ await dismissCitationPopover(page);
1231
+ const previousPopoverTexts = await captureVisibleCitationPopoverTexts(page);
1232
+ await markerLocator.click({ timeout: 1_000 });
1233
+ const popoverText = await waitForCitationPopoverText(page, previousPopoverTexts, 2_000);
1234
+ if (popoverText) {
1235
+ popoverTextsByMarkerId.set(markerId, popoverText);
1236
+ }
1237
+ }
1238
+ catch {
1239
+ try {
1240
+ await dismissCitationPopover(page);
1241
+ const previousPopoverTexts = await captureVisibleCitationPopoverTexts(page);
1242
+ await markerLocator.hover({ timeout: 1_000 });
1243
+ const popoverText = await waitForCitationPopoverText(page, previousPopoverTexts, 1_500);
1244
+ if (popoverText) {
1245
+ popoverTextsByMarkerId.set(markerId, popoverText);
1246
+ }
1247
+ }
1248
+ catch { }
1249
+ }
1250
+ finally {
1251
+ await dismissCitationPopover(page);
1252
+ }
1253
+ }
1254
+ return popoverTextsByMarkerId;
1255
+ }
1256
+ async function captureVisibleCitationPopoverTexts(page) {
1257
+ return page.evaluate((selectors) => {
1258
+ const g = globalThis;
1259
+ const normalize = (value) => (value ?? "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
1260
+ const values = selectors.flatMap((selector) => [...g.document.querySelectorAll(selector)].map((node) => {
1261
+ const element = node;
1262
+ const style = g.getComputedStyle(element);
1263
+ const rect = element.getBoundingClientRect();
1264
+ if (style.visibility === "hidden" || style.display === "none" || rect.width === 0 || rect.height === 0) {
1265
+ return "";
1266
+ }
1267
+ return normalize(element.innerText || element.textContent || "");
1268
+ }));
1269
+ return [...new Set(values.filter((value) => value && value !== "인용 세부정보"))];
1270
+ }, NOTEBOOKLM_CITATION_POPOVER_SELECTORS);
1271
+ }
1272
+ async function waitForCitationPopoverText(page, previousTexts, timeoutMs) {
1273
+ const deadline = Date.now() + timeoutMs;
1274
+ const previous = new Set(previousTexts);
1275
+ while (Date.now() < deadline) {
1276
+ const currentTexts = await captureVisibleCitationPopoverTexts(page);
1277
+ const next = currentTexts.find((text) => !previous.has(text));
1278
+ if (next) {
1279
+ return next;
1280
+ }
1281
+ const stable = currentTexts.find((text) => text.length > 0 && text !== "인용 세부정보");
1282
+ if (stable && currentTexts.length === 1 && previousTexts.length === 0) {
1283
+ return stable;
1284
+ }
1285
+ await sleep(150);
1286
+ }
1287
+ return undefined;
1288
+ }
1289
+ async function dismissCitationPopover(page) {
1290
+ await page.keyboard.press("Escape").catch(() => undefined);
1291
+ await sleep(100);
1292
+ }
1293
+ async function clearTemporaryCitationMarkers(page, markerAttribute) {
1294
+ await page
1295
+ .evaluate((attributeName) => {
1296
+ const g = globalThis;
1297
+ for (const node of g.document.querySelectorAll(`[${attributeName}]`)) {
1298
+ node.removeAttribute(attributeName);
1299
+ }
1300
+ }, markerAttribute)
1301
+ .catch(() => undefined);
1302
+ }
1303
+ function escapeAttributeValue(value) {
1304
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1305
+ }
1306
+ function normalizeControlText(value) {
1307
+ return (value ?? "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
1308
+ }
1309
+ async function clearQueryInput(page, selector) {
1310
+ await clearInputLike(page, selector);
1311
+ }
1312
+ async function clearInputLike(page, selector) {
1313
+ await page.click(selector);
1314
+ await page.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A");
1315
+ await page.keyboard.press("Backspace");
1316
+ }
1317
+ async function setQueryInputText(page, selector, text) {
1318
+ await fillInputLike(page, selector, text);
1319
+ }
1320
+ async function fillInputLike(page, selector, text) {
1321
+ const input = page.locator(selector).first();
1322
+ await input.click();
1323
+ try {
1324
+ await input.fill(text);
1325
+ return;
1326
+ }
1327
+ catch { }
1328
+ await page.locator(selector).evaluate((element, value) => {
1329
+ const candidate = element;
1330
+ if ((candidate.tagName?.toLowerCase() !== "textarea" && candidate.tagName?.toLowerCase() !== "input") ||
1331
+ typeof candidate.value !== "string" ||
1332
+ !candidate.dispatchEvent) {
1333
+ throw new Error("NotebookLM input is not a text field.");
1334
+ }
1335
+ candidate.value = value;
1336
+ candidate.dispatchEvent(new Event("input", { bubbles: true }));
1337
+ candidate.dispatchEvent(new Event("change", { bubbles: true }));
1338
+ }, text);
1339
+ }
1340
+ async function submitQuery(page, inputSelector) {
1341
+ if (await clickSubmitButton(page, inputSelector)) {
1342
+ return;
1343
+ }
1344
+ await page.click(inputSelector);
1345
+ await page.keyboard.press("Enter");
1346
+ await sleep(250);
1347
+ await page.keyboard.press(process.platform === "darwin" ? "Meta+Enter" : "Control+Enter");
1348
+ }
1349
+ async function clickSubmitButton(page, inputSelector) {
1350
+ for (const selector of NOTEBOOKLM_SUBMIT_SELECTORS) {
1351
+ const button = page.locator(selector).first();
1352
+ if ((await button.count()) === 0) {
1353
+ continue;
1354
+ }
1355
+ try {
1356
+ if (await button.isVisible()) {
1357
+ await button.click();
1358
+ return true;
1359
+ }
1360
+ }
1361
+ catch { }
1362
+ }
1363
+ const input = page.locator(inputSelector).first();
1364
+ const submitHandle = await input.evaluateHandle((element) => {
1365
+ let current = element;
1366
+ while (current) {
1367
+ const buttons = current.querySelectorAll ? [...current.querySelectorAll("button")] : [];
1368
+ const candidate = buttons.find((button) => {
1369
+ const candidateButton = button;
1370
+ const disabled = candidateButton.hasAttribute?.("disabled") ||
1371
+ candidateButton.getAttribute?.("aria-disabled") === "true";
1372
+ const visible = candidateButton.offsetParent !== null && candidateButton.offsetParent !== undefined;
1373
+ return !disabled && visible;
1374
+ });
1375
+ if (candidate) {
1376
+ return candidate;
1377
+ }
1378
+ current = current.parentElement ?? null;
1379
+ }
1380
+ return null;
1381
+ });
1382
+ const submitButton = submitHandle.asElement();
1383
+ if (!submitButton) {
1384
+ await submitHandle.dispose();
1385
+ return false;
1386
+ }
1387
+ try {
1388
+ await submitButton.click();
1389
+ return true;
1390
+ }
1391
+ catch {
1392
+ return false;
1393
+ }
1394
+ finally {
1395
+ await submitHandle.dispose();
1396
+ }
1397
+ }
1398
+ async function waitForImportFailure(page, timeoutMs) {
1399
+ const deadline = Date.now() + timeoutMs;
1400
+ while (Date.now() < deadline) {
1401
+ for (const selector of NOTEBOOKLM_IMPORT_ERROR_SELECTORS) {
1402
+ const locator = page.locator(selector).first();
1403
+ try {
1404
+ if ((await locator.count()) > 0 && (await locator.isVisible())) {
1405
+ const text = normalizeControlText(await locator.innerText());
1406
+ if (text) {
1407
+ return text;
1408
+ }
1409
+ }
1410
+ }
1411
+ catch { }
1412
+ }
1413
+ await sleep(200);
1414
+ }
1415
+ return undefined;
1416
+ }
1417
+ async function waitForImportOutcome(page, baselineCandidates, baselineSourceCount, input, importSurface, timeoutMs) {
1418
+ const deadline = Date.now() + timeoutMs;
1419
+ while (Date.now() < deadline) {
1420
+ const failureReason = await waitForImportFailure(page, 200);
1421
+ if (failureReason) {
1422
+ return {
1423
+ status: "failed",
1424
+ failureReason
1425
+ };
1426
+ }
1427
+ try {
1428
+ const currentCandidates = await captureImportSuccessCandidates(page);
1429
+ const currentSourceCount = await captureVisibleSourceCount(page).catch(() => undefined);
1430
+ if (didImportProduceNewMatchingCandidate(baselineCandidates, currentCandidates, input) &&
1431
+ (await hasImportSurfaceSettled(page, importSurface))) {
1432
+ return {
1433
+ status: "imported"
1434
+ };
1435
+ }
1436
+ if (didImportProduceNewSourceCandidate(baselineCandidates, currentCandidates) &&
1437
+ (await hasImportSurfaceSettled(page, importSurface))) {
1438
+ return {
1439
+ status: "imported"
1440
+ };
1441
+ }
1442
+ if (didImportIncreaseVisibleSourceCount(baselineSourceCount, currentSourceCount) &&
1443
+ (await hasImportSurfaceSettled(page, importSurface))) {
1444
+ return {
1445
+ status: "imported"
1446
+ };
1447
+ }
1448
+ }
1449
+ catch { }
1450
+ await sleep(150);
1451
+ }
1452
+ return {
1453
+ status: "queued"
1454
+ };
1455
+ }
1456
+ async function waitForImportSuccess(page, baselineCandidates, input, importSurface, timeoutMs) {
1457
+ const deadline = Date.now() + timeoutMs;
1458
+ while (Date.now() < deadline) {
1459
+ try {
1460
+ const currentCandidates = await captureImportSuccessCandidates(page);
1461
+ if (didImportProduceNewMatchingCandidate(baselineCandidates, currentCandidates, input) &&
1462
+ (await hasImportSurfaceSettled(page, importSurface))) {
1463
+ return true;
1464
+ }
1465
+ }
1466
+ catch { }
1467
+ await sleep(400);
1468
+ }
1469
+ return false;
1470
+ }
1471
+ async function hasImportSurfaceSettled(page, importSurface) {
1472
+ if (importSurface === "initial_source_intake") {
1473
+ if (page.url().includes("addSource=true")) {
1474
+ return false;
1475
+ }
1476
+ if (await hasVisibleSelector(page, NOTEBOOKLM_INITIAL_SOURCE_INTAKE_SELECTORS)) {
1477
+ return false;
1478
+ }
1479
+ return true;
1480
+ }
1481
+ return !(await hasVisibleImportDialog(page));
1482
+ }
1483
+ async function hasVisibleImportDialog(page) {
1484
+ return page.evaluate(({ urlSelectors, fileSelectors }) => {
1485
+ const g = globalThis;
1486
+ const selectors = [...urlSelectors, ...fileSelectors];
1487
+ const elements = selectors.flatMap((selector) => Array.from(g.document.querySelectorAll(selector)));
1488
+ return elements.some((element) => {
1489
+ const candidate = element;
1490
+ const style = g.getComputedStyle(candidate);
1491
+ const visible = style.display !== "none" && style.visibility !== "hidden" && candidate.offsetParent !== null;
1492
+ const insideModal = Boolean(candidate.closest('[role="dialog"], dialog, [aria-modal="true"], .cdk-overlay-pane'));
1493
+ return visible && insideModal;
1494
+ });
1495
+ }, {
1496
+ urlSelectors: NOTEBOOKLM_IMPORT_URL_INPUT_SELECTORS,
1497
+ fileSelectors: NOTEBOOKLM_IMPORT_FILE_INPUT_SELECTORS
1498
+ });
1499
+ }
1500
+ async function captureImportSuccessCandidates(page) {
1501
+ return page.evaluate(({ selectors }) => {
1502
+ const g = globalThis;
1503
+ const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
1504
+ const isVisible = (element) => {
1505
+ const style = g.getComputedStyle(element);
1506
+ return style.display !== "none" && style.visibility !== "hidden" && element.offsetParent !== null;
1507
+ };
1508
+ const isInsideModal = (element) => Boolean(element.closest('[role="dialog"], dialog, [aria-modal="true"], .cdk-overlay-pane'));
1509
+ const unique = new Map();
1510
+ const elements = Array.from(g.document.querySelectorAll(selectors.join(",")));
1511
+ for (const element of elements) {
1512
+ if (!isVisible(element) || isInsideModal(element)) {
1513
+ continue;
1514
+ }
1515
+ const text = normalize([
1516
+ element.innerText,
1517
+ element.textContent,
1518
+ element.getAttribute("aria-label"),
1519
+ element.getAttribute("title")
1520
+ ]
1521
+ .filter(Boolean)
1522
+ .join(" "));
1523
+ if (!text) {
1524
+ continue;
1525
+ }
1526
+ const signature = [
1527
+ normalize(element.getAttribute("data-testid")),
1528
+ normalize(element.getAttribute("data-test-id")),
1529
+ normalize(element.getAttribute("aria-label")),
1530
+ element.tagName.toLowerCase(),
1531
+ text
1532
+ ].join("|");
1533
+ if (!unique.has(signature)) {
1534
+ unique.set(signature, {
1535
+ signature,
1536
+ text
1537
+ });
1538
+ }
1539
+ }
1540
+ return Array.from(unique.values());
1541
+ }, {
1542
+ selectors: [...NOTEBOOKLM_IMPORT_SUCCESS_CANDIDATE_SELECTORS]
1543
+ });
1544
+ }
1545
+ async function captureVisibleSourceCount(page) {
1546
+ const bodyText = await page.locator("body").innerText().catch(() => "");
1547
+ return parseNotebookSourceCount(bodyText);
1548
+ }
1549
+ function didImportProduceNewSourceCandidate(baselineCandidates, currentCandidates) {
1550
+ const baselineSignatures = new Set(baselineCandidates.map((candidate) => candidate.signature));
1551
+ return currentCandidates.some((candidate) => !baselineSignatures.has(candidate.signature));
1552
+ }
1553
+ export function parseNotebookSourceCount(text) {
1554
+ const normalized = normalizeControlText(text);
1555
+ if (!normalized) {
1556
+ return undefined;
1557
+ }
1558
+ const patterns = [
1559
+ /\bsources?\s*(\d+)\b/i,
1560
+ /\b(\d+)\s+sources?\b/i,
1561
+ /소스\s*(\d+)\s*개/,
1562
+ /(\d+)\s*개\s*소스/
1563
+ ];
1564
+ for (const pattern of patterns) {
1565
+ const match = normalized.match(pattern);
1566
+ if (match?.[1]) {
1567
+ const parsed = Number.parseInt(match[1], 10);
1568
+ if (Number.isFinite(parsed)) {
1569
+ return parsed;
1570
+ }
1571
+ }
1572
+ }
1573
+ return undefined;
1574
+ }
1575
+ export function didImportIncreaseVisibleSourceCount(baselineSourceCount, currentSourceCount) {
1576
+ if (baselineSourceCount === undefined || currentSourceCount === undefined) {
1577
+ return false;
1578
+ }
1579
+ return currentSourceCount > baselineSourceCount;
1580
+ }
1581
+ export function getManagedImportSuccessNeedles(input) {
1582
+ const values = [input.title, input.sourceUri, input.importKind === "file_upload" ? input.filePath : input.url]
1583
+ .map((value) => normalizeControlText(value).toLowerCase())
1584
+ .filter((value) => Boolean(value));
1585
+ return Array.from(new Set(values));
1586
+ }
1587
+ export function didImportProduceNewMatchingCandidate(baselineCandidates, currentCandidates, input) {
1588
+ const baselineSignatures = new Set(baselineCandidates.map((candidate) => candidate.signature));
1589
+ const needles = getManagedImportSuccessNeedles(input);
1590
+ return currentCandidates.some((candidate) => {
1591
+ if (baselineSignatures.has(candidate.signature)) {
1592
+ return false;
1593
+ }
1594
+ const normalizedText = normalizeControlText(candidate.text);
1595
+ return Boolean(normalizedText) && needles.some((needle) => normalizedText.includes(needle));
1596
+ });
1597
+ }
1598
+ async function sleep(milliseconds) {
1599
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
1600
+ }
1601
+ function formatError(error) {
1602
+ return error instanceof Error ? error.message : String(error);
1603
+ }
1604
+ //# sourceMappingURL=browser-agent.js.map