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.
- package/LICENSE +22 -0
- package/README.md +4102 -0
- package/dist/src/auth/auth.d.ts +46 -0
- package/dist/src/auth/auth.d.ts.map +1 -0
- package/dist/src/auth/auth.js +323 -0
- package/dist/src/auth/auth.js.map +1 -0
- package/dist/src/auth/refresh.d.ts +150 -0
- package/dist/src/auth/refresh.d.ts.map +1 -0
- package/dist/src/auth/refresh.js +433 -0
- package/dist/src/auth/refresh.js.map +1 -0
- package/dist/src/client/notebooklm-client.d.ts +372 -0
- package/dist/src/client/notebooklm-client.d.ts.map +1 -0
- package/dist/src/client/notebooklm-client.js +550 -0
- package/dist/src/client/notebooklm-client.js.map +1 -0
- package/dist/src/index.d.ts +50 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +45 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/rpc/rpc-client.d.ts +48 -0
- package/dist/src/rpc/rpc-client.d.ts.map +1 -0
- package/dist/src/rpc/rpc-client.js +94 -0
- package/dist/src/rpc/rpc-client.js.map +1 -0
- package/dist/src/rpc/rpc-methods.d.ts +127 -0
- package/dist/src/rpc/rpc-methods.d.ts.map +1 -0
- package/dist/src/rpc/rpc-methods.js +169 -0
- package/dist/src/rpc/rpc-methods.js.map +1 -0
- package/dist/src/services/artifacts.d.ts +1017 -0
- package/dist/src/services/artifacts.d.ts.map +1 -0
- package/dist/src/services/artifacts.js +5413 -0
- package/dist/src/services/artifacts.js.map +1 -0
- package/dist/src/services/generation.d.ts +147 -0
- package/dist/src/services/generation.d.ts.map +1 -0
- package/dist/src/services/generation.js +479 -0
- package/dist/src/services/generation.js.map +1 -0
- package/dist/src/services/notebook-language.d.ts +109 -0
- package/dist/src/services/notebook-language.d.ts.map +1 -0
- package/dist/src/services/notebook-language.js +204 -0
- package/dist/src/services/notebook-language.js.map +1 -0
- package/dist/src/services/notebooks.d.ts +26 -0
- package/dist/src/services/notebooks.d.ts.map +1 -0
- package/dist/src/services/notebooks.js +539 -0
- package/dist/src/services/notebooks.js.map +1 -0
- package/dist/src/services/notes.d.ts +72 -0
- package/dist/src/services/notes.d.ts.map +1 -0
- package/dist/src/services/notes.js +340 -0
- package/dist/src/services/notes.js.map +1 -0
- package/dist/src/services/sources.d.ts +1085 -0
- package/dist/src/services/sources.d.ts.map +1 -0
- package/dist/src/services/sources.js +2675 -0
- package/dist/src/services/sources.js.map +1 -0
- package/dist/src/types/artifact.d.ts +258 -0
- package/dist/src/types/artifact.d.ts.map +1 -0
- package/dist/src/types/artifact.js +42 -0
- package/dist/src/types/artifact.js.map +1 -0
- package/dist/src/types/common.d.ts +226 -0
- package/dist/src/types/common.d.ts.map +1 -0
- package/dist/src/types/common.js +80 -0
- package/dist/src/types/common.js.map +1 -0
- package/dist/src/types/languages.d.ts +179 -0
- package/dist/src/types/languages.d.ts.map +1 -0
- package/dist/src/types/languages.js +254 -0
- package/dist/src/types/languages.js.map +1 -0
- package/dist/src/types/note.d.ts +41 -0
- package/dist/src/types/note.d.ts.map +1 -0
- package/dist/src/types/note.js +12 -0
- package/dist/src/types/note.js.map +1 -0
- package/dist/src/types/notebook.d.ts +81 -0
- package/dist/src/types/notebook.d.ts.map +1 -0
- package/dist/src/types/notebook.js +5 -0
- package/dist/src/types/notebook.js.map +1 -0
- package/dist/src/types/source.d.ts +241 -0
- package/dist/src/types/source.d.ts.map +1 -0
- package/dist/src/types/source.js +60 -0
- package/dist/src/types/source.js.map +1 -0
- package/dist/src/utils/batch-execute.d.ts +58 -0
- package/dist/src/utils/batch-execute.d.ts.map +1 -0
- package/dist/src/utils/batch-execute.js +398 -0
- package/dist/src/utils/batch-execute.js.map +1 -0
- package/dist/src/utils/chunked-decoder.d.ts +11 -0
- package/dist/src/utils/chunked-decoder.d.ts.map +1 -0
- package/dist/src/utils/chunked-decoder.js +326 -0
- package/dist/src/utils/chunked-decoder.js.map +1 -0
- package/dist/src/utils/chunked-parser.d.ts +61 -0
- package/dist/src/utils/chunked-parser.d.ts.map +1 -0
- package/dist/src/utils/chunked-parser.js +609 -0
- package/dist/src/utils/chunked-parser.js.map +1 -0
- package/dist/src/utils/errors.d.ts +58 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +357 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/quota.d.ts +213 -0
- package/dist/src/utils/quota.d.ts.map +1 -0
- package/dist/src/utils/quota.js +518 -0
- package/dist/src/utils/quota.js.map +1 -0
- package/dist/src/utils/streaming-client.d.ts +129 -0
- package/dist/src/utils/streaming-client.d.ts.map +1 -0
- package/dist/src/utils/streaming-client.js +559 -0
- package/dist/src/utils/streaming-client.js.map +1 -0
- package/package.json +85 -7
- 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
|