notebooklm-kit 0.0.1 → 2.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 (100) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +4102 -0
  3. package/dist/src/auth/auth.d.ts +46 -0
  4. package/dist/src/auth/auth.d.ts.map +1 -0
  5. package/dist/src/auth/auth.js +323 -0
  6. package/dist/src/auth/auth.js.map +1 -0
  7. package/dist/src/auth/refresh.d.ts +150 -0
  8. package/dist/src/auth/refresh.d.ts.map +1 -0
  9. package/dist/src/auth/refresh.js +433 -0
  10. package/dist/src/auth/refresh.js.map +1 -0
  11. package/dist/src/client/notebooklm-client.d.ts +372 -0
  12. package/dist/src/client/notebooklm-client.d.ts.map +1 -0
  13. package/dist/src/client/notebooklm-client.js +550 -0
  14. package/dist/src/client/notebooklm-client.js.map +1 -0
  15. package/dist/src/index.d.ts +50 -0
  16. package/dist/src/index.d.ts.map +1 -0
  17. package/dist/src/index.js +45 -0
  18. package/dist/src/index.js.map +1 -0
  19. package/dist/src/rpc/rpc-client.d.ts +48 -0
  20. package/dist/src/rpc/rpc-client.d.ts.map +1 -0
  21. package/dist/src/rpc/rpc-client.js +94 -0
  22. package/dist/src/rpc/rpc-client.js.map +1 -0
  23. package/dist/src/rpc/rpc-methods.d.ts +127 -0
  24. package/dist/src/rpc/rpc-methods.d.ts.map +1 -0
  25. package/dist/src/rpc/rpc-methods.js +169 -0
  26. package/dist/src/rpc/rpc-methods.js.map +1 -0
  27. package/dist/src/services/artifacts.d.ts +1017 -0
  28. package/dist/src/services/artifacts.d.ts.map +1 -0
  29. package/dist/src/services/artifacts.js +5413 -0
  30. package/dist/src/services/artifacts.js.map +1 -0
  31. package/dist/src/services/generation.d.ts +147 -0
  32. package/dist/src/services/generation.d.ts.map +1 -0
  33. package/dist/src/services/generation.js +479 -0
  34. package/dist/src/services/generation.js.map +1 -0
  35. package/dist/src/services/notebook-language.d.ts +109 -0
  36. package/dist/src/services/notebook-language.d.ts.map +1 -0
  37. package/dist/src/services/notebook-language.js +204 -0
  38. package/dist/src/services/notebook-language.js.map +1 -0
  39. package/dist/src/services/notebooks.d.ts +26 -0
  40. package/dist/src/services/notebooks.d.ts.map +1 -0
  41. package/dist/src/services/notebooks.js +539 -0
  42. package/dist/src/services/notebooks.js.map +1 -0
  43. package/dist/src/services/notes.d.ts +72 -0
  44. package/dist/src/services/notes.d.ts.map +1 -0
  45. package/dist/src/services/notes.js +340 -0
  46. package/dist/src/services/notes.js.map +1 -0
  47. package/dist/src/services/sources.d.ts +1085 -0
  48. package/dist/src/services/sources.d.ts.map +1 -0
  49. package/dist/src/services/sources.js +2675 -0
  50. package/dist/src/services/sources.js.map +1 -0
  51. package/dist/src/types/artifact.d.ts +258 -0
  52. package/dist/src/types/artifact.d.ts.map +1 -0
  53. package/dist/src/types/artifact.js +42 -0
  54. package/dist/src/types/artifact.js.map +1 -0
  55. package/dist/src/types/common.d.ts +226 -0
  56. package/dist/src/types/common.d.ts.map +1 -0
  57. package/dist/src/types/common.js +80 -0
  58. package/dist/src/types/common.js.map +1 -0
  59. package/dist/src/types/languages.d.ts +179 -0
  60. package/dist/src/types/languages.d.ts.map +1 -0
  61. package/dist/src/types/languages.js +254 -0
  62. package/dist/src/types/languages.js.map +1 -0
  63. package/dist/src/types/note.d.ts +41 -0
  64. package/dist/src/types/note.d.ts.map +1 -0
  65. package/dist/src/types/note.js +12 -0
  66. package/dist/src/types/note.js.map +1 -0
  67. package/dist/src/types/notebook.d.ts +81 -0
  68. package/dist/src/types/notebook.d.ts.map +1 -0
  69. package/dist/src/types/notebook.js +5 -0
  70. package/dist/src/types/notebook.js.map +1 -0
  71. package/dist/src/types/source.d.ts +241 -0
  72. package/dist/src/types/source.d.ts.map +1 -0
  73. package/dist/src/types/source.js +60 -0
  74. package/dist/src/types/source.js.map +1 -0
  75. package/dist/src/utils/batch-execute.d.ts +58 -0
  76. package/dist/src/utils/batch-execute.d.ts.map +1 -0
  77. package/dist/src/utils/batch-execute.js +398 -0
  78. package/dist/src/utils/batch-execute.js.map +1 -0
  79. package/dist/src/utils/chunked-decoder.d.ts +11 -0
  80. package/dist/src/utils/chunked-decoder.d.ts.map +1 -0
  81. package/dist/src/utils/chunked-decoder.js +326 -0
  82. package/dist/src/utils/chunked-decoder.js.map +1 -0
  83. package/dist/src/utils/chunked-parser.d.ts +61 -0
  84. package/dist/src/utils/chunked-parser.d.ts.map +1 -0
  85. package/dist/src/utils/chunked-parser.js +609 -0
  86. package/dist/src/utils/chunked-parser.js.map +1 -0
  87. package/dist/src/utils/errors.d.ts +58 -0
  88. package/dist/src/utils/errors.d.ts.map +1 -0
  89. package/dist/src/utils/errors.js +357 -0
  90. package/dist/src/utils/errors.js.map +1 -0
  91. package/dist/src/utils/quota.d.ts +213 -0
  92. package/dist/src/utils/quota.d.ts.map +1 -0
  93. package/dist/src/utils/quota.js +518 -0
  94. package/dist/src/utils/quota.js.map +1 -0
  95. package/dist/src/utils/streaming-client.d.ts +129 -0
  96. package/dist/src/utils/streaming-client.d.ts.map +1 -0
  97. package/dist/src/utils/streaming-client.js +559 -0
  98. package/dist/src/utils/streaming-client.js.map +1 -0
  99. package/package.json +85 -7
  100. package/index.js +0 -2
@@ -0,0 +1,2675 @@
1
+ /**
2
+ * Sources service
3
+ * Handles source operations (add URLs, files, text, Google Drive, YouTube, etc.)
4
+ *
5
+ * WORKFLOW USAGE NOTES:
6
+ * - Individual add methods (addFromURL, addFromText, etc.) return immediately after source is queued
7
+ * - Use pollProcessing() to check if sources are ready, or use workflow functions that handle waiting
8
+ * - For web search: Use searchWebAndWait() to get results, then addDiscovered() to add them
9
+ * - For batch operations: Use addBatch() to add multiple sources efficiently
10
+ * - All add methods check quota before adding and record usage after success
11
+ */
12
+ import * as RPC from '../rpc/rpc-methods.js';
13
+ import { ResearchMode, SearchSourceType, SourceType, SourceStatus } from '../types/source.js';
14
+ import { NotebookLMError } from '../types/common.js';
15
+ /**
16
+ * Web search sub-service for sources
17
+ * Handles web search operations (search, wait, get results, add discovered)
18
+ */
19
+ export class WebSearchService {
20
+ rpc;
21
+ quota;
22
+ constructor(rpc, quota) {
23
+ this.rpc = rpc;
24
+ this.quota = quota;
25
+ }
26
+ /**
27
+ * Search web sources (STEP 1 of multi-step workflow)
28
+ *
29
+ * **Use this for multi-step workflows where you want to see results before deciding next steps.**
30
+ *
31
+ * **Multi-Step Workflow (for user decision-making):**
32
+ * 1. `search()` → Returns `sessionId` (start here - this method)
33
+ * - Shows you the search has started
34
+ * - Returns immediately with sessionId
35
+ * 2. `getResults(sessionId)` → Returns discovered sources (you can validate/filter)
36
+ * - Shows you what was found
37
+ * - You can inspect, filter, or select which sources to add
38
+ * 3. `addDiscovered(sessionId, selectedSources)` → Adds your selected sources
39
+ * - You decide which sources from step 2 to actually add
40
+ *
41
+ * **Simple Alternative (RECOMMENDED for automated workflows):**
42
+ * - Use `searchAndWait()` instead - one call, returns all results for validation
43
+ * - Then use `addDiscovered()` to add selected sources
44
+ *
45
+ * **Customization Options:**
46
+ * - `mode: ResearchMode.FAST` - Quick search (default)
47
+ * - `mode: ResearchMode.DEEP` - Comprehensive research (web only)
48
+ * - `sourceType: SearchSourceType.WEB` - Search web (default)
49
+ * - `sourceType: SearchSourceType.GOOGLE_DRIVE` - Search Google Drive (FAST mode only)
50
+ *
51
+ * @param notebookId - The notebook ID
52
+ * @param options - Search options (query, mode, sourceType)
53
+ * @returns sessionId - Required for steps 2 and 3
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // Multi-step: Start search, then check results later
58
+ * const sessionId = await client.sources.add.web.search('notebook-id', {
59
+ * query: 'AI research',
60
+ * mode: ResearchMode.DEEP,
61
+ * });
62
+ *
63
+ * // Later... check what was found
64
+ * const results = await client.sources.add.web.getResults('notebook-id', sessionId);
65
+ * console.log(`Found ${results.web.length} sources`);
66
+ *
67
+ * // User decides which ones to add
68
+ * const selected = results.web.filter(s => s.url.includes('arxiv.org'));
69
+ * await client.sources.add.web.addDiscovered('notebook-id', {
70
+ * sessionId,
71
+ * webSources: selected,
72
+ * });
73
+ * ```
74
+ */
75
+ async search(notebookId, options) {
76
+ const { query, sourceType = SearchSourceType.WEB, mode = ResearchMode.FAST, } = options;
77
+ // Validate: Deep research only works for web sources
78
+ if (mode === ResearchMode.DEEP && sourceType !== SearchSourceType.WEB) {
79
+ throw new NotebookLMError('Deep research mode is only available for web sources');
80
+ }
81
+ // Validate: Drive only supports fast mode
82
+ if (sourceType === SearchSourceType.GOOGLE_DRIVE && mode !== ResearchMode.FAST) {
83
+ throw new NotebookLMError('Google Drive search only supports fast research mode');
84
+ }
85
+ // RPC structure from curl: [["query", sourceType], null, researchMode, notebookId]
86
+ const response = await this.rpc.call(RPC.RPC_SEARCH_WEB_SOURCES, [
87
+ [query, sourceType],
88
+ null,
89
+ mode,
90
+ notebookId,
91
+ ], notebookId);
92
+ // Extract session ID from response
93
+ let data = response;
94
+ if (typeof response === 'string') {
95
+ try {
96
+ data = JSON.parse(response);
97
+ }
98
+ catch (e) {
99
+ // If parsing fails, use response as-is
100
+ }
101
+ }
102
+ // Handle different response formats
103
+ if (Array.isArray(data)) {
104
+ if (data.length > 0 && typeof data[0] === 'string') {
105
+ return data[0];
106
+ }
107
+ if (data.length > 0 && Array.isArray(data[0]) && data[0].length > 0) {
108
+ return data[0][0];
109
+ }
110
+ }
111
+ return data?.sessionId || data?.searchId || (typeof data === 'string' ? data : '');
112
+ }
113
+ /**
114
+ * Search web sources and wait for results (SIMPLE - one call, returns results for validation)
115
+ *
116
+ * **RECOMMENDED FOR SIMPLE WORKFLOWS** - One call, returns all results you can validate.
117
+ *
118
+ * **What it does:**
119
+ * - Starts search, waits for results automatically
120
+ * - Returns all discovered sources once available (or timeout)
121
+ * - Returns results + sessionId for validation
122
+ * - No user decision needed during search - just wait and get results
123
+ *
124
+ * **Simple Workflow:**
125
+ * 1. `searchAndWait()` → Returns results + sessionId (this method - validates results)
126
+ * 2. `addDiscovered(sessionId, selectedSources)` → Add your selected sources
127
+ *
128
+ * **Customization Options:**
129
+ * - `mode: ResearchMode.FAST` - Quick search (default, ~10-30 seconds)
130
+ * - `mode: ResearchMode.DEEP` - Comprehensive research (web only, ~60-120 seconds)
131
+ * - `sourceType: SearchSourceType.WEB` - Search web (default)
132
+ * - `sourceType: SearchSourceType.GOOGLE_DRIVE` - Search Google Drive (FAST mode only)
133
+ * - `timeout` - Max wait time (default: 60000ms = 60 seconds)
134
+ * - `pollInterval` - How often to check for results (default: 2000ms = 2 seconds)
135
+ * - `onProgress` - Callback to track progress
136
+ *
137
+ * **When to use:**
138
+ * - Automated workflows where you don't need to see intermediate steps
139
+ * - Simple cases where you just want to search and get results
140
+ * - When you want to validate all results before deciding which to add
141
+ *
142
+ * **When NOT to use:**
143
+ * - If you need to see results as they come in (use `search()` + `getResults()` instead)
144
+ * - If you want to make decisions during the search process
145
+ *
146
+ * @param notebookId - The notebook ID
147
+ * @param options - Search options with waiting configuration
148
+ * @returns WebSearchResult with sessionId, web sources, and drive sources
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * // Simple: Search and get all results for validation
153
+ * const result = await client.sources.add.web.searchAndWait('notebook-id', {
154
+ * query: 'quantum computing research',
155
+ * mode: ResearchMode.DEEP, // Comprehensive search
156
+ * timeout: 120000, // Wait up to 2 minutes
157
+ * onProgress: (status) => {
158
+ * console.log(`Found ${status.resultCount} results so far...`);
159
+ * },
160
+ * });
161
+ *
162
+ * // Validate results
163
+ * console.log(`Found ${result.web.length} web sources`);
164
+ * console.log(`Found ${result.drive.length} drive sources`);
165
+ *
166
+ * // User decides which to add (or add all)
167
+ * const topSources = result.web.slice(0, 10); // Top 10
168
+ * await client.sources.add.web.addDiscovered('notebook-id', {
169
+ * sessionId: result.sessionId, // Required!
170
+ * webSources: topSources,
171
+ * });
172
+ * ```
173
+ */
174
+ async searchAndWait(notebookId, options) {
175
+ const { query, sourceType = SearchSourceType.WEB, mode = ResearchMode.FAST, timeout = 60000, pollInterval = 2000, onProgress, } = options;
176
+ // Step 1: Initiate search
177
+ const sessionId = await this.search(notebookId, { query, sourceType, mode });
178
+ // Step 2: Poll for results
179
+ const startTime = Date.now();
180
+ let lastResultCount = 0;
181
+ while (Date.now() - startTime < timeout) {
182
+ const results = await this.getResults(notebookId, sessionId);
183
+ const totalCount = results.web.length + results.drive.length;
184
+ // Call progress callback if provided
185
+ if (onProgress) {
186
+ onProgress({
187
+ hasResults: totalCount > 0,
188
+ resultCount: totalCount,
189
+ });
190
+ }
191
+ // If we have results and count hasn't changed, assume search is complete
192
+ if (totalCount > 0 && totalCount === lastResultCount) {
193
+ return { ...results, sessionId };
194
+ }
195
+ lastResultCount = totalCount;
196
+ // Wait before next poll
197
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
198
+ }
199
+ // Timeout - return whatever results we have
200
+ const results = await this.getResults(notebookId, sessionId);
201
+ return { ...results, sessionId };
202
+ }
203
+ /**
204
+ * Get search results (STEP 2 of multi-step workflow - returns results for validation)
205
+ *
206
+ * **REQUIRES:** You must have a `sessionId` from `search()` (step 1) to use this method.
207
+ *
208
+ * **What it does:**
209
+ * - Returns discovered sources from a search session
210
+ * - Shows you what was found so you can validate/filter/select
211
+ * - Returns results immediately (doesn't wait - call multiple times to poll)
212
+ *
213
+ * **Multi-Step Workflow:**
214
+ * 1. `search()` → Returns `sessionId` (step 1)
215
+ * 2. `getResults(sessionId)` → Returns discovered sources (this method - step 2)
216
+ * - **You can validate results here** - see what was found
217
+ * - **You can filter/select** - decide which sources to add
218
+ * - **You can call multiple times** - to poll for more results
219
+ * 3. `addDiscovered(sessionId, selectedSources)` → Add your selected sources (step 3)
220
+ *
221
+ * **Simple Alternative:**
222
+ * - Use `searchAndWait()` to combine steps 1-2 automatically
223
+ *
224
+ * **Usage Patterns:**
225
+ * - Call once after `search()` to get initial results
226
+ * - Call multiple times to poll for more results (results accumulate)
227
+ * - Filter results before passing to `addDiscovered()`
228
+ *
229
+ * @param notebookId - The notebook ID
230
+ * @param sessionId - Session ID from `search()` to filter results (optional - if omitted, returns all results)
231
+ * @returns Discovered sources (web and drive) for validation
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * // Step 1: Start search
236
+ * const sessionId = await client.sources.add.web.search('notebook-id', {
237
+ * query: 'AI research',
238
+ * });
239
+ *
240
+ * // Step 2: Get results (can call multiple times to poll)
241
+ * let results;
242
+ * do {
243
+ * await new Promise(r => setTimeout(r, 2000)); // Wait 2 seconds
244
+ * results = await client.sources.add.web.getResults('notebook-id', sessionId);
245
+ * console.log(`Found ${results.web.length} sources so far...`);
246
+ * } while (results.web.length === 0);
247
+ *
248
+ * // Validate and filter results
249
+ * const relevant = results.web.filter(s =>
250
+ * s.title.includes('machine learning') ||
251
+ * s.url.includes('arxiv.org')
252
+ * );
253
+ *
254
+ * // Step 3: Add selected sources
255
+ * await client.sources.add.web.addDiscovered('notebook-id', {
256
+ * sessionId,
257
+ * webSources: relevant,
258
+ * });
259
+ * ```
260
+ */
261
+ async getResults(notebookId, sessionId) {
262
+ const response = await this.rpc.call(RPC.RPC_GET_SEARCH_RESULTS, [null, null, notebookId], notebookId);
263
+ // Response structure: [[[sessionId, [notebookId, [query, type], mode, [webSources]], ...]]]
264
+ // Example: [[["0057e489-...", ["notebook-id", ["query", 1], 1, [["url", "title", "description", 1], ...]], ...]]]
265
+ // Web sources are at session[1][3] (index 3 of the metadata array, which is the 4th element)
266
+ // Handle JSON string response
267
+ let data = response;
268
+ if (typeof response === 'string') {
269
+ try {
270
+ data = JSON.parse(response);
271
+ }
272
+ catch (e) {
273
+ // If parsing fails, use response as-is
274
+ }
275
+ }
276
+ // Extract the sessions array
277
+ // Response might be: [[sessions]] or [sessions] or sessions
278
+ let sessions = [];
279
+ if (Array.isArray(data)) {
280
+ if (data.length > 0 && Array.isArray(data[0])) {
281
+ // Check if first element is an array of sessions
282
+ if (data[0].length > 0 && Array.isArray(data[0][0])) {
283
+ sessions = data[0]; // [[[session], ...]]
284
+ }
285
+ else {
286
+ sessions = data; // [[session], ...]
287
+ }
288
+ }
289
+ else {
290
+ sessions = data; // [session, ...]
291
+ }
292
+ }
293
+ const web = [];
294
+ const drive = [];
295
+ for (const session of sessions) {
296
+ if (!Array.isArray(session) || session.length < 2) {
297
+ continue;
298
+ }
299
+ // session[0] = sessionId
300
+ // session[1] = [notebookId, [query, type], mode, [webSources]]
301
+ // Example: ["9c40da15-...", ["nit kkr", 1], 1, [[["https://...", "title", ...], ...]]]
302
+ const currentSessionId = session[0];
303
+ // Filter by sessionId if provided (normalize both to strings for comparison)
304
+ if (sessionId) {
305
+ const normalizedSessionId = String(sessionId).trim();
306
+ const normalizedCurrentId = String(currentSessionId || '').trim();
307
+ if (normalizedSessionId && normalizedCurrentId !== normalizedSessionId) {
308
+ continue; // Skip sessions that don't match
309
+ }
310
+ }
311
+ const metadata = session[1];
312
+ if (Array.isArray(metadata) && metadata.length > 3) {
313
+ // Web sources are at metadata[3] (index 3, the 4th element)
314
+ const webSources = metadata[3];
315
+ // Skip if webSources is null (search is still in progress)
316
+ if (webSources === null || webSources === undefined) {
317
+ continue;
318
+ }
319
+ if (Array.isArray(webSources) && webSources.length > 0) {
320
+ // Helper function to recursively flatten arrays until we find source arrays
321
+ const flattenSources = (arr) => {
322
+ const result = [];
323
+ for (const item of arr) {
324
+ if (Array.isArray(item)) {
325
+ // Check if this array looks like a source: [url, title, ...]
326
+ if (item.length >= 2 && typeof item[0] === 'string' && item[0].startsWith('http')) {
327
+ result.push(item);
328
+ }
329
+ else {
330
+ // Recursively flatten nested arrays
331
+ result.push(...flattenSources(item));
332
+ }
333
+ }
334
+ }
335
+ return result;
336
+ };
337
+ // Flatten the webSources array
338
+ const sourcesToProcess = flattenSources(webSources);
339
+ // Process all sources
340
+ for (const source of sourcesToProcess) {
341
+ if (Array.isArray(source) && source.length >= 2) {
342
+ // Format: [url, title, description, typeCode?, ...]
343
+ // Check for type indicator in the array - might be at index 3 or later
344
+ const url = source[0];
345
+ const title = source[1];
346
+ // Check if there's a type code in the array (typically a number)
347
+ // Common positions: index 2 or 3 might contain type info
348
+ let detectedType;
349
+ for (let i = 2; i < Math.min(source.length, 5); i++) {
350
+ const item = source[i];
351
+ // Type codes: 9 = YouTube, 1 = URL, etc.
352
+ if (typeof item === 'number' && item === 9) {
353
+ detectedType = 'youtube';
354
+ break;
355
+ }
356
+ else if (typeof item === 'number' && item === 1) {
357
+ detectedType = 'url';
358
+ break;
359
+ }
360
+ }
361
+ // Only add if URL exists, is a string, and is a valid URL
362
+ if (url && typeof url === 'string' && url.startsWith('http')) {
363
+ web.push({
364
+ url: url,
365
+ title: (typeof title === 'string' ? title : '') || '',
366
+ id: url, // Use URL as ID
367
+ type: detectedType, // Store detected type
368
+ });
369
+ }
370
+ }
371
+ else if (typeof source === 'object' && source && 'url' in source) {
372
+ web.push({
373
+ url: source.url,
374
+ title: source.title || '',
375
+ id: source.id || source.url,
376
+ type: source.type,
377
+ });
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ return { web, drive };
384
+ }
385
+ /**
386
+ * Add discovered sources from search results (final step - adds your selected sources)
387
+ *
388
+ * **REQUIRES:** You must have a `sessionId` from `search()` or `searchAndWait()`.
389
+ *
390
+ * **What it does:**
391
+ * - Adds the sources you selected from search results
392
+ * - You decide which sources to add (from `getResults()` or `searchAndWait()`)
393
+ * - Returns array of added source IDs for validation
394
+ *
395
+ * **Workflow Patterns:**
396
+ *
397
+ * **Simple Pattern:**
398
+ * ```typescript
399
+ * const result = await client.sources.add.web.searchAndWait(...);
400
+ * const addedIds = await client.sources.add.web.addDiscovered('notebook-id', {
401
+ * sessionId: result.sessionId,
402
+ * webSources: result.web, // Add all, or filter first
403
+ * });
404
+ * ```
405
+ *
406
+ * **Multi-Step Pattern:**
407
+ * ```typescript
408
+ * const sessionId = await client.sources.add.web.search(...);
409
+ * const results = await client.sources.add.web.getResults(..., sessionId);
410
+ * const selected = results.web.filter(...); // Your selection logic
411
+ * const addedIds = await client.sources.add.web.addDiscovered('notebook-id', {
412
+ * sessionId,
413
+ * webSources: selected,
414
+ * });
415
+ * ```
416
+ *
417
+ * **Important:**
418
+ * - `sessionId` must match the one from your search (from `search()` or `searchAndWait()`)
419
+ * - You can add web sources, drive sources, or both
420
+ * - Returns source IDs so you can validate what was added
421
+ *
422
+ * @param notebookId - The notebook ID
423
+ * @param options - Session ID and sources to add (web and/or drive)
424
+ * @returns Array of added source IDs (for validation)
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * // After searchAndWait() - add selected sources
429
+ * const result = await client.sources.add.web.searchAndWait('notebook-id', {
430
+ * query: 'research papers',
431
+ * });
432
+ *
433
+ * // Validate results, then add top 5
434
+ * const top5 = result.web.slice(0, 5);
435
+ * const addedIds = await client.sources.add.web.addDiscovered('notebook-id', {
436
+ * sessionId: result.sessionId,
437
+ * webSources: top5,
438
+ * });
439
+ *
440
+ * console.log(`Added ${addedIds.length} sources:`, addedIds);
441
+ * ```
442
+ */
443
+ async addDiscovered(notebookId, options) {
444
+ const { sessionId, webSources = [], driveSources = [] } = options;
445
+ if (webSources.length === 0 && driveSources.length === 0) {
446
+ throw new NotebookLMError('At least one source (web or drive) must be provided');
447
+ }
448
+ // Check quota before adding sources
449
+ const totalSources = webSources.length + driveSources.length;
450
+ for (let i = 0; i < totalSources; i++) {
451
+ this.quota?.checkQuota('addSource', notebookId);
452
+ }
453
+ // Build request structure
454
+ const sourcesToAdd = [];
455
+ // Add web sources
456
+ // Regular URL format: [null, null, [url], null, null, null, null, null, null, null, 1]
457
+ // URL goes at index 2, not index 7 (index 7 is for YouTube)
458
+ for (const webSource of webSources) {
459
+ const url = webSource.url || webSource.id;
460
+ // Use type from response if available, otherwise detect from URL
461
+ const isYouTube = webSource.type === 'youtube' ||
462
+ (url && (url.includes('youtube.com') || url.includes('youtu.be')));
463
+ if (isYouTube) {
464
+ // YouTube format: [null, null, null, null, null, null, null, [youtubeUrl], null, null, 1]
465
+ sourcesToAdd.push([
466
+ null,
467
+ null,
468
+ null,
469
+ null,
470
+ null,
471
+ null,
472
+ null,
473
+ [url],
474
+ null,
475
+ null,
476
+ 1,
477
+ ]);
478
+ }
479
+ else {
480
+ // Regular URL format: [null, null, [url], null, null, null, null, null, null, null, 1]
481
+ sourcesToAdd.push([
482
+ null,
483
+ null,
484
+ [url], // URL at index 2 for regular URLs
485
+ null,
486
+ null,
487
+ null,
488
+ null,
489
+ null,
490
+ null,
491
+ null,
492
+ 1,
493
+ ]);
494
+ }
495
+ }
496
+ // Add drive sources
497
+ for (const driveSource of driveSources) {
498
+ const driveArgs = [driveSource.fileId];
499
+ if (driveSource.mimeType) {
500
+ driveArgs.push(driveSource.mimeType);
501
+ }
502
+ if (driveSource.title) {
503
+ driveArgs.push(driveSource.title);
504
+ }
505
+ sourcesToAdd.push([
506
+ null,
507
+ null,
508
+ null,
509
+ driveArgs,
510
+ 5, // Google Drive source type
511
+ ]);
512
+ }
513
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [sourcesToAdd, notebookId], notebookId);
514
+ // Extract source IDs from response
515
+ // Use same extraction logic as batch() method for consistency
516
+ const sourceIds = [];
517
+ const uuidRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
518
+ const extractIds = (data, depth = 0) => {
519
+ // Prevent infinite recursion
520
+ if (depth > 10) {
521
+ return;
522
+ }
523
+ if (typeof data === 'string') {
524
+ // Try parsing as JSON string first (might be double-encoded)
525
+ if (data.trim().startsWith('[') || data.trim().startsWith('{')) {
526
+ try {
527
+ const parsed = JSON.parse(data);
528
+ extractIds(parsed, depth + 1);
529
+ return;
530
+ }
531
+ catch {
532
+ // Not JSON, continue as regular string
533
+ }
534
+ }
535
+ // Check if it's a UUID
536
+ if (uuidRegex.test(data.trim())) {
537
+ sourceIds.push(data.trim());
538
+ }
539
+ }
540
+ else if (Array.isArray(data)) {
541
+ for (const item of data) {
542
+ extractIds(item, depth + 1);
543
+ }
544
+ }
545
+ else if (data && typeof data === 'object') {
546
+ // Check object values and keys
547
+ for (const key in data) {
548
+ if (uuidRegex.test(key)) {
549
+ sourceIds.push(key);
550
+ }
551
+ extractIds(data[key], depth + 1);
552
+ }
553
+ }
554
+ };
555
+ extractIds(response);
556
+ // Remove duplicates
557
+ const uniqueIds = Array.from(new Set(sourceIds));
558
+ // Limit to expected number of sources (to avoid returning extra IDs from nested structures)
559
+ const expectedCount = totalSources;
560
+ const limitedIds = uniqueIds.slice(0, expectedCount);
561
+ // Record usage after successful addition
562
+ if (limitedIds.length > 0) {
563
+ for (let i = 0; i < limitedIds.length; i++) {
564
+ this.quota?.recordUsage('addSource', notebookId);
565
+ }
566
+ }
567
+ return limitedIds;
568
+ }
569
+ }
570
+ /**
571
+ * Add sources sub-service
572
+ * Handles adding sources of various types (URL, text, file, YouTube, Google Drive, batch)
573
+ */
574
+ export class AddSourcesService {
575
+ rpc;
576
+ quota;
577
+ web;
578
+ constructor(rpc, quota) {
579
+ this.rpc = rpc;
580
+ this.quota = quota;
581
+ this.web = new WebSearchService(rpc, quota);
582
+ }
583
+ // Helper method to extract source ID from response
584
+ extractSourceId(response) {
585
+ try {
586
+ let parsedResponse = response;
587
+ if (typeof response === 'string' && (response.startsWith('[') || response.startsWith('{'))) {
588
+ try {
589
+ parsedResponse = JSON.parse(response);
590
+ }
591
+ catch {
592
+ // If parsing fails, continue with original response
593
+ }
594
+ }
595
+ const findId = (data, depth = 0) => {
596
+ if (depth > 5)
597
+ return null;
598
+ if (typeof data === 'string' && data.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) {
599
+ return data;
600
+ }
601
+ if (typeof data === 'string' && (data.startsWith('[') || data.startsWith('{'))) {
602
+ try {
603
+ const parsed = JSON.parse(data);
604
+ const id = findId(parsed, depth + 1);
605
+ if (id)
606
+ return id;
607
+ }
608
+ catch {
609
+ // Continue searching
610
+ }
611
+ }
612
+ if (Array.isArray(data)) {
613
+ for (const item of data) {
614
+ const id = findId(item, depth + 1);
615
+ if (id)
616
+ return id;
617
+ }
618
+ }
619
+ if (data && typeof data === 'object') {
620
+ for (const key in data) {
621
+ const id = findId(data[key], depth + 1);
622
+ if (id)
623
+ return id;
624
+ }
625
+ }
626
+ return null;
627
+ };
628
+ const sourceId = findId(parsedResponse);
629
+ if (!sourceId) {
630
+ throw new Error('Could not extract source ID from response');
631
+ }
632
+ return sourceId;
633
+ }
634
+ catch (error) {
635
+ throw new NotebookLMError(`Failed to extract source ID: ${error.message}`);
636
+ }
637
+ }
638
+ /**
639
+ * Add a URL source
640
+ */
641
+ async url(notebookId, options) {
642
+ const { url, title } = options;
643
+ if (!url || typeof url !== 'string') {
644
+ throw new NotebookLMError('URL is required and must be a string');
645
+ }
646
+ this.quota?.checkQuota('addSource', notebookId);
647
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
648
+ [
649
+ [
650
+ null,
651
+ null,
652
+ [url],
653
+ null,
654
+ null,
655
+ null,
656
+ null,
657
+ null,
658
+ null,
659
+ null,
660
+ 1,
661
+ ],
662
+ ],
663
+ notebookId,
664
+ ], notebookId);
665
+ const sourceId = this.extractSourceId(response);
666
+ if (sourceId) {
667
+ this.quota?.recordUsage('addSource', notebookId);
668
+ }
669
+ // If a custom title was provided, update the source with it
670
+ // (The API uses the website's title by default, but we can override it)
671
+ if (title && typeof title === 'string' && title.trim().length > 0) {
672
+ try {
673
+ // RPC structure for updating source title: [null, ["sourceId"], [[["title"]]]]
674
+ await this.rpc.call(RPC.RPC_MUTATE_SOURCE, [
675
+ null,
676
+ [sourceId],
677
+ [[[title.trim()]]],
678
+ ], notebookId);
679
+ }
680
+ catch (error) {
681
+ // If update fails, log a warning but don't fail the whole operation
682
+ // The source was still added successfully, just without the custom title
683
+ console.warn(`Warning: Failed to update source title: ${error.message}`);
684
+ }
685
+ }
686
+ return sourceId;
687
+ }
688
+ /**
689
+ * Add a text source
690
+ */
691
+ async text(notebookId, options) {
692
+ const { content, title } = options;
693
+ if (!content || typeof content !== 'string') {
694
+ throw new NotebookLMError('Content is required and must be a string');
695
+ }
696
+ this.quota?.checkQuota('addSource', notebookId);
697
+ // Text format: [null, [title, content], null, 2, ...]
698
+ // Index 1 = [title, content], Index 3 = type code 2
699
+ const textData = title ? [title, content] : [null, content];
700
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
701
+ [
702
+ [
703
+ null,
704
+ textData,
705
+ null,
706
+ 2,
707
+ null,
708
+ null,
709
+ null,
710
+ null,
711
+ null,
712
+ null,
713
+ 1,
714
+ ],
715
+ ],
716
+ notebookId,
717
+ ], notebookId);
718
+ const sourceId = this.extractSourceId(response);
719
+ if (sourceId) {
720
+ this.quota?.recordUsage('addSource', notebookId);
721
+ }
722
+ return sourceId;
723
+ }
724
+ /**
725
+ * Add a file source
726
+ */
727
+ async file(notebookId, options) {
728
+ const { fileName, content } = options;
729
+ if (!fileName || typeof fileName !== 'string') {
730
+ throw new NotebookLMError('File name is required and must be a string');
731
+ }
732
+ if (!content) {
733
+ throw new NotebookLMError('File content is required');
734
+ }
735
+ let base64Content;
736
+ let sizeBytes = 0;
737
+ if (Buffer.isBuffer(content)) {
738
+ sizeBytes = content.length;
739
+ base64Content = content.toString('base64');
740
+ }
741
+ else if (typeof content === 'string') {
742
+ base64Content = content;
743
+ sizeBytes = Math.floor((content.length * 3) / 4);
744
+ }
745
+ else {
746
+ throw new NotebookLMError('Invalid content type for file');
747
+ }
748
+ this.quota?.validateFileSize(sizeBytes);
749
+ this.quota?.checkQuota('addSource', notebookId);
750
+ const response = await this.rpc.call(RPC.RPC_UPLOAD_FILE_BY_FILENAME, [
751
+ [
752
+ [fileName, 13],
753
+ ],
754
+ notebookId,
755
+ [2],
756
+ [1, null, null, null, null, null, null, null, null, null, [1]],
757
+ ], notebookId);
758
+ const sourceId = this.extractSourceId(response);
759
+ if (sourceId) {
760
+ this.quota?.recordUsage('addSource', notebookId);
761
+ }
762
+ return sourceId;
763
+ }
764
+ /**
765
+ * Add a YouTube video source
766
+ */
767
+ async youtube(notebookId, options) {
768
+ const { urlOrId } = options;
769
+ this.quota?.checkQuota('addSource', notebookId);
770
+ const youtubeUrl = this.isYouTubeURL(urlOrId)
771
+ ? urlOrId
772
+ : `https://www.youtube.com/watch?v=${urlOrId}`;
773
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
774
+ [
775
+ [
776
+ null,
777
+ null,
778
+ null,
779
+ null,
780
+ null,
781
+ null,
782
+ null,
783
+ [youtubeUrl],
784
+ null,
785
+ null,
786
+ 1,
787
+ ],
788
+ ],
789
+ notebookId,
790
+ ], notebookId);
791
+ const sourceId = this.extractSourceId(response);
792
+ if (sourceId) {
793
+ this.quota?.recordUsage('addSource', notebookId);
794
+ }
795
+ return sourceId;
796
+ }
797
+ /**
798
+ * Add a Google Drive source
799
+ *
800
+ * @deprecated This method is deprecated. Use `batch()` with `type: 'gdrive'` instead.
801
+ */
802
+ async drive(notebookId, options) {
803
+ console.warn('⚠️ WARNING: sources.add.drive() is deprecated. ' +
804
+ 'Use add.batch() with type: \'gdrive\' instead.');
805
+ const { fileId, mimeType } = options;
806
+ this.quota?.checkQuota('addSource', notebookId);
807
+ // Google Drive format: [fileId, mimeType, 1, title] at index 0
808
+ // Structure: [[fileId, mimeType, 1, title], null, null, null, null, null, null, null, null, null, 1]
809
+ const driveArgs = [fileId];
810
+ if (mimeType) {
811
+ driveArgs.push(mimeType);
812
+ }
813
+ // Always add 1 after fileId (and mimeType if present)
814
+ driveArgs.push(1);
815
+ if (options.title) {
816
+ driveArgs.push(options.title);
817
+ }
818
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
819
+ [
820
+ [
821
+ driveArgs, // Index 0: [fileId, mimeType?, 1, title?]
822
+ null, // Index 1
823
+ null, // Index 2
824
+ null, // Index 3
825
+ null, // Index 4
826
+ null, // Index 5
827
+ null, // Index 6
828
+ null, // Index 7
829
+ null, // Index 8
830
+ null, // Index 9
831
+ 1, // Index 10
832
+ ],
833
+ ],
834
+ notebookId,
835
+ ], notebookId);
836
+ const sourceId = this.extractSourceId(response);
837
+ if (sourceId) {
838
+ this.quota?.recordUsage('addSource', notebookId);
839
+ }
840
+ return sourceId;
841
+ }
842
+ /**
843
+ * Add multiple sources in a batch
844
+ */
845
+ async batch(notebookId, options) {
846
+ const { sources } = options;
847
+ if (!sources || sources.length === 0) {
848
+ throw new NotebookLMError('At least one source is required for batch add');
849
+ }
850
+ // Check quota for all sources
851
+ for (let i = 0; i < sources.length; i++) {
852
+ this.quota?.checkQuota('addSource', notebookId);
853
+ }
854
+ const sourcesToAdd = [];
855
+ for (const source of sources) {
856
+ if (source.type === 'url') {
857
+ // Always treat as regular URL when type is explicitly 'url'
858
+ // Regular URL format - URL at index 2
859
+ sourcesToAdd.push([
860
+ null,
861
+ null,
862
+ [source.url],
863
+ null,
864
+ null,
865
+ null,
866
+ null,
867
+ null,
868
+ null,
869
+ null,
870
+ 1,
871
+ ]);
872
+ }
873
+ else if (source.type === 'text') {
874
+ // Text format: [null, [title, content], null, 2, ...]
875
+ // Index 1 = [title, content], Index 3 = type code 2
876
+ const textData = source.title ? [source.title, source.content] : [null, source.content];
877
+ sourcesToAdd.push([
878
+ null,
879
+ textData,
880
+ null,
881
+ 2,
882
+ null,
883
+ null,
884
+ null,
885
+ null,
886
+ null,
887
+ null,
888
+ 1,
889
+ ]);
890
+ }
891
+ else if (source.type === 'gdrive') {
892
+ // Google Drive format: [fileId, mimeType, 1, title] at index 0
893
+ // Structure: [[fileId, mimeType, 1, title], null, null, null, null, null, null, null, null, null, 1]
894
+ // Based on curl: ["1kVJu1NZmhCHoQRWS1RmldOkC4n4f6WNST_N4upJuba4","application/vnd.google-apps.document",1,"Test Document"]
895
+ // Always include all 4 elements: fileId, mimeType (or null), 1, title (or null)
896
+ const driveArgs = [
897
+ source.fileId,
898
+ source.mimeType || null, // Always include mimeType position (null if not provided)
899
+ 1,
900
+ source.title || null, // Always include title position (null if not provided)
901
+ ];
902
+ sourcesToAdd.push([
903
+ driveArgs, // Index 0: [fileId, mimeType, 1, title]
904
+ null, // Index 1
905
+ null, // Index 2
906
+ null, // Index 3
907
+ null, // Index 4
908
+ null, // Index 5
909
+ null, // Index 6
910
+ null, // Index 7
911
+ null, // Index 8
912
+ null, // Index 9
913
+ 1, // Index 10
914
+ ]);
915
+ }
916
+ else if (source.type === 'youtube') {
917
+ const youtubeUrl = this.isYouTubeURL(source.urlOrId)
918
+ ? source.urlOrId
919
+ : `https://www.youtube.com/watch?v=${source.urlOrId}`;
920
+ sourcesToAdd.push([
921
+ null,
922
+ null,
923
+ null,
924
+ null,
925
+ null,
926
+ null,
927
+ null,
928
+ [youtubeUrl],
929
+ null,
930
+ null,
931
+ 1,
932
+ ]);
933
+ }
934
+ }
935
+ // Check if all sources are Google Drive - they require a different RPC structure
936
+ const allGDrive = sources.every(s => s.type === 'gdrive');
937
+ let rpcArgs;
938
+ if (allGDrive) {
939
+ // Google Drive sources require extended RPC structure:
940
+ // Wrap the entire sourcesToAdd array in an extra array layer
941
+ // [[sourcesToAdd], notebookId, [2], [1, null, ..., null, [1]]]
942
+ // Based on curl: [[source1, source2, source3]], notebookId, [2], [1, null, ..., null, [1]]
943
+ rpcArgs = [
944
+ [sourcesToAdd], // Wrap sourcesToAdd in an extra array layer
945
+ notebookId,
946
+ [2],
947
+ [1, null, null, null, null, null, null, null, null, null, [1]]
948
+ ];
949
+ }
950
+ else {
951
+ // For mixed or non-GDrive sources, use standard structure
952
+ rpcArgs = [sourcesToAdd, notebookId];
953
+ }
954
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, rpcArgs, notebookId);
955
+ // Extract all source IDs from response
956
+ // Response can be:
957
+ // - Array of arrays: [[id1], [id2], ...]
958
+ // - Array of IDs: [id1, id2, ...]
959
+ // - Single ID string
960
+ // - Nested structure
961
+ const sourceIds = [];
962
+ const uuidRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
963
+ const extractIds = (data, depth = 0) => {
964
+ if (depth > 10)
965
+ return; // Prevent infinite recursion
966
+ if (typeof data === 'string') {
967
+ // Try parsing JSON string
968
+ if ((data.startsWith('[') || data.startsWith('{'))) {
969
+ try {
970
+ const parsed = JSON.parse(data);
971
+ extractIds(parsed, depth + 1);
972
+ return;
973
+ }
974
+ catch {
975
+ // Not JSON, continue as regular string
976
+ }
977
+ }
978
+ // Check if it's a UUID
979
+ if (uuidRegex.test(data.trim())) {
980
+ sourceIds.push(data.trim());
981
+ }
982
+ }
983
+ else if (Array.isArray(data)) {
984
+ for (const item of data) {
985
+ extractIds(item, depth + 1);
986
+ }
987
+ }
988
+ else if (data && typeof data === 'object') {
989
+ // Check object values and keys
990
+ for (const key in data) {
991
+ if (uuidRegex.test(key)) {
992
+ sourceIds.push(key);
993
+ }
994
+ extractIds(data[key], depth + 1);
995
+ }
996
+ }
997
+ };
998
+ extractIds(response);
999
+ // Remove duplicates
1000
+ const uniqueIds = Array.from(new Set(sourceIds));
1001
+ // Limit to expected number of sources (to avoid returning extra IDs from nested structures)
1002
+ // Since sources are added in order, take the first N where N = number of sources added
1003
+ const expectedCount = sources.length;
1004
+ const limitedIds = uniqueIds.slice(0, expectedCount);
1005
+ // Record usage after successful addition
1006
+ if (limitedIds.length > 0) {
1007
+ for (let i = 0; i < limitedIds.length; i++) {
1008
+ this.quota?.recordUsage('addSource', notebookId);
1009
+ }
1010
+ }
1011
+ return limitedIds;
1012
+ }
1013
+ isYouTubeURL(url) {
1014
+ return url.includes('youtube.com') || url.includes('youtu.be');
1015
+ }
1016
+ }
1017
+ /**
1018
+ * Service for source operations
1019
+ */
1020
+ export class SourcesService {
1021
+ rpc;
1022
+ quota;
1023
+ add;
1024
+ constructor(rpc, quota) {
1025
+ this.rpc = rpc;
1026
+ this.quota = quota;
1027
+ this.add = new AddSourcesService(rpc, quota);
1028
+ }
1029
+ // ========================================================================
1030
+ // Source Listing Methods
1031
+ // ========================================================================
1032
+ /**
1033
+ * List all sources in a notebook
1034
+ *
1035
+ * **What it does:** Retrieves a list of all sources (URLs, text, files, YouTube videos,
1036
+ * Google Drive files, etc.) associated with a notebook.
1037
+ *
1038
+ * **Input:**
1039
+ * - `notebookId` (string, required): The ID of the notebook to list sources from
1040
+ *
1041
+ * **Output:** Returns an array of `Source` objects, each containing:
1042
+ * - `sourceId`: Unique identifier for the source
1043
+ * - `title`: Source title/name
1044
+ * - `type`: Source type (URL, TEXT, FILE, YOUTUBE_VIDEO, GOOGLE_DRIVE, etc.)
1045
+ * - `url`: Source URL (for URL/YouTube sources)
1046
+ * - `createdAt`: Creation timestamp
1047
+ * - `updatedAt`: Last modified timestamp
1048
+ * - `status`: Processing status (PROCESSING, READY, FAILED)
1049
+ * - `metadata`: Additional metadata (file size, MIME type, etc.)
1050
+ *
1051
+ * **Note:**
1052
+ * - Sources are extracted from the notebook response (same RPC as `notebooks.get()`)
1053
+ * - This method efficiently reuses the notebook data without requiring a separate RPC call
1054
+ * - Processing status is inferred from the source metadata
1055
+ *
1056
+ * @param notebookId - The notebook ID
1057
+ *
1058
+ * @example
1059
+ * ```typescript
1060
+ * // List all sources
1061
+ * const sources = await client.sources.list('notebook-id');
1062
+ * console.log(`Found ${sources.length} sources`);
1063
+ *
1064
+ * // Filter by type
1065
+ * const pdfs = sources.filter(s => s.type === SourceType.FILE);
1066
+ * const urls = sources.filter(s => s.type === SourceType.URL);
1067
+ *
1068
+ * // Check processing status
1069
+ * const ready = sources.filter(s => s.status === SourceStatus.READY);
1070
+ * const processing = sources.filter(s => s.status === SourceStatus.PROCESSING);
1071
+ *
1072
+ * // Get source details
1073
+ * sources.forEach(source => {
1074
+ * console.log(`${source.title} (${source.type}) - ${source.status}`);
1075
+ * });
1076
+ * ```
1077
+ */
1078
+ async list(notebookId) {
1079
+ if (!notebookId || typeof notebookId !== 'string') {
1080
+ throw new NotebookLMError('Invalid notebook ID format');
1081
+ }
1082
+ // Call RPC_GET_PROJECT to get notebook data (includes sources)
1083
+ const response = await this.rpc.call(RPC.RPC_GET_PROJECT, [notebookId, null, [2], null, 0], notebookId);
1084
+ return this.parseSourcesFromResponse(response);
1085
+ }
1086
+ /**
1087
+ * Parse sources from notebook response
1088
+ *
1089
+ * Source structure in response:
1090
+ * [
1091
+ * ["source-id"],
1092
+ * "filename.pdf",
1093
+ * [null, fileSize, [timestamp], ["processed-id", timestamp], type_code, null, 1],
1094
+ * [null, 2]
1095
+ * ]
1096
+ *
1097
+ * Type codes:
1098
+ * - 1 = Google Drive
1099
+ * - 2 = Text
1100
+ * - 3 = File/PDF
1101
+ * - 4 = Text note
1102
+ * - 5 = URL
1103
+ * - 8 = Mind map note
1104
+ * - 9 = YouTube
1105
+ * - 10 = Video file
1106
+ * - 13 = Image
1107
+ * - 14 = PDF from Drive
1108
+ */
1109
+ parseSourcesFromResponse(response) {
1110
+ try {
1111
+ let parsedResponse = response;
1112
+ if (typeof response === 'string') {
1113
+ parsedResponse = JSON.parse(response);
1114
+ }
1115
+ if (!Array.isArray(parsedResponse) || parsedResponse.length === 0) {
1116
+ return [];
1117
+ }
1118
+ let data = parsedResponse;
1119
+ // Handle nested array structure
1120
+ if (Array.isArray(parsedResponse[0])) {
1121
+ data = parsedResponse[0];
1122
+ }
1123
+ // Sources are in data[1]
1124
+ if (!Array.isArray(data[1])) {
1125
+ return [];
1126
+ }
1127
+ const sources = [];
1128
+ for (const sourceData of data[1]) {
1129
+ if (!Array.isArray(sourceData) || sourceData.length === 0) {
1130
+ continue;
1131
+ }
1132
+ // Extract source ID from [0][0]
1133
+ let sourceId;
1134
+ if (Array.isArray(sourceData[0]) && sourceData[0].length > 0) {
1135
+ sourceId = sourceData[0][0];
1136
+ }
1137
+ else if (typeof sourceData[0] === 'string') {
1138
+ sourceId = sourceData[0];
1139
+ }
1140
+ if (!sourceId || typeof sourceId !== 'string') {
1141
+ continue;
1142
+ }
1143
+ // Extract title from [1]
1144
+ const title = typeof sourceData[1] === 'string' ? sourceData[1] : 'Untitled';
1145
+ // Extract metadata from [2]
1146
+ const metadata = Array.isArray(sourceData[2]) ? sourceData[2] : [];
1147
+ // Parse type code from metadata[4]
1148
+ const typeCode = metadata[4];
1149
+ const sourceType = this.mapTypeCodeToSourceType(typeCode);
1150
+ // Parse timestamps
1151
+ let createdAt;
1152
+ let updatedAt;
1153
+ // Creation timestamp from metadata[2] = [seconds, nanoseconds]
1154
+ if (Array.isArray(metadata[2]) && metadata[2].length >= 2) {
1155
+ const [seconds, nanoseconds] = metadata[2];
1156
+ if (typeof seconds === 'number') {
1157
+ const timestamp = seconds * 1000 + (nanoseconds || 0) / 1000000;
1158
+ createdAt = new Date(timestamp).toISOString();
1159
+ }
1160
+ }
1161
+ // Updated/processed timestamp from metadata[3] = ["processed-id", timestamp]
1162
+ if (Array.isArray(metadata[3]) && metadata[3].length >= 2) {
1163
+ const timestamp = metadata[3][1];
1164
+ if (Array.isArray(timestamp) && timestamp.length >= 2) {
1165
+ const [seconds, nanoseconds] = timestamp;
1166
+ if (typeof seconds === 'number') {
1167
+ const ts = seconds * 1000 + (nanoseconds || 0) / 1000000;
1168
+ updatedAt = new Date(ts).toISOString();
1169
+ }
1170
+ }
1171
+ else if (typeof timestamp === 'number') {
1172
+ updatedAt = new Date(timestamp).toISOString();
1173
+ }
1174
+ }
1175
+ // If no updatedAt, use createdAt
1176
+ if (!updatedAt && createdAt) {
1177
+ updatedAt = createdAt;
1178
+ }
1179
+ // Parse URL (for URL/YouTube sources) from metadata[6] or title
1180
+ let url;
1181
+ if (sourceType === SourceType.URL || sourceType === SourceType.YOUTUBE_VIDEO) {
1182
+ // Check metadata[6] for URL array
1183
+ if (Array.isArray(metadata[6]) && metadata[6].length > 0) {
1184
+ url = metadata[6][0];
1185
+ }
1186
+ else if (title.startsWith('http://') || title.startsWith('https://')) {
1187
+ url = title;
1188
+ }
1189
+ }
1190
+ // Parse file size from metadata[1]
1191
+ const fileSize = typeof metadata[1] === 'number' ? metadata[1] : undefined;
1192
+ // Determine processing status
1193
+ // If metadata[3] exists (processed info), source is likely ready
1194
+ // If metadata[6] === 1, source is processed
1195
+ let status = SourceStatus.UNKNOWN;
1196
+ if (metadata[3] && Array.isArray(metadata[3]) && metadata[3].length > 0) {
1197
+ status = SourceStatus.READY;
1198
+ }
1199
+ else if (metadata[6] === 1) {
1200
+ status = SourceStatus.READY;
1201
+ }
1202
+ else if (sourceId && !metadata[3]) {
1203
+ // Has ID but no processed info - might be processing
1204
+ status = SourceStatus.PROCESSING;
1205
+ }
1206
+ // Build metadata object
1207
+ const sourceMetadata = {};
1208
+ if (fileSize !== undefined) {
1209
+ sourceMetadata.fileSize = fileSize;
1210
+ }
1211
+ // Extract MIME type or additional info if available
1212
+ if (metadata.length > 7 && Array.isArray(metadata[7])) {
1213
+ // Sometimes additional metadata is in nested arrays
1214
+ if (metadata[7].length > 2 && typeof metadata[7][2] === 'string') {
1215
+ sourceMetadata.mimeType = metadata[7][2];
1216
+ }
1217
+ }
1218
+ sources.push({
1219
+ sourceId,
1220
+ title,
1221
+ type: sourceType,
1222
+ url,
1223
+ createdAt,
1224
+ updatedAt,
1225
+ status,
1226
+ metadata: Object.keys(sourceMetadata).length > 0 ? sourceMetadata : undefined,
1227
+ });
1228
+ }
1229
+ return sources;
1230
+ }
1231
+ catch (error) {
1232
+ throw new NotebookLMError(`Failed to parse sources: ${error.message}`);
1233
+ }
1234
+ }
1235
+ /**
1236
+ * Map type code from API response to SourceType enum
1237
+ *
1238
+ * **What this does:**
1239
+ * The NotebookLM API returns sources with numeric type codes in the metadata array.
1240
+ * This function converts those internal API codes to our public SourceType enum.
1241
+ *
1242
+ * **Where the type code comes from:**
1243
+ * In the API response, each source has this structure:
1244
+ * ```
1245
+ * [
1246
+ * ["source-id"], // [0] = source ID
1247
+ * "filename.pdf", // [1] = title
1248
+ * [null, 602, [...], [...], 3, ...], // [2] = metadata array
1249
+ * // [2][4] = type code (the number we map)
1250
+ * ]
1251
+ * ```
1252
+ *
1253
+ * **Type Code Examples from Real API Responses:**
1254
+ * - `3` = PDF file: `["fnz offer.pdf", [null, 602, [...], [...], 3, ...]`
1255
+ * - `5` = URL: `["AI SDK", [null, 571, [...], [...], 5, ..., ["https://ai-sdk.dev/"]]`
1256
+ * - `9` = YouTube: `["Building an iMessage AI Chatbot", [null, 793, [...], [...], 9, [...]]`
1257
+ * - `4` = Text note: `["A Pussycat's Discourse", [null, 1, [...], [...], 4, ...]`
1258
+ * - `1` = Google Drive: `["offer_letter_photon", [[...], 492, [...], [...], 1, ...]`
1259
+ * - `13` = Image: `["Screenshot 2025-12-28.png", [null, 0, [...], [...], 13, ...]`
1260
+ *
1261
+ * **Mapping:**
1262
+ * Each API type code maps to a specific SourceType enum value to preserve the distinction
1263
+ * between different file types (PDF, video, image, etc.).
1264
+ *
1265
+ * @param typeCode - The numeric type code from API response metadata[4]
1266
+ * @returns The corresponding SourceType enum value
1267
+ */
1268
+ mapTypeCodeToSourceType(typeCode) {
1269
+ if (typeof typeCode !== 'number') {
1270
+ return SourceType.UNKNOWN;
1271
+ }
1272
+ switch (typeCode) {
1273
+ case 1:
1274
+ return SourceType.GOOGLE_DRIVE; // Google Drive file
1275
+ case 2:
1276
+ return SourceType.TEXT; // Regular text source
1277
+ case 3:
1278
+ return SourceType.PDF; // PDF file
1279
+ case 4:
1280
+ return SourceType.TEXT_NOTE; // Text note
1281
+ case 5:
1282
+ return SourceType.URL; // Web URL
1283
+ case 8:
1284
+ return SourceType.MIND_MAP_NOTE; // Mind map note
1285
+ case 9:
1286
+ return SourceType.YOUTUBE_VIDEO; // YouTube video
1287
+ case 10:
1288
+ return SourceType.VIDEO_FILE; // Video file (uploaded)
1289
+ case 13:
1290
+ return SourceType.IMAGE; // Image file
1291
+ case 14:
1292
+ return SourceType.PDF_FROM_DRIVE; // PDF from Google Drive
1293
+ default:
1294
+ return SourceType.UNKNOWN; // Unknown/unsupported type
1295
+ }
1296
+ }
1297
+ /**
1298
+ * Get source(s) from a notebook
1299
+ *
1300
+ * **What it does:**
1301
+ * - If `sourceId` is provided: Returns a single source by ID
1302
+ * - If `sourceId` is not provided: Returns all sources (same as `list()`)
1303
+ *
1304
+ * **Input:**
1305
+ * - `notebookId` (string, required): The notebook ID
1306
+ * - `sourceId` (string, optional): The source ID to get. If omitted, returns all sources.
1307
+ *
1308
+ * **Output:**
1309
+ * - If `sourceId` provided: Returns a single `Source` object
1310
+ * - If `sourceId` omitted: Returns an array of `Source` objects
1311
+ *
1312
+ * @param notebookId - The notebook ID
1313
+ * @param sourceId - Optional source ID to get a single source
1314
+ *
1315
+ * @example
1316
+ * ```typescript
1317
+ * // Get all sources
1318
+ * const allSources = await client.sources.get('notebook-id');
1319
+ *
1320
+ * // Get a specific source
1321
+ * const source = await client.sources.get('notebook-id', 'source-id');
1322
+ * console.log(source.title);
1323
+ * ```
1324
+ */
1325
+ async get(notebookId, sourceId) {
1326
+ if (!notebookId || typeof notebookId !== 'string') {
1327
+ throw new NotebookLMError('Invalid notebook ID format');
1328
+ }
1329
+ // Get all sources
1330
+ const allSources = await this.list(notebookId);
1331
+ // If sourceId provided, return single source
1332
+ if (sourceId) {
1333
+ const source = allSources.find(s => s.sourceId === sourceId);
1334
+ if (!source) {
1335
+ throw new NotebookLMError(`Source not found: ${sourceId}`);
1336
+ }
1337
+ return source;
1338
+ }
1339
+ // Return all sources
1340
+ return allSources;
1341
+ }
1342
+ // ========================================================================
1343
+ // Individual Source Addition Methods (DEPRECATED - Use sources.add.* instead)
1344
+ // ========================================================================
1345
+ /**
1346
+ * Add a source from URL
1347
+ *
1348
+ * WORKFLOW USAGE:
1349
+ * - Returns immediately after source is queued (does not wait for processing)
1350
+ * - Use pollProcessing() to check if source is ready
1351
+ * - Or use workflow functions like addSourceAndWait() that handle waiting automatically
1352
+ * - Automatically detects YouTube URLs and routes to addYouTube()
1353
+ *
1354
+ * @param notebookId - The notebook ID
1355
+ * @param options - URL and optional title
1356
+ *
1357
+ * @example
1358
+ * ```typescript
1359
+ * // Add a regular URL
1360
+ * const sourceId = await client.sources.addFromURL('notebook-id', {
1361
+ * url: 'https://example.com/article',
1362
+ * });
1363
+ *
1364
+ * // Check if ready (manual polling)
1365
+ * let status;
1366
+ * do {
1367
+ * status = await client.sources.pollProcessing('notebook-id');
1368
+ * await new Promise(r => setTimeout(r, 2000));
1369
+ * } while (!status.allReady);
1370
+ * ```
1371
+ */
1372
+ async addFromURL(notebookId, options) {
1373
+ const { url } = options;
1374
+ // Check quota before adding source
1375
+ this.quota?.checkQuota('addSource', notebookId);
1376
+ // Check if it's a YouTube URL
1377
+ if (this.isYouTubeURL(url)) {
1378
+ return this.addYouTube(notebookId, { urlOrId: url });
1379
+ }
1380
+ // Regular URL
1381
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
1382
+ [
1383
+ [
1384
+ null,
1385
+ null,
1386
+ [url],
1387
+ ],
1388
+ ],
1389
+ notebookId,
1390
+ ], notebookId);
1391
+ const sourceId = this.extractSourceId(response);
1392
+ // Record usage after successful addition
1393
+ if (sourceId) {
1394
+ this.quota?.recordUsage('addSource', notebookId);
1395
+ }
1396
+ return sourceId;
1397
+ }
1398
+ /**
1399
+ * Add a source from text (copied text)
1400
+ *
1401
+ * WORKFLOW USAGE:
1402
+ * - Returns immediately after source is queued
1403
+ * - Use pollProcessing() to check if source is ready
1404
+ * - Or use workflow functions that handle waiting automatically
1405
+ *
1406
+ * @param notebookId - The notebook ID
1407
+ * @param options - Text content and title
1408
+ *
1409
+ * @example
1410
+ * ```typescript
1411
+ * const sourceId = await client.sources.addFromText('notebook-id', {
1412
+ * title: 'My Notes',
1413
+ * content: 'This is my research content...',
1414
+ * });
1415
+ * ```
1416
+ */
1417
+ async addFromText(notebookId, options) {
1418
+ const { title, content } = options;
1419
+ // Validate text length
1420
+ this.quota?.validateTextSource(content);
1421
+ // Check quota before adding source
1422
+ this.quota?.checkQuota('addSource', notebookId);
1423
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
1424
+ [
1425
+ [
1426
+ null,
1427
+ [title, content],
1428
+ null,
1429
+ 2, // text source type
1430
+ ],
1431
+ ],
1432
+ notebookId,
1433
+ ], notebookId);
1434
+ const sourceId = this.extractSourceId(response);
1435
+ // Record usage after successful addition
1436
+ if (sourceId) {
1437
+ this.quota?.recordUsage('addSource', notebookId);
1438
+ }
1439
+ return sourceId;
1440
+ }
1441
+ /**
1442
+ * Add a source from uploaded file
1443
+ *
1444
+ * WORKFLOW USAGE:
1445
+ * - Returns immediately after file is uploaded and queued
1446
+ * - File processing may take longer than URLs/text
1447
+ * - Use pollProcessing() to check if source is ready
1448
+ * - Or use workflow functions that handle waiting automatically
1449
+ *
1450
+ * @param notebookId - The notebook ID
1451
+ * @param options - File content, name, and MIME type
1452
+ *
1453
+ * @example
1454
+ * ```typescript
1455
+ * // From Buffer (Node.js)
1456
+ * const fileBuffer = await fs.readFile('document.pdf');
1457
+ * const sourceId = await client.sources.addFromFile('notebook-id', {
1458
+ * content: fileBuffer,
1459
+ * fileName: 'document.pdf',
1460
+ * mimeType: 'application/pdf',
1461
+ * });
1462
+ *
1463
+ * // From base64 string
1464
+ * const sourceId = await client.sources.addFromFile('notebook-id', {
1465
+ * content: base64String,
1466
+ * fileName: 'document.pdf',
1467
+ * });
1468
+ * ```
1469
+ */
1470
+ async addFromFile(notebookId, options) {
1471
+ const { content, fileName, mimeType = 'application/octet-stream' } = options;
1472
+ // Validate file size and check quota
1473
+ let sizeBytes;
1474
+ let base64Content;
1475
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(content)) {
1476
+ sizeBytes = content.length;
1477
+ base64Content = content.toString('base64');
1478
+ }
1479
+ else if (typeof content === 'string') {
1480
+ // Already base64
1481
+ base64Content = content;
1482
+ // Estimate size from base64 (base64 is ~33% larger than binary)
1483
+ sizeBytes = Math.floor((content.length * 3) / 4);
1484
+ }
1485
+ else {
1486
+ throw new NotebookLMError('Invalid content type for file');
1487
+ }
1488
+ this.quota?.validateFileSize(sizeBytes);
1489
+ this.quota?.checkQuota('addSource', notebookId);
1490
+ // Files use o4cbdc RPC with structure: [[[fileName, 13]], notebookId, [2], [1,null,...,[1]]]
1491
+ // The number 13 is the file type indicator
1492
+ // Note: Files may need to be uploaded via a separate endpoint first, then referenced by filename
1493
+ // For now, we'll try using o4cbdc with just the filename
1494
+ const response = await this.rpc.call(RPC.RPC_UPLOAD_FILE_BY_FILENAME, [
1495
+ [
1496
+ [fileName, 13], // [filename, fileType] where 13 = file upload
1497
+ ],
1498
+ notebookId,
1499
+ [2],
1500
+ [1, null, null, null, null, null, null, null, null, null, [1]],
1501
+ ], notebookId);
1502
+ const sourceId = this.extractSourceId(response);
1503
+ // Record usage after successful addition
1504
+ if (sourceId) {
1505
+ this.quota?.recordUsage('addSource', notebookId);
1506
+ }
1507
+ return sourceId;
1508
+ }
1509
+ /**
1510
+ * Add a YouTube video source
1511
+ *
1512
+ * WORKFLOW USAGE:
1513
+ * - Returns immediately after source is queued
1514
+ * - YouTube videos may take longer to process than URLs/text
1515
+ * - Use pollProcessing() to check if source is ready
1516
+ * - Or use workflow functions that handle waiting automatically
1517
+ *
1518
+ * @param notebookId - The notebook ID
1519
+ * @param options - YouTube URL or video ID
1520
+ *
1521
+ * @example
1522
+ * ```typescript
1523
+ * // From YouTube URL
1524
+ * const sourceId = await client.sources.addYouTube('notebook-id', {
1525
+ * urlOrId: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
1526
+ * });
1527
+ *
1528
+ * // From video ID
1529
+ * const sourceId = await client.sources.addYouTube('notebook-id', {
1530
+ * urlOrId: 'dQw4w9WgXcQ',
1531
+ * });
1532
+ * ```
1533
+ */
1534
+ async addYouTube(notebookId, options) {
1535
+ const { urlOrId } = options;
1536
+ // Check quota before adding source
1537
+ this.quota?.checkQuota('addSource', notebookId);
1538
+ // Use the full URL (not just video ID) - based on RPC examples
1539
+ // Structure: [null, null, null, null, null, null, null, [url], null, null, 1]
1540
+ const youtubeUrl = this.isYouTubeURL(urlOrId)
1541
+ ? urlOrId
1542
+ : `https://www.youtube.com/watch?v=${urlOrId}`;
1543
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
1544
+ [
1545
+ [
1546
+ null,
1547
+ null,
1548
+ null,
1549
+ null,
1550
+ null,
1551
+ null,
1552
+ null,
1553
+ [youtubeUrl], // URL at index 7 as an array
1554
+ null,
1555
+ null,
1556
+ 1, // YouTube source type indicator
1557
+ ],
1558
+ ],
1559
+ notebookId,
1560
+ ], notebookId);
1561
+ const sourceId = this.extractSourceId(response);
1562
+ // Record usage after successful addition
1563
+ if (sourceId) {
1564
+ this.quota?.recordUsage('addSource', notebookId);
1565
+ }
1566
+ return sourceId;
1567
+ }
1568
+ /**
1569
+ * Add a Google Drive source directly (by file ID)
1570
+ *
1571
+ * @deprecated This method is deprecated. Use `addBatch()` with `type: 'gdrive'` instead.
1572
+ *
1573
+ * WORKFLOW USAGE:
1574
+ * - Returns immediately after source is queued
1575
+ * - Use pollProcessing() to check if source is ready
1576
+ * - Or use workflow functions that handle waiting automatically
1577
+ * - For searching Drive files first, use searchWeb() with sourceType: GOOGLE_DRIVE
1578
+ *
1579
+ * **Note:** This method is deprecated. Use `addBatch()` with `type: 'gdrive'` instead.
1580
+ *
1581
+ * @param notebookId - The notebook ID
1582
+ * @param options - Google Drive file ID and optional metadata
1583
+ *
1584
+ * @example
1585
+ * ```typescript
1586
+ * // DEPRECATED: Use addBatch() instead
1587
+ * const sourceId = await client.sources.addGoogleDrive('notebook-id', {
1588
+ * fileId: '1a2b3c4d5e6f7g8h9i0j',
1589
+ * mimeType: 'application/vnd.google-apps.document',
1590
+ * title: 'My Document',
1591
+ * });
1592
+ *
1593
+ * // RECOMMENDED: Use addBatch() instead
1594
+ * const sourceIds = await client.sources.addBatch('notebook-id', {
1595
+ * sources: [{
1596
+ * type: 'gdrive',
1597
+ * fileId: '1a2b3c4d5e6f7g8h9i0j',
1598
+ * mimeType: 'application/vnd.google-apps.document',
1599
+ * title: 'My Document',
1600
+ * }],
1601
+ * });
1602
+ * ```
1603
+ *
1604
+ * **Note:** For finding Drive files, use `searchWeb()` with `sourceType: SearchSourceType.GOOGLE_DRIVE`
1605
+ * to search your Drive, then use `addDiscovered()` to add the found files.
1606
+ */
1607
+ async addGoogleDrive(notebookId, options) {
1608
+ console.warn('⚠️ WARNING: sources.addGoogleDrive() is deprecated. ' +
1609
+ 'Use addBatch() with type: \'gdrive\' instead.');
1610
+ const { fileId, mimeType } = options;
1611
+ // Check quota before adding source
1612
+ this.quota?.checkQuota('addSource', notebookId);
1613
+ // Build request for Google Drive source
1614
+ // Format: [fileId, mimeType?, title?]
1615
+ // Note: The backend may accept [fileId, mimeType, 1, title] format as well,
1616
+ // but the current flexible format works correctly
1617
+ const driveArgs = [fileId];
1618
+ if (mimeType) {
1619
+ driveArgs.push(mimeType);
1620
+ }
1621
+ if (options.title) {
1622
+ driveArgs.push(options.title);
1623
+ }
1624
+ const response = await this.rpc.call(RPC.RPC_ADD_SOURCES, [
1625
+ [
1626
+ [
1627
+ null,
1628
+ null,
1629
+ null,
1630
+ driveArgs,
1631
+ 5, // Google Drive source type
1632
+ ],
1633
+ ],
1634
+ notebookId,
1635
+ ], notebookId);
1636
+ const sourceId = this.extractSourceId(response);
1637
+ // Record usage after successful addition
1638
+ if (sourceId) {
1639
+ this.quota?.recordUsage('addSource', notebookId);
1640
+ }
1641
+ return sourceId;
1642
+ }
1643
+ // ========================================================================
1644
+ // Web Search & Discovery Methods
1645
+ // ========================================================================
1646
+ /**
1647
+ * Search web sources (initiate search, returns sessionId)
1648
+ *
1649
+ * **NOTE: For most use cases, use `searchWebAndWait()` instead, which handles the complete workflow automatically.**
1650
+ *
1651
+ * **IMPORTANT: This is STEP 1 of a 3-step sequential workflow.**
1652
+ *
1653
+ * You must complete the steps in order - you cannot skip to step 2 or 3 without completing step 1 first.
1654
+ *
1655
+ * **Complete Workflow (3 steps):**
1656
+ * 1. `searchWeb()` → Returns `sessionId` (start here - this method)
1657
+ * 2. `getSearchResults()` → Returns discovered sources (requires step 1)
1658
+ * 3. `addDiscovered()` → Adds selected sources (requires sessionId from step 1)
1659
+ *
1660
+ * **Simplified Alternative (RECOMMENDED):**
1661
+ * - Use `searchWebAndWait()` instead - it combines steps 1-2 with automatic polling
1662
+ * - Then use `addDiscovered()` to add sources (step 3)
1663
+ *
1664
+ * @param notebookId - The notebook ID
1665
+ * @param options - Search options
1666
+ * @returns sessionId - Required for steps 2 and 3
1667
+ *
1668
+ * @example
1669
+ * ```typescript
1670
+ * // STEP 1: Initiate search (you must start here)
1671
+ * const sessionId = await client.sources.searchWeb('notebook-id', {
1672
+ * query: 'AI research',
1673
+ * sourceType: SearchSourceType.WEB,
1674
+ * mode: ResearchMode.FAST,
1675
+ * });
1676
+ *
1677
+ * // STEP 2: Get results (requires sessionId from step 1)
1678
+ * const results = await client.sources.getSearchResults('notebook-id');
1679
+ *
1680
+ * // STEP 3: Add selected sources (requires sessionId from step 1)
1681
+ * const addedIds = await client.sources.addDiscovered('notebook-id', {
1682
+ * sessionId: sessionId,
1683
+ * webSources: results.web.slice(0, 5),
1684
+ * });
1685
+ * ```
1686
+ *
1687
+ * @example
1688
+ * ```typescript
1689
+ * // Deep research (web sources only - DEEP mode not available for Drive)
1690
+ * const sessionId = await client.sources.searchWeb('notebook-id', {
1691
+ * query: 'Machine learning',
1692
+ * mode: ResearchMode.DEEP, // Only for WEB sources
1693
+ * });
1694
+ *
1695
+ * // Google Drive search (FAST mode only)
1696
+ * const sessionId = await client.sources.searchWeb('notebook-id', {
1697
+ * query: 'presentation',
1698
+ * sourceType: SearchSourceType.GOOGLE_DRIVE, // FAST mode only
1699
+ * });
1700
+ * ```
1701
+ */
1702
+ async searchWeb(notebookId, options) {
1703
+ const { query, sourceType = SearchSourceType.WEB, mode = ResearchMode.FAST, } = options;
1704
+ // Validate: Deep research only works for web sources
1705
+ if (mode === ResearchMode.DEEP && sourceType !== SearchSourceType.WEB) {
1706
+ throw new NotebookLMError('Deep research mode is only available for web sources');
1707
+ }
1708
+ // Validate: Drive only supports fast mode
1709
+ if (sourceType === SearchSourceType.GOOGLE_DRIVE && mode !== ResearchMode.FAST) {
1710
+ throw new NotebookLMError('Google Drive search only supports fast research mode');
1711
+ }
1712
+ // RPC structure from curl: [["query", sourceType], null, researchMode, notebookId]
1713
+ // Example: [["photon-hq", 1], null, 1, "notebook-id"]
1714
+ const response = await this.rpc.call(RPC.RPC_SEARCH_WEB_SOURCES, [
1715
+ [query, sourceType], // [query, source_type] - 1=WEB, 2=GOOGLE_DRIVE
1716
+ null, // null
1717
+ mode, // research_mode: 1=Fast, 2=Deep
1718
+ notebookId,
1719
+ ], notebookId);
1720
+ // Extract session ID from response
1721
+ // Response might be a JSON string like "[\"sessionId\"]" or an array
1722
+ let data = response;
1723
+ if (typeof response === 'string') {
1724
+ try {
1725
+ data = JSON.parse(response);
1726
+ }
1727
+ catch (e) {
1728
+ // If parsing fails, use response as-is
1729
+ }
1730
+ }
1731
+ // Handle different response formats
1732
+ if (Array.isArray(data)) {
1733
+ // If it's ["sessionId"], return the first element
1734
+ if (data.length > 0 && typeof data[0] === 'string') {
1735
+ return data[0];
1736
+ }
1737
+ // If it's [["sessionId"]], return the nested first element
1738
+ if (data.length > 0 && Array.isArray(data[0]) && data[0].length > 0) {
1739
+ return data[0][0];
1740
+ }
1741
+ }
1742
+ return data?.sessionId || data?.searchId || (typeof data === 'string' ? data : '');
1743
+ }
1744
+ /**
1745
+ * Search web sources and wait for results (complete workflow)
1746
+ *
1747
+ * **RECOMMENDED METHOD** - Use this instead of `searchWeb()` + `getSearchResults()` manually.
1748
+ *
1749
+ * WORKFLOW USAGE:
1750
+ * - This is a complete workflow that combines searchWeb() + getSearchResults() with polling
1751
+ * - Returns results once they're available (or timeout)
1752
+ * - Use the returned sessionId with addDiscovered() to add sources
1753
+ * - This is the recommended method for web search workflows
1754
+ * - Automatically filters results by sessionId to avoid mixing with previous searches
1755
+ *
1756
+ * @param notebookId - The notebook ID
1757
+ * @param options - Search options with waiting configuration
1758
+ *
1759
+ * @example
1760
+ * ```typescript
1761
+ * // Search and wait for results
1762
+ * const result = await client.sources.searchWebAndWait('notebook-id', {
1763
+ * query: 'AI research',
1764
+ * mode: ResearchMode.DEEP,
1765
+ * timeout: 60000, // Wait up to 60 seconds
1766
+ * onProgress: (status) => {
1767
+ * console.log(`Has results: ${status.hasResults}, Count: ${status.resultCount}`);
1768
+ * },
1769
+ * });
1770
+ *
1771
+ * // Then add selected sources
1772
+ * const addedIds = await client.sources.addDiscovered('notebook-id', {
1773
+ * sessionId: result.sessionId,
1774
+ * webSources: result.web.slice(0, 5), // Add first 5
1775
+ * });
1776
+ * ```
1777
+ */
1778
+ async searchWebAndWait(notebookId, options) {
1779
+ const { timeout = 30000, pollInterval = 2000, onProgress, ...searchOptions } = options;
1780
+ // Step 1: Initiate search
1781
+ const sessionId = await this.searchWeb(notebookId, searchOptions);
1782
+ if (!sessionId) {
1783
+ throw new NotebookLMError('Failed to initiate search - no sessionId returned');
1784
+ }
1785
+ // Step 2: Poll for results
1786
+ const startTime = Date.now();
1787
+ let results = { web: [], drive: [] };
1788
+ let lastResultCount = 0;
1789
+ let stableCount = 0; // Count consecutive polls with same result count
1790
+ while (Date.now() - startTime < timeout) {
1791
+ results = await this.getSearchResults(notebookId, sessionId);
1792
+ const hasResults = results.web.length > 0 || results.drive.length > 0;
1793
+ const resultCount = results.web.length + results.drive.length;
1794
+ if (onProgress) {
1795
+ onProgress({ hasResults, resultCount });
1796
+ }
1797
+ // If we have results, check if they're stable (same count for 2 consecutive polls)
1798
+ // This indicates search is complete
1799
+ if (hasResults) {
1800
+ if (resultCount === lastResultCount && resultCount > 0) {
1801
+ stableCount++;
1802
+ // If results are stable for 2 polls, consider search complete
1803
+ if (stableCount >= 2) {
1804
+ return { sessionId, ...results };
1805
+ }
1806
+ }
1807
+ else {
1808
+ stableCount = 0; // Reset if count changed
1809
+ }
1810
+ lastResultCount = resultCount;
1811
+ }
1812
+ else {
1813
+ stableCount = 0;
1814
+ lastResultCount = 0;
1815
+ }
1816
+ // Wait before next poll
1817
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
1818
+ }
1819
+ // Timeout reached - return whatever we have (even if empty)
1820
+ return { sessionId, ...results };
1821
+ }
1822
+ /**
1823
+ * Get search results (STEP 2 of 3-step workflow)
1824
+ *
1825
+ * **REQUIRES:** You must call `searchWeb()` first (step 1) before calling this method.
1826
+ *
1827
+ * This method retrieves the search results from a previously initiated search.
1828
+ * The search was started by `searchWeb()` in step 1.
1829
+ *
1830
+ * **Complete Workflow (3 steps):**
1831
+ * 1. `searchWeb()` → Returns `sessionId` (required first step)
1832
+ * 2. `getSearchResults()` → Returns discovered sources (this method - step 2)
1833
+ * 3. `addDiscovered()` → Adds selected sources (requires sessionId from step 1)
1834
+ *
1835
+ * **Note:** If you haven't called `searchWeb()` yet, you'll get empty results.
1836
+ * Use `searchWebAndWait()` if you want to combine steps 1-2 automatically.
1837
+ *
1838
+ * **Filtering:** If `sessionId` is provided, only results from that session will be returned.
1839
+ * Otherwise, results from all sessions will be returned (may include previous searches).
1840
+ *
1841
+ * @param notebookId - The notebook ID (must match the notebookId used in step 1)
1842
+ * @param sessionId - Optional session ID to filter results to only this search session
1843
+ * @returns Discovered sources (web and/or drive)
1844
+ *
1845
+ * @example
1846
+ * ```typescript
1847
+ * // STEP 1: Initiate search first
1848
+ * const sessionId = await client.sources.searchWeb('notebook-id', { query: 'AI' });
1849
+ *
1850
+ * // STEP 2: Get results (only works after step 1)
1851
+ * // Option 1: Get all results (may include previous searches)
1852
+ * const results = await client.sources.getSearchResults('notebook-id');
1853
+ *
1854
+ * // Option 2: Get results only from current search (recommended)
1855
+ * const results = await client.sources.getSearchResults('notebook-id', sessionId);
1856
+ * console.log(`Found ${results.web.length} web sources and ${results.drive.length} drive sources`);
1857
+ *
1858
+ * // STEP 3: Add selected sources
1859
+ * const addedIds = await client.sources.addDiscovered('notebook-id', {
1860
+ * sessionId: sessionId,
1861
+ * webSources: results.web,
1862
+ * });
1863
+ * ```
1864
+ */
1865
+ async getSearchResults(notebookId, sessionId) {
1866
+ const response = await this.rpc.call(RPC.RPC_GET_SEARCH_RESULTS, [null, null, notebookId], notebookId);
1867
+ // Response structure: [[[sessionId, [notebookId, [query, type], mode, [webSources]], ...]]]
1868
+ // Example: [[["0057e489-...", ["notebook-id", ["query", 1], 1, [["url", "title", "description", 1], ...]], ...]]]
1869
+ // Web sources are at session[1][4] (index 4 of the metadata array, which is the 5th element)
1870
+ // Handle JSON string response
1871
+ let data = response;
1872
+ if (typeof response === 'string') {
1873
+ try {
1874
+ data = JSON.parse(response);
1875
+ }
1876
+ catch (e) {
1877
+ // If parsing fails, use response as-is
1878
+ }
1879
+ }
1880
+ // Extract the sessions array
1881
+ // Response might be: [[sessions]] or [sessions] or sessions
1882
+ let sessions = [];
1883
+ if (Array.isArray(data)) {
1884
+ if (data.length > 0 && Array.isArray(data[0])) {
1885
+ // Check if first element is an array of sessions
1886
+ if (data[0].length > 0 && Array.isArray(data[0][0])) {
1887
+ sessions = data[0]; // [[[session], ...]]
1888
+ }
1889
+ else {
1890
+ sessions = data; // [[session], ...]
1891
+ }
1892
+ }
1893
+ else {
1894
+ sessions = data; // [session, ...]
1895
+ }
1896
+ }
1897
+ const web = [];
1898
+ const drive = [];
1899
+ for (const session of sessions) {
1900
+ if (!Array.isArray(session) || session.length < 2) {
1901
+ continue;
1902
+ }
1903
+ // session[0] = sessionId
1904
+ // session[1] = [notebookId, [query, type], mode, [webSources]]
1905
+ // Example: ["9c40da15-...", ["nit kkr", 1], 1, [[["https://...", "title", ...], ...]]]
1906
+ const currentSessionId = session[0];
1907
+ // Filter by sessionId if provided (normalize both to strings for comparison)
1908
+ if (sessionId) {
1909
+ const normalizedSessionId = String(sessionId).trim();
1910
+ const normalizedCurrentId = String(currentSessionId || '').trim();
1911
+ if (normalizedSessionId && normalizedCurrentId !== normalizedSessionId) {
1912
+ continue; // Skip sessions that don't match
1913
+ }
1914
+ }
1915
+ const metadata = session[1];
1916
+ if (Array.isArray(metadata) && metadata.length > 3) {
1917
+ // Web sources are at metadata[3] (index 3, the 4th element)
1918
+ const webSources = metadata[3];
1919
+ // Skip if webSources is null (search is still in progress)
1920
+ if (webSources === null || webSources === undefined) {
1921
+ continue;
1922
+ }
1923
+ if (Array.isArray(webSources) && webSources.length > 0) {
1924
+ // Helper function to recursively flatten arrays until we find source arrays
1925
+ const flattenSources = (arr) => {
1926
+ const result = [];
1927
+ for (const item of arr) {
1928
+ if (Array.isArray(item)) {
1929
+ // Check if this array looks like a source: [url, title, ...]
1930
+ if (item.length >= 2 && typeof item[0] === 'string' && item[0].startsWith('http')) {
1931
+ result.push(item);
1932
+ }
1933
+ else {
1934
+ // Recursively flatten nested arrays
1935
+ result.push(...flattenSources(item));
1936
+ }
1937
+ }
1938
+ }
1939
+ return result;
1940
+ };
1941
+ // Flatten the webSources array
1942
+ const sourcesToProcess = flattenSources(webSources);
1943
+ // Process all sources
1944
+ for (const source of sourcesToProcess) {
1945
+ if (Array.isArray(source) && source.length >= 2) {
1946
+ // Format: [url, title, description, typeCode?, ...]
1947
+ // Check for type indicator in the array - might be at index 3 or later
1948
+ const url = source[0];
1949
+ const title = source[1];
1950
+ // Check if there's a type code in the array (typically a number)
1951
+ // Common positions: index 2 or 3 might contain type info
1952
+ let detectedType;
1953
+ for (let i = 2; i < Math.min(source.length, 5); i++) {
1954
+ const item = source[i];
1955
+ // Type codes: 9 = YouTube, 1 = URL, etc.
1956
+ if (typeof item === 'number' && item === 9) {
1957
+ detectedType = 'youtube';
1958
+ break;
1959
+ }
1960
+ else if (typeof item === 'number' && item === 1) {
1961
+ detectedType = 'url';
1962
+ break;
1963
+ }
1964
+ }
1965
+ // Only add if URL exists, is a string, and is a valid URL
1966
+ if (url && typeof url === 'string' && url.startsWith('http')) {
1967
+ web.push({
1968
+ url: url,
1969
+ title: (typeof title === 'string' ? title : '') || '',
1970
+ id: url, // Use URL as ID
1971
+ type: detectedType, // Store detected type
1972
+ });
1973
+ }
1974
+ }
1975
+ else if (typeof source === 'object' && source && 'url' in source) {
1976
+ web.push({
1977
+ url: source.url,
1978
+ title: source.title || '',
1979
+ id: source.id || source.url,
1980
+ type: source.type,
1981
+ });
1982
+ }
1983
+ }
1984
+ }
1985
+ }
1986
+ // Drive sources might be at a different index, need to check structure
1987
+ // For now, we'll parse drive sources if they exist in the same structure
1988
+ }
1989
+ return { web, drive };
1990
+ }
1991
+ /**
1992
+ * Add discovered sources from search results (STEP 3 of 3-step workflow)
1993
+ *
1994
+ * **REQUIRES:** You must have a `sessionId` from `searchWeb()` (step 1) to use this method.
1995
+ *
1996
+ * This is the final step - it adds the selected discovered sources to your notebook.
1997
+ *
1998
+ * **Complete Workflow (3 steps):**
1999
+ * 1. `searchWeb()` → Returns `sessionId` (required first step)
2000
+ * 2. `getSearchResults()` → Returns discovered sources (optional - if you need to filter/select)
2001
+ * 3. `addDiscovered()` → Adds selected sources (this method - final step)
2002
+ *
2003
+ * **Simplified Alternative:**
2004
+ * - Use `searchWebAndWait()` to combine steps 1-2, then use this method for step 3
2005
+ *
2006
+ * @param notebookId - The notebook ID
2007
+ * @param options - Session ID (from step 1) and sources to add
2008
+ * @returns Array of added source IDs
2009
+ *
2010
+ * @example
2011
+ * ```typescript
2012
+ * // Option 1: Complete 3-step workflow
2013
+ * const sessionId = await client.sources.searchWeb('notebook-id', {
2014
+ * query: 'AI research',
2015
+ * mode: ResearchMode.DEEP,
2016
+ * });
2017
+ * const results = await client.sources.getSearchResults('notebook-id');
2018
+ * const addedIds = await client.sources.addDiscovered('notebook-id', {
2019
+ * sessionId: sessionId, // Required: from step 1
2020
+ * webSources: results.web.slice(0, 5), // Add first 5 web sources
2021
+ * });
2022
+ *
2023
+ * // Option 2: Simplified workflow (recommended)
2024
+ * const result = await client.sources.searchWebAndWait('notebook-id', {
2025
+ * query: 'AI research',
2026
+ * mode: ResearchMode.DEEP,
2027
+ * });
2028
+ * const addedIds = await client.sources.addDiscovered('notebook-id', {
2029
+ * sessionId: result.sessionId, // From searchWebAndWait
2030
+ * webSources: result.web.slice(0, 5),
2031
+ * driveSources: result.drive.slice(0, 2), // Can also add Drive sources
2032
+ * });
2033
+ * ```
2034
+ */
2035
+ async addDiscovered(notebookId, options) {
2036
+ const { sessionId, webSources = [], driveSources = [] } = options;
2037
+ // Check quota before adding sources
2038
+ const totalSources = webSources.length + driveSources.length;
2039
+ for (let i = 0; i < totalSources; i++) {
2040
+ this.quota?.checkQuota('addSource', notebookId);
2041
+ }
2042
+ // RPC structure from curl: [null, [1], sessionId, notebookId, [[null, null, [url, title], null, null, null, null, null, null, null, 2]]]
2043
+ // Each web source: [null, null, [url, title], null, null, null, null, null, null, null, 2]
2044
+ const webSourceArgs = webSources.map(src => [
2045
+ null,
2046
+ null,
2047
+ [src.url, src.title],
2048
+ null,
2049
+ null,
2050
+ null,
2051
+ null,
2052
+ null,
2053
+ null,
2054
+ null,
2055
+ 2, // Type indicator for web source
2056
+ ]);
2057
+ // Drive sources structure (if needed in future)
2058
+ const driveSourceArgs = driveSources.map(src => [
2059
+ null,
2060
+ null,
2061
+ [src.fileId, src.title],
2062
+ null,
2063
+ null,
2064
+ null,
2065
+ null,
2066
+ null,
2067
+ null,
2068
+ null,
2069
+ 1, // Type indicator for drive source (if different)
2070
+ ]);
2071
+ const allSources = [...webSourceArgs, ...driveSourceArgs];
2072
+ const response = await this.rpc.call(RPC.RPC_ADD_DISCOVERED_SOURCES, [
2073
+ null, // null
2074
+ [1], // [1] - flag
2075
+ sessionId, // session ID from searchWeb
2076
+ notebookId, // notebook ID
2077
+ allSources, // array of source arrays
2078
+ ], notebookId);
2079
+ const addedIds = [];
2080
+ // Handle JSON string response
2081
+ let data = response;
2082
+ if (typeof response === 'string') {
2083
+ try {
2084
+ data = JSON.parse(response);
2085
+ }
2086
+ catch (e) {
2087
+ // If parsing fails, use response as-is
2088
+ }
2089
+ }
2090
+ // Response structure: [[[[sourceId], title, [...metadata...], [null, 2]]]]
2091
+ // Example: [[[[\"435170a5-...\"], \"Title\", [...], [null, 2]]]]
2092
+ // We need to extract sourceId from the nested structure
2093
+ // Helper function to recursively find source IDs
2094
+ const extractSourceIds = (arr) => {
2095
+ const ids = [];
2096
+ if (Array.isArray(arr)) {
2097
+ for (const item of arr) {
2098
+ if (Array.isArray(item)) {
2099
+ // Check if first element is a UUID-like string (source ID)
2100
+ if (item.length > 0 && typeof item[0] === 'string' &&
2101
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item[0])) {
2102
+ ids.push(item[0]);
2103
+ }
2104
+ else {
2105
+ // Recursively search nested arrays
2106
+ ids.push(...extractSourceIds(item));
2107
+ }
2108
+ }
2109
+ else if (typeof item === 'string' &&
2110
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item)) {
2111
+ // Direct UUID string
2112
+ ids.push(item);
2113
+ }
2114
+ }
2115
+ }
2116
+ return ids;
2117
+ };
2118
+ // Extract source IDs from the response
2119
+ const extractedIds = extractSourceIds(data);
2120
+ // Remove duplicates and add to result
2121
+ const uniqueIds = [...new Set(extractedIds)];
2122
+ for (const sourceId of uniqueIds) {
2123
+ if (sourceId) {
2124
+ addedIds.push(sourceId);
2125
+ this.quota?.recordUsage('addSource', notebookId);
2126
+ }
2127
+ }
2128
+ return addedIds;
2129
+ }
2130
+ // ========================================================================
2131
+ // Batch Operations
2132
+ // ========================================================================
2133
+ /**
2134
+ * Add multiple sources in batch
2135
+ *
2136
+ * WORKFLOW USAGE:
2137
+ * - Efficiently adds multiple sources of different types in one call
2138
+ * - All source types are supported EXCEPT web sources (which come from search)
2139
+ * - Optionally waits for all sources to be processed
2140
+ * - Use this for adding multiple sources at once instead of individual calls
2141
+ * - All sources are added in parallel (server-side)
2142
+ *
2143
+ * **Supported Source Types:**
2144
+ * - `url` - Regular URLs (via `addFromURL()`)
2145
+ * - `text` - Text content (via `addFromText()`)
2146
+ * - `file` - File uploads (via `addFromFile()`)
2147
+ * - `youtube` - YouTube videos (via `addYouTube()`)
2148
+ * - `gdrive` - Google Drive files (via `addGoogleDrive()`)
2149
+ *
2150
+ * **NOT Supported:**
2151
+ * - Web sources from search - Use `searchWebAndWait()` + `addDiscovered()` instead
2152
+ *
2153
+ * @param notebookId - The notebook ID
2154
+ * @param options - Batch addition options
2155
+ * @returns Array of source IDs for all added sources
2156
+ *
2157
+ * @example
2158
+ * ```typescript
2159
+ * // Add multiple sources without waiting
2160
+ * const sourceIds = await client.sources.addBatch('notebook-id', {
2161
+ * sources: [
2162
+ * { type: 'url', url: 'https://example.com/article1' },
2163
+ * { type: 'url', url: 'https://example.com/article2' },
2164
+ * { type: 'text', title: 'Notes', content: 'My research notes...' },
2165
+ * { type: 'youtube', urlOrId: 'https://youtube.com/watch?v=...' },
2166
+ * { type: 'gdrive', fileId: '1a2b3c4d5e6f7g8h9i0j', mimeType: 'application/pdf' },
2167
+ * ],
2168
+ * });
2169
+ *
2170
+ * // Add and wait for processing
2171
+ * const sourceIds = await client.sources.addBatch('notebook-id', {
2172
+ * sources: [
2173
+ * { type: 'url', url: 'https://example.com/article' },
2174
+ * { type: 'text', title: 'Notes', content: 'Content...' },
2175
+ * ],
2176
+ * waitForProcessing: true,
2177
+ * timeout: 300000, // 5 minutes
2178
+ * onProgress: (ready, total) => {
2179
+ * console.log(`${ready}/${total} sources ready`);
2180
+ * },
2181
+ * });
2182
+ * ```
2183
+ */
2184
+ async addBatch(notebookId, options) {
2185
+ const { sources, waitForProcessing = false, timeout = 300000, pollInterval = 2000, onProgress, } = options;
2186
+ if (sources.length === 0) {
2187
+ return [];
2188
+ }
2189
+ // Check quota for all sources
2190
+ for (let i = 0; i < sources.length; i++) {
2191
+ this.quota?.checkQuota('addSource', notebookId);
2192
+ }
2193
+ // Add all sources
2194
+ const addPromises = sources.map(source => {
2195
+ switch (source.type) {
2196
+ case 'url':
2197
+ return this.addFromURL(notebookId, { url: source.url, title: source.title });
2198
+ case 'text':
2199
+ return this.addFromText(notebookId, { title: source.title, content: source.content });
2200
+ case 'file':
2201
+ return this.addFromFile(notebookId, {
2202
+ content: source.content,
2203
+ fileName: source.fileName,
2204
+ mimeType: source.mimeType,
2205
+ });
2206
+ case 'youtube':
2207
+ return this.addYouTube(notebookId, { urlOrId: source.urlOrId, title: source.title });
2208
+ case 'gdrive':
2209
+ return this.addGoogleDrive(notebookId, {
2210
+ fileId: source.fileId,
2211
+ title: source.title,
2212
+ mimeType: source.mimeType,
2213
+ });
2214
+ default:
2215
+ throw new NotebookLMError(`Unsupported source type: ${source.type}`);
2216
+ }
2217
+ });
2218
+ const sourceIds = await Promise.all(addPromises);
2219
+ // Wait for processing if requested
2220
+ if (waitForProcessing) {
2221
+ const startTime = Date.now();
2222
+ const total = sourceIds.length;
2223
+ while (Date.now() - startTime < timeout) {
2224
+ const status = await this.status(notebookId);
2225
+ if (onProgress) {
2226
+ const ready = total - status.processing.length;
2227
+ onProgress(ready, total);
2228
+ }
2229
+ if (status.allReady) {
2230
+ break;
2231
+ }
2232
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
2233
+ }
2234
+ }
2235
+ return sourceIds;
2236
+ }
2237
+ // ========================================================================
2238
+ // Source Management Methods
2239
+ // ========================================================================
2240
+ /**
2241
+ * Delete a source from a notebook
2242
+ *
2243
+ * WORKFLOW USAGE:
2244
+ * - Permanently removes a source from the notebook
2245
+ * - This action cannot be undone
2246
+ * - To delete multiple sources, call this method multiple times
2247
+ *
2248
+ * @param notebookId - The notebook ID
2249
+ * @param sourceId - The source ID to delete
2250
+ *
2251
+ * @example
2252
+ * ```typescript
2253
+ * // Delete a single source
2254
+ * await client.sources.delete('notebook-id', 'source-id-123');
2255
+ *
2256
+ * // Delete multiple sources (call multiple times)
2257
+ * await client.sources.delete('notebook-id', 'source-id-1');
2258
+ * await client.sources.delete('notebook-id', 'source-id-2');
2259
+ * await client.sources.delete('notebook-id', 'source-id-3');
2260
+ * ```
2261
+ */
2262
+ async delete(notebookId, sourceId) {
2263
+ // RPC structure: [[["sourceId"]], [2]]
2264
+ // The source ID must be triple-nested: [[["sourceId"]]], then [2] as a separate element
2265
+ const formattedIds = [[[sourceId]]];
2266
+ formattedIds.push([2]);
2267
+ await this.rpc.call(RPC.RPC_DELETE_SOURCES, formattedIds, notebookId);
2268
+ }
2269
+ /**
2270
+ * Update source metadata
2271
+ *
2272
+ * WORKFLOW USAGE:
2273
+ * - Updates source properties like title, metadata, etc.
2274
+ * - This updates the source information, not the content itself
2275
+ * - Returns immediately (no waiting required)
2276
+ *
2277
+ * **Common Updates:**
2278
+ * - `title` - Change the source title/name
2279
+ * - `metadata` - Update custom metadata
2280
+ * - Other source properties as defined in the Source interface
2281
+ *
2282
+ * @param notebookId - The notebook ID
2283
+ * @param sourceId - The source ID to update
2284
+ * @param updates - Partial Source object with fields to update
2285
+ *
2286
+ * @example
2287
+ * ```typescript
2288
+ * // Update source title
2289
+ * await client.sources.update('notebook-id', 'source-id', {
2290
+ * title: 'Updated Source Title',
2291
+ * });
2292
+ *
2293
+ * // Update metadata
2294
+ * await client.sources.update('notebook-id', 'source-id', {
2295
+ * metadata: {
2296
+ * category: 'research',
2297
+ * priority: 'high',
2298
+ * },
2299
+ * });
2300
+ * ```
2301
+ */
2302
+ async update(notebookId, sourceId, updates) {
2303
+ // RPC structure: [null, ["sourceId"], [[["title"]]]]
2304
+ // Based on mm1.txt: [null, ["cfb47db0-..."], [[["1234"]]]]
2305
+ // The title must be triple-nested: [[["title"]]]
2306
+ const title = updates.title;
2307
+ if (!title) {
2308
+ throw new NotebookLMError('Title is required for source update');
2309
+ }
2310
+ const args = [
2311
+ null,
2312
+ [sourceId],
2313
+ [[[title]]],
2314
+ ];
2315
+ await this.rpc.call(RPC.RPC_MUTATE_SOURCE, args, notebookId);
2316
+ }
2317
+ /**
2318
+ * Poll source processing status
2319
+ *
2320
+ * WORKFLOW USAGE:
2321
+ * - Call this repeatedly to check if sources are ready
2322
+ * - Use in loops with setTimeout for manual polling
2323
+ * - Or use workflow functions that handle polling automatically
2324
+ * - This is a single check - does not wait or retry
2325
+ *
2326
+ * @param notebookId - The notebook ID
2327
+ *
2328
+ * @example
2329
+ * ```typescript
2330
+ * // Manual polling
2331
+ * let status;
2332
+ * do {
2333
+ * status = await client.sources.pollProcessing('notebook-id');
2334
+ * if (!status.allReady) {
2335
+ * await new Promise(r => setTimeout(r, 2000)); // Wait 2s
2336
+ * }
2337
+ * } while (!status.allReady);
2338
+ * ```
2339
+ */
2340
+ /**
2341
+ * Get source processing status
2342
+ *
2343
+ * **What it does:** Checks the processing status of all sources in a notebook.
2344
+ * Returns information about which sources are still processing and whether all sources are ready.
2345
+ *
2346
+ * **Input:**
2347
+ * - `notebookId` (string, required): The notebook ID
2348
+ *
2349
+ * **Output:** Returns a `SourceProcessingStatus` object containing:
2350
+ * - `allReady` (boolean): Whether all sources are ready
2351
+ * - `processing` (string[]): Array of source IDs that are still processing
2352
+ *
2353
+ * @param notebookId - The notebook ID
2354
+ *
2355
+ * @example
2356
+ * ```typescript
2357
+ * // Check processing status
2358
+ * const status = await client.sources.status('notebook-id');
2359
+ *
2360
+ * if (status.allReady) {
2361
+ * console.log('All sources are ready!');
2362
+ * } else {
2363
+ * console.log(`Still processing: ${status.processing.length} sources`);
2364
+ * console.log('Processing IDs:', status.processing);
2365
+ * }
2366
+ * ```
2367
+ */
2368
+ async status(notebookId) {
2369
+ const response = await this.rpc.call(RPC.RPC_POLL_SOURCE_PROCESSING, [notebookId, null, [2], null, 1], notebookId);
2370
+ // Parse response to extract processing status
2371
+ const data = Array.isArray(response) ? response[0] : response;
2372
+ const sources = data?.[0] || [];
2373
+ const processing = [];
2374
+ let allReady = true;
2375
+ if (Array.isArray(sources)) {
2376
+ for (const source of sources) {
2377
+ if (source && typeof source === 'object') {
2378
+ const sourceId = source[0] || source.id || '';
2379
+ const status = source[1] || source.status || 0;
2380
+ if (status !== 2) { // 2 = ready
2381
+ allReady = false;
2382
+ if (sourceId)
2383
+ processing.push(sourceId);
2384
+ }
2385
+ }
2386
+ }
2387
+ }
2388
+ return { allReady, processing };
2389
+ }
2390
+ /**
2391
+ * Select/prepare source for viewing
2392
+ *
2393
+ * @deprecated This method is deprecated. It's only used with loadContent(),
2394
+ * which is also deprecated due to "Service unavailable" errors.
2395
+ *
2396
+ * WORKFLOW USAGE:
2397
+ * - REQUIRED: Must call this before loadContent() for reliable content loading
2398
+ * - NotebookLM requires sources to be selected before they can be loaded
2399
+ * - Use in sequence: selectSource() → loadContent()
2400
+ *
2401
+ * @param sourceId - The source ID
2402
+ *
2403
+ * @example
2404
+ * ```typescript
2405
+ * // REQUIRED: select first, then load
2406
+ * await client.sources.selectSource('source-id');
2407
+ * const content = await client.sources.loadContent('source-id');
2408
+ * console.log(content.text);
2409
+ * ```
2410
+ */
2411
+ async selectSource(sourceId) {
2412
+ console.warn('⚠️ WARNING: sources.selectSource() is deprecated. ' +
2413
+ 'It\'s only used with loadContent(), which is also deprecated due to API reliability issues.');
2414
+ // RPC structure from curl: [["sourceId"], [2], [2]]
2415
+ await this.rpc.call(RPC.RPC_LOAD_SOURCE, [[sourceId], [2], [2]]);
2416
+ }
2417
+ /**
2418
+ * Load source content
2419
+ *
2420
+ * @deprecated This method is deprecated and may not work reliably.
2421
+ * The API endpoint returns "Service unavailable" errors.
2422
+ *
2423
+ * WORKFLOW USAGE:
2424
+ * - REQUIRED: Must call selectSource() first before calling this method
2425
+ * - Returns full text content of the source
2426
+ * - Use this to read source content after it's ready
2427
+ *
2428
+ * @param sourceId - The source ID
2429
+ *
2430
+ * @example
2431
+ * ```typescript
2432
+ * // REQUIRED: select first, then load
2433
+ * await client.sources.selectSource('source-id');
2434
+ * const content = await client.sources.loadContent('source-id');
2435
+ * console.log(content.text);
2436
+ * ```
2437
+ */
2438
+ async loadContent(sourceId) {
2439
+ console.warn('⚠️ WARNING: sources.loadContent() is deprecated and may not work reliably. ' +
2440
+ 'The API endpoint returns "Service unavailable" errors.');
2441
+ // RPC structure from curl: [[[["sourceId"]]]]
2442
+ // Note: Must call selectSource() first before calling this method
2443
+ const response = await this.rpc.call(RPC.RPC_LOAD_SOURCE_CONTENT, [[[[sourceId]]]]);
2444
+ // Parse response - extract text content
2445
+ const data = Array.isArray(response) ? response[0] : response;
2446
+ const text = data?.[0]?.[0]?.[0] || data?.text || '';
2447
+ const metadata = data?.[0]?.[0]?.[1] || data?.metadata;
2448
+ return { text, metadata };
2449
+ }
2450
+ /**
2451
+ * Check source freshness
2452
+ *
2453
+ * @deprecated This method is deprecated and may not work reliably.
2454
+ * The API endpoint returns "Service unavailable" errors.
2455
+ *
2456
+ * WORKFLOW USAGE:
2457
+ * - Use this to check if source content is up-to-date
2458
+ * - Can be used before refresh() to determine if refresh is needed
2459
+ *
2460
+ * @param sourceId - The source ID
2461
+ */
2462
+ async checkFreshness(sourceId) {
2463
+ console.warn('⚠️ WARNING: sources.checkFreshness() is deprecated and may not work reliably. ' +
2464
+ 'The API endpoint returns "Service unavailable" errors.');
2465
+ const response = await this.rpc.call(RPC.RPC_CHECK_SOURCE_FRESHNESS, [sourceId]);
2466
+ const data = Array.isArray(response) ? response[0] : response;
2467
+ const isFresh = data?.[0] === true || data?.isFresh === true;
2468
+ const lastChecked = data?.[1] ? new Date(data[1]) : undefined;
2469
+ return { isFresh, lastChecked };
2470
+ }
2471
+ /**
2472
+ * Add deep research report as a source
2473
+ *
2474
+ * @deprecated This method is deprecated and may not work reliably.
2475
+ * The API endpoint may return "Service unavailable" errors.
2476
+ *
2477
+ * WORKFLOW USAGE:
2478
+ * - Creates an AI-generated deep research report on a topic and adds it as a source
2479
+ * - Monthly quota limit: 10 reports per month
2480
+ * - Returns immediately after research is queued
2481
+ * - Use `pollProcessing()` to check when the research report is ready
2482
+ * - Once ready, the report appears as a source in your notebook
2483
+ *
2484
+ * **Important Notes:**
2485
+ * - This is DIFFERENT from `searchWeb()` with `ResearchMode.DEEP`
2486
+ * - `searchWeb(..., mode: ResearchMode.DEEP)` - Searches web and finds relevant sources
2487
+ * - `addDeepResearch()` - Creates a complete research report as a source itself
2488
+ * - Monthly limit: 10 reports per month (enforced by quota system)
2489
+ * - The generated report becomes a source that can be used for chat, artifacts, etc.
2490
+ * - Processing can take several minutes for comprehensive research
2491
+ *
2492
+ * **Use Cases:**
2493
+ * - Need a comprehensive research report on a complex topic
2494
+ * - Want AI-generated analysis compiled into a single source
2495
+ * - Starting research on a new domain and need foundational content
2496
+ *
2497
+ * @param notebookId - The notebook ID
2498
+ * @param query - Research query/question (what you want researched)
2499
+ * @returns Source ID of the generated research report
2500
+ *
2501
+ * @example
2502
+ * ```typescript
2503
+ * // Create a deep research report
2504
+ * const sourceId = await client.sources.addDeepResearch('notebook-id',
2505
+ * 'Latest developments in quantum computing and their applications'
2506
+ * );
2507
+ *
2508
+ * // Wait for research report to be ready
2509
+ * let status;
2510
+ * do {
2511
+ * status = await client.sources.pollProcessing('notebook-id');
2512
+ * if (!status.allReady) {
2513
+ * console.log('Research report still being generated...');
2514
+ * await new Promise(r => setTimeout(r, 5000)); // Wait 5s between checks
2515
+ * }
2516
+ * } while (!status.allReady);
2517
+ *
2518
+ * console.log(`Research report ready! Source ID: ${sourceId}`);
2519
+ * ```
2520
+ */
2521
+ async addDeepResearch(notebookId, query) {
2522
+ console.warn('⚠️ WARNING: sources.addDeepResearch() is deprecated and may not work reliably. ' +
2523
+ 'The API endpoint may return "Service unavailable" errors.');
2524
+ // Check monthly quota
2525
+ this.quota?.checkQuota('deepResearch');
2526
+ const response = await this.rpc.call(RPC.RPC_ADD_DEEP_RESEARCH_REPORT, [notebookId, query], notebookId);
2527
+ const data = Array.isArray(response) ? response[0] : response;
2528
+ const sourceId = data?.[0] || data?.sourceId || '';
2529
+ // Record usage after successful creation
2530
+ if (sourceId) {
2531
+ this.quota?.recordUsage('deepResearch');
2532
+ }
2533
+ return sourceId;
2534
+ }
2535
+ /**
2536
+ * Act on multiple sources (bulk action)
2537
+ *
2538
+ * @deprecated This method is deprecated and may not work reliably.
2539
+ * The RPC endpoint returns "Service unavailable" errors.
2540
+ *
2541
+ * WORKFLOW USAGE:
2542
+ * - Use this for bulk operations on multiple sources
2543
+ * - Different from update() which works on a single source
2544
+ * - Supports various AI-powered content transformation actions
2545
+ *
2546
+ * **Note:** This method is deprecated due to API reliability issues.
2547
+ * Consider using artifact creation methods (e.g., `sdk.artifacts.create()`)
2548
+ * for similar functionality.
2549
+ *
2550
+ * @param notebookId - The notebook ID
2551
+ * @param action - Action to perform (see supported actions below)
2552
+ * @param sourceIds - Array of source IDs to act on
2553
+ *
2554
+ * @example
2555
+ * ```typescript
2556
+ * // Rephrase content from multiple sources
2557
+ * await client.sources.actOn('notebook-id', 'rephrase', ['source-1', 'source-2']);
2558
+ *
2559
+ * // Generate study guide from sources
2560
+ * await client.sources.actOn('notebook-id', 'study_guide', ['source-1']);
2561
+ *
2562
+ * // Create interactive mindmap
2563
+ * await client.sources.actOn('notebook-id', 'interactive_mindmap', ['source-1', 'source-2']);
2564
+ * ```
2565
+ *
2566
+ * **Supported Actions:**
2567
+ * - `rephrase` - Rephrase content from sources
2568
+ * - `expand` - Expand content from sources
2569
+ * - `summarize` - Summarize content from sources
2570
+ * - `critique` - Critique content from sources
2571
+ * - `brainstorm` - Brainstorm ideas from sources
2572
+ * - `verify` - Verify information from sources
2573
+ * - `explain` - Explain concepts from sources
2574
+ * - `outline` - Create outline from sources
2575
+ * - `study_guide` - Generate study guide from sources
2576
+ * - `faq` - Generate FAQ from sources
2577
+ * - `briefing_doc` - Create briefing document from sources
2578
+ * - `interactive_mindmap` - Generate interactive mindmap from sources
2579
+ * - `timeline` - Create timeline from sources
2580
+ * - `table_of_contents` - Generate table of contents from sources
2581
+ */
2582
+ async actOn(notebookId, action, sourceIds) {
2583
+ console.warn('⚠️ WARNING: sources.actOn() is deprecated and may not work reliably. ' +
2584
+ 'The API endpoint returns "Service unavailable" errors. ' +
2585
+ 'Consider using artifact creation methods instead.');
2586
+ if (sourceIds.length === 0) {
2587
+ throw new NotebookLMError('At least one source ID is required');
2588
+ }
2589
+ await this.rpc.call(RPC.RPC_ACT_ON_SOURCES, [notebookId, action, sourceIds], notebookId);
2590
+ }
2591
+ // ========================================================================
2592
+ // Helper methods
2593
+ // ========================================================================
2594
+ isYouTubeURL(url) {
2595
+ return url.includes('youtube.com') || url.includes('youtu.be');
2596
+ }
2597
+ extractYouTubeVideoId(url) {
2598
+ try {
2599
+ const urlObj = new URL(url);
2600
+ // youtu.be format
2601
+ if (urlObj.hostname === 'youtu.be') {
2602
+ return urlObj.pathname.substring(1);
2603
+ }
2604
+ // youtube.com/watch format
2605
+ if (urlObj.hostname.includes('youtube.com') && urlObj.pathname === '/watch') {
2606
+ const videoId = urlObj.searchParams.get('v');
2607
+ if (videoId)
2608
+ return videoId;
2609
+ }
2610
+ throw new Error('Unsupported YouTube URL format');
2611
+ }
2612
+ catch (error) {
2613
+ throw new NotebookLMError(`Invalid YouTube URL: ${error.message}`);
2614
+ }
2615
+ }
2616
+ extractSourceId(response) {
2617
+ try {
2618
+ // Handle JSON string responses (common in batch operations)
2619
+ let parsedResponse = response;
2620
+ if (typeof response === 'string' && (response.startsWith('[') || response.startsWith('{'))) {
2621
+ try {
2622
+ parsedResponse = JSON.parse(response);
2623
+ }
2624
+ catch {
2625
+ // If parsing fails, continue with original response
2626
+ }
2627
+ }
2628
+ // Try different response formats
2629
+ const findId = (data, depth = 0) => {
2630
+ if (depth > 5)
2631
+ return null; // Prevent infinite recursion
2632
+ // Check if this is a UUID string
2633
+ if (typeof data === 'string' && data.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) {
2634
+ return data;
2635
+ }
2636
+ // Check if this is a JSON string containing arrays/objects
2637
+ if (typeof data === 'string' && (data.startsWith('[') || data.startsWith('{'))) {
2638
+ try {
2639
+ const parsed = JSON.parse(data);
2640
+ const id = findId(parsed, depth + 1);
2641
+ if (id)
2642
+ return id;
2643
+ }
2644
+ catch {
2645
+ // Continue searching
2646
+ }
2647
+ }
2648
+ if (Array.isArray(data)) {
2649
+ for (const item of data) {
2650
+ const id = findId(item, depth + 1);
2651
+ if (id)
2652
+ return id;
2653
+ }
2654
+ }
2655
+ if (data && typeof data === 'object') {
2656
+ for (const key in data) {
2657
+ const id = findId(data[key], depth + 1);
2658
+ if (id)
2659
+ return id;
2660
+ }
2661
+ }
2662
+ return null;
2663
+ };
2664
+ const sourceId = findId(parsedResponse);
2665
+ if (!sourceId) {
2666
+ throw new Error('Could not extract source ID from response');
2667
+ }
2668
+ return sourceId;
2669
+ }
2670
+ catch (error) {
2671
+ throw new NotebookLMError(`Failed to extract source ID: ${error.message}`);
2672
+ }
2673
+ }
2674
+ }
2675
+ //# sourceMappingURL=sources.js.map