sumulige-claude 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.claude/hooks/code-formatter.cjs +7 -2
  2. package/.claude/hooks/multi-session.cjs +9 -3
  3. package/.claude/hooks/pre-commit.cjs +0 -0
  4. package/.claude/hooks/pre-push.cjs +0 -0
  5. package/.claude/hooks/project-kickoff.cjs +22 -11
  6. package/.claude/hooks/rag-skill-loader.cjs +7 -0
  7. package/.claude/hooks/thinking-silent.cjs +9 -3
  8. package/.claude/hooks/todo-manager.cjs +19 -13
  9. package/.claude/hooks/verify-work.cjs +10 -4
  10. package/.claude/quality-gate.json +9 -3
  11. package/.claude/settings.local.json +16 -1
  12. package/.claude/templates/hooks/README.md +302 -0
  13. package/.claude/templates/hooks/hook.sh.template +94 -0
  14. package/.claude/templates/hooks/user-prompt-submit.cjs.template +116 -0
  15. package/.claude/templates/hooks/user-response-submit.cjs.template +94 -0
  16. package/.claude/templates/hooks/validate.js +173 -0
  17. package/.claude/workflow/document-scanner.js +426 -0
  18. package/.claude/workflow/knowledge-engine.js +941 -0
  19. package/.claude/workflow/notebooklm/browser.js +1028 -0
  20. package/.claude/workflow/phases/phase1-research.js +578 -0
  21. package/.claude/workflow/phases/phase1-research.ts +465 -0
  22. package/.claude/workflow/phases/phase2-approve.js +722 -0
  23. package/.claude/workflow/phases/phase3-plan.js +1200 -0
  24. package/.claude/workflow/phases/phase4-develop.js +894 -0
  25. package/.claude/workflow/search-cache.js +230 -0
  26. package/.claude/workflow/templates/approval.md +315 -0
  27. package/.claude/workflow/templates/development.md +377 -0
  28. package/.claude/workflow/templates/planning.md +328 -0
  29. package/.claude/workflow/templates/research.md +250 -0
  30. package/.claude/workflow/types.js +37 -0
  31. package/.claude/workflow/web-search.js +278 -0
  32. package/.claude-plugin/marketplace.json +2 -2
  33. package/AGENTS.md +176 -0
  34. package/CHANGELOG.md +7 -14
  35. package/cli.js +20 -0
  36. package/config/quality-gate.json +9 -3
  37. package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +36 -0
  38. package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +36 -0
  39. package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +36 -0
  40. package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +36 -0
  41. package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +36 -0
  42. package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +36 -0
  43. package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +36 -0
  44. package/development/knowledge-base/.index.clean.json +0 -0
  45. package/development/knowledge-base/.index.json +486 -0
  46. package/development/knowledge-base/test-best-practices.md +29 -0
  47. package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +160 -0
  48. package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +160 -0
  49. package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +160 -0
  50. package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +160 -0
  51. package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +160 -0
  52. package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +160 -0
  53. package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +160 -0
  54. package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +160 -0
  55. package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +160 -0
  56. package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +160 -0
  57. package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +226 -0
  58. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +345 -0
  59. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +284 -0
  60. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +14 -0
  61. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +35 -0
  62. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +34 -0
  63. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +5 -0
  64. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +60 -0
  65. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +25 -0
  66. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +70 -0
  67. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +48 -0
  68. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +20 -0
  69. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +21 -0
  70. package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +160 -0
  71. package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +226 -0
  72. package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +345 -0
  73. package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +284 -0
  74. package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +14 -0
  75. package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +160 -0
  76. package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +178 -0
  77. package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +377 -0
  78. package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +442 -0
  79. package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +800 -0
  80. package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +625 -0
  81. package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +830 -0
  82. package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +957 -0
  83. package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +381 -0
  84. package/development/todos/.state.json +14 -1
  85. package/development/todos/INDEX.md +31 -73
  86. package/development/todos/completed/develop/local-knowledge-index.md +85 -0
  87. package/development/todos/{active → completed/develop}/todo-system.md +13 -3
  88. package/development/todos/completed/develop/web-search-integration.md +83 -0
  89. package/development/todos/completed/test/phase1-e2e-test.md +103 -0
  90. package/lib/commands.js +388 -0
  91. package/package.json +3 -2
  92. package/tests/config-manager.test.js +677 -0
  93. package/tests/config-validator.test.js +436 -0
  94. package/tests/errors.test.js +477 -0
  95. package/tests/manual/phase1-e2e.sh +389 -0
  96. package/tests/manual/phase2-test-cases.md +311 -0
  97. package/tests/manual/phase3-test-cases.md +309 -0
  98. package/tests/manual/phase4-test-cases.md +414 -0
  99. package/tests/manual/test-cases.md +417 -0
  100. package/tests/quality-gate.test.js +679 -0
  101. package/tests/quality-rules.test.js +619 -0
  102. package/tests/version-check.test.js +75 -0
@@ -0,0 +1,1028 @@
1
+ /**
2
+ * NotebookLM Browser Automation Module (Simplified)
3
+ *
4
+ * Provides browser automation for NotebookLM interactions:
5
+ * - Authentication management
6
+ * - Session management
7
+ * - Question/answer with streaming detection
8
+ * - Human-like behavior simulation
9
+ *
10
+ * This is a simplified version of notebooklm-mcp core modules
11
+ * adapted for integration into sumulige-claude.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // ============================================================================
18
+ // Configuration
19
+ // ============================================================================
20
+
21
+ const DEFAULT_CONFIG = {
22
+ // Browser settings
23
+ headless: true,
24
+ viewport: { width: 1024, height: 768 },
25
+
26
+ // Timeout settings
27
+ browserTimeout: 120000, // 2 minutes
28
+ responseTimeout: 60000, // 1 minute
29
+ sessionTimeout: 900000, // 15 minutes
30
+
31
+ // Stealth settings
32
+ stealthEnabled: true,
33
+ humanTyping: true,
34
+ typingWpmMin: 90,
35
+ typingWpmMax: 120,
36
+ randomDelays: true,
37
+ minDelayMs: 100,
38
+ maxDelayMs: 300,
39
+
40
+ // URLs
41
+ notebookUrl: 'https://notebooklm.google.com',
42
+ authUrl: 'https://accounts.google.com'
43
+ };
44
+
45
+ // ============================================================================
46
+ // Path Management
47
+ // ============================================================================
48
+
49
+ class PathManager {
50
+ constructor() {
51
+ // Use cross-platform home directory
52
+ const homeDir = require('os').homedir();
53
+ const appName = 'sumulige-claude';
54
+
55
+ // Platform-specific data directory
56
+ const platform = require('os').platform();
57
+ let baseDir;
58
+
59
+ if (platform === 'darwin') {
60
+ baseDir = path.join(homeDir, 'Library', 'Application Support', appName);
61
+ } else if (platform === 'win32') {
62
+ baseDir = path.join(homeDir, 'AppData', 'Roaming', appName);
63
+ } else {
64
+ baseDir = path.join(homeDir, '.local', 'share', appName);
65
+ }
66
+
67
+ // Fallback to home directory if platform-specific path fails
68
+ this.dataDir = baseDir || path.join(homeDir, `.${appName}`);
69
+ this.browserStateDir = path.join(this.dataDir, 'notebooklm-state');
70
+ this.stateFilePath = path.join(this.browserStateDir, 'state.json');
71
+ this.sessionFilePath = path.join(this.browserStateDir, 'session.json');
72
+ this.ensureDirectories();
73
+ }
74
+
75
+ ensureDirectories() {
76
+ if (!fs.existsSync(this.browserStateDir)) {
77
+ fs.mkdirSync(this.browserStateDir, { recursive: true });
78
+ }
79
+ }
80
+
81
+ getStatePath() {
82
+ return fs.existsSync(this.stateFilePath) ? this.stateFilePath : null;
83
+ }
84
+
85
+ getSessionPath() {
86
+ return this.sessionFilePath;
87
+ }
88
+
89
+ getDataDir() {
90
+ return this.dataDir;
91
+ }
92
+ }
93
+
94
+ // ============================================================================
95
+ // Stealth Utilities (Human-like behavior)
96
+ // ============================================================================
97
+
98
+ class StealthUtils {
99
+ static sleep(ms) {
100
+ return new Promise(resolve => setTimeout(resolve, ms));
101
+ }
102
+
103
+ static randomInt(min, max) {
104
+ return Math.floor(Math.random() * (max - min + 1)) + min;
105
+ }
106
+
107
+ static randomFloat(min, max) {
108
+ return Math.random() * (max - min) + min;
109
+ }
110
+
111
+ // Gaussian distribution (Box-Muller transform)
112
+ static gaussian(mean, stdDev) {
113
+ const u1 = Math.random();
114
+ const u2 = Math.random();
115
+ const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
116
+ return z0 * stdDev + mean;
117
+ }
118
+
119
+ static async randomDelay(minMs, maxMs) {
120
+ minMs = minMs ?? DEFAULT_CONFIG.minDelayMs;
121
+ maxMs = maxMs ?? DEFAULT_CONFIG.maxDelayMs;
122
+
123
+ if (!DEFAULT_CONFIG.stealthEnabled || !DEFAULT_CONFIG.randomDelays) {
124
+ const target = (minMs + maxMs) / 2;
125
+ if (target > 0) await this.sleep(target);
126
+ return;
127
+ }
128
+
129
+ const mean = minMs + (maxMs - minMs) * 0.6;
130
+ const stdDev = (maxMs - minMs) * 0.2;
131
+ let delay = this.gaussian(mean, stdDev);
132
+ delay = Math.max(minMs, Math.min(maxMs, delay));
133
+
134
+ await this.sleep(delay);
135
+ }
136
+
137
+ // Calculate delay per character based on WPM
138
+ static getTypingDelay(wpm) {
139
+ const charsPerMinute = wpm * 5;
140
+ return (60 * 1000) / charsPerMinute;
141
+ }
142
+
143
+ // Get pause duration for punctuation
144
+ static getPunctuationPause(char) {
145
+ const pauses = {
146
+ '.': 300,
147
+ '!': 350,
148
+ '?': 300,
149
+ ',': 150,
150
+ ';': 200,
151
+ ':': 200,
152
+ ' ': 50,
153
+ '\n': 400
154
+ };
155
+ return pauses[char] || 50;
156
+ }
157
+ }
158
+
159
+ // ============================================================================
160
+ // Page Utilities (NotebookLM specific)
161
+ // ============================================================================
162
+
163
+ class PageUtils {
164
+ // CSS selectors for NotebookLM responses
165
+ static RESPONSE_SELECTORS = [
166
+ // Primary NotebookLM selectors
167
+ '.to-user-container .message-text-content',
168
+ '.to-user-container .markdown-content',
169
+ '.to-user-container',
170
+ // Generic bot/assistant selectors
171
+ '[data-message-author="bot"]',
172
+ '[data-message-author="assistant"]',
173
+ '[data-message-role="assistant"]',
174
+ // Content containers
175
+ 'markdown-response',
176
+ '.response-text',
177
+ '.message-text-content',
178
+ // Data attributes
179
+ '[data-automation-id="response-text"]',
180
+ '[data-automation-id="assistant-response"]',
181
+ // Last resort
182
+ '.message-text',
183
+ '[role="log"]'
184
+ ];
185
+
186
+ // Placeholder texts to ignore
187
+ static PLACEHOLDERS = new Set([
188
+ '',
189
+ '...',
190
+ 'Thinking...',
191
+ 'Generating response...',
192
+ 'Please wait...',
193
+ 'Finding relevant info...',
194
+ 'Searching',
195
+ 'Loading'
196
+ ]);
197
+
198
+ /**
199
+ * Extract latest response from NotebookLM page
200
+ */
201
+ static async extractLatestResponse(page, existingHashes = new Set()) {
202
+ for (const selector of this.RESPONSE_SELECTORS) {
203
+ try {
204
+ const elements = await page.$$(selector);
205
+ if (elements.length > 0) {
206
+ // Get the last (latest) element
207
+ const latest = elements[elements.length - 1];
208
+ const text = await latest.evaluate(el => el.textContent?.trim() || '');
209
+ const hash = this.hashString(text);
210
+
211
+ // Only return if it's a new response (not in existing hashes)
212
+ if (text && !this.PLACEHOLDERS.has(text) && !existingHashes.has(hash)) {
213
+ return { text, hash, selector };
214
+ }
215
+ }
216
+ } catch (e) {
217
+ continue;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * Snapshot all existing responses before asking a question
225
+ */
226
+ static async snapshotAllResponses(page) {
227
+ const hashes = new Set();
228
+ const texts = [];
229
+
230
+ try {
231
+ const containers = await page.$$('.to-user-container');
232
+ for (const container of containers) {
233
+ try {
234
+ const textEl = await container.$('.message-text-content');
235
+ if (textEl) {
236
+ const text = await textEl.evaluate(el => el.textContent?.trim() || '');
237
+ if (text) {
238
+ hashes.add(this.hashString(text));
239
+ texts.push(text);
240
+ }
241
+ }
242
+ } catch (e) {
243
+ continue;
244
+ }
245
+ }
246
+ } catch (e) {
247
+ // Ignore errors
248
+ }
249
+
250
+ return { hashes, texts };
251
+ }
252
+
253
+ /**
254
+ * Wait for new response with streaming detection
255
+ */
256
+ static async waitForNewResponse(page, existingHashes, timeout = 120000) {
257
+ const startTime = Date.now();
258
+ let stableCount = 0;
259
+ const STABLE_THRESHOLD = 5; // Increased for better detection
260
+ const POLL_INTERVAL = 1000; // Increased to reduce CPU usage
261
+ let lastResponse = null;
262
+ let lastLength = 0;
263
+
264
+ console.log(`ā³ Waiting for response (timeout: ${timeout/1000}s)...`);
265
+
266
+ while (Date.now() - startTime < timeout) {
267
+ await StealthUtils.sleep(POLL_INTERVAL);
268
+
269
+ const response = await this.extractLatestResponse(page, existingHashes);
270
+
271
+ if (response) {
272
+ const currentLength = response.text.length;
273
+
274
+ // Check if response is growing (streaming) or stable
275
+ if (lastResponse && lastResponse.text === response.text) {
276
+ stableCount++;
277
+ if (stableCount >= STABLE_THRESHOLD) {
278
+ console.log(`āœ… Response complete (${response.text.length} chars)`);
279
+ return response.text;
280
+ }
281
+ } else if (currentLength > lastLength) {
282
+ // Response is still growing
283
+ stableCount = 0;
284
+ lastResponse = response;
285
+ lastLength = currentLength;
286
+ console.log(`ā³ Receiving... (${currentLength} chars)`);
287
+ } else {
288
+ // Different response (unlikely but handle it)
289
+ stableCount = 0;
290
+ lastResponse = response;
291
+ lastLength = currentLength;
292
+ }
293
+ } else {
294
+ // No new response yet, check existing ones
295
+ const allTexts = await this.snapshotAllResponses(page);
296
+ if (allTexts.texts.length > 0) {
297
+ const latest = allTexts.texts[allTexts.texts.length - 1];
298
+ if (latest && latest.length > 0 && !this.PLACEHOLDERS.has(latest)) {
299
+ lastResponse = { text: latest };
300
+ lastLength = latest.length;
301
+ }
302
+ }
303
+ }
304
+
305
+ // Progress indicator
306
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
307
+ process.stdout.write(`\rā³ ${elapsed}s / ${timeout/1000}s`);
308
+ }
309
+
310
+ process.stdout.write('\n');
311
+
312
+ if (lastResponse) {
313
+ console.log(`āœ… Got response (${lastResponse.text.length} chars)`);
314
+ return lastResponse.text;
315
+ }
316
+
317
+ return null;
318
+ }
319
+
320
+ static hashString(str) {
321
+ let hash = 0;
322
+ for (let i = 0; i < str.length; i++) {
323
+ const char = str.charCodeAt(i);
324
+ hash = ((hash << 5) - hash + char) & 0xffffffff;
325
+ }
326
+ return hash.toString(36);
327
+ }
328
+ }
329
+
330
+ // ============================================================================
331
+ // Authentication Manager
332
+ // ============================================================================
333
+
334
+ class AuthManager {
335
+ constructor() {
336
+ this.paths = new PathManager();
337
+ }
338
+
339
+ /**
340
+ * Check if saved state exists and is valid
341
+ */
342
+ hasValidState() {
343
+ const statePath = this.paths.getStatePath();
344
+ if (!statePath) return false;
345
+
346
+ // Check if state is less than 24 hours old
347
+ const stats = fs.statSync(statePath);
348
+ const age = Date.now() - stats.mtime.getTime();
349
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
350
+
351
+ return age < maxAge;
352
+ }
353
+
354
+ /**
355
+ * Get the state file path for loading
356
+ */
357
+ getStatePath() {
358
+ return this.paths.getStatePath();
359
+ }
360
+
361
+ /**
362
+ * Save browser state after authentication
363
+ */
364
+ async saveState(context, page) {
365
+ try {
366
+ // Save cookies and localStorage
367
+ await context.storageState({ path: this.paths.stateFilePath });
368
+
369
+ // Save sessionStorage separately
370
+ if (page) {
371
+ const sessionStorage = await page.evaluate(() => {
372
+ const storage = {};
373
+ for (let i = 0; i < sessionStorage.length; i++) {
374
+ const key = sessionStorage.key(i);
375
+ if (key) {
376
+ storage[key] = sessionStorage.getItem(key) || '';
377
+ }
378
+ }
379
+ return JSON.stringify(storage);
380
+ });
381
+
382
+ await fs.promises.writeFile(
383
+ this.paths.sessionFilePath,
384
+ sessionStorage,
385
+ 'utf-8'
386
+ );
387
+ }
388
+
389
+ return true;
390
+ } catch (error) {
391
+ console.error('Failed to save state:', error.message);
392
+ return false;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Clear all authentication data
398
+ */
399
+ async clearState() {
400
+ try {
401
+ if (fs.existsSync(this.paths.stateFilePath)) {
402
+ fs.unlinkSync(this.paths.stateFilePath);
403
+ }
404
+ if (fs.existsSync(this.paths.sessionFilePath)) {
405
+ fs.unlinkSync(this.paths.sessionFilePath);
406
+ }
407
+ return true;
408
+ } catch (error) {
409
+ console.error('Failed to clear state:', error.message);
410
+ return false;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Load sessionStorage data
416
+ */
417
+ async loadSessionStorage() {
418
+ try {
419
+ const data = await fs.promises.readFile(this.paths.sessionFilePath, 'utf-8');
420
+ return JSON.parse(data);
421
+ } catch (error) {
422
+ return null;
423
+ }
424
+ }
425
+ }
426
+
427
+ // ============================================================================
428
+ // NotebookLM Session
429
+ // ============================================================================
430
+
431
+ class NotebookLMSession {
432
+ constructor(notebookUrl, options = {}) {
433
+ this.notebookUrl = notebookUrl || DEFAULT_CONFIG.notebookUrl;
434
+ this.options = { ...DEFAULT_CONFIG, ...options };
435
+ this.authManager = new AuthManager();
436
+ this.browser = null;
437
+ this.context = null;
438
+ this.page = null;
439
+ this.isActive = false;
440
+ this.createdAt = Date.now();
441
+ this.lastActivity = Date.now();
442
+ }
443
+
444
+ /**
445
+ * Initialize the browser session
446
+ */
447
+ async init() {
448
+ // Check if patchright is available
449
+ let patchright;
450
+ try {
451
+ patchright = require('patchright');
452
+ } catch (e) {
453
+ throw new Error('patchright is not installed. Install with: npm install patchright');
454
+ }
455
+
456
+ const paths = new PathManager();
457
+ const statePath = this.authManager.getStatePath();
458
+ const needsAuth = !this.authManager.hasValidState();
459
+
460
+ console.log(`šŸ”‘ Authentication needed: ${needsAuth ? 'YES' : 'NO (using saved state)'}`);
461
+
462
+ // Launch browser
463
+ this.browser = await patchright.chromium.launchPersistentContext(
464
+ paths.getDataDir(),
465
+ {
466
+ headless: this.options.headless && !needsAuth, // Show browser for auth
467
+ channel: 'chrome',
468
+ viewport: this.options.viewport,
469
+ args: [
470
+ '--disable-blink-features=AutomationControlled'
471
+ ]
472
+ }
473
+ );
474
+
475
+ // Get or create page
476
+ const pages = this.browser.pages();
477
+ this.page = pages[0] || await this.browser.newPage();
478
+ this.context = this.browser;
479
+
480
+ // Load existing state if available
481
+ if (statePath && !needsAuth) {
482
+ // State is loaded automatically by launchPersistentContext
483
+
484
+ // Restore sessionStorage
485
+ const sessionData = await this.authManager.loadSessionStorage();
486
+ if (sessionData) {
487
+ await this.page.evaluate((data) => {
488
+ for (const [key, value] of Object.entries(data)) {
489
+ sessionStorage.setItem(key, String(value));
490
+ }
491
+ }, sessionData);
492
+ }
493
+ }
494
+
495
+ // Navigate to NotebookLM with longer timeout
496
+ await this.page.goto(this.notebookUrl, { waitUntil: 'domcontentloaded', timeout: 120000 });
497
+
498
+ // Wait for page to be fully loaded
499
+ await StealthUtils.sleep(2000);
500
+
501
+ // Check if we need to create/select a notebook
502
+ const currentUrl = this.page.url();
503
+ console.log(`Current URL: ${currentUrl}`);
504
+
505
+ // Check if authentication is needed
506
+ if (needsAuth) {
507
+ console.log('šŸ” Please authenticate in the browser window...');
508
+ console.log('āœ‹ Sign in with your Google account, then close the browser when done.');
509
+
510
+ // Wait for authentication (user will close browser when done)
511
+ await this.waitForAuthentication();
512
+
513
+ // Save the authenticated state
514
+ await this.authManager.saveState(this.context, this.page);
515
+ console.log('āœ… Authentication state saved!');
516
+ console.log('');
517
+ console.log('šŸ’” Next: Create a notebook in NotebookLM and save its URL for asking questions.');
518
+ }
519
+
520
+ this.isActive = true;
521
+ this.lastActivity = Date.now();
522
+
523
+ return true;
524
+ }
525
+
526
+ /**
527
+ * Wait for user to complete authentication
528
+ */
529
+ async waitForAuthentication() {
530
+ // Wait for navigation to authenticated state
531
+ // This is a simple implementation - in production you'd poll for auth cookies
532
+ return new Promise((resolve) => {
533
+ const checkAuth = async () => {
534
+ const cookies = await this.context.cookies();
535
+ const hasAuthCookie = cookies.some(c =>
536
+ c.name === 'SID' || c.name === 'HSID' || c.name === 'SSID'
537
+ );
538
+
539
+ if (hasAuthCookie) {
540
+ // Wait a bit more for page to load
541
+ await StealthUtils.sleep(2000);
542
+ resolve();
543
+ } else {
544
+ setTimeout(checkAuth, 2000);
545
+ }
546
+ };
547
+
548
+ // Start checking after initial page load
549
+ setTimeout(checkAuth, 3000);
550
+ });
551
+ }
552
+
553
+ /**
554
+ * Ask a question to NotebookLM
555
+ */
556
+ async ask(question, progressCallback) {
557
+ if (!this.isActive) {
558
+ throw new Error('Session is not active. Call init() first.');
559
+ }
560
+
561
+ await progressCallback?.('Asking NotebookLM...');
562
+
563
+ // Snapshot existing responses
564
+ const { hashes: existingHashes } = await PageUtils.snapshotAllResponses(this.page);
565
+
566
+ // Find and click the input field - expanded selectors for NotebookLM
567
+ const inputSelectors = [
568
+ // Google-specific selectors
569
+ 'textarea[aria-label*="ask" i]',
570
+ 'textarea[aria-label*="message" i]',
571
+ 'textarea[aria-label*="chat" i]',
572
+ 'textarea[placeholder*="ask" i]',
573
+ 'textarea[placeholder*="Ask" i]',
574
+ 'textarea[placeholder*="Chat" i]',
575
+ // Generic selectors
576
+ 'textarea.contenteditable',
577
+ 'rich-textarea',
578
+ 'div[contenteditable="true"] textarea',
579
+ 'div[contenteditable="true"]',
580
+ 'textarea',
581
+ // Data attributes
582
+ '[data-test-id="chat-input"]',
583
+ '[data-testid="chat-input"]',
584
+ '[data-test-id="prompt-input"]',
585
+ '[data-testid="prompt-input"]',
586
+ '[data-automation-id="chat-input"]',
587
+ // Specific classes
588
+ '.ql-editor',
589
+ '.ProseMirror',
590
+ 'markdown-textarea',
591
+ // If all else fails, get all textareas
592
+ 'textarea'
593
+ ];
594
+
595
+ let inputField = null;
596
+ let usedSelector = null;
597
+
598
+ for (const selector of inputSelectors) {
599
+ try {
600
+ const elements = await this.page.$$(selector);
601
+ for (const el of elements) {
602
+ try {
603
+ const isVisible = await el.isVisible();
604
+ const isEnabled = await el.isEnabled().catch(() => true);
605
+ if (isVisible && isEnabled) {
606
+ inputField = el;
607
+ usedSelector = selector;
608
+ break;
609
+ }
610
+ } catch (e) {
611
+ continue;
612
+ }
613
+ }
614
+ if (inputField) break;
615
+ } catch (e) {
616
+ continue;
617
+ }
618
+ }
619
+
620
+ if (!inputField) {
621
+ // Debug: list all potential input elements on page
622
+ const allTextareas = await this.page.$$('textarea, [contenteditable="true"]');
623
+ console.log(`Found ${allTextareas.length} potential input elements on page`);
624
+
625
+ // Try to get page title for debugging
626
+ const title = await this.page.title().catch(() => 'Unknown');
627
+ const url = this.page.url();
628
+ console.log(`Page title: ${title}`);
629
+ console.log(`Current URL: ${url}`);
630
+
631
+ // Check if we're on the NotebookLM homepage
632
+ const isHomepage = url.includes('notebooklm.google') &&
633
+ (url === 'https://notebooklm.google.com/' ||
634
+ url === 'https://notebooklm.google.com' ||
635
+ url.includes('notebooklm.google/?'));
636
+
637
+ if (isHomepage) {
638
+ throw new Error('You are on the NotebookLM homepage. Please:\n' +
639
+ '1. Create a new notebook or open an existing one\n' +
640
+ '2. Copy the notebook URL\n' +
641
+ '3. Use that URL with the command: smc notebooklm ask "<question>" <notebook-url>');
642
+ }
643
+
644
+ throw new Error('Could not find input field on the page. Make sure you are on a NotebookLM notebook page.');
645
+ }
646
+
647
+ console.log(`āœ… Found input field using selector: ${usedSelector}`);
648
+
649
+ // Type the question with human-like behavior
650
+ await progressCallback?.('Typing question...');
651
+
652
+ if (this.options.humanTyping) {
653
+ // Use the selector we found, not inputField.selector()
654
+ await this.humanType(usedSelector, question);
655
+ } else {
656
+ await inputField.fill(question);
657
+ }
658
+
659
+ await StealthUtils.randomDelay(200, 500);
660
+
661
+ // Submit (press Enter)
662
+ await this.page.keyboard.press('Enter');
663
+
664
+ await progressCallback?.('Waiting for response...');
665
+
666
+ // Wait for new response
667
+ const response = await PageUtils.waitForNewResponse(
668
+ this.page,
669
+ existingHashes,
670
+ this.options.responseTimeout
671
+ );
672
+
673
+ this.lastActivity = Date.now();
674
+
675
+ if (!response) {
676
+ throw new Error('No response received within timeout period');
677
+ }
678
+
679
+ await progressCallback?.('Response received!');
680
+
681
+ return response;
682
+ }
683
+
684
+ /**
685
+ * Type text with human-like behavior
686
+ */
687
+ async humanType(selector, text) {
688
+ const avgDelay = StealthUtils.getTypingDelay(
689
+ StealthUtils.randomInt(this.options.typingWpmMin, this.options.typingWpmMax)
690
+ );
691
+
692
+ await this.page.fill(selector, '');
693
+ await this.page.click(selector);
694
+ await StealthUtils.randomDelay(50, 150);
695
+
696
+ for (const char of text) {
697
+ await this.page.keyboard.type(char);
698
+ const pause = StealthUtils.getPunctuationPause(char);
699
+ const variance = StealthUtils.randomFloat(-30, 30);
700
+ await StealthUtils.sleep(Math.max(20, avgDelay + pause + variance));
701
+ }
702
+ }
703
+
704
+ /**
705
+ * Check if session has expired
706
+ */
707
+ isExpired() {
708
+ const age = Date.now() - this.lastActivity;
709
+ return age > this.options.sessionTimeout;
710
+ }
711
+
712
+ /**
713
+ * Close the session
714
+ */
715
+ async close() {
716
+ this.isActive = false;
717
+
718
+ if (this.page) {
719
+ await this.page.close().catch(() => {});
720
+ }
721
+
722
+ if (this.context) {
723
+ await this.context.close().catch(() => {});
724
+ }
725
+
726
+ if (this.browser) {
727
+ await this.browser.close().catch(() => {});
728
+ }
729
+
730
+ this.page = null;
731
+ this.context = null;
732
+ this.browser = null;
733
+ }
734
+
735
+ /**
736
+ * Get session info
737
+ */
738
+ getInfo() {
739
+ return {
740
+ notebookUrl: this.notebookUrl,
741
+ isActive: this.isActive,
742
+ createdAt: this.createdAt,
743
+ lastActivity: this.lastActivity,
744
+ age: Date.now() - this.createdAt,
745
+ inactiveTime: Date.now() - this.lastActivity
746
+ };
747
+ }
748
+ }
749
+
750
+ // ============================================================================
751
+ // Session Manager
752
+ // ============================================================================
753
+
754
+ class SessionManager {
755
+ constructor() {
756
+ this.sessions = new Map();
757
+ this.maxSessions = 5;
758
+ }
759
+
760
+ /**
761
+ * Get or create a session for a notebook
762
+ */
763
+ async getSession(notebookUrl, options = {}) {
764
+ // Clean up expired sessions first
765
+ this.cleanupExpired();
766
+
767
+ // Find existing session for this notebook
768
+ for (const [id, session] of this.sessions) {
769
+ if (session.notebookUrl === notebookUrl && session.isActive && !session.isExpired()) {
770
+ session.lastActivity = Date.now();
771
+ return session;
772
+ }
773
+ }
774
+
775
+ // Create new session
776
+ if (this.sessions.size >= this.maxSessions) {
777
+ // Close oldest inactive session
778
+ const oldest = [...this.sessions.entries()].sort((a, b) => a[1].lastActivity - b[1].lastActivity)[0];
779
+ await this.sessions.get(oldest)?.close();
780
+ this.sessions.delete(oldest);
781
+ }
782
+
783
+ const session = new NotebookLMSession(notebookUrl, options);
784
+ await session.init();
785
+ const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
786
+ this.sessions.set(sessionId, session);
787
+
788
+ return session;
789
+ }
790
+
791
+ /**
792
+ * Close a specific session
793
+ */
794
+ async closeSession(sessionId) {
795
+ const session = this.sessions.get(sessionId);
796
+ if (session) {
797
+ await session.close();
798
+ this.sessions.delete(sessionId);
799
+ return true;
800
+ }
801
+ return false;
802
+ }
803
+
804
+ /**
805
+ * Close all sessions
806
+ */
807
+ async closeAll() {
808
+ const promises = [...this.sessions.values()].map(s => s.close());
809
+ await Promise.all(promises);
810
+ this.sessions.clear();
811
+ }
812
+
813
+ /**
814
+ * Clean up expired sessions
815
+ */
816
+ cleanupExpired() {
817
+ for (const [id, session] of this.sessions) {
818
+ if (session.isExpired()) {
819
+ session.close().catch(() => {});
820
+ this.sessions.delete(id);
821
+ }
822
+ }
823
+ }
824
+
825
+ /**
826
+ * Get session statistics
827
+ */
828
+ getStats() {
829
+ const active = [...this.sessions.values()].filter(s => s.isActive).length;
830
+ return {
831
+ total: this.sessions.size,
832
+ active,
833
+ maxSessions: this.maxSessions
834
+ };
835
+ }
836
+ }
837
+
838
+ // ============================================================================
839
+ // Main NotebookLM Client
840
+ // ============================================================================
841
+
842
+ class NotebookLMClient {
843
+ constructor() {
844
+ this.sessionManager = new SessionManager();
845
+ this.authManager = new AuthManager();
846
+ }
847
+
848
+ /**
849
+ * Check if authenticated
850
+ */
851
+ isAuthenticated() {
852
+ return this.authManager.hasValidState();
853
+ }
854
+
855
+ /**
856
+ * Setup authentication (opens browser for manual login)
857
+ */
858
+ async setup(progressCallback) {
859
+ await progressCallback?.('Launching browser for authentication...');
860
+
861
+ const session = new NotebookLMSession(null, { headless: false });
862
+ await session.init();
863
+ await session.close();
864
+
865
+ await progressCallback?.('Authentication complete!');
866
+
867
+ return true;
868
+ }
869
+
870
+ /**
871
+ * Ask a question to NotebookLM
872
+ */
873
+ async ask(notebookUrl, question, progressCallback) {
874
+ const session = await this.sessionManager.getSession(notebookUrl);
875
+ return await session.ask(question, progressCallback);
876
+ }
877
+
878
+ /**
879
+ * Close all sessions
880
+ */
881
+ async closeAll() {
882
+ await this.sessionManager.closeAll();
883
+ }
884
+
885
+ /**
886
+ * Get statistics
887
+ */
888
+ getStats() {
889
+ return {
890
+ authenticated: this.isAuthenticated(),
891
+ sessions: this.sessionManager.getStats()
892
+ };
893
+ }
894
+ }
895
+
896
+ // ============================================================================
897
+ // Module Singleton
898
+ // ============================================================================
899
+
900
+ let clientInstance = null;
901
+
902
+ function getClient() {
903
+ if (!clientInstance) {
904
+ clientInstance = new NotebookLMClient();
905
+ }
906
+ return clientInstance;
907
+ }
908
+
909
+ // ============================================================================
910
+ // CLI Command Handlers
911
+ // ============================================================================
912
+
913
+ async function handleNotebookLMCommand(args) {
914
+ const client = getClient();
915
+ const [action, ...rest] = args;
916
+
917
+ switch (action) {
918
+ case 'auth':
919
+ case 'setup': {
920
+ console.log('\nšŸ” NotebookLM Authentication\n');
921
+
922
+ if (client.isAuthenticated()) {
923
+ console.log('āœ… Already authenticated!');
924
+ console.log('šŸ’” To re-authenticate, run: smc notebooklm clear && smc notebooklm auth');
925
+ return;
926
+ }
927
+
928
+ try {
929
+ await client.setup((msg) => console.log(` ${msg}`));
930
+ console.log('\nāœ… Authentication successful!');
931
+ } catch (error) {
932
+ console.error(`\nāŒ Authentication failed: ${error.message}`);
933
+ console.log('\nšŸ’” Make sure you have Google Chrome installed');
934
+ console.log('šŸ’” Install patchright: npm install patchright');
935
+ process.exit(1);
936
+ }
937
+ break;
938
+ }
939
+
940
+ case 'ask': {
941
+ const [question, ...urlParts] = rest;
942
+ const notebookUrl = urlParts.join(' ') || null;
943
+
944
+ if (!question) {
945
+ console.error('Usage: smc notebooklm ask "<question>" [notebook-url]');
946
+ console.error('');
947
+ console.error('Examples:');
948
+ console.error(' smc notebooklm ask "What are best practices for API design?"');
949
+ console.error(' smc notebooklm ask "Summarize this document" https://notebooklm.google.com/notebook/...');
950
+ process.exit(1);
951
+ }
952
+
953
+ console.log(`\nšŸ““ Asking: ${question}\n`);
954
+
955
+ if (!notebookUrl) {
956
+ console.log('āš ļø No notebook URL provided. You need to:');
957
+ console.log(' 1. Go to https://notebooklm.google.com');
958
+ console.log(' 2. Create a new notebook (or open an existing one)');
959
+ console.log(' 3. Add sources to your notebook (PDFs, docs, etc.)');
960
+ console.log(' 4. Copy the notebook URL from the address bar');
961
+ console.log(' 5. Run: smc notebooklm ask "' + question + '" <your-notebook-url>');
962
+ console.log('');
963
+ console.log('šŸ’” The notebook URL looks like:');
964
+ console.log(' https://notebooklm.google.com/notebook/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
965
+ console.log('');
966
+ process.exit(1);
967
+ }
968
+
969
+ try {
970
+ const response = await client.ask(notebookUrl, question, (msg) => {
971
+ console.log(` ${msg}`);
972
+ });
973
+
974
+ console.log('\nšŸ“ Response:\n');
975
+ console.log(response);
976
+ console.log('\n');
977
+ } catch (error) {
978
+ console.error(`\nāŒ Error: ${error.message}`);
979
+ process.exit(1);
980
+ }
981
+ break;
982
+ }
983
+
984
+ case 'status': {
985
+ const stats = client.getStats();
986
+
987
+ console.log('\nšŸ“Š NotebookLM Status\n');
988
+ console.log(` Authenticated: ${stats.authenticated ? 'āœ… Yes' : 'āŒ No'}`);
989
+ console.log(` Active Sessions: ${stats.sessions.active}/${stats.sessions.maxSessions}`);
990
+ console.log('');
991
+ break;
992
+ }
993
+
994
+ case 'clear': {
995
+ const authManager = new AuthManager();
996
+ await authManager.clearState();
997
+ await client.closeAll();
998
+ console.log('\nāœ… Cleared authentication data and closed all sessions\n');
999
+ break;
1000
+ }
1001
+
1002
+ default:
1003
+ console.log(`
1004
+ NotebookLM Commands:
1005
+
1006
+ smc notebooklm auth Authenticate with NotebookLM
1007
+ smc notebooklm ask "<question>" Ask a question
1008
+ smc notebooklm status Show authentication status
1009
+ smc notebooklm clear Clear authentication data
1010
+
1011
+ Examples:
1012
+ smc notebooklm auth
1013
+ smc notebooklm ask "What are the best practices for REST API design?"
1014
+ smc notebooklm status
1015
+ `);
1016
+ }
1017
+ }
1018
+
1019
+ module.exports = {
1020
+ NotebookLMClient,
1021
+ NotebookLMSession,
1022
+ AuthManager,
1023
+ SessionManager,
1024
+ getClient,
1025
+ handleNotebookLMCommand,
1026
+ PageUtils,
1027
+ StealthUtils
1028
+ };