sumulige-claude 1.5.1 ā 1.5.2
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/.claude/hooks/hook-registry.json +0 -15
- package/.claude/rules/coding-style.md +18 -7
- package/.claude/rules/hooks.md +15 -4
- package/.claude/rules/performance.md +15 -5
- package/.claude/rules/security.md +140 -4
- package/.claude/rules/testing.md +138 -9
- package/.claude/rules/web-design-standard.md +16 -5
- package/.claude/skills/algorithmic-art/metadata.yaml +28 -0
- package/.claude/skills/api-tester/SKILL.md +61 -0
- package/.claude/skills/api-tester/examples/basic.md +3 -0
- package/.claude/skills/api-tester/metadata.yaml +30 -0
- package/.claude/skills/api-tester/templates/default.md +3 -0
- package/.claude/skills/brand-guidelines/metadata.yaml +26 -0
- package/.claude/skills/canvas-design/metadata.yaml +27 -0
- package/.claude/skills/code-reviewer-123/SKILL.md +61 -0
- package/.claude/skills/code-reviewer-123/examples/basic.md +3 -0
- package/.claude/skills/code-reviewer-123/metadata.yaml +30 -0
- package/.claude/skills/code-reviewer-123/templates/default.md +3 -0
- package/.claude/skills/doc-coauthoring/metadata.yaml +27 -0
- package/.claude/skills/docx/metadata.yaml +30 -0
- package/.claude/skills/frontend-design/metadata.yaml +28 -0
- package/.claude/skills/internal-comms/metadata.yaml +28 -0
- package/.claude/skills/mcp-builder/metadata.yaml +26 -0
- package/.claude/skills/my-skill/SKILL.md +61 -0
- package/.claude/skills/my-skill/examples/basic.md +3 -0
- package/.claude/skills/my-skill/metadata.yaml +30 -0
- package/.claude/skills/my-skill/templates/default.md +3 -0
- package/.claude/skills/pdf/metadata.yaml +29 -0
- package/.claude/skills/pptx/metadata.yaml +29 -0
- package/.claude/skills/react-best-practices/metadata.yaml +26 -0
- package/.claude/skills/react-node-practices/SKILL.md +409 -0
- package/.claude/skills/react-node-practices/metadata.yaml +56 -0
- package/.claude/skills/skill-creator/metadata.yaml +25 -0
- package/.claude/skills/slack-gif-creator/metadata.yaml +28 -0
- package/.claude/skills/test-skill-name/SKILL.md +61 -0
- package/.claude/skills/test-skill-name/examples/basic.md +3 -0
- package/.claude/skills/test-skill-name/metadata.yaml +30 -0
- package/.claude/skills/test-skill-name/templates/default.md +3 -0
- package/.claude/skills/test-workflow/metadata.yaml +32 -0
- package/.claude/skills/theme-factory/metadata.yaml +26 -0
- package/.claude/skills/threejs-fundamentals/metadata.yaml +27 -0
- package/.claude/skills/web-artifacts-builder/metadata.yaml +30 -0
- package/.claude/skills/web-design-guidelines/metadata.yaml +26 -0
- package/.claude/skills/webapp-testing/metadata.yaml +26 -0
- package/.claude/skills/xlsx/metadata.yaml +29 -0
- package/LICENSE +21 -0
- package/cli.js +1 -1
- package/package.json +25 -3
- package/.claude/.kickoff-hint.txt +0 -52
- package/.claude/.sumulige-claude-version +0 -1
- package/.claude/.version +0 -1
- package/.claude/AGENTS.md +0 -42
- package/.claude/ANCHORS.md +0 -40
- package/.claude/CLAUDE.md +0 -138
- package/.claude/MEMORY.md +0 -69
- package/.claude/PROJECT_LOG.md +0 -101
- package/.claude/THINKING_CHAIN_GUIDE.md +0 -287
- package/.claude/USAGE.md +0 -175
- package/.claude/boris-optimizations.md +0 -167
- package/.claude/handoffs/INDEX.md +0 -21
- package/.claude/handoffs/LATEST.md +0 -76
- package/.claude/handoffs/handoff_2026-01-22T13-07-04-757Z.md +0 -76
- package/.claude/quality-gate.json +0 -82
- package/.claude/rag/skill-index.json +0 -135
- package/.claude/settings.json +0 -99
- package/.claude/settings.local.json +0 -175
- package/.claude/templates/PROJECT_KICKOFF.md +0 -89
- package/.claude/templates/PROJECT_PROPOSAL.md +0 -227
- package/.claude/templates/TASK_PLAN.md +0 -121
- package/.claude/templates/hooks/README.md +0 -302
- package/.claude/templates/hooks/hook.sh.template +0 -94
- package/.claude/templates/hooks/user-prompt-submit.cjs.template +0 -116
- package/.claude/templates/hooks/user-response-submit.cjs.template +0 -94
- package/.claude/templates/hooks/validate.js +0 -173
- package/.claude/templates/tasks/develop.md +0 -69
- package/.claude/templates/tasks/research.md +0 -64
- package/.claude/templates/tasks/test.md +0 -96
- package/.claude/thinking-routes/.last-sync +0 -1
- package/.claude/thinking-routes/QUICKREF.md +0 -98
- package/.claude/workflow/document-scanner.js +0 -426
- package/.claude/workflow/knowledge-engine.js +0 -941
- package/.claude/workflow/notebooklm/browser.js +0 -1028
- package/.claude/workflow/phases/phase1-research.js +0 -578
- package/.claude/workflow/phases/phase1-research.ts +0 -465
- package/.claude/workflow/phases/phase2-approve.js +0 -722
- package/.claude/workflow/phases/phase3-plan.js +0 -1200
- package/.claude/workflow/phases/phase4-develop.js +0 -894
- package/.claude/workflow/search-cache.js +0 -230
- package/.claude/workflow/templates/approval.md +0 -315
- package/.claude/workflow/templates/development.md +0 -377
- package/.claude/workflow/templates/planning.md +0 -328
- package/.claude/workflow/templates/research.md +0 -250
- package/.claude/workflow/types.js +0 -37
- package/.claude/workflow/web-search.js +0 -278
- package/.claude-plugin/marketplace.json +0 -71
- package/.github/workflows/sync-skills.yml +0 -74
- package/.versionrc +0 -25
- package/AGENTS.md +0 -580
- package/CHANGELOG.md +0 -481
- package/CLAUDE-template.md +0 -114
- package/DEV_TOOLS_GUIDE.md +0 -190
- package/PROJECT_STRUCTURE.md +0 -266
- package/Q&A.md +0 -325
- package/config/defaults.json +0 -34
- package/config/official-skills.json +0 -183
- package/config/quality-gate.json +0 -67
- package/config/skill-categories.json +0 -40
- package/config/version-manifest.json +0 -85
- package/demos/power-3d-scatter.html +0 -683
- package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +0 -36
- package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +0 -36
- package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +0 -36
- package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +0 -36
- package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +0 -36
- package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +0 -36
- package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +0 -36
- package/development/knowledge-base/.index.clean.json +0 -1
- package/development/knowledge-base/.index.json +0 -486
- package/development/knowledge-base/test-best-practices.md +0 -29
- package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +0 -226
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +0 -345
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +0 -284
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +0 -14
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +0 -35
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +0 -34
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +0 -5
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +0 -60
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +0 -25
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +0 -70
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +0 -48
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +0 -20
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +0 -21
- package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +0 -226
- package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +0 -345
- package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +0 -284
- package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +0 -14
- package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +0 -178
- package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +0 -377
- package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +0 -442
- package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +0 -800
- package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +0 -625
- package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +0 -830
- package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +0 -957
- package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +0 -381
- package/development/todos/.state.json +0 -19
- package/development/todos/INDEX.md +0 -63
- package/development/todos/active/_README.md +0 -49
- package/development/todos/archived/_README.md +0 -11
- package/development/todos/backlog/_README.md +0 -11
- package/development/todos/backlog/mcp-integration.md +0 -35
- package/development/todos/completed/_README.md +0 -11
- package/development/todos/completed/boris-optimizations.md +0 -39
- package/development/todos/completed/develop/local-knowledge-index.md +0 -85
- package/development/todos/completed/develop/todo-system.md +0 -47
- package/development/todos/completed/develop/web-search-integration.md +0 -83
- package/development/todos/completed/test/phase1-e2e-test.md +0 -103
- package/docs/DEVELOPMENT.md +0 -461
- package/docs/MARKETPLACE.md +0 -352
- package/docs/RELEASE.md +0 -93
- package/jest.config.js +0 -63
- package/lib/commands.js +0 -3588
- package/lib/config-manager.js +0 -441
- package/lib/config-schema.js +0 -408
- package/lib/config-validator.js +0 -330
- package/lib/config.js +0 -122
- package/lib/errors.js +0 -305
- package/lib/incremental-sync.js +0 -274
- package/lib/marketplace.js +0 -487
- package/lib/migrations.js +0 -154
- package/lib/permission-audit.js +0 -255
- package/lib/quality-gate.js +0 -431
- package/lib/quality-rules.js +0 -373
- package/lib/utils.js +0 -150
- package/lib/version-check.js +0 -169
- package/lib/version-manifest.js +0 -171
- package/project-paradigm.md +0 -313
- package/prompts/how-to-find.md +0 -163
- package/prompts/linus-architect.md +0 -71
- package/prompts/software-architect.md +0 -173
- package/prompts/web-designer.md +0 -249
- package/scripts/fix-hooks.mjs +0 -97
- package/scripts/sync-external.mjs +0 -298
- package/scripts/sync-to-home.sh +0 -108
- package/scripts/update-registry.mjs +0 -325
- package/sources.yaml +0 -83
- package/tests/README.md +0 -263
- package/tests/commands.test.js +0 -1086
- package/tests/config-manager.test.js +0 -677
- package/tests/config-schema.test.js +0 -425
- package/tests/config-validator.test.js +0 -436
- package/tests/config.test.js +0 -100
- package/tests/errors.test.js +0 -477
- package/tests/manual/phase1-e2e.sh +0 -389
- package/tests/manual/phase2-test-cases.md +0 -311
- package/tests/manual/phase3-test-cases.md +0 -309
- package/tests/manual/phase4-test-cases.md +0 -414
- package/tests/manual/test-cases.md +0 -417
- package/tests/marketplace.test.js +0 -420
- package/tests/migrations.test.js +0 -187
- package/tests/quality-gate.test.js +0 -679
- package/tests/quality-rules.test.js +0 -619
- package/tests/sync-external.test.js +0 -214
- package/tests/update-registry.test.js +0 -251
- package/tests/utils.test.js +0 -171
- package/tests/version-check.test.js +0 -75
- package/tests/web-search.test.js +0 -392
- package/thinkinglens-silent.md +0 -138
|
@@ -1,1028 +0,0 @@
|
|
|
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
|
-
};
|